From 8a523adaba4e8d0bb8affe115a5782206dca8c53 Mon Sep 17 00:00:00 2001 From: Pascal Sthamer <10992664+P4sca1@users.noreply.github.com> Date: Tue, 31 Dec 2024 02:42:51 +0100 Subject: [PATCH 01/67] add shuffle helm chart remove gitlab ci file --- charts/shuffle/.gitignore | 3 + charts/shuffle/.helmignore | 23 + charts/shuffle/.yamllint | 11 + charts/shuffle/Chart.yaml | 14 + charts/shuffle/README.md | 570 ++++ charts/shuffle/templates/NOTES.txt | 25 + charts/shuffle/templates/_helpers.tpl | 215 ++ .../backend/backend-apps-claim-pvc.yaml | 28 + .../templates/backend/backend-apps-pvc.yaml | 30 + .../templates/backend/backend-cm-env.yaml | 30 + .../templates/backend/backend-dpl.yaml | 221 ++ .../templates/backend/backend-files-pvc.yaml | 30 + .../templates/backend/backend-hpa.yaml | 43 + .../backend/backend-network-policy.yaml | 66 + .../templates/backend/backend-pdb.yaml | 21 + .../backend/backend-role-binding.yaml | 18 + .../templates/backend/backend-role.yaml | 18 + .../backend/backend-service-account.yaml | 13 + .../templates/backend/backend-svc.yaml | 18 + .../templates/backend/backend-vpa.yaml | 38 + charts/shuffle/templates/extra-list.yaml | 4 + .../templates/frontend/frontend-cm-env.yaml | 10 + .../templates/frontend/frontend-dpl.yaml | 151 + .../templates/frontend/frontend-hpa.yaml | 43 + .../frontend/frontend-network-policy.yaml | 44 + .../templates/frontend/frontend-pdb.yaml | 21 + .../frontend/frontend-service-account.yaml | 13 + .../templates/frontend/frontend-svc.yaml | 18 + .../templates/frontend/frontend-vpa.yaml | 38 + charts/shuffle/templates/ingress/ingress.yaml | 60 + .../shuffle/templates/ingress/tls-secret.yaml | 39 + charts/shuffle/templates/istio/gateway.yaml | 39 + .../templates/istio/virtual-service.yaml | 30 + .../orborus-app-network-policy.yaml | 61 + .../orborus-app/orborus-app-role-binding.yaml | 20 + .../orborus-app/orborus-app-role.yaml | 15 + .../orborus-app-service-account.yaml | 13 + .../orborus-worker-network-policy.yaml | 69 + .../orborus-worker-role-binding.yaml | 20 + .../orborus-worker/orborus-worker-role.yaml | 21 + .../orborus-worker-service-account.yaml | 13 + .../templates/orborus/orborus-cm-env.yaml | 19 + .../templates/orborus/orborus-dpl.yaml | 155 + .../templates/orborus/orborus-hpa.yaml | 43 + .../orborus/orborus-network-policy.yaml | 72 + .../templates/orborus/orborus-pdb.yaml | 21 + .../orborus/orborus-role-binding.yaml | 18 + .../templates/orborus/orborus-role.yaml | 29 + .../orborus/orborus-service-account.yaml | 13 + .../templates/orborus/orborus-vpa.yaml | 38 + charts/shuffle/templates/vault-secrets.yaml | 19 + charts/shuffle/values.schema.json | 2562 +++++++++++++++++ charts/shuffle/values.yaml | 1772 ++++++++++++ 53 files changed, 6938 insertions(+) create mode 100644 charts/shuffle/.gitignore create mode 100644 charts/shuffle/.helmignore create mode 100644 charts/shuffle/.yamllint create mode 100644 charts/shuffle/Chart.yaml create mode 100644 charts/shuffle/README.md create mode 100644 charts/shuffle/templates/NOTES.txt create mode 100644 charts/shuffle/templates/_helpers.tpl create mode 100644 charts/shuffle/templates/backend/backend-apps-claim-pvc.yaml create mode 100644 charts/shuffle/templates/backend/backend-apps-pvc.yaml create mode 100644 charts/shuffle/templates/backend/backend-cm-env.yaml create mode 100644 charts/shuffle/templates/backend/backend-dpl.yaml create mode 100644 charts/shuffle/templates/backend/backend-files-pvc.yaml create mode 100644 charts/shuffle/templates/backend/backend-hpa.yaml create mode 100644 charts/shuffle/templates/backend/backend-network-policy.yaml create mode 100644 charts/shuffle/templates/backend/backend-pdb.yaml create mode 100644 charts/shuffle/templates/backend/backend-role-binding.yaml create mode 100644 charts/shuffle/templates/backend/backend-role.yaml create mode 100644 charts/shuffle/templates/backend/backend-service-account.yaml create mode 100644 charts/shuffle/templates/backend/backend-svc.yaml create mode 100644 charts/shuffle/templates/backend/backend-vpa.yaml create mode 100644 charts/shuffle/templates/extra-list.yaml create mode 100644 charts/shuffle/templates/frontend/frontend-cm-env.yaml create mode 100644 charts/shuffle/templates/frontend/frontend-dpl.yaml create mode 100644 charts/shuffle/templates/frontend/frontend-hpa.yaml create mode 100644 charts/shuffle/templates/frontend/frontend-network-policy.yaml create mode 100644 charts/shuffle/templates/frontend/frontend-pdb.yaml create mode 100644 charts/shuffle/templates/frontend/frontend-service-account.yaml create mode 100644 charts/shuffle/templates/frontend/frontend-svc.yaml create mode 100644 charts/shuffle/templates/frontend/frontend-vpa.yaml create mode 100644 charts/shuffle/templates/ingress/ingress.yaml create mode 100644 charts/shuffle/templates/ingress/tls-secret.yaml create mode 100644 charts/shuffle/templates/istio/gateway.yaml create mode 100644 charts/shuffle/templates/istio/virtual-service.yaml create mode 100644 charts/shuffle/templates/orborus-app/orborus-app-network-policy.yaml create mode 100644 charts/shuffle/templates/orborus-app/orborus-app-role-binding.yaml create mode 100644 charts/shuffle/templates/orborus-app/orborus-app-role.yaml create mode 100644 charts/shuffle/templates/orborus-app/orborus-app-service-account.yaml create mode 100644 charts/shuffle/templates/orborus-worker/orborus-worker-network-policy.yaml create mode 100644 charts/shuffle/templates/orborus-worker/orborus-worker-role-binding.yaml create mode 100644 charts/shuffle/templates/orborus-worker/orborus-worker-role.yaml create mode 100644 charts/shuffle/templates/orborus-worker/orborus-worker-service-account.yaml create mode 100644 charts/shuffle/templates/orborus/orborus-cm-env.yaml create mode 100644 charts/shuffle/templates/orborus/orborus-dpl.yaml create mode 100644 charts/shuffle/templates/orborus/orborus-hpa.yaml create mode 100644 charts/shuffle/templates/orborus/orborus-network-policy.yaml create mode 100644 charts/shuffle/templates/orborus/orborus-pdb.yaml create mode 100644 charts/shuffle/templates/orborus/orborus-role-binding.yaml create mode 100644 charts/shuffle/templates/orborus/orborus-role.yaml create mode 100644 charts/shuffle/templates/orborus/orborus-service-account.yaml create mode 100644 charts/shuffle/templates/orborus/orborus-vpa.yaml create mode 100644 charts/shuffle/templates/vault-secrets.yaml create mode 100644 charts/shuffle/values.schema.json create mode 100644 charts/shuffle/values.yaml diff --git a/charts/shuffle/.gitignore b/charts/shuffle/.gitignore new file mode 100644 index 00000000..1fab0544 --- /dev/null +++ b/charts/shuffle/.gitignore @@ -0,0 +1,3 @@ +.DS_Store +*.tgz +charts/ diff --git a/charts/shuffle/.helmignore b/charts/shuffle/.helmignore new file mode 100644 index 00000000..0e8a0eb3 --- /dev/null +++ b/charts/shuffle/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/charts/shuffle/.yamllint b/charts/shuffle/.yamllint new file mode 100644 index 00000000..8b3ce4aa --- /dev/null +++ b/charts/shuffle/.yamllint @@ -0,0 +1,11 @@ +--- +extends: default + +rules: + line-length: disable + braces: disable + comments: + require-starting-space: true + ignore-shebangs: true + min-spaces-from-content: 1 + diff --git a/charts/shuffle/Chart.yaml b/charts/shuffle/Chart.yaml new file mode 100644 index 00000000..3d74f418 --- /dev/null +++ b/charts/shuffle/Chart.yaml @@ -0,0 +1,14 @@ +apiVersion: v2 +name: shuffle +description: A Helm chart for deploying Shuffle on Kubernetes +type: application +version: 0.0.0 +appVersion: nightly +dependencies: + - name: common + version: ^2.23.0 + repository: oci://registry-1.docker.io/bitnamicharts + - name: opensearch + version: ^1.3.0 + repository: oci://registry-1.docker.io/bitnamicharts + condition: opensearch.enabled diff --git a/charts/shuffle/README.md b/charts/shuffle/README.md new file mode 100644 index 00000000..5b5ebe55 --- /dev/null +++ b/charts/shuffle/README.md @@ -0,0 +1,570 @@ +# Shuffle Helm chart + +## Chart Template + +The Bitnami Chart Template was used for creating this chart: + +https://github.com/bitnami/charts/tree/7e44e64626f5b1fc6d56889cdfdeadc1f62c7cf1/template/CHART_NAME + +Original license text: + +``` +Copyright Broadcom, Inc. All Rights Reserved. + +SPDX-License-Identifier: APACHE-2.0 +``` + +## Usage + +```sh +# Lint chart +helm lint . + +# Package chart +helm package . + +# Install (the shuffle namespace is hardcoded into the shuffle source code) +helm install shuffle oci://TODO -n shuffle +``` + +## Secret Parameters + +The helm chart was designed to not contain any secret data and does not allow configuring secret data using helm values. +Instead, secret values must be passed to services using `extraEnvVarsSecret`. The secrets need to be manually created. + +### Creating secrets using vault-secrets-operator + +If you are using [vault-secret-operator by Rico Berger](https://github.com/ricoberger/vault-secrets-operator), +then you can create VaultSecret resources via Helm. +Note that the resulting (Vault)Secret is prefixed with the release name of the chart. + +```yaml +vault: + secrets: + - name: backend-env + type: Opaque + path: shuffle/backend/env +``` + +### Mounting env variables into a service + +After creating a secret which holds the environment variables (either manually or via a VaultSecret), you can then +use that secret to mount environment variables into a service via the `extraEnvVarsSecret` value. + +You can use helm templates for generating the secret name as shown in the example below. + +```yaml +backend: + extraEnvVarsSecret: "{{ include \"common.names.fullname\" . }}-backend-env" +``` + +### Backend + +A list of environment variables containing secret values for the backend. + +```yaml +# OpenSearch password +SHUFFLE_OPENSEARCH_PASSWORD: "" + +# Basic auth credentials for downloading apps from git +SHUFFLE_DOWNLOAD_AUTH_USERNAME: "" +SHUFFLE_DOWNLOAD_AUTH_PASSWORD: "" + +# Automatically create the initial admin user. Username and password have a min length of 3. +# If not set, you are prompted with an admin user creation dialog when visiting the shuffle frontend for the first time. +SHUFFLE_DEFAULT_USERNAME: admin +SHUFFLE_DEFAULT_PASSWORD: MySecretAdminPassword1234! +SHUFFLE_DEFAULT_APIKEY: "72E41083-A6F6-4A1B-8538-B06B577F47F0" # Shuffle uses uuid v4 + +# Encryption modifier. This HAS to be set to encrypt any authentication being used in Shuffle. +# This is put together with other relevant values to ensure multiple parts are needed to decrypt. +# If this key is lost or changed, you will have to reauthenticate all apps. +# The encryption modifier is added to encrypted values to prevent rainbow table attacks. It can be any random string. +SHUFFLE_ENCRYPTION_MODIFIER: "MyShuffleEncryptionModifier" +``` + +## Parameters + +### Global parameters + +| Name | Description | Value | +| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- | +| `global.imageRegistry` | Global Docker image registry | `""` | +| `global.imagePullSecrets` | Global Docker registry secret names as an array | `[]` | +| `global.defaultStorageClass` | Global default StorageClass for Persistent Volume(s) | `""` | +| `global.compatibility.openshift.adaptSecurityContext` | Adapt the securityContext sections of the deployment to make them compatible with Openshift restricted-v2 SCC: remove runAsUser, runAsGroup and fsGroup and let the platform use their allowed default IDs. Possible values: auto (apply if the detected running cluster is Openshift), force (perform the adaptation always), disabled (do not perform adaptation) | `auto` | +| `global.compatibility.omitEmptySeLinuxOptions` | If set to true, removes the seLinuxOptions from the securityContexts when it is set to an empty object | `false` | + +### Common parameters + +| Name | Description | Value | +| ------------------------ | --------------------------------------------------------------------------------------- | --------------- | +| `kubeVersion` | Override Kubernetes version | `""` | +| `nameOverride` | String to partially override common.names.name | `""` | +| `fullnameOverride` | String to fully override common.names.fullname | `""` | +| `namespaceOverride` | String to fully override common.names.namespace | `""` | +| `commonLabels` | Labels to add to all deployed objects | `{}` | +| `commonAnnotations` | Annotations to add to all deployed objects | `{}` | +| `clusterDomain` | Kubernetes cluster domain name | `cluster.local` | +| `extraDeploy` | Array of extra objects to deploy with the release | `[]` | +| `diagnosticMode.enabled` | Enable diagnostic mode (all probes will be disabled and the command will be overridden) | `false` | +| `diagnosticMode.command` | Command to override all containers in the chart release | `["sleep"]` | +| `diagnosticMode.args` | Args to override all containers in the chart release | `["infinity"]` | + +### Shared Shuffle Parameters + +| Name | Description | Value | +| --------------------- | ------------------------------------------------------------- | --------------- | +| `shuffle.baseUrl` | The external base URL under which Shuffle is reachable. | `""` | +| `shuffle.org` | Default shuffle organization | `Shuffle` | +| `shuffle.appRegistry` | The registry from / to which shuffle apps are pulled / pushed | `""` | +| `shuffle.timezone` | The timezone used by Shuffle | `Europe/Berlin` | + +### backend Parameters + +| Name | Description | Value | +| ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | +| `backend.image.registry` | backend image registry | `ghcr.io` | +| `backend.image.repository` | backend image repository | `shuffle/shuffle-backend` | +| `backend.image.digest` | backend image digest in the way sha256:aa.... Please note this parameter, if set, will override the tag image tag (immutable tags are recommended) | `""` | +| `backend.image.pullPolicy` | backend image pull policy | `IfNotPresent` | +| `backend.image.pullSecrets` | backend image pull secrets | `[]` | +| `backend.replicaCount` | Number of backend replicas to deploy | `1` | +| `backend.livenessProbe.enabled` | Enable livenessProbe on backend containers | `false` | +| `backend.livenessProbe.initialDelaySeconds` | Initial delay seconds for livenessProbe | `0` | +| `backend.livenessProbe.periodSeconds` | Period seconds for livenessProbe | `15` | +| `backend.livenessProbe.timeoutSeconds` | Timeout seconds for livenessProbe | `1` | +| `backend.livenessProbe.failureThreshold` | Failure threshold for livenessProbe | `4` | +| `backend.livenessProbe.successThreshold` | Success threshold for livenessProbe | `1` | +| `backend.readinessProbe.enabled` | Enable readinessProbe on backend containers | `false` | +| `backend.readinessProbe.initialDelaySeconds` | Initial delay seconds for readinessProbe | `0` | +| `backend.readinessProbe.periodSeconds` | Period seconds for readinessProbe | `5` | +| `backend.readinessProbe.timeoutSeconds` | Timeout seconds for readinessProbe | `1` | +| `backend.readinessProbe.failureThreshold` | Failure threshold for readinessProbe | `3` | +| `backend.readinessProbe.successThreshold` | Success threshold for readinessProbe | `1` | +| `backend.startupProbe.enabled` | Enable startupProbe on backend containers | `false` | +| `backend.startupProbe.initialDelaySeconds` | Initial delay seconds for startupProbe | `0` | +| `backend.startupProbe.periodSeconds` | Period seconds for startupProbe | `1` | +| `backend.startupProbe.timeoutSeconds` | Timeout seconds for startupProbe | `1` | +| `backend.startupProbe.failureThreshold` | Failure threshold for startupProbe | `60` | +| `backend.startupProbe.successThreshold` | Success threshold for startupProbe | `1` | +| `backend.customLivenessProbe` | Custom livenessProbe that overrides the default one | `{}` | +| `backend.customReadinessProbe` | Custom readinessProbe that overrides the default one | `{}` | +| `backend.customStartupProbe` | Custom startupProbe that overrides the default one | `{}` | +| `backend.resourcesPreset` | Set backend container resources according to one common preset (allowed values: none, nano, small, medium, large, xlarge, 2xlarge). This is ignored if backend.resources is set (backend.resources is recommended for production). | `small` | +| `backend.resources` | Set backend container requests and limits for different resources like CPU or memory (essential for production workloads) | `{}` | +| `backend.podSecurityContext.enabled` | Enable backend pods' Security Context | `true` | +| `backend.podSecurityContext.fsGroupChangePolicy` | Set filesystem group change policy for backend pods | `Always` | +| `backend.podSecurityContext.sysctls` | Set kernel settings using the sysctl interface for backend pods | `[]` | +| `backend.podSecurityContext.supplementalGroups` | Set filesystem extra groups for backend pods | `[]` | +| `backend.podSecurityContext.fsGroup` | Set fsGroup in backend pods' Security Context | `1001` | +| `backend.containerSecurityContext.enabled` | Enabled backend container' Security Context | `true` | +| `backend.containerSecurityContext.seLinuxOptions` | Set SELinux options in backend container | `{}` | +| `backend.containerSecurityContext.runAsUser` | Set runAsUser in backend container' Security Context | `1000` | +| `backend.containerSecurityContext.runAsGroup` | Set runAsGroup in backend container' Security Context | `1000` | +| `backend.containerSecurityContext.runAsNonRoot` | Set runAsNonRoot in backend container' Security Context | `true` | +| `backend.containerSecurityContext.readOnlyRootFilesystem` | Set readOnlyRootFilesystem in backend container' Security Context | `true` | +| `backend.containerSecurityContext.privileged` | Set privileged in backend container' Security Context | `false` | +| `backend.containerSecurityContext.allowPrivilegeEscalation` | Set allowPrivilegeEscalation in backend container' Security Context | `false` | +| `backend.containerSecurityContext.capabilities.drop` | List of capabilities to be dropped in backend container | `["ALL"]` | +| `backend.containerSecurityContext.seccompProfile.type` | Set seccomp profile in backend container | `RuntimeDefault` | +| `backend.command` | Override default backend container command (useful when using custom images) | `[]` | +| `backend.args` | Override default backend container args (useful when using custom images) | `[]` | +| `backend.automountServiceAccountToken` | Mount Service Account token in backend pods | `true` | +| `backend.hostAliases` | backend pods host aliases | `[]` | +| `backend.daemonsetAnnotations` | Annotations for backend daemonset | `{}` | +| `backend.deploymentAnnotations` | Annotations for backend deployment | `{}` | +| `backend.statefulsetAnnotations` | Annotations for backend statefulset | `{}` | +| `backend.podLabels` | Extra labels for backend pods | `{}` | +| `backend.podAnnotations` | Annotations for backend pods | `{}` | +| `backend.podAffinityPreset` | Pod affinity preset. Ignored if `backend.affinity` is set. Allowed values: `soft` or `hard` | `""` | +| `backend.podAntiAffinityPreset` | Pod anti-affinity preset. Ignored if `backend.affinity` is set. Allowed values: `soft` or `hard` | `soft` | +| `backend.nodeAffinityPreset.type` | Node affinity preset type. Ignored if `backend.affinity` is set. Allowed values: `soft` or `hard` | `""` | +| `backend.nodeAffinityPreset.key` | Node label key to match. Ignored if `backend.affinity` is set | `""` | +| `backend.nodeAffinityPreset.values` | Node label values to match. Ignored if `backend.affinity` is set | `[]` | +| `backend.affinity` | Affinity for backend pods assignment | `{}` | +| `backend.nodeSelector` | Node labels for backend pods assignment | `{}` | +| `backend.tolerations` | Tolerations for backend pods assignment | `[]` | +| `backend.updateStrategy.type` | backend deployment strategy type | `RollingUpdate` | +| `backend.updateStrategy.type` | backend statefulset strategy type | `RollingUpdate` | +| `backend.podManagementPolicy` | Pod management policy for backend statefulset | `OrderedReady` | +| `backend.priorityClassName` | backend pods' priorityClassName | `""` | +| `backend.topologySpreadConstraints` | Topology Spread Constraints for backend pod assignment spread across your cluster among failure-domains | `[]` | +| `backend.schedulerName` | Name of the k8s scheduler (other than default) for backend pods | `""` | +| `backend.terminationGracePeriodSeconds` | Seconds backend pods need to terminate gracefully | `""` | +| `backend.lifecycleHooks` | for backend containers to automate configuration before or after startup | `{}` | +| `backend.extraEnvVars` | Array with extra environment variables to add to backend containers | `[]` | +| `backend.extraEnvVarsCM` | Name of existing ConfigMap containing extra env vars for backend containers | `""` | +| `backend.extraEnvVarsSecret` | Name of existing Secret containing extra env vars for backend containers | `""` | +| `backend.extraVolumes` | Optionally specify extra list of additional volumes for the backend pods | `[]` | +| `backend.extraVolumeMounts` | Optionally specify extra list of additional volumeMounts for the backend containers | `[]` | +| `backend.sidecars` | Add additional sidecar containers to the backend pods | `[]` | +| `backend.initContainers` | Add additional init containers to the backend pods | `[]` | +| `backend.pdb.create` | Enable/disable a Pod Disruption Budget creation | `true` | +| `backend.pdb.minAvailable` | Minimum number/percentage of pods that should remain scheduled | `""` | +| `backend.pdb.maxUnavailable` | Maximum number/percentage of pods that may be made unavailable. Defaults to `1` if both `backend.pdb.minAvailable` and `backend.pdb.maxUnavailable` are empty. | `""` | +| `backend.autoscaling.vpa.enabled` | Enable VPA for backend pods | `false` | +| `backend.autoscaling.vpa.annotations` | Annotations for VPA resource | `{}` | +| `backend.autoscaling.vpa.controlledResources` | VPA List of resources that the vertical pod autoscaler can control. Defaults to cpu and memory | `[]` | +| `backend.autoscaling.vpa.maxAllowed` | VPA Max allowed resources for the pod | `{}` | +| `backend.autoscaling.vpa.minAllowed` | VPA Min allowed resources for the pod | `{}` | +| `backend.autoscaling.vpa.updatePolicy.updateMode` | Autoscaling update policy | `Auto` | +| `backend.autoscaling.hpa.enabled` | Enable HPA for backend pods | `false` | +| `backend.autoscaling.hpa.minReplicas` | Minimum number of replicas | `""` | +| `backend.autoscaling.hpa.maxReplicas` | Maximum number of replicas | `""` | +| `backend.autoscaling.hpa.targetCPU` | Target CPU utilization percentage | `""` | +| `backend.autoscaling.hpa.targetMemory` | Target Memory utilization percentage | `""` | +| `backend.serviceAccount.create` | Specifies whether a ServiceAccount should be created | `true` | +| `backend.serviceAccount.name` | The name of the ServiceAccount to use. | `""` | +| `backend.serviceAccount.annotations` | Additional Service Account annotations (evaluated as a template) | `{}` | +| `backend.serviceAccount.automountServiceAccountToken` | Automount service account token for the server service account | `true` | +| `backend.rbac.create` | Specifies whether RBAC resources should be created | `true` | +| `backend.networkPolicy.enabled` | Specifies whether a NetworkPolicy should be created | `true` | +| `backend.networkPolicy.allowExternal` | Don't require server label for connections | `true` | +| `backend.networkPolicy.allowExternalEgress` | Allow the pod to access any range of port and all destinations. | `true` | +| `backend.networkPolicy.extraIngress` | Add extra ingress rules to the NetworkPolicy | `[]` | +| `backend.networkPolicy.extraEgress` | Add extra ingress rules to the NetworkPolicy (ignored if allowExternalEgress=true) | `[]` | +| `backend.cleanupSchedule` | The interval in seconds at which the cleanup job runs | `300` | +| `backend.openSearch.url` | The URL at which OpenSearch is available | `http://{{ .Release.Name }}-opensearch:9200` | +| `backend.openSearch.username` | The username that is used for authenticating with OpenSearch | `admin` | +| `backend.openSearch.certificateFile` | The path to a custom OpenSearch certificate file | `""` | +| `backend.openSearch.skipSSLVerify` | Skip SSL verification | `false` | +| `backend.openSearch.indexPrefix` | A prefix for OpenSearch indices | `""` | +| `backend.apps.downloadLocation` | The location to a git repository from which default appps are downloaded on startup. | `https://github.com/shuffle/python-apps` | +| `backend.apps.downloadBranch` | The branch from which apps should be downloaded on startup. | `master` | +| `backend.apps.forceUpdate` | Force an update of apps on startup. | `false` | + +### frontend Parameters + +| Name | Description | Value | +| ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- | +| `frontend.image.registry` | frontend image registry | `ghcr.io` | +| `frontend.image.repository` | frontend image repository | `shuffle/shuffle-frontend` | +| `frontend.image.digest` | frontend image digest in the way sha256:aa.... Please note this parameter, if set, will override the tag image tag (immutable tags are recommended) | `""` | +| `frontend.image.pullPolicy` | frontend image pull policy | `IfNotPresent` | +| `frontend.image.pullSecrets` | frontend image pull secrets | `[]` | +| `frontend.replicaCount` | Number of frontend replicas to deploy | `1` | +| `frontend.livenessProbe.enabled` | Enable livenessProbe on frontend containers | `false` | +| `frontend.livenessProbe.initialDelaySeconds` | Initial delay seconds for livenessProbe | `0` | +| `frontend.livenessProbe.periodSeconds` | Period seconds for livenessProbe | `15` | +| `frontend.livenessProbe.timeoutSeconds` | Timeout seconds for livenessProbe | `1` | +| `frontend.livenessProbe.failureThreshold` | Failure threshold for livenessProbe | `4` | +| `frontend.livenessProbe.successThreshold` | Success threshold for livenessProbe | `1` | +| `frontend.readinessProbe.enabled` | Enable readinessProbe on frontend containers | `false` | +| `frontend.readinessProbe.initialDelaySeconds` | Initial delay seconds for readinessProbe | `0` | +| `frontend.readinessProbe.periodSeconds` | Period seconds for readinessProbe | `5` | +| `frontend.readinessProbe.timeoutSeconds` | Timeout seconds for readinessProbe | `1` | +| `frontend.readinessProbe.failureThreshold` | Failure threshold for readinessProbe | `3` | +| `frontend.readinessProbe.successThreshold` | Success threshold for readinessProbe | `1` | +| `frontend.startupProbe.enabled` | Enable startupProbe on frontend containers | `false` | +| `frontend.startupProbe.initialDelaySeconds` | Initial delay seconds for startupProbe | `0` | +| `frontend.startupProbe.periodSeconds` | Period seconds for startupProbe | `1` | +| `frontend.startupProbe.timeoutSeconds` | Timeout seconds for startupProbe | `1` | +| `frontend.startupProbe.failureThreshold` | Failure threshold for startupProbe | `60` | +| `frontend.startupProbe.successThreshold` | Success threshold for startupProbe | `1` | +| `frontend.customLivenessProbe` | Custom livenessProbe that overrides the default one | `{}` | +| `frontend.customReadinessProbe` | Custom readinessProbe that overrides the default one | `{}` | +| `frontend.customStartupProbe` | Custom startupProbe that overrides the default one | `{}` | +| `frontend.resourcesPreset` | Set frontend container resources according to one common preset (allowed values: none, nano, small, medium, large, xlarge, 2xlarge). This is ignored if frontend.resources is set (frontend.resources is recommended for production). | `nano` | +| `frontend.resources` | Set frontend container requests and limits for different resources like CPU or memory (essential for production workloads) | `{}` | +| `frontend.podSecurityContext.enabled` | Enable frontend pods' Security Context | `true` | +| `frontend.podSecurityContext.fsGroupChangePolicy` | Set filesystem group change policy for frontend pods | `Always` | +| `frontend.podSecurityContext.sysctls` | Set kernel settings using the sysctl interface for frontend pods | `[]` | +| `frontend.podSecurityContext.supplementalGroups` | Set filesystem extra groups for frontend pods | `[]` | +| `frontend.podSecurityContext.fsGroup` | Set fsGroup in frontend pods' Security Context | `1001` | +| `frontend.containerSecurityContext.enabled` | Enabled frontend container' Security Context | `true` | +| `frontend.containerSecurityContext.seLinuxOptions` | Set SELinux options in frontend container | `{}` | +| `frontend.containerSecurityContext.runAsUser` | Set runAsUser in frontend container' Security Context | `101` | +| `frontend.containerSecurityContext.runAsGroup` | Set runAsGroup in frontend container' Security Context | `101` | +| `frontend.containerSecurityContext.runAsNonRoot` | Set runAsNonRoot in frontend container' Security Context | `true` | +| `frontend.containerSecurityContext.readOnlyRootFilesystem` | Set readOnlyRootFilesystem in frontend container' Security Context | `true` | +| `frontend.containerSecurityContext.privileged` | Set privileged in frontend container' Security Context | `false` | +| `frontend.containerSecurityContext.allowPrivilegeEscalation` | Set allowPrivilegeEscalation in frontend container' Security Context | `false` | +| `frontend.containerSecurityContext.capabilities.drop` | List of capabilities to be dropped in frontend container | `["ALL"]` | +| `frontend.containerSecurityContext.seccompProfile.type` | Set seccomp profile in frontend container | `RuntimeDefault` | +| `frontend.command` | Override default frontend container command (useful when using custom images) | `[]` | +| `frontend.args` | Override default frontend container args (useful when using custom images) | `[]` | +| `frontend.automountServiceAccountToken` | Mount Service Account token in frontend pods | `false` | +| `frontend.hostAliases` | frontend pods host aliases | `[]` | +| `frontend.daemonsetAnnotations` | Annotations for frontend daemonset | `{}` | +| `frontend.deploymentAnnotations` | Annotations for frontend deployment | `{}` | +| `frontend.statefulsetAnnotations` | Annotations for frontend statefulset | `{}` | +| `frontend.podLabels` | Extra labels for frontend pods | `{}` | +| `frontend.podAnnotations` | Annotations for frontend pods | `{}` | +| `frontend.podAffinityPreset` | Pod affinity preset. Ignored if `frontend.affinity` is set. Allowed values: `soft` or `hard` | `""` | +| `frontend.podAntiAffinityPreset` | Pod anti-affinity preset. Ignored if `frontend.affinity` is set. Allowed values: `soft` or `hard` | `soft` | +| `frontend.nodeAffinityPreset.type` | Node affinity preset type. Ignored if `frontend.affinity` is set. Allowed values: `soft` or `hard` | `""` | +| `frontend.nodeAffinityPreset.key` | Node label key to match. Ignored if `frontend.affinity` is set | `""` | +| `frontend.nodeAffinityPreset.values` | Node label values to match. Ignored if `frontend.affinity` is set | `[]` | +| `frontend.affinity` | Affinity for frontend pods assignment | `{}` | +| `frontend.nodeSelector` | Node labels for frontend pods assignment | `{}` | +| `frontend.tolerations` | Tolerations for frontend pods assignment | `[]` | +| `frontend.updateStrategy.type` | frontend deployment strategy type | `RollingUpdate` | +| `frontend.updateStrategy.type` | frontend statefulset strategy type | `RollingUpdate` | +| `frontend.podManagementPolicy` | Pod management policy for frontend statefulset | `OrderedReady` | +| `frontend.priorityClassName` | frontend pods' priorityClassName | `""` | +| `frontend.topologySpreadConstraints` | Topology Spread Constraints for frontend pod assignment spread across your cluster among failure-domains | `[]` | +| `frontend.schedulerName` | Name of the k8s scheduler (other than default) for frontend pods | `""` | +| `frontend.terminationGracePeriodSeconds` | Seconds frontend pods need to terminate gracefully | `""` | +| `frontend.lifecycleHooks` | for frontend containers to automate configuration before or after startup | `{}` | +| `frontend.extraEnvVars` | Array with extra environment variables to add to frontend containers | `[]` | +| `frontend.extraEnvVarsCM` | Name of existing ConfigMap containing extra env vars for frontend containers | `""` | +| `frontend.extraEnvVarsSecret` | Name of existing Secret containing extra env vars for frontend containers | `""` | +| `frontend.extraVolumes` | Optionally specify extra list of additional volumes for the frontend pods | `[]` | +| `frontend.extraVolumeMounts` | Optionally specify extra list of additional volumeMounts for the frontend containers | `[]` | +| `frontend.sidecars` | Add additional sidecar containers to the frontend pods | `[]` | +| `frontend.initContainers` | Add additional init containers to the frontend pods | `[]` | +| `frontend.pdb.create` | Enable/disable a Pod Disruption Budget creation | `true` | +| `frontend.pdb.minAvailable` | Minimum number/percentage of pods that should remain scheduled | `""` | +| `frontend.pdb.maxUnavailable` | Maximum number/percentage of pods that may be made unavailable. Defaults to `1` if both `frontend.pdb.minAvailable` and `frontend.pdb.maxUnavailable` are empty. | `""` | +| `frontend.autoscaling.vpa.enabled` | Enable VPA for frontend pods | `false` | +| `frontend.autoscaling.vpa.annotations` | Annotations for VPA resource | `{}` | +| `frontend.autoscaling.vpa.controlledResources` | VPA List of resources that the vertical pod autoscaler can control. Defaults to cpu and memory | `[]` | +| `frontend.autoscaling.vpa.maxAllowed` | VPA Max allowed resources for the pod | `{}` | +| `frontend.autoscaling.vpa.minAllowed` | VPA Min allowed resources for the pod | `{}` | +| `frontend.autoscaling.vpa.updatePolicy.updateMode` | Autoscaling update policy | `Auto` | +| `frontend.autoscaling.hpa.enabled` | Enable HPA for frontend pods | `false` | +| `frontend.autoscaling.hpa.minReplicas` | Minimum number of replicas | `""` | +| `frontend.autoscaling.hpa.maxReplicas` | Maximum number of replicas | `""` | +| `frontend.autoscaling.hpa.targetCPU` | Target CPU utilization percentage | `""` | +| `frontend.autoscaling.hpa.targetMemory` | Target Memory utilization percentage | `""` | +| `frontend.serviceAccount.create` | Specifies whether a ServiceAccount should be created | `true` | +| `frontend.serviceAccount.name` | The name of the ServiceAccount to use. | `""` | +| `frontend.serviceAccount.annotations` | Additional Service Account annotations (evaluated as a template) | `{}` | +| `frontend.serviceAccount.automountServiceAccountToken` | Automount service account token for the server service account | `true` | +| `frontend.networkPolicy.enabled` | Specifies whether a NetworkPolicy should be created | `true` | +| `frontend.networkPolicy.allowExternal` | Don't require server label for connections | `true` | +| `frontend.networkPolicy.allowExternalEgress` | Allow the pod to access any range of port and all destinations. | `true` | +| `frontend.networkPolicy.extraIngress` | Add extra ingress rules to the NetworkPolicy | `[]` | +| `frontend.networkPolicy.extraEgress` | Add extra ingress rules to the NetworkPolicy (ignored if allowExternalEgress=true) | `[]` | + +### orborus Parameters + +| Name | Description | Value | +| ----------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | +| `orborus.image.registry` | orborus image registry | `ghcr.io` | +| `orborus.image.repository` | orborus image repository | `shuffle/shuffle-orborus` | +| `orborus.image.digest` | orborus image digest in the way sha256:aa.... Please note this parameter, if set, will override the tag image tag (immutable tags are recommended) | `""` | +| `orborus.image.pullPolicy` | orborus image pull policy | `IfNotPresent` | +| `orborus.image.pullSecrets` | orborus image pull secrets | `[]` | +| `orborus.replicaCount` | Number of orborus replicas to deploy | `1` | +| `orborus.livenessProbe.enabled` | Enable livenessProbe on orborus containers | `false` | +| `orborus.livenessProbe.initialDelaySeconds` | Initial delay seconds for livenessProbe | `0` | +| `orborus.livenessProbe.periodSeconds` | Period seconds for livenessProbe | `15` | +| `orborus.livenessProbe.timeoutSeconds` | Timeout seconds for livenessProbe | `1` | +| `orborus.livenessProbe.failureThreshold` | Failure threshold for livenessProbe | `4` | +| `orborus.livenessProbe.successThreshold` | Success threshold for livenessProbe | `1` | +| `orborus.readinessProbe.enabled` | Enable readinessProbe on orborus containers | `false` | +| `orborus.readinessProbe.initialDelaySeconds` | Initial delay seconds for readinessProbe | `0` | +| `orborus.readinessProbe.periodSeconds` | Period seconds for readinessProbe | `5` | +| `orborus.readinessProbe.timeoutSeconds` | Timeout seconds for readinessProbe | `1` | +| `orborus.readinessProbe.failureThreshold` | Failure threshold for readinessProbe | `3` | +| `orborus.readinessProbe.successThreshold` | Success threshold for readinessProbe | `1` | +| `orborus.startupProbe.enabled` | Enable startupProbe on orborus containers | `false` | +| `orborus.startupProbe.initialDelaySeconds` | Initial delay seconds for startupProbe | `0` | +| `orborus.startupProbe.periodSeconds` | Period seconds for startupProbe | `1` | +| `orborus.startupProbe.timeoutSeconds` | Timeout seconds for startupProbe | `1` | +| `orborus.startupProbe.failureThreshold` | Failure threshold for startupProbe | `60` | +| `orborus.startupProbe.successThreshold` | Success threshold for startupProbe | `1` | +| `orborus.customLivenessProbe` | Custom livenessProbe that overrides the default one | `{}` | +| `orborus.customReadinessProbe` | Custom readinessProbe that overrides the default one | `{}` | +| `orborus.customStartupProbe` | Custom startupProbe that overrides the default one | `{}` | +| `orborus.resourcesPreset` | Set orborus container resources according to one common preset (allowed values: none, nano, small, medium, large, xlarge, 2xlarge). This is ignored if orborus.resources is set (orborus.resources is recommended for production). | `nano` | +| `orborus.resources` | Set orborus container requests and limits for different resources like CPU or memory (essential for production workloads) | `{}` | +| `orborus.podSecurityContext.enabled` | Enable orborus pods' Security Context | `true` | +| `orborus.podSecurityContext.fsGroupChangePolicy` | Set filesystem group change policy for orborus pods | `Always` | +| `orborus.podSecurityContext.sysctls` | Set kernel settings using the sysctl interface for orborus pods | `[]` | +| `orborus.podSecurityContext.supplementalGroups` | Set filesystem extra groups for orborus pods | `[]` | +| `orborus.podSecurityContext.fsGroup` | Set fsGroup in orborus pods' Security Context | `1001` | +| `orborus.containerSecurityContext.enabled` | Enabled orborus container' Security Context | `true` | +| `orborus.containerSecurityContext.seLinuxOptions` | Set SELinux options in orborus container | `{}` | +| `orborus.containerSecurityContext.runAsUser` | Set runAsUser in orborus container' Security Context | `101` | +| `orborus.containerSecurityContext.runAsGroup` | Set runAsGroup in orborus container' Security Context | `101` | +| `orborus.containerSecurityContext.runAsNonRoot` | Set runAsNonRoot in orborus container' Security Context | `true` | +| `orborus.containerSecurityContext.readOnlyRootFilesystem` | Set readOnlyRootFilesystem in orborus container' Security Context | `true` | +| `orborus.containerSecurityContext.privileged` | Set privileged in orborus container' Security Context | `false` | +| `orborus.containerSecurityContext.allowPrivilegeEscalation` | Set allowPrivilegeEscalation in orborus container' Security Context | `false` | +| `orborus.containerSecurityContext.capabilities.drop` | List of capabilities to be dropped in orborus container | `["ALL"]` | +| `orborus.containerSecurityContext.seccompProfile.type` | Set seccomp profile in orborus container | `RuntimeDefault` | +| `orborus.command` | Override default orborus container command (useful when using custom images) | `[]` | +| `orborus.args` | Override default orborus container args (useful when using custom images) | `[]` | +| `orborus.automountServiceAccountToken` | Mount Service Account token in orborus pods | `true` | +| `orborus.hostAliases` | orborus pods host aliases | `[]` | +| `orborus.daemonsetAnnotations` | Annotations for orborus daemonset | `{}` | +| `orborus.deploymentAnnotations` | Annotations for orborus deployment | `{}` | +| `orborus.statefulsetAnnotations` | Annotations for orborus statefulset | `{}` | +| `orborus.podLabels` | Extra labels for orborus pods | `{}` | +| `orborus.podAnnotations` | Annotations for orborus pods | `{}` | +| `orborus.podAffinityPreset` | Pod affinity preset. Ignored if `orborus.affinity` is set. Allowed values: `soft` or `hard` | `""` | +| `orborus.podAntiAffinityPreset` | Pod anti-affinity preset. Ignored if `orborus.affinity` is set. Allowed values: `soft` or `hard` | `soft` | +| `orborus.nodeAffinityPreset.type` | Node affinity preset type. Ignored if `orborus.affinity` is set. Allowed values: `soft` or `hard` | `""` | +| `orborus.nodeAffinityPreset.key` | Node label key to match. Ignored if `orborus.affinity` is set | `""` | +| `orborus.nodeAffinityPreset.values` | Node label values to match. Ignored if `orborus.affinity` is set | `[]` | +| `orborus.affinity` | Affinity for orborus pods assignment | `{}` | +| `orborus.nodeSelector` | Node labels for orborus pods assignment | `{}` | +| `orborus.tolerations` | Tolerations for orborus pods assignment | `[]` | +| `orborus.updateStrategy.type` | orborus deployment strategy type | `RollingUpdate` | +| `orborus.updateStrategy.type` | orborus statefulset strategy type | `RollingUpdate` | +| `orborus.podManagementPolicy` | Pod management policy for orborus statefulset | `OrderedReady` | +| `orborus.priorityClassName` | orborus pods' priorityClassName | `""` | +| `orborus.topologySpreadConstraints` | Topology Spread Constraints for orborus pod assignment spread across your cluster among failure-domains | `[]` | +| `orborus.schedulerName` | Name of the k8s scheduler (other than default) for orborus pods | `""` | +| `orborus.terminationGracePeriodSeconds` | Seconds orborus pods need to terminate gracefully | `""` | +| `orborus.lifecycleHooks` | for orborus containers to automate configuration before or after startup | `{}` | +| `orborus.extraEnvVars` | Array with extra environment variables to add to orborus containers | `[]` | +| `orborus.extraEnvVarsCM` | Name of existing ConfigMap containing extra env vars for orborus containers | `""` | +| `orborus.extraEnvVarsSecret` | Name of existing Secret containing extra env vars for orborus containers | `""` | +| `orborus.extraVolumes` | Optionally specify extra list of additional volumes for the orborus pods | `[]` | +| `orborus.extraVolumeMounts` | Optionally specify extra list of additional volumeMounts for the orborus containers | `[]` | +| `orborus.sidecars` | Add additional sidecar containers to the orborus pods | `[]` | +| `orborus.initContainers` | Add additional init containers to the orborus pods | `[]` | +| `orborus.pdb.create` | Enable/disable a Pod Disruption Budget creation | `true` | +| `orborus.pdb.minAvailable` | Minimum number/percentage of pods that should remain scheduled | `""` | +| `orborus.pdb.maxUnavailable` | Maximum number/percentage of pods that may be made unavailable. Defaults to `1` if both `orborus.pdb.minAvailable` and `orborus.pdb.maxUnavailable` are empty. | `""` | +| `orborus.autoscaling.vpa.enabled` | Enable VPA for orborus pods | `false` | +| `orborus.autoscaling.vpa.annotations` | Annotations for VPA resource | `{}` | +| `orborus.autoscaling.vpa.controlledResources` | VPA List of resources that the vertical pod autoscaler can control. Defaults to cpu and memory | `[]` | +| `orborus.autoscaling.vpa.maxAllowed` | VPA Max allowed resources for the pod | `{}` | +| `orborus.autoscaling.vpa.minAllowed` | VPA Min allowed resources for the pod | `{}` | +| `orborus.autoscaling.vpa.updatePolicy.updateMode` | Autoscaling update policy | `Auto` | +| `orborus.autoscaling.hpa.enabled` | Enable HPA for orborus pods | `false` | +| `orborus.autoscaling.hpa.minReplicas` | Minimum number of replicas | `""` | +| `orborus.autoscaling.hpa.maxReplicas` | Maximum number of replicas | `""` | +| `orborus.autoscaling.hpa.targetCPU` | Target CPU utilization percentage | `""` | +| `orborus.autoscaling.hpa.targetMemory` | Target Memory utilization percentage | `""` | +| `orborus.serviceAccount.create` | Specifies whether a ServiceAccount should be created | `true` | +| `orborus.serviceAccount.name` | The name of the ServiceAccount to use. | `""` | +| `orborus.serviceAccount.annotations` | Additional Service Account annotations (evaluated as a template) | `{}` | +| `orborus.serviceAccount.automountServiceAccountToken` | Automount service account token for the server service account | `true` | +| `orborus.rbac.create` | Specifies whether RBAC resources should be created | `true` | +| `orborus.networkPolicy.enabled` | Specifies whether a NetworkPolicy should be created | `true` | +| `orborus.networkPolicy.allowExternal` | Don't require server label for connections | `true` | +| `orborus.networkPolicy.allowExternalEgress` | Allow the pod to access any range of port and all destinations. | `true` | +| `orborus.networkPolicy.extraIngress` | Add extra ingress rules to the NetworkPolicy | `[]` | +| `orborus.networkPolicy.extraEgress` | Add extra ingress rules to the NetworkPolicy (ignored if allowExternalEgress=true) | `[]` | + +### worker Parameters + +| Name | Description | Value | +| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ | +| `worker.image.registry` | worker image registry | `ghcr.io` | +| `worker.image.repository` | worker image repository | `shuffle/shuffle-worker` | +| `worker.image.digest` | worker image digest in the way sha256:aa.... Please note this parameter, if set, will override the tag image tag (immutable tags are recommended) | `""` | +| `worker.serviceAccount.create` | Specifies whether a ServiceAccount should be created | `true` | +| `worker.serviceAccount.name` | The name of the ServiceAccount to use. | `""` | +| `worker.serviceAccount.annotations` | Additional Service Account annotations (evaluated as a template) | `{}` | +| `worker.serviceAccount.automountServiceAccountToken` | Automount service account token for the server service account | `true` | +| `worker.rbac.create` | Specifies whether RBAC resources should be created | `true` | +| `worker.networkPolicy.enabled` | Specifies whether a NetworkPolicy should be created | `true` | +| `worker.networkPolicy.allowExternal` | Don't require server label for connections | `true` | +| `worker.networkPolicy.allowExternalEgress` | Allow the pod to access any range of port and all destinations. | `true` | +| `worker.networkPolicy.extraIngress` | Add extra ingress rules to the NetworkPolicy | `[]` | +| `worker.networkPolicy.extraEgress` | Add extra ingress rules to the NetworkPolicy (ignored if allowExternalEgress=true) | `[]` | + +### app Parameters + +| Name | Description | Value | +| ------------------------------------------------- | ---------------------------------------------------------------------------------- | ------ | +| `app.serviceAccount.create` | Specifies whether a ServiceAccount should be created | `true` | +| `app.serviceAccount.name` | The name of the ServiceAccount to use. | `""` | +| `app.serviceAccount.annotations` | Additional Service Account annotations (evaluated as a template) | `{}` | +| `app.serviceAccount.automountServiceAccountToken` | Automount service account token for the server service account | `true` | +| `app.rbac.create` | Specifies whether RBAC resources should be created | `true` | +| `app.networkPolicy.enabled` | Specifies whether a NetworkPolicy should be created | `true` | +| `app.networkPolicy.allowExternal` | Don't require server label for connections | `true` | +| `app.networkPolicy.allowExternalEgress` | Allow the pod to access any range of port and all destinations. | `true` | +| `app.networkPolicy.extraIngress` | Add extra ingress rules to the NetworkPolicy | `[]` | +| `app.networkPolicy.extraEgress` | Add extra ingress rules to the NetworkPolicy (ignored if allowExternalEgress=true) | `[]` | + +### Traffic Exposure Parameters + +| Name | Description | Value | +| -------------------------- | ----------------------------------------------------------------------------------------------------- | --------------- | +| `ingress.enabled` | Enable ingress record generation for frontend and backend | `false` | +| `ingress.pathType` | Ingress path type for the frontend path | `Prefix` | +| `ingress.backendPathType` | Ingress path type for the backend path | `Prefix` | +| `ingress.apiVersion` | Force Ingress API version (automatically detected if not set) | `""` | +| `ingress.hostname` | Default host for the ingress record | `shuffle.local` | +| `ingress.ingressClassName` | IngressClass that will be be used to implement the Ingress (Kubernetes 1.18+) | `nginx` | +| `ingress.path` | Ingress path for Shuffle frontend | `"/"` | +| `ingress.backendPath` | Ingress path for Shuffle backend | `"/api/"` | +| `ingress.annotations` | Additional annotations for the Ingress resource. | `{}` | +| `ingress.tls` | Enable TLS configuration for the host defined at `ingress.hostname` parameter | `false` | +| `ingress.selfSigned` | Create a TLS secret for this ingress record using self-signed certificates generated by Helm | `false` | +| `ingress.extraHosts` | An array with additional hostname(s) to be covered with the ingress record | `[]` | +| `ingress.extraPaths` | An array with additional arbitrary paths that may need to be added to the ingress under the main host | `[]` | +| `ingress.extraTls` | TLS configuration for additional hostname(s) to be covered with this ingress record | `[]` | +| `ingress.secrets` | Custom TLS certificates as secrets | `[]` | +| `ingress.extraRules` | Additional rules to be covered with this ingress record | `[]` | + +### Istio Parameters + +| Name | Description | Value | +| --------------------------------------- | ------------------------------------------------------------------------------- | ------------------------ | +| `istio.enabled` | Enable creation of an Istio Gateway and VirtualService for frontend and backend | `false` | +| `istio.apiVersion` | The istio apiVersion to use for Gateway and VirtualService resources | `networking.istio.io/v1` | +| `istio.hosts` | One or more hosts exposed by Istio | `[]` | +| `istio.gateway.annotations` | Additional annotations for the Gateway resource | `{}` | +| `istio.gateway.selector` | The selector matches the ingress gateway pod labels | `{ istio: ingress }` | +| `istio.gateway.http.enabled` | Enable HTTP server port 80 | `true` | +| `istio.gateway.http.httpsRedirect` | If set to true, a 301 redirect is send for all HTTP connections | `false` | +| `istio.gateway.https.enabled` | Enable HTTPS server on port 443 | `false` | +| `istio.gateway.https.tlsCredentialName` | The name of the secret that holds the TLS certs including the CA certificates. | `""` | +| `istio.gateway.extraServers` | Additional servers for the Gateway resource | `[]` | +| `istio.virtualService.annotations` | Additional annotations for the VirtualService resource. | `{}` | + +### Persistence Parameters + +| Name | Description | Value | +| ------------------------------------- | ------------------------------------------------- | ------------------- | +| `persistence.enabled` | Enable persistence using Persistent Volume Claims | `true` | +| `persistence.apps.existingClaim` | Name of an existing PVC to use | `""` | +| `persistence.apps.storageClass` | PVC Storage Class for shuffle-apps volume | `""` | +| `persistence.apps.subPath` | The sub path used in the volume | `""` | +| `persistence.apps.accessModes` | The access mode of the volume | `["ReadWriteOnce"]` | +| `persistence.apps.size` | The size of the volume | `5Gi` | +| `persistence.apps.annotations` | Annotations for the PVC | `{}` | +| `persistence.apps.selector` | Selector to match an existing Persistent Volume | `{}` | +| `persistence.appBuilder.storageClass` | PVC Storage Class for backend-apps-claim volume | `""` | +| `persistence.appBuilder.accessModes` | The access mode of the volume | `["ReadWriteOnce"]` | +| `persistence.appBuilder.size` | The size of the volume | `5Gi` | +| `persistence.appBuilder.annotations` | Annotations for the PVC | `{}` | +| `persistence.appBuilder.selector` | Selector to match an existing Persistent Volume | `{}` | +| `persistence.files.existingClaim` | Name of an existing PVC to use | `""` | +| `persistence.files.storageClass` | PVC Storage Class for shuffle-files volume | `""` | +| `persistence.files.subPath` | The sub path used in the volume | `""` | +| `persistence.files.accessModes` | The access mode of the volume | `["ReadWriteOnce"]` | +| `persistence.files.size` | The size of the volume | `5Gi` | +| `persistence.files.annotations` | Annotations for the PVC | `{}` | +| `persistence.files.selector` | Selector to match an existing Persistent Volume | `{}` | + +### Init Container Parameters + +| Name | Description | Value | +| ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | +| `volumePermissions.enabled` | Enable init container that changes the owner/group of the PV mount point to `runAsUser:fsGroup` | `false` | +| `volumePermissions.image.registry` | OS Shell + Utility image registry | `docker.io` | +| `volumePermissions.image.repository` | OS Shell + Utility image repository | `bitnami/os-shell` | +| `volumePermissions.image.pullPolicy` | OS Shell + Utility image pull policy | `IfNotPresent` | +| `volumePermissions.image.pullSecrets` | OS Shell + Utility image pull secrets | `[]` | +| `volumePermissions.resourcesPreset` | Set init container resources according to one common preset (allowed values: none, nano, small, medium, large, xlarge, 2xlarge). This is ignored if volumePermissions.resources is set (volumePermissions.resources is recommended for production). | `nano` | +| `volumePermissions.resources` | Set init container requests and limits for different resources like CPU or memory (essential for production workloads) | `{}` | +| `volumePermissions.containerSecurityContext.enabled` | Enabled init container' Security Context | `true` | +| `volumePermissions.containerSecurityContext.seLinuxOptions` | Set SELinux options in init container | `{}` | +| `volumePermissions.containerSecurityContext.runAsUser` | Set init container's Security Context runAsUser | `0` | + +### OpenSearch Parameters + +| Name | Description | Value | +| -------------------- | ----------------------------------------------------- | ------ | +| `opensearch.enabled` | Switch to enable or disable the opensearch helm chart | `true` | + +### Vault Parameters + +| Name | Description | Value | +| --------------- | -------------------------------------------------------------------------- | ----- | +| `vault.role` | Specify the Vault role, which should be used to get the secret from Vault. | `""` | +| `vault.secrets` | A list of VaultSecrets to create | `[]` | + +### Other Parameters + diff --git a/charts/shuffle/templates/NOTES.txt b/charts/shuffle/templates/NOTES.txt new file mode 100644 index 00000000..1091d443 --- /dev/null +++ b/charts/shuffle/templates/NOTES.txt @@ -0,0 +1,25 @@ +CHART NAME: {{ .Chart.Name }} +CHART VERSION: {{ .Chart.Version }} +APP VERSION: {{ .Chart.AppVersion }} + +** Please be patient while the chart is being deployed ** + +{{- if .Values.diagnosticMode.enabled }} +The chart has been deployed in diagnostic mode. All probes have been disabled and the command has been overwritten with: + + command: {{- include "common.tplvalues.render" (dict "value" .Values.diagnosticMode.command "context" $) | nindent 4 }} + args: {{- include "common.tplvalues.render" (dict "value" .Values.diagnosticMode.args "context" $) | nindent 4 }} + +Get the list of pods by executing: + + kubectl get pods --namespace {{ include "common.names.namespace" . | quote }} -l app.kubernetes.io/instance={{ .Release.Name }} + +Access the pod you want to debug by executing + + kubectl exec --namespace {{ include "common.names.namespace" . | quote }} -ti -- bash + +{{- end }} + +TODO + +{{- include "common.warnings.rollingTag" .Values.backend.image }} diff --git a/charts/shuffle/templates/_helpers.tpl b/charts/shuffle/templates/_helpers.tpl new file mode 100644 index 00000000..0e90cbe9 --- /dev/null +++ b/charts/shuffle/templates/_helpers.tpl @@ -0,0 +1,215 @@ +{{/* +Return the proper Docker Image Registry Secret Names +*/}} +{{- define "shuffle.imagePullSecrets" -}} +{{- include "common.images.renderPullSecrets" (dict "images" (list .Values.backend.image .Values.frontend.image .Values.orborus.image .Values.worker.image .Values.volumePermissions.image) "context" $) -}} +{{- end -}} + +{{/* +Return the common name for backend componentes +*/}} +{{- define "shuffle.backend.name" -}} + {{- printf "%s-backend" (include "common.names.fullname" .) | trunc 63 -}} +{{- end -}} + +{{/* +Return the common name for frontend components +*/}} +{{- define "shuffle.frontend.name" -}} + {{- printf "%s-frontend" (include "common.names.fullname" .) | trunc 63 -}} +{{- end -}} + +{{/* +Return the common name for orborus components +*/}} +{{- define "shuffle.orborus.name" -}} + {{- printf "%s-orborus" (include "common.names.fullname" .) | trunc 63 -}} +{{- end -}} + +{{/* +Return the common name for worker components +*/}} +{{- define "shuffle.worker.name" -}} + {{- printf "%s-worker" (include "common.names.fullname" .) | trunc 63 -}} +{{- end -}} + +{{/* +Return the common name for app components +*/}} +{{- define "shuffle.app.name" -}} + {{- printf "%s-app" (include "common.names.fullname" .) | trunc 63 -}} +{{- end -}} + +{{/* +Return the common labels for backend components +The shuffle app builder requires the io.kompose.service=backend label to be set on the backend pod. +*/}} +{{- define "shuffle.backend.labels" -}} +{{- include "common.labels.standard" . }} +app.kubernetes.io/component: backend +io.kompose.service: backend +{{- end -}} + +{{/* +Return the common labels for frontend components +*/}} +{{- define "shuffle.frontend.labels" -}} +{{- include "common.labels.standard" . }} +app.kubernetes.io/component: frontend +{{- end -}} + +{{/* +Return the common labels for orborus components +*/}} +{{- define "shuffle.orborus.labels" -}} +{{- include "common.labels.standard" . }} +app.kubernetes.io/component: orborus +{{- end -}} + +{{/* +Return the common labels for worker components +*/}} +{{- define "shuffle.worker.labels" -}} +{{- include "common.labels.standard" . }} +app.kubernetes.io/component: worker +{{- end -}} + +{{/* +Return the common labels for app components +*/}} +{{- define "shuffle.app.labels" -}} +{{- include "common.labels.standard" . }} +app.kubernetes.io/component: app +{{- end -}} + +{{/* +Return the match labels for backend components +*/}} +{{- define "shuffle.backend.matchLabels" -}} +{{- include "common.labels.matchLabels" . }} +app.kubernetes.io/component: backend +{{- end -}} + +{{/* +Return the match labels for frontend components +*/}} +{{- define "shuffle.frontend.matchLabels" -}} +{{- include "common.labels.matchLabels" . }} +app.kubernetes.io/component: frontend +{{- end -}} + +{{/* +Return the match labels for orborus components +*/}} +{{- define "shuffle.orborus.matchLabels" -}} +{{- include "common.labels.matchLabels" . }} +app.kubernetes.io/component: orborus +{{- end -}} + +{{/* +Return the match labels for worker components +NOTE: This does not match the labels from shuffle.worker.labels, but the labels set by the orborus GoLang app. +*/}} +{{- define "shuffle.worker.matchLabels" -}} +container: shuffle-worker +{{- end -}} + +{{/* +Return the match labels for app components +NOTE: This does not match the labels from shuffle.worker.labels, but the labels set by the orborus GoLang app. +*/}} +{{- define "shuffle.app.matchLabels" -}} +app: shuffle-app +{{- end -}} + +{{/* +Return the proper image name (for the init container volume-permissions image) +*/}} +{{- define "shuffle.volumePermissions.image" -}} +{{- include "common.images.image" ( dict "imageRoot" .Values.volumePermissions.image "global" .Values.global ) -}} +{{- end -}} + +{{/* +Return the proper Shuffle backend image name +*/}} +{{- define "shuffle.backend.image" -}} +{{- include "common.images.image" ( dict "imageRoot" .Values.backend.image "global" .Values.global ) -}} +{{- end -}} + +{{/* +Return the proper Shuffle frontend image name +*/}} +{{- define "shuffle.frontend.image" -}} +{{- include "common.images.image" ( dict "imageRoot" .Values.frontend.image "global" .Values.global ) -}} +{{- end -}} + +{{/* +Return the proper Shuffle orborus image name +*/}} +{{- define "shuffle.orborus.image" -}} +{{- include "common.images.image" ( dict "imageRoot" .Values.orborus.image "global" .Values.global ) -}} +{{- end -}} + +{{/* +Return the proper Shuffle worker image name +*/}} +{{- define "shuffle.worker.image" -}} +{{- include "common.images.image" ( dict "imageRoot" .Values.worker.image "global" .Values.global ) -}} +{{- end -}} + +{{/* +Create the name of the service account to use for the Shuffle backend +*/}} +{{- define "shuffle.backend.serviceAccountName" -}} +{{- if .Values.backend.serviceAccount.create -}} + {{ default (include "shuffle.backend.name" .) .Values.backend.serviceAccount.name | trunc 63 | trimSuffix "-" }} +{{- else -}} + {{ default "default" .Values.backend.serviceAccount.name }} +{{- end -}} +{{- end -}} + +{{/* +Create the name of the service account to use for the Shuffle frontend +*/}} +{{- define "shuffle.frontend.serviceAccountName" -}} +{{- if .Values.frontend.serviceAccount.create -}} + {{ default (include "shuffle.frontend.name" .) .Values.frontend.serviceAccount.name | trunc 63 | trimSuffix "-" }} +{{- else -}} + {{ default "default" .Values.frontend.serviceAccount.name }} +{{- end -}} +{{- end -}} + +{{/* +Create the name of the service account to use for Shuffle orborus +*/}} +{{- define "shuffle.orborus.serviceAccountName" -}} +{{- if .Values.orborus.serviceAccount.create -}} + {{ default (include "shuffle.orborus.name" .) .Values.orborus.serviceAccount.name | trunc 63 | trimSuffix "-" }} +{{- else -}} + {{ default "default" .Values.orborus.serviceAccount.name }} +{{- end -}} +{{- end -}} + +{{/* +Create the name of the service account to use for Shuffle workers +*/}} +{{- define "shuffle.worker.serviceAccountName" -}} +{{- if .Values.worker.serviceAccount.create -}} + {{ default (include "shuffle.worker.name" .) .Values.worker.serviceAccount.name | trunc 63 | trimSuffix "-" }} +{{- else -}} + {{ default "default" .Values.worker.serviceAccount.name }} +{{- end -}} +{{- end -}} + +{{/* +Create the name of the service account to use for Shuffle apps +*/}} +{{- define "shuffle.app.serviceAccountName" -}} +{{- if .Values.app.serviceAccount.create -}} + {{ default (include "shuffle.app.name" .) .Values.app.serviceAccount.name | trunc 63 | trimSuffix "-" }} +{{- else -}} + {{ default "default" .Values.app.serviceAccount.name }} +{{- end -}} +{{- end -}} + + diff --git a/charts/shuffle/templates/backend/backend-apps-claim-pvc.yaml b/charts/shuffle/templates/backend/backend-apps-claim-pvc.yaml new file mode 100644 index 00000000..e0e8ef7d --- /dev/null +++ b/charts/shuffle/templates/backend/backend-apps-claim-pvc.yaml @@ -0,0 +1,28 @@ +# This PVC is always enabled, regardless of .Values.persistence.enabled, +# as app building does not work without it. +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: backend-apps-claim # Hardcoded by shuffle-app-builder + namespace: {{ include "common.names.namespace" . | quote }} + labels: {{- include "shuffle.backend.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }} + annotations: + {{- if eq .Values.persistence.resourcePolicy "keep" }} + helm.sh/resource-policy: keep + {{- end }} + {{- if or .Values.persistence.appBuilder.annotations .Values.commonAnnotations }} + {{- $annotations := include "common.tplvalues.merge" ( dict "values" ( list .Values.persistence.appBuilder.annotations .Values.commonAnnotations ) "context" . ) }} + {{- include "common.tplvalues.render" ( dict "value" $annotations "context" $) | nindent 4 }} + {{- end }} +spec: + accessModes: + {{- range .Values.persistence.appBuilder.accessModes }} + - {{ . | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.persistence.appBuilder.size }} + {{- if .Values.persistence.appBuilder.selector }} + selector: {{- include "common.tplvalues.render" (dict "value" .Values.persistence.appBuilder.selector "context" $) | nindent 2 }} + {{- end }} + {{- include "common.storage.class" ( dict "persistence" .Values.persistence.appBuilder "global" .Values.global ) | nindent 2 }} diff --git a/charts/shuffle/templates/backend/backend-apps-pvc.yaml b/charts/shuffle/templates/backend/backend-apps-pvc.yaml new file mode 100644 index 00000000..b26dfdf3 --- /dev/null +++ b/charts/shuffle/templates/backend/backend-apps-pvc.yaml @@ -0,0 +1,30 @@ +{{- if .Values.persistence.enabled }} +{{- if (not .Values.persistence.apps.existingClaim) }} +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: {{ printf "%s-apps" (include "shuffle.backend.name" .) }} + namespace: {{ include "common.names.namespace" . | quote }} + labels: {{- include "shuffle.backend.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }} + annotations: + {{- if eq .Values.persistence.resourcePolicy "keep" }} + helm.sh/resource-policy: keep + {{- end }} + {{- if or .Values.persistence.apps.annotations .Values.commonAnnotations }} + {{- $annotations := include "common.tplvalues.merge" ( dict "values" ( list .Values.persistence.apps.annotations .Values.commonAnnotations ) "context" . ) }} + {{- include "common.tplvalues.render" ( dict "value" $annotations "context" $) | nindent 4 }} + {{- end }} +spec: + accessModes: + {{- range .Values.persistence.apps.accessModes }} + - {{ . | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.persistence.apps.size }} + {{- if .Values.persistence.apps.selector }} + selector: {{- include "common.tplvalues.render" (dict "value" .Values.persistence.apps.selector "context" $) | nindent 2 }} + {{- end }} + {{- include "common.storage.class" ( dict "persistence" .Values.persistence.apps "global" .Values.global ) | nindent 2 }} +{{- end }} +{{- end }} diff --git a/charts/shuffle/templates/backend/backend-cm-env.yaml b/charts/shuffle/templates/backend/backend-cm-env.yaml new file mode 100644 index 00000000..e7138dc0 --- /dev/null +++ b/charts/shuffle/templates/backend/backend-cm-env.yaml @@ -0,0 +1,30 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "shuffle.backend.name" . }}-env + namespace: {{ include "common.names.namespace" . | quote }} + labels: {{- include "shuffle.backend.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- if .Values.commonAnnotations }} + annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +data: + BACKEND_PORT: "5001" + {{- if .Values.shuffle.baseUrl }} + BASE_URL: "{{ .Values.shuffle.baseUrl }}" + SSO_REDIRECT_URL: "{{ .Values.shuffle.baseUrl }}" + {{- else }} + BASE_URL: "http://{{ include "shuffle.backend.name" . }}:5001" + {{- end }} + ORG_ID: "{{ .Values.shuffle.org }}" + SHUFFLE_APP_DOWNLOAD_LOCATION: "{{ .Values.backend.apps.downloadLocation }}" + SHUFFLE_DOWNLOAD_AUTH_BRANCH: "{{ .Values.backend.apps.downloadBranch }}" + SHUFFLE_APP_FORCE_UPDATE: "{{ .Values.backend.apps.forceUpdate }}" + SHUFFLE_CHAT_DISABLED: "true" + SHUFFLE_OPENSEARCH_URL: {{ include "common.tplvalues.render" (dict "value" .Values.backend.openSearch.url "context" $) }} + SHUFFLE_OPENSEARCH_USERNAME: "{{ .Values.backend.openSearch.username }}" + SHUFFLE_OPENSEARCH_CERTIFICATE_FILE: "{{ .Values.backend.openSearch.certificateFile }}" + SHUFFLE_OPENSEARCH_SKIPSSL_VERIFY: "{{ .Values.backend.openSearch.skipSSLVerify }}" + SHUFFLE_OPENSEARCH_INDEX_PREFIX: "{{ .Values.backend.openSearch.indexPrefix }}" + SHUFFLE_RERUN_SCHEDULE: "{{ .Values.backend.cleanupSchedule }}" + TZ: "{{ .Values.shuffle.timezone }}" + REGISTRY_URL: "{{ .Values.shuffle.appRegistry }}" diff --git a/charts/shuffle/templates/backend/backend-dpl.yaml b/charts/shuffle/templates/backend/backend-dpl.yaml new file mode 100644 index 00000000..8a4cfc36 --- /dev/null +++ b/charts/shuffle/templates/backend/backend-dpl.yaml @@ -0,0 +1,221 @@ +apiVersion: {{ include "common.capabilities.deployment.apiVersion" . }} +kind: Deployment +metadata: + name: {{ template "shuffle.backend.name" . }} + namespace: {{ include "common.names.namespace" . | quote }} + labels: {{- include "shuffle.backend.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- if or .Values.backend.deploymentAnnotations .Values.commonAnnotations }} + {{- $annotations := include "common.tplvalues.merge" (dict "values" (list .Values.backend.deploymentAnnotations .Values.commonAnnotations) "context" .) }} + annotations: {{- include "common.tplvalues.render" ( dict "value" $annotations "context" $ ) | nindent 4 }} + {{- end }} +spec: + {{- if not .Values.backend.autoscaling.hpa.enabled }} + replicas: {{ .Values.backend.replicaCount }} + {{- end }} + {{- if .Values.backend.updateStrategy }} + strategy: {{- toYaml .Values.backend.updateStrategy | nindent 4 }} + {{- end }} + {{- $podLabels := include "common.tplvalues.merge" (dict "values" (list .Values.backend.podLabels .Values.commonLabels) "context" .) }} + selector: + matchLabels: {{- include "shuffle.backend.matchLabels" ( dict "customLabels" $podLabels "context" $ ) | nindent 6 }} + template: + metadata: + {{- if .Values.backend.podAnnotations }} + annotations: {{- include "common.tplvalues.render" (dict "value" .Values.backend.podAnnotations "context" $) | nindent 8 }} + {{- end }} + labels: {{- include "shuffle.backend.labels" ( dict "customLabels" $podLabels "context" $ ) | nindent 8 }} + spec: + {{- include "shuffle.imagePullSecrets" . | nindent 6 }} + serviceAccountName: {{ template "shuffle.backend.serviceAccountName" . }} + automountServiceAccountToken: {{ .Values.backend.automountServiceAccountToken }} + {{- if .Values.backend.hostAliases }} + hostAliases: {{- include "common.tplvalues.render" (dict "value" .Values.backend.hostAliases "context" $) | nindent 8 }} + {{- end }} + {{- if .Values.backend.affinity }} + affinity: {{- include "common.tplvalues.render" ( dict "value" .Values.backend.affinity "context" $) | nindent 8 }} + {{- else }} + affinity: + podAffinity: {{- include "common.affinities.pods" (dict "type" .Values.backend.podAffinityPreset "component" "backend" "customLabels" $podLabels "context" $) | nindent 10 }} + podAntiAffinity: {{- include "common.affinities.pods" (dict "type" .Values.backend.podAntiAffinityPreset "component" "backend" "customLabels" $podLabels "context" $) | nindent 10 }} + nodeAffinity: {{- include "common.affinities.nodes" (dict "type" .Values.backend.nodeAffinityPreset.type "key" .Values.backend.nodeAffinityPreset.key "values" .Values.backend.nodeAffinityPreset.values) | nindent 10 }} + {{- end }} + {{- if .Values.backend.nodeSelector }} + nodeSelector: {{- include "common.tplvalues.render" ( dict "value" .Values.backend.nodeSelector "context" $) | nindent 8 }} + {{- end }} + {{- if .Values.backend.tolerations }} + tolerations: {{- include "common.tplvalues.render" (dict "value" .Values.backend.tolerations "context" .) | nindent 8 }} + {{- end }} + {{- if .Values.backend.priorityClassName }} + priorityClassName: {{ .Values.backend.priorityClassName | quote }} + {{- end }} + {{- if .Values.backend.schedulerName }} + schedulerName: {{ .Values.backend.schedulerName | quote }} + {{- end }} + {{- if .Values.backend.topologySpreadConstraints }} + topologySpreadConstraints: {{- include "common.tplvalues.render" (dict "value" .Values.backend.topologySpreadConstraints "context" .) | nindent 8 }} + {{- end }} + {{- if .Values.backend.podSecurityContext.enabled }} + securityContext: {{- omit .Values.backend.podSecurityContext "enabled" | toYaml | nindent 8 }} + {{- end }} + {{- if .Values.backend.terminationGracePeriodSeconds }} + terminationGracePeriodSeconds: {{ .Values.backend.terminationGracePeriodSeconds }} + {{- end }} + initContainers: + {{- if and .Values.volumePermissions.enabled .Values.persistence.enabled }} + - name: volume-permissions + image: {{ include "shuffle.volumePermissions.image" . }} + imagePullPolicy: {{ .Values.volumePermissions.image.pullPolicy | quote }} + command: + - /bin/bash + - -ec + - | + chown -vR {{ .Values.backend.containerSecurityContext.runAsUser }}:{{ .Values.backend.podSecurityContext.fsGroup }} /app/generated && \ + chown -vR {{ .Values.backend.containerSecurityContext.runAsUser }}:{{ .Values.backend.podSecurityContext.fsGroup }} /shuffle-apps && \ + chown -vR {{ .Values.backend.containerSecurityContext.runAsUser }}:{{ .Values.backend.podSecurityContext.fsGroup }} /shuffle-files + {{- if .Values.volumePermissions.containerSecurityContext.enabled }} + securityContext: {{- include "common.compatibility.renderSecurityContext" (dict "secContext" .Values.volumePermissions.containerSecurityContext "context" $) | nindent 12 }} + {{- end }} + {{- if .Values.volumePermissions.resources }} + resources: {{- toYaml .Values.volumePermissions.resources | nindent 12 }} + {{- else if ne .Values.volumePermissions.resourcesPreset "none" }} + resources: {{- include "common.resources.preset" (dict "type" .Values.volumePermissions.resourcesPreset) | nindent 12 }} + {{- end }} + volumeMounts: + - name: shuffle-app-builder + mountPath: /app/generated + - name: shuffle-apps + mountPath: /shuffle-apps + {{- if .Values.persistence.apps.subPath }} + subPath: {{ .Values.persistence.apps.subPath }} + {{- end }} + - name: shuffle-files + mountPath: /shuffle-files + {{- if .Values.persistence.files.subPath }} + subPath: {{ .Values.persistence.apps.subPath }} + {{- end }} + {{- end }} + {{- if .Values.backend.initContainers }} + {{- include "common.tplvalues.render" (dict "value" .Values.backend.initContainers "context" $) | nindent 8 }} + {{- end }} + containers: + - name: backend + image: {{ template "shuffle.backend.image" . }} + imagePullPolicy: {{ .Values.backend.image.pullPolicy }} + {{- if .Values.backend.containerSecurityContext.enabled }} + securityContext: {{- include "common.compatibility.renderSecurityContext" (dict "secContext" .Values.backend.containerSecurityContext "context" $) | nindent 12 }} + {{- end }} + {{- if .Values.diagnosticMode.enabled }} + command: {{- include "common.tplvalues.render" (dict "value" .Values.diagnosticMode.command "context" $) | nindent 12 }} + {{- else if .Values.backend.command }} + command: {{- include "common.tplvalues.render" (dict "value" .Values.backend.command "context" $) | nindent 12 }} + {{- end }} + {{- if .Values.diagnosticMode.enabled }} + args: {{- include "common.tplvalues.render" (dict "value" .Values.diagnosticMode.args "context" $) | nindent 12 }} + {{- else if .Values.backend.args }} + args: {{- include "common.tplvalues.render" (dict "value" .Values.backend.args "context" $) | nindent 12 }} + {{- end }} + env: + - name: RUNNING_MODE + value: kubernetes + - name: IS_KUBERNETES + value: "true" + - name: SHUFFLE_APP_HOTLOAD_FOLDER + value: /shuffle-apps + - name: SHUFFLE_FILE_LOCATION + value: /shuffle-files + {{- if .Values.backend.extraEnvVars }} + {{- include "common.tplvalues.render" (dict "value" .Values.backend.extraEnvVars "context" $) | nindent 12 }} + {{- end }} + envFrom: + - configMapRef: + name: {{ include "shuffle.backend.name" . }}-env + {{- if .Values.backend.extraEnvVarsCM }} + - configMapRef: + name: {{ include "common.tplvalues.render" (dict "value" .Values.backend.extraEnvVarsCM "context" $) }} + {{- end }} + {{- if .Values.backend.extraEnvVarsSecret }} + - secretRef: + name: {{ include "common.tplvalues.render" (dict "value" .Values.backend.extraEnvVarsSecret "context" $) }} + {{- end }} + {{- if .Values.backend.resources }} + resources: {{- toYaml .Values.backend.resources | nindent 12 }} + {{- else if ne .Values.backend.resourcesPreset "none" }} + resources: {{- include "common.resources.preset" (dict "type" .Values.backend.resourcesPreset) | nindent 12 }} + {{- end }} + ports: + - name: http + containerPort: 5001 + {{- if not .Values.diagnosticMode.enabled }} + {{- if .Values.backend.customLivenessProbe }} + livenessProbe: {{- include "common.tplvalues.render" (dict "value" .Values.backend.customLivenessProbe "context" $) | nindent 12 }} + {{- else if .Values.backend.livenessProbe.enabled }} + livenessProbe: {{- include "common.tplvalues.render" (dict "value" (omit .Values.backend.livenessProbe "enabled") "context" $) | nindent 12 }} + httpGet: + path: /api/v1/health + port: 5001 + {{- end }} + {{- if .Values.backend.customReadinessProbe }} + readinessProbe: {{- include "common.tplvalues.render" (dict "value" .Values.backend.customReadinessProbe "context" $) | nindent 12 }} + {{- else if .Values.backend.readinessProbe.enabled }} + readinessProbe: {{- include "common.tplvalues.render" (dict "value" (omit .Values.backend.readinessProbe "enabled") "context" $) | nindent 12 }} + httpGet: + path: /api/v1/health + port: 5001 + {{- end }} + {{- if .Values.backend.customStartupProbe }} + startupProbe: {{- include "common.tplvalues.render" (dict "value" .Values.backend.customStartupProbe "context" $) | nindent 12 }} + {{- else if .Values.backend.startupProbe.enabled }} + startupProbe: {{- include "common.tplvalues.render" (dict "value" (omit .Values.backend.startupProbe "enabled") "context" $) | nindent 12 }} + httpGet: + path: /api/v1/health + port: 5001 + {{- end }} + {{- end }} + {{- if .Values.backend.lifecycleHooks }} + lifecycle: {{- include "common.tplvalues.render" (dict "value" .Values.backend.lifecycleHooks "context" $) | nindent 12 }} + {{- end }} + volumeMounts: + - name: shuffle-app-builder + mountPath: /app/generated + - name: shuffle-apps + mountPath: /shuffle-apps + {{- if .Values.persistence.apps.subPath }} + subPath: {{ .Values.persistence.apps.subPath }} + {{- end }} + - name: shuffle-files + mountPath: /shuffle-files + {{- if .Values.persistence.files.subPath }} + subPath: {{ .Values.persistence.apps.subPath }} + {{- end }} + - name: empty-dir + mountPath: /tmp + subPath: tmp-dir + {{- if .Values.backend.extraVolumeMounts }} + {{- include "common.tplvalues.render" (dict "value" .Values.backend.extraVolumeMounts "context" $) | nindent 12 }} + {{- end }} + {{- if .Values.backend.sidecars }} + {{- include "common.tplvalues.render" ( dict "value" .Values.backend.sidecars "context" $) | nindent 8 }} + {{- end }} + volumes: + - name: empty-dir + emptyDir: {} + - name: shuffle-app-builder + persistentVolumeClaim: + claimName: backend-apps-claim + - name: shuffle-apps + {{- if .Values.persistence.enabled }} + persistentVolumeClaim: + claimName: {{ default (printf "%s-apps" (include "shuffle.backend.name" .)) .Values.persistence.apps.existingClaim }} + {{- else }} + emptyDir: {} + {{- end }} + - name: shuffle-files + {{- if .Values.persistence.enabled }} + persistentVolumeClaim: + claimName: {{ default (printf "%s-files" (include "shuffle.backend.name" .)) .Values.persistence.apps.existingClaim }} + {{- else }} + emptyDir: {} + {{- end }} + {{- if .Values.backend.extraVolumes }} + {{- include "common.tplvalues.render" (dict "value" .Values.backend.extraVolumes "context" $) | nindent 8 }} + {{- end }} diff --git a/charts/shuffle/templates/backend/backend-files-pvc.yaml b/charts/shuffle/templates/backend/backend-files-pvc.yaml new file mode 100644 index 00000000..86faac82 --- /dev/null +++ b/charts/shuffle/templates/backend/backend-files-pvc.yaml @@ -0,0 +1,30 @@ +{{- if .Values.persistence.enabled }} +{{- if (not .Values.persistence.files.existingClaim) }} +kind: PersistentVolumeClaim +apiVersion: v1 +metadata: + name: {{ printf "%s-files" (include "shuffle.backend.name" .) }} + namespace: {{ include "common.names.namespace" . | quote }} + labels: {{- include "shuffle.backend.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }} + annotations: + {{- if eq .Values.persistence.resourcePolicy "keep" }} + helm.sh/resource-policy: keep + {{- end }} + {{- if or .Values.persistence.files.annotations .Values.commonAnnotations }} + {{- $annotations := include "common.tplvalues.merge" ( dict "values" ( list .Values.persistence.files.annotations .Values.commonAnnotations ) "context" . ) }} + {{- include "common.tplvalues.render" ( dict "value" $annotations "context" $) | nindent 4 }} + {{- end }} +spec: + accessModes: + {{- range .Values.persistence.files.accessModes }} + - {{ . | quote }} + {{- end }} + resources: + requests: + storage: {{ .Values.persistence.files.size }} + {{- if .Values.persistence.files.selector }} + selector: {{- include "common.tplvalues.render" (dict "value" .Values.persistence.files.selector "context" $) | nindent 2 }} + {{- end }} + {{- include "common.storage.class" ( dict "persistence" .Values.persistence.files "global" .Values.global ) | nindent 2 }} +{{- end }} +{{- end }} diff --git a/charts/shuffle/templates/backend/backend-hpa.yaml b/charts/shuffle/templates/backend/backend-hpa.yaml new file mode 100644 index 00000000..bd099242 --- /dev/null +++ b/charts/shuffle/templates/backend/backend-hpa.yaml @@ -0,0 +1,43 @@ +{{- if .Values.backend.autoscaling.hpa.enabled }} +apiVersion: {{ include "common.capabilities.hpa.apiVersion" . }} +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "shuffle.backend.name" . }} + namespace: {{ include "common.names.namespace" . | quote }} + labels: {{- include "shuffle.backend.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- if .Values.commonAnnotations }} + annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +spec: + scaleTargetRef: + apiVersion: {{ include "common.capabilities.deployment.apiVersion" . }} + kind: Deployment + name: {{ include "shuffle.backend.name" . }} + minReplicas: {{ .Values.backend.autoscaling.hpa.minReplicas }} + maxReplicas: {{ .Values.backend.autoscaling.hpa.maxReplicas }} + metrics: + {{- if .Values.backend.autoscaling.hpa.targetMemory }} + - type: Resource + resource: + name: memory + {{- if semverCompare "<1.23-0" (include "common.capabilities.kubeVersion" .) }} + targetAverageUtilization: {{ .Values.backend.autoscaling.hpa.targetMemory }} + {{- else }} + target: + type: Utilization + averageUtilization: {{ .Values.worker.autoscaling.hpa.targetMemory }} + {{- end }} + {{- end }} + {{- if .Values.backend.autoscaling.hpa.targetCPU }} + - type: Resource + resource: + name: cpu + {{- if semverCompare "<1.23-0" (include "common.capabilities.kubeVersion" .) }} + targetAverageUtilization: {{ .Values.backend.autoscaling.hpa.targetCPU }} + {{- else }} + target: + type: Utilization + averageUtilization: {{ .Values.worker.autoscaling.hpa.targetCPU }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/shuffle/templates/backend/backend-network-policy.yaml b/charts/shuffle/templates/backend/backend-network-policy.yaml new file mode 100644 index 00000000..47d7bd3b --- /dev/null +++ b/charts/shuffle/templates/backend/backend-network-policy.yaml @@ -0,0 +1,66 @@ +{{- if .Values.backend.networkPolicy.enabled }} +kind: NetworkPolicy +apiVersion: {{ include "common.capabilities.networkPolicy.apiVersion" . }} +metadata: + name: {{ template "shuffle.backend.name" . }} + namespace: {{ include "common.names.namespace" . | quote }} + labels: {{- include "shuffle.backend.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- if .Values.commonAnnotations }} + annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +spec: + {{- $podLabels := include "common.tplvalues.merge" ( dict "values" ( list .Values.backend.podLabels .Values.commonLabels ) "context" . ) }} + podSelector: + matchLabels: {{- include "shuffle.backend.matchLabels" ( dict "customLabels" $podLabels "context" $ ) | nindent 6 }} + policyTypes: + - Ingress + - Egress + egress: + {{- if .Values.backend.networkPolicy.allowExternalEgress }} + - {} + {{- else }} + # Allow DNS resolution with an in-cluster DNS server + - ports: + - port: 53 + protocol: UDP + - port: 53 + protocol: TCP + to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + {{- if .Values.backend.networkPolicy.extraEgress }} + {{- include "common.tplvalues.render" ( dict "value" .Values.backend.networkPolicy.extraEgress "context" $ ) | nindent 4 }} + {{- end }} + {{- end }} + ingress: + - ports: + - port: 5001 + protocol: TCP + {{- if not .Values.backend.networkPolicy.allowExternal }} + from: + # Allow traffic from orborus + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: {{ .Release.Namespace }} + podSelector: + matchLabels: {{ include "shuffle.orborus.matchLabels" . | nindent 14 }} + + # Allow traffic from workers + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: {{ .Release.Namespace }} + podSelector: + matchLabels: {{ include "shuffle.worker.matchLabels" . | nindent 14 }} + + # Allow traffic from apps + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: {{ .Release.Namespace }} + podSelector: + matchLabels: {{ include "shuffle.app.matchLabels" . | nindent 14 }} + {{- end }} + {{- if .Values.backend.networkPolicy.extraIngress }} + {{- include "common.tplvalues.render" ( dict "value" .Values.backend.networkPolicy.extraIngress "context" $ ) | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/shuffle/templates/backend/backend-pdb.yaml b/charts/shuffle/templates/backend/backend-pdb.yaml new file mode 100644 index 00000000..5b253ca6 --- /dev/null +++ b/charts/shuffle/templates/backend/backend-pdb.yaml @@ -0,0 +1,21 @@ +{{- if .Values.backend.pdb.create }} +apiVersion: {{ include "common.capabilities.policy.apiVersion" . }} +kind: PodDisruptionBudget +metadata: + name: {{ include "shuffle.backend.name" . }} + namespace: {{ include "common.names.namespace" . | quote }} + labels: {{- include "shuffle.backend.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- if .Values.commonAnnotations }} + annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +spec: + {{- if .Values.backend.pdb.minAvailable }} + minAvailable: {{ .Values.backend.pdb.minAvailable }} + {{- end }} + {{- if or .Values.backend.pdb.maxUnavailable ( not .Values.backend.pdb.minAvailable ) }} + maxUnavailable: {{ .Values.backend.pdb.maxUnavailable | default 1 }} + {{- end }} + {{- $podLabels := include "common.tplvalues.merge" ( dict "values" ( list .Values.backend.podLabels .Values.commonLabels ) "context" . ) }} + selector: + matchLabels: {{- include "shuffle.backend.matchLabels" ( dict "customLabels" $podLabels "context" $ ) | nindent 6 }} +{{- end }} diff --git a/charts/shuffle/templates/backend/backend-role-binding.yaml b/charts/shuffle/templates/backend/backend-role-binding.yaml new file mode 100644 index 00000000..ffbe3ce0 --- /dev/null +++ b/charts/shuffle/templates/backend/backend-role-binding.yaml @@ -0,0 +1,18 @@ +{{ if .Values.backend.rbac.create }} +kind: RoleBinding +apiVersion: {{ include "common.capabilities.rbac.apiVersion" . }} +metadata: + name: {{ include "shuffle.backend.name" . }} + namespace: {{ include "common.names.namespace" . | quote }} + labels: {{- include "shuffle.backend.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- if .Values.commonAnnotations }} + annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +subjects: + - kind: ServiceAccount + name: {{ include "shuffle.backend.serviceAccountName" . }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "shuffle.backend.name" . }} +{{- end }} diff --git a/charts/shuffle/templates/backend/backend-role.yaml b/charts/shuffle/templates/backend/backend-role.yaml new file mode 100644 index 00000000..790ea462 --- /dev/null +++ b/charts/shuffle/templates/backend/backend-role.yaml @@ -0,0 +1,18 @@ +{{ if .Values.backend.rbac.create }} +kind: Role +apiVersion: {{ include "common.capabilities.rbac.apiVersion" . }} +metadata: + name: {{ include "shuffle.backend.name" . }} + namespace: {{ include "common.names.namespace" . | quote }} + labels: {{- include "shuffle.backend.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- if .Values.commonAnnotations }} + annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +rules: + - apiGroups: [""] + resources: ["pods"] + verbs: ["list"] + - apiGroups: ["batch"] + resources: ["jobs"] + verbs: ["get", "create", "delete"] +{{- end }} diff --git a/charts/shuffle/templates/backend/backend-service-account.yaml b/charts/shuffle/templates/backend/backend-service-account.yaml new file mode 100644 index 00000000..efe96b6a --- /dev/null +++ b/charts/shuffle/templates/backend/backend-service-account.yaml @@ -0,0 +1,13 @@ +{{- if .Values.backend.serviceAccount.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "shuffle.backend.serviceAccountName" . }} + namespace: {{ include "common.names.namespace" . | quote }} + labels: {{- include "shuffle.backend.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- if or .Values.backend.serviceAccount.annotations .Values.commonAnnotations }} + {{- $annotations := include "common.tplvalues.merge" (dict "values" (list .Values.backend.serviceAccount.annotations .Values.commonAnnotations) "context" .) }} + annotations: {{- include "common.tplvalues.render" ( dict "value" $annotations "context" $ ) | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.backend.serviceAccount.automountServiceAccountToken }} +{{- end }} diff --git a/charts/shuffle/templates/backend/backend-svc.yaml b/charts/shuffle/templates/backend/backend-svc.yaml new file mode 100644 index 00000000..c41aa3bb --- /dev/null +++ b/charts/shuffle/templates/backend/backend-svc.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ template "shuffle.backend.name" . }} + namespace: {{ include "common.names.namespace" . | quote }} + labels: {{- include "shuffle.backend.labels" (dict "customLabels" .Values.commonLabels "context" $) | nindent 4 }} + {{- if .Values.commonAnnotations }} + annotations: {{- include "common.tplvalues.render" (dict "value" .Values.commonAnnotations "context" $) | nindent 4 }} + {{- end }} +spec: + type: ClusterIP + ports: + - name: http + port: 5001 + targetPort: http + protocol: TCP + {{- $podLabels := include "common.tplvalues.merge" (dict "values" (list .Values.backend.podLabels .Values.commonLabels) "context" .) }} + selector: {{- include "shuffle.backend.matchLabels" (dict "customLabels" $podLabels "context" $) | nindent 4 }} diff --git a/charts/shuffle/templates/backend/backend-vpa.yaml b/charts/shuffle/templates/backend/backend-vpa.yaml new file mode 100644 index 00000000..dfdd0bf5 --- /dev/null +++ b/charts/shuffle/templates/backend/backend-vpa.yaml @@ -0,0 +1,38 @@ +{{- if and (.Capabilities.APIVersions.Has "autoscaling.k8s.io/v1/VerticalPodAutoscaler") .Values.backend.autoscaling.vpa.enabled }} +apiVersion: autoscaling.k8s.io/v1 +kind: VerticalPodAutoscaler +metadata: + name: {{ include "shuffle.backend.name" . }} + namespace: {{ include "common.names.namespace" . | quote }} + labels: {{- include "shuffle.backend.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- if or .Values.backend.autoscaling.vpa.annotations .Values.commonAnnotations }} + {{- $annotations := include "common.tplvalues.merge" ( dict "values" ( list .Values.backend.autoscaling.vpa.annotations .Values.commonAnnotations ) "context" . ) }} + annotations: {{- include "common.tplvalues.render" ( dict "value" $annotations "context" $) | nindent 4 }} + {{- end }} +spec: + resourcePolicy: + containerPolicies: + - containerName: backend + {{- with .Values.backend.autoscaling.vpa.controlledResources }} + controlledResources: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.backend.autoscaling.vpa.maxAllowed }} + maxAllowed: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.backend.autoscaling.vpa.minAllowed }} + minAllowed: + {{- toYaml . | nindent 8 }} + {{- end }} + targetRef: + apiVersion: {{ include "common.capabilities.deployment.apiVersion" . }} + kind: Deployment + name: {{ include "backend.names.name" . }} + {{- if .Values.backend.autoscaling.vpa.updatePolicy }} + updatePolicy: + {{- with .Values.backend.autoscaling.vpa.updatePolicy.updateMode }} + updateMode: {{ . }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/shuffle/templates/extra-list.yaml b/charts/shuffle/templates/extra-list.yaml new file mode 100644 index 00000000..9ac65f9e --- /dev/null +++ b/charts/shuffle/templates/extra-list.yaml @@ -0,0 +1,4 @@ +{{- range .Values.extraDeploy }} +--- +{{ include "common.tplvalues.render" (dict "value" . "context" $) }} +{{- end }} diff --git a/charts/shuffle/templates/frontend/frontend-cm-env.yaml b/charts/shuffle/templates/frontend/frontend-cm-env.yaml new file mode 100644 index 00000000..9d9f8e72 --- /dev/null +++ b/charts/shuffle/templates/frontend/frontend-cm-env.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "shuffle.frontend.name" . }}-env + namespace: {{ include "common.names.namespace" . | quote }} + labels: {{- include "shuffle.frontend.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- if .Values.commonAnnotations }} + annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +data: diff --git a/charts/shuffle/templates/frontend/frontend-dpl.yaml b/charts/shuffle/templates/frontend/frontend-dpl.yaml new file mode 100644 index 00000000..659167df --- /dev/null +++ b/charts/shuffle/templates/frontend/frontend-dpl.yaml @@ -0,0 +1,151 @@ +apiVersion: {{ include "common.capabilities.deployment.apiVersion" . }} +kind: Deployment +metadata: + name: {{ template "shuffle.frontend.name" . }} + namespace: {{ include "common.names.namespace" . | quote }} + labels: {{- include "shuffle.frontend.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- if or .Values.frontend.deploymentAnnotations .Values.commonAnnotations }} + {{- $annotations := include "common.tplvalues.merge" (dict "values" (list .Values.frontend.deploymentAnnotations .Values.commonAnnotations) "context" .) }} + annotations: {{- include "common.tplvalues.render" ( dict "value" $annotations "context" $ ) | nindent 4 }} + {{- end }} +spec: + {{- if not .Values.frontend.autoscaling.hpa.enabled }} + replicas: {{ .Values.frontend.replicaCount }} + {{- end }} + {{- if .Values.frontend.updateStrategy }} + strategy: {{- toYaml .Values.frontend.updateStrategy | nindent 4 }} + {{- end }} + {{- $podLabels := include "common.tplvalues.merge" (dict "values" (list .Values.frontend.podLabels .Values.commonLabels) "context" .) }} + selector: + matchLabels: {{- include "shuffle.frontend.matchLabels" ( dict "customLabels" $podLabels "context" $ ) | nindent 6 }} + template: + metadata: + {{- if .Values.frontend.podAnnotations }} + annotations: {{- include "common.tplvalues.render" (dict "value" .Values.frontend.podAnnotations "context" $) | nindent 8 }} + {{- end }} + labels: {{- include "shuffle.frontend.labels" ( dict "customLabels" $podLabels "context" $ ) | nindent 8 }} + spec: + {{- include "shuffle.imagePullSecrets" . | nindent 6 }} + serviceAccountName: {{ template "shuffle.frontend.serviceAccountName" . }} + automountServiceAccountToken: {{ .Values.frontend.automountServiceAccountToken }} + {{- if .Values.frontend.hostAliases }} + hostAliases: {{- include "common.tplvalues.render" (dict "value" .Values.frontend.hostAliases "context" $) | nindent 8 }} + {{- end }} + {{- if .Values.frontend.affinity }} + affinity: {{- include "common.tplvalues.render" ( dict "value" .Values.frontend.affinity "context" $) | nindent 8 }} + {{- else }} + affinity: + podAffinity: {{- include "common.affinities.pods" (dict "type" .Values.frontend.podAffinityPreset "component" "frontend" "customLabels" $podLabels "context" $) | nindent 10 }} + podAntiAffinity: {{- include "common.affinities.pods" (dict "type" .Values.frontend.podAntiAffinityPreset "component" "frontend" "customLabels" $podLabels "context" $) | nindent 10 }} + nodeAffinity: {{- include "common.affinities.nodes" (dict "type" .Values.frontend.nodeAffinityPreset.type "key" .Values.frontend.nodeAffinityPreset.key "values" .Values.frontend.nodeAffinityPreset.values) | nindent 10 }} + {{- end }} + {{- if .Values.frontend.nodeSelector }} + nodeSelector: {{- include "common.tplvalues.render" ( dict "value" .Values.frontend.nodeSelector "context" $) | nindent 8 }} + {{- end }} + {{- if .Values.frontend.tolerations }} + tolerations: {{- include "common.tplvalues.render" (dict "value" .Values.frontend.tolerations "context" .) | nindent 8 }} + {{- end }} + {{- if .Values.frontend.priorityClassName }} + priorityClassName: {{ .Values.frontend.priorityClassName | quote }} + {{- end }} + {{- if .Values.frontend.schedulerName }} + schedulerName: {{ .Values.frontend.schedulerName | quote }} + {{- end }} + {{- if .Values.frontend.topologySpreadConstraints }} + topologySpreadConstraints: {{- include "common.tplvalues.render" (dict "value" .Values.frontend.topologySpreadConstraints "context" .) | nindent 8 }} + {{- end }} + {{- if .Values.frontend.podSecurityContext.enabled }} + securityContext: {{- omit .Values.frontend.podSecurityContext "enabled" | toYaml | nindent 8 }} + {{- end }} + {{- if .Values.frontend.terminationGracePeriodSeconds }} + terminationGracePeriodSeconds: {{ .Values.frontend.terminationGracePeriodSeconds }} + {{- end }} + initContainers: + {{- if .Values.frontend.initContainers }} + {{- include "common.tplvalues.render" (dict "value" .Values.frontend.initContainers "context" $) | nindent 8 }} + {{- end }} + containers: + - name: frontend + image: {{ template "shuffle.frontend.image" . }} + imagePullPolicy: {{ .Values.frontend.image.pullPolicy }} + {{- if .Values.frontend.containerSecurityContext.enabled }} + securityContext: {{- include "common.compatibility.renderSecurityContext" (dict "secContext" .Values.frontend.containerSecurityContext "context" $) | nindent 12 }} + {{- end }} + {{- if .Values.diagnosticMode.enabled }} + command: {{- include "common.tplvalues.render" (dict "value" .Values.diagnosticMode.command "context" $) | nindent 12 }} + {{- else if .Values.frontend.command }} + command: {{- include "common.tplvalues.render" (dict "value" .Values.frontend.command "context" $) | nindent 12 }} + {{- end }} + {{- if .Values.diagnosticMode.enabled }} + args: {{- include "common.tplvalues.render" (dict "value" .Values.diagnosticMode.args "context" $) | nindent 12 }} + {{- else if .Values.frontend.args }} + args: {{- include "common.tplvalues.render" (dict "value" .Values.frontend.args "context" $) | nindent 12 }} + {{- end }} + env: + {{- if .Values.frontend.extraEnvVars }} + {{- include "common.tplvalues.render" (dict "value" .Values.frontend.extraEnvVars "context" $) | nindent 12 }} + {{- end }} + envFrom: + - configMapRef: + name: {{ include "shuffle.frontend.name" . }}-env + {{- if .Values.frontend.extraEnvVarsCM }} + - configMapRef: + name: {{ include "common.tplvalues.render" (dict "value" .Values.frontend.extraEnvVarsCM "context" $) }} + {{- end }} + {{- if .Values.frontend.extraEnvVarsSecret }} + - secretRef: + name: {{ include "common.tplvalues.render" (dict "value" .Values.frontend.extraEnvVarsSecret "context" $) }} + {{- end }} + {{- if .Values.frontend.resources }} + resources: {{- toYaml .Values.frontend.resources | nindent 12 }} + {{- else if ne .Values.frontend.resourcesPreset "none" }} + resources: {{- include "common.resources.preset" (dict "type" .Values.frontend.resourcesPreset) | nindent 12 }} + {{- end }} + ports: + - name: http + containerPort: 8080 + {{- if not .Values.diagnosticMode.enabled }} + {{- if .Values.frontend.customLivenessProbe }} + livenessProbe: {{- include "common.tplvalues.render" (dict "value" .Values.frontend.customLivenessProbe "context" $) | nindent 12 }} + {{- else if .Values.frontend.livenessProbe.enabled }} + livenessProbe: {{- include "common.tplvalues.render" (dict "value" (omit .Values.frontend.livenessProbe "enabled") "context" $) | nindent 12 }} + httpGet: + path: / + port: 8080 + {{- end }} + {{- if .Values.frontend.customReadinessProbe }} + readinessProbe: {{- include "common.tplvalues.render" (dict "value" .Values.frontend.customReadinessProbe "context" $) | nindent 12 }} + {{- else if .Values.frontend.readinessProbe.enabled }} + readinessProbe: {{- include "common.tplvalues.render" (dict "value" (omit .Values.frontend.readinessProbe "enabled") "context" $) | nindent 12 }} + httpGet: + path: / + port: 8080 + {{- end }} + {{- if .Values.frontend.customStartupProbe }} + startupProbe: {{- include "common.tplvalues.render" (dict "value" .Values.frontend.customStartupProbe "context" $) | nindent 12 }} + {{- else if .Values.frontend.startupProbe.enabled }} + startupProbe: {{- include "common.tplvalues.render" (dict "value" (omit .Values.frontend.startupProbe "enabled") "context" $) | nindent 12 }} + httpGet: + path: / + port: 8080 + {{- end }} + {{- end }} + {{- if .Values.frontend.lifecycleHooks }} + lifecycle: {{- include "common.tplvalues.render" (dict "value" .Values.frontend.lifecycleHooks "context" $) | nindent 12 }} + {{- end }} + volumeMounts: + - name: empty-dir + mountPath: /tmp + subPath: tmp-dir + {{- if .Values.frontend.extraVolumeMounts }} + {{- include "common.tplvalues.render" (dict "value" .Values.frontend.extraVolumeMounts "context" $) | nindent 12 }} + {{- end }} + {{- if .Values.frontend.sidecars }} + {{- include "common.tplvalues.render" ( dict "value" .Values.frontend.sidecars "context" $) | nindent 8 }} + {{- end }} + volumes: + - name: empty-dir + emptyDir: {} + {{- if .Values.frontend.extraVolumes }} + {{- include "common.tplvalues.render" (dict "value" .Values.frontend.extraVolumes "context" $) | nindent 8 }} + {{- end }} diff --git a/charts/shuffle/templates/frontend/frontend-hpa.yaml b/charts/shuffle/templates/frontend/frontend-hpa.yaml new file mode 100644 index 00000000..d1b31e11 --- /dev/null +++ b/charts/shuffle/templates/frontend/frontend-hpa.yaml @@ -0,0 +1,43 @@ +{{- if .Values.frontend.autoscaling.hpa.enabled }} +apiVersion: {{ include "common.capabilities.hpa.apiVersion" . }} +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "shuffle.frontend.name" . }} + namespace: {{ include "common.names.namespace" . | quote }} + labels: {{- include "shuffle.frontend.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- if .Values.commonAnnotations }} + annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +spec: + scaleTargetRef: + apiVersion: {{ include "common.capabilities.deployment.apiVersion" . }} + kind: Deployment + name: {{ include "shuffle.frontend.name" . }} + minReplicas: {{ .Values.frontend.autoscaling.hpa.minReplicas }} + maxReplicas: {{ .Values.frontend.autoscaling.hpa.maxReplicas }} + metrics: + {{- if .Values.frontend.autoscaling.hpa.targetMemory }} + - type: Resource + resource: + name: memory + {{- if semverCompare "<1.23-0" (include "common.capabilities.kubeVersion" .) }} + targetAverageUtilization: {{ .Values.frontend.autoscaling.hpa.targetMemory }} + {{- else }} + target: + type: Utilization + averageUtilization: {{ .Values.worker.autoscaling.hpa.targetMemory }} + {{- end }} + {{- end }} + {{- if .Values.frontend.autoscaling.hpa.targetCPU }} + - type: Resource + resource: + name: cpu + {{- if semverCompare "<1.23-0" (include "common.capabilities.kubeVersion" .) }} + targetAverageUtilization: {{ .Values.frontend.autoscaling.hpa.targetCPU }} + {{- else }} + target: + type: Utilization + averageUtilization: {{ .Values.worker.autoscaling.hpa.targetCPU }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/shuffle/templates/frontend/frontend-network-policy.yaml b/charts/shuffle/templates/frontend/frontend-network-policy.yaml new file mode 100644 index 00000000..b8143312 --- /dev/null +++ b/charts/shuffle/templates/frontend/frontend-network-policy.yaml @@ -0,0 +1,44 @@ +{{- if .Values.frontend.networkPolicy.enabled }} +kind: NetworkPolicy +apiVersion: {{ include "common.capabilities.networkPolicy.apiVersion" . }} +metadata: + name: {{ template "shuffle.frontend.name" . }} + namespace: {{ include "common.names.namespace" . | quote }} + labels: {{- include "shuffle.frontend.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- if .Values.commonAnnotations }} + annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +spec: + {{- $podLabels := include "common.tplvalues.merge" ( dict "values" ( list .Values.frontend.podLabels .Values.commonLabels ) "context" . ) }} + podSelector: + matchLabels: {{- include "shuffle.frontend.matchLabels" ( dict "customLabels" $podLabels "context" $ ) | nindent 6 }} + policyTypes: + - Ingress + - Egress + egress: + {{- if .Values.frontend.networkPolicy.allowExternalEgress }} + - {} + {{- else }} + # Allow DNS resolution with an in-cluster DNS server + - ports: + - port: 53 + protocol: UDP + - port: 53 + protocol: TCP + to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + {{- if .Values.frontend.networkPolicy.extraEgress }} + {{- include "common.tplvalues.render" ( dict "value" .Values.frontend.networkPolicy.extraEgress "context" $ ) | nindent 4 }} + {{- end }} + {{- end }} + ingress: + {{ if .Values.frontend.networkPolicy.allowExternal }} + - ports: + - port: 8080 + {{- end }} + {{- if .Values.frontend.networkPolicy.extraIngress }} + {{- include "common.tplvalues.render" ( dict "value" .Values.frontend.networkPolicy.extraIngress "context" $ ) | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/shuffle/templates/frontend/frontend-pdb.yaml b/charts/shuffle/templates/frontend/frontend-pdb.yaml new file mode 100644 index 00000000..b1c2eb28 --- /dev/null +++ b/charts/shuffle/templates/frontend/frontend-pdb.yaml @@ -0,0 +1,21 @@ +{{- if .Values.frontend.pdb.create }} +apiVersion: {{ include "common.capabilities.policy.apiVersion" . }} +kind: PodDisruptionBudget +metadata: + name: {{ include "shuffle.frontend.name" . }} + namespace: {{ include "common.names.namespace" . | quote }} + labels: {{- include "shuffle.frontend.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- if .Values.commonAnnotations }} + annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +spec: + {{- if .Values.frontend.pdb.minAvailable }} + minAvailable: {{ .Values.frontend.pdb.minAvailable }} + {{- end }} + {{- if or .Values.frontend.pdb.maxUnavailable ( not .Values.frontend.pdb.minAvailable ) }} + maxUnavailable: {{ .Values.frontend.pdb.maxUnavailable | default 1 }} + {{- end }} + {{- $podLabels := include "common.tplvalues.merge" ( dict "values" ( list .Values.frontend.podLabels .Values.commonLabels ) "context" . ) }} + selector: + matchLabels: {{- include "shuffle.frontend.matchLabels" ( dict "customLabels" $podLabels "context" $ ) | nindent 6 }} +{{- end }} diff --git a/charts/shuffle/templates/frontend/frontend-service-account.yaml b/charts/shuffle/templates/frontend/frontend-service-account.yaml new file mode 100644 index 00000000..a55fad19 --- /dev/null +++ b/charts/shuffle/templates/frontend/frontend-service-account.yaml @@ -0,0 +1,13 @@ +{{- if .Values.frontend.serviceAccount.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "shuffle.frontend.serviceAccountName" . }} + namespace: {{ include "common.names.namespace" . | quote }} + labels: {{- include "shuffle.frontend.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- if or .Values.frontend.serviceAccount.annotations .Values.commonAnnotations }} + {{- $annotations := include "common.tplvalues.merge" (dict "values" (list .Values.frontend.serviceAccount.annotations .Values.commonAnnotations) "context" .) }} + annotations: {{- include "common.tplvalues.render" ( dict "value" $annotations "context" $ ) | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.frontend.serviceAccount.automountServiceAccountToken }} +{{- end }} diff --git a/charts/shuffle/templates/frontend/frontend-svc.yaml b/charts/shuffle/templates/frontend/frontend-svc.yaml new file mode 100644 index 00000000..8a5ca84c --- /dev/null +++ b/charts/shuffle/templates/frontend/frontend-svc.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ template "shuffle.frontend.name" . }} + namespace: {{ include "common.names.namespace" . | quote }} + labels: {{- include "shuffle.frontend.labels" (dict "customLabels" .Values.commonLabels "context" $) | nindent 4 }} + {{- if .Values.commonAnnotations }} + annotations: {{- include "common.tplvalues.render" (dict "value" .Values.commonAnnotations "context" $) | nindent 4 }} + {{- end }} +spec: + type: ClusterIP + ports: + - name: http + port: 8080 + targetPort: http + protocol: TCP + {{- $podLabels := include "common.tplvalues.merge" (dict "values" (list .Values.frontend.podLabels .Values.commonLabels) "context" .) }} + selector: {{- include "shuffle.frontend.matchLabels" (dict "customLabels" $podLabels "context" $) | nindent 4 }} diff --git a/charts/shuffle/templates/frontend/frontend-vpa.yaml b/charts/shuffle/templates/frontend/frontend-vpa.yaml new file mode 100644 index 00000000..8dd26333 --- /dev/null +++ b/charts/shuffle/templates/frontend/frontend-vpa.yaml @@ -0,0 +1,38 @@ +{{- if and (.Capabilities.APIVersions.Has "autoscaling.k8s.io/v1/VerticalPodAutoscaler") .Values.frontend.autoscaling.vpa.enabled }} +apiVersion: autoscaling.k8s.io/v1 +kind: VerticalPodAutoscaler +metadata: + name: {{ include "shuffle.frontend.name" . }} + namespace: {{ include "common.names.namespace" . | quote }} + labels: {{- include "shuffle.frontend.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- if or .Values.frontend.autoscaling.vpa.annotations .Values.commonAnnotations }} + {{- $annotations := include "common.tplvalues.merge" ( dict "values" ( list .Values.frontend.autoscaling.vpa.annotations .Values.commonAnnotations ) "context" . ) }} + annotations: {{- include "common.tplvalues.render" ( dict "value" $annotations "context" $) | nindent 4 }} + {{- end }} +spec: + resourcePolicy: + containerPolicies: + - containerName: frontend + {{- with .Values.frontend.autoscaling.vpa.controlledResources }} + controlledResources: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.frontend.autoscaling.vpa.maxAllowed }} + maxAllowed: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.frontend.autoscaling.vpa.minAllowed }} + minAllowed: + {{- toYaml . | nindent 8 }} + {{- end }} + targetRef: + apiVersion: {{ include "common.capabilities.deployment.apiVersion" . }} + kind: Deployment + name: {{ include "frontend.names.name" . }} + {{- if .Values.frontend.autoscaling.vpa.updatePolicy }} + updatePolicy: + {{- with .Values.frontend.autoscaling.vpa.updatePolicy.updateMode }} + updateMode: {{ . }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/shuffle/templates/ingress/ingress.yaml b/charts/shuffle/templates/ingress/ingress.yaml new file mode 100644 index 00000000..cd08c0f9 --- /dev/null +++ b/charts/shuffle/templates/ingress/ingress.yaml @@ -0,0 +1,60 @@ +{{- if .Values.ingress.enabled }} +apiVersion: {{ include "common.capabilities.ingress.apiVersion" . }} +kind: Ingress +metadata: + name: {{ template "common.names.fullname" . }} + namespace: {{ include "common.names.namespace" . | quote }} + labels: {{- include "common.labels.standard" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- if or .Values.ingress.annotations .Values.commonAnnotations }} + {{- $annotations := include "common.tplvalues.merge" (dict "values" (list .Values.ingress.annotations .Values.commonAnnotations) "context" .) }} + annotations: {{- include "common.tplvalues.render" ( dict "value" $annotations "context" $ ) | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.ingressClassName (eq "true" (include "common.ingress.supportsIngressClassname" .)) }} + ingressClassName: {{ .Values.ingress.ingressClassName | quote }} + {{- end }} + rules: + {{- if .Values.ingress.hostname }} + - host: {{ .Values.ingress.hostname }} + http: + paths: + {{- if .Values.ingress.extraPaths }} + {{- toYaml .Values.ingress.extraPaths | nindent 10 }} + {{- end }} + - path: {{ .Values.ingress.path }} + {{- if eq "true" (include "common.ingress.supportsPathType" .) }} + pathType: {{ .Values.ingress.pathType }} + {{- end }} + backend: {{- include "common.ingress.backend" (dict "serviceName" (include "shuffle.frontend.name" .) "servicePort" "http" "context" $) | nindent 14 }} + - path: {{ .Values.ingress.backendPath }} + {{- if eq "true" (include "common.ingress.supportsPathType" .) }} + pathType: {{ .Values.ingress.backendPathType }} + {{- end }} + backend: {{- include "common.ingress.backend" (dict "serviceName" (include "shuffle.backend.name" .) "servicePort" "http" "context" $) | nindent 14 }} + + {{- end }} + {{- range .Values.ingress.extraHosts }} + - host: {{ .name | quote }} + http: + paths: + - path: {{ default "/" .path }} + {{- if eq "true" (include "common.ingress.supportsPathType" $) }} + pathType: {{ default "ImplementationSpecific" .pathType }} + {{- end }} + backend: {{- include "common.ingress.backend" (dict "serviceName" (include "common.names.fullname" $) "servicePort" "http" "context" $) | nindent 14 }} + {{- end }} + {{- if .Values.ingress.extraRules }} + {{- include "common.tplvalues.render" (dict "value" .Values.ingress.extraRules "context" $) | nindent 4 }} + {{- end }} + {{- if or (and .Values.ingress.tls (or (include "common.ingress.certManagerRequest" ( dict "annotations" .Values.ingress.annotations )) .Values.ingress.selfSigned)) .Values.ingress.extraTls }} + tls: + {{- if and .Values.ingress.tls (or (include "common.ingress.certManagerRequest" ( dict "annotations" .Values.ingress.annotations )) .Values.ingress.selfSigned) }} + - hosts: + - {{ .Values.ingress.hostname | quote }} + secretName: {{ printf "%s-tls" .Values.ingress.hostname }} + {{- end }} + {{- if .Values.ingress.extraTls }} + {{- include "common.tplvalues.render" (dict "value" .Values.ingress.extraTls "context" $) | nindent 4 }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/shuffle/templates/ingress/tls-secret.yaml b/charts/shuffle/templates/ingress/tls-secret.yaml new file mode 100644 index 00000000..4e35a9aa --- /dev/null +++ b/charts/shuffle/templates/ingress/tls-secret.yaml @@ -0,0 +1,39 @@ +{{- if .Values.ingress.enabled }} +{{- if .Values.ingress.secrets }} +{{- range .Values.ingress.secrets }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ .name }} + namespace: {{ include "common.names.namespace" $ | quote }} + labels: {{- include "common.labels.standard" ( dict "customLabels" $.Values.commonLabels "context" $ ) | nindent 4 }} + {{- if $.Values.commonAnnotations }} + annotations: {{- include "common.tplvalues.render" ( dict "value" $.Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +type: kubernetes.io/tls +data: + tls.crt: {{ .certificate | b64enc }} + tls.key: {{ .key | b64enc }} +--- +{{- end }} +{{- end }} +{{- if and .Values.ingress.tls .Values.ingress.selfSigned }} +{{- $secretName := printf "%s-tls" .Values.ingress.hostname }} +{{- $ca := genCA "shuffle-ca" 365 }} +{{- $cert := genSignedCert .Values.ingress.hostname nil (list .Values.ingress.hostname) 365 $ca }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ $secretName }} + namespace: {{ include "common.names.namespace" . | quote }} + labels: {{- include "common.labels.standard" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- if .Values.commonAnnotations }} + annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +type: kubernetes.io/tls +data: + tls.crt: {{ include "common.secrets.lookup" (dict "secret" $secretName "key" "tls.crt" "defaultValue" $cert.Cert "context" $) }} + tls.key: {{ include "common.secrets.lookup" (dict "secret" $secretName "key" "tls.key" "defaultValue" $cert.Key "context" $) }} + ca.crt: {{ include "common.secrets.lookup" (dict "secret" $secretName "key" "ca.crt" "defaultValue" $ca.Cert "context" $) }} +{{- end }} +{{- end }} diff --git a/charts/shuffle/templates/istio/gateway.yaml b/charts/shuffle/templates/istio/gateway.yaml new file mode 100644 index 00000000..fe80084f --- /dev/null +++ b/charts/shuffle/templates/istio/gateway.yaml @@ -0,0 +1,39 @@ +{{- if .Values.istio.enabled }} +apiVersion: "{{ .Values.istio.apiVersion }}" +kind: Gateway +metadata: + name: {{ template "common.names.fullname" . }} + namespace: {{ include "common.names.namespace" . | quote }} + labels: {{- include "common.labels.standard" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- if or .Values.istio.gateway.annotations .Values.commonAnnotations }} + {{- $annotations := include "common.tplvalues.merge" (dict "values" (list .Values.istio.gateway.annotations .Values.commonAnnotations) "context" .) }} + annotations: {{- include "common.tplvalues.render" ( dict "value" $annotations "context" $ ) | nindent 4 }} + {{- end }} +spec: + selector: {{- include "common.tplvalues.render" ( dict "value" .Values.istio.gateway.selector "context" $ ) | nindent 4 }} + servers: + {{- if .Values.istio.gateway.http.enabled }} + - name: "{{ include "common.names.fullname" . }}-http" + port: + number: 80 + name: http + protocol: HTTP + hosts: {{ .Values.istio.hosts }} + tls: + httpsRedirect: {{ .Values.istio.gateway.http.httpsRedirect }} + {{- end }} + {{- if .Values.istio.gateway.https.enabled }} + - name: "{{ include "common.names.fullname" . }}-https" + port: + number: 443 + name: https + protocol: HTTPS + hosts: {{ .Values.istio.hosts }} + tls: + credentialName: {{ .Values.istio.gateway.https.tlsCredentialName }} + mode: SIMPLE + {{- end }} + {{- if .Values.istio.gateway.extraServers }} + {{- include "common.tplvalues.render" (dict "value" .Values.istio.gateway.extraServers "context" $) | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/shuffle/templates/istio/virtual-service.yaml b/charts/shuffle/templates/istio/virtual-service.yaml new file mode 100644 index 00000000..ed74004b --- /dev/null +++ b/charts/shuffle/templates/istio/virtual-service.yaml @@ -0,0 +1,30 @@ +{{- if .Values.istio.enabled }} +apiVersion: "{{ .Values.istio.apiVersion }}" +kind: VirtualService +metadata: + name: {{ template "common.names.fullname" . }} + namespace: {{ include "common.names.namespace" . | quote }} + labels: {{- include "common.labels.standard" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- if or .Values.istio.gateway.annotations .Values.commonAnnotations }} + {{- $annotations := include "common.tplvalues.merge" (dict "values" (list .Values.istio.virtualService.annotations .Values.commonAnnotations) "context" .) }} + annotations: {{- include "common.tplvalues.render" ( dict "value" $annotations "context" $ ) | nindent 4 }} + {{- end }} +spec: + hosts: {{ .Values.istio.hosts }} + gateways: + - {{ include "common.names.fullname" . }} + http: + - match: + - uri: + prefix: /api + route: + - destination: + host: {{ include "shuffle.backend.name" . }} + port: + number: 5001 + - route: + - destination: + host: {{ include "shuffle.frontend.name" . }} + port: + number: 8080 + {{- end }} diff --git a/charts/shuffle/templates/orborus-app/orborus-app-network-policy.yaml b/charts/shuffle/templates/orborus-app/orborus-app-network-policy.yaml new file mode 100644 index 00000000..d4a24fe6 --- /dev/null +++ b/charts/shuffle/templates/orborus-app/orborus-app-network-policy.yaml @@ -0,0 +1,61 @@ +{{- if .Values.app.networkPolicy.enabled }} +kind: NetworkPolicy +apiVersion: {{ include "common.capabilities.networkPolicy.apiVersion" . }} +metadata: + name: {{ template "shuffle.app.name" . }} + namespace: {{ include "common.names.namespace" . | quote }} + labels: {{- include "shuffle.app.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- if .Values.commonAnnotations }} + annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +spec: + {{- $podLabels := include "common.tplvalues.merge" ( dict "values" ( list .Values.app.podLabels .Values.commonLabels ) "context" . ) }} + podSelector: + matchLabels: {{- include "shuffle.app.matchLabels" ( dict "customLabels" $podLabels "context" $ ) | nindent 6 }} + policyTypes: + - Ingress + - Egress + egress: + {{- if .Values.app.networkPolicy.allowExternalEgress }} + - {} + {{- else }} + # Allow DNS resolution with an in-cluster DNS server + - ports: + - port: 53 + protocol: UDP + - port: 53 + protocol: TCP + to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + # Allow access to workers + - ports: + - port: 33333 + protocol: TCP + to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: {{ .Release.Namespace }} + podSelector: + matchLabels: {{ include "shuffle.worker.matchLabels" . | nindent 14 }} + {{- if .Values.app.networkPolicy.extraEgress }} + {{- include "common.tplvalues.render" ( dict "value" .Values.app.networkPolicy.extraEgress "context" $ ) | nindent 4 }} + {{- end }} + {{- end }} + ingress: + {{- if .Values.app.networkPolicy.allowExternal }} + - {} + {{- else }} + # Allow access from workers. Apps will typicaly use port 80/TCP, but this is not enforced. + - from: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: {{ .Release.Namespace }} + podSelector: + matchLabels: {{ include "shuffle.worker.matchLabels" . | nindent 14 }} + {{- end }} + {{- if .Values.app.networkPolicy.extraIngress }} + {{- include "common.tplvalues.render" ( dict "value" .Values.app.networkPolicy.extraIngress "context" $ ) | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/shuffle/templates/orborus-app/orborus-app-role-binding.yaml b/charts/shuffle/templates/orborus-app/orborus-app-role-binding.yaml new file mode 100644 index 00000000..91115fb4 --- /dev/null +++ b/charts/shuffle/templates/orborus-app/orborus-app-role-binding.yaml @@ -0,0 +1,20 @@ +{{ if .Values.app.rbac.create }} +kind: RoleBinding +apiVersion: {{ include "common.capabilities.rbac.apiVersion" . }} +metadata: + name: {{ template "shuffle.app.name" . }} + namespace: {{ include "common.names.namespace" . | quote }} + labels: {{- include "shuffle.app.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- if .Values.commonAnnotations }} + annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +# Deployed shuffle apps will use the default service account. There is currently no way to change that. +# https://github.com/Shuffle/Shuffle/pull/1421#issuecomment-2382623260 +subjects: + - kind: ServiceAccount + name: default +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ template "shuffle.app.name" . }} +{{- end }} diff --git a/charts/shuffle/templates/orborus-app/orborus-app-role.yaml b/charts/shuffle/templates/orborus-app/orborus-app-role.yaml new file mode 100644 index 00000000..8be2a6f0 --- /dev/null +++ b/charts/shuffle/templates/orborus-app/orborus-app-role.yaml @@ -0,0 +1,15 @@ +{{ if .Values.app.rbac.create }} +kind: Role +apiVersion: {{ include "common.capabilities.rbac.apiVersion" . }} +metadata: + name: {{ template "shuffle.app.name" . }} + namespace: {{ include "common.names.namespace" . | quote }} + labels: {{- include "shuffle.app.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- if .Values.commonAnnotations }} + annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +rules: + - apiGroups: [""] + resources: [""] + verbs: [""] +{{- end }} diff --git a/charts/shuffle/templates/orborus-app/orborus-app-service-account.yaml b/charts/shuffle/templates/orborus-app/orborus-app-service-account.yaml new file mode 100644 index 00000000..f5e4befe --- /dev/null +++ b/charts/shuffle/templates/orborus-app/orborus-app-service-account.yaml @@ -0,0 +1,13 @@ +{{- if .Values.app.serviceAccount.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "shuffle.app.serviceAccountName" . }} + namespace: {{ include "common.names.namespace" . | quote }} + labels: {{- include "shuffle.app.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- if or .Values.app.serviceAccount.annotations .Values.commonAnnotations }} + {{- $annotations := include "common.tplvalues.merge" (dict "values" (list .Values.app.serviceAccount.annotations .Values.commonAnnotations) "context" .) }} + annotations: {{- include "common.tplvalues.render" ( dict "value" $annotations "context" $ ) | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.app.serviceAccount.automountServiceAccountToken }} +{{- end }} diff --git a/charts/shuffle/templates/orborus-worker/orborus-worker-network-policy.yaml b/charts/shuffle/templates/orborus-worker/orborus-worker-network-policy.yaml new file mode 100644 index 00000000..f4ee4aba --- /dev/null +++ b/charts/shuffle/templates/orborus-worker/orborus-worker-network-policy.yaml @@ -0,0 +1,69 @@ +{{- if .Values.worker.networkPolicy.enabled }} +kind: NetworkPolicy +apiVersion: {{ include "common.capabilities.networkPolicy.apiVersion" . }} +metadata: + name: {{ template "shuffle.worker.name" . }} + namespace: {{ include "common.names.namespace" . | quote }} + labels: {{- include "shuffle.worker.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- if .Values.commonAnnotations }} + annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +spec: + {{- $podLabels := include "common.tplvalues.merge" ( dict "values" ( list .Values.worker.podLabels .Values.commonLabels ) "context" . ) }} + podSelector: + matchLabels: {{- include "shuffle.worker.matchLabels" ( dict "customLabels" $podLabels "context" $ ) | nindent 6 }} + policyTypes: + - Ingress + - Egress + egress: + {{- if .Values.worker.networkPolicy.allowExternalEgress }} + - {} + {{- else }} + # Allow DNS resolution with an in-cluster DNS server + - ports: + - port: 53 + protocol: UDP + - port: 53 + protocol: TCP + to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + # Allow access to orborus + - ports: + - port: 8080 + protocol: TCP + to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: {{ .Release.Namespace }} + podSelector: + matchLabels: {{ include "shuffle.orborus.matchLabels" . | nindent 14 }} + # Allow arbitrary connections to apps. Apps will typically use port 80/TCP, but this is not enforced. + - to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: {{ .Release.Namespace }} + podSelector: + matchLabels: {{ include "shuffle.app.matchLabels" . | nindent 14 }} + {{- if .Values.worker.networkPolicy.extraEgress }} + {{- include "common.tplvalues.render" ( dict "value" .Values.worker.networkPolicy.extraEgress "context" $ ) | nindent 4 }} + {{- end }} + {{- end }} + ingress: + - ports: + - port: 33333 + protocol: TCP + {{- if not .Values.worker.networkPolicy.allowExternal }} + from: + # Allow traffic from orborus + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: {{ .Release.Namespace }} + podSelector: + matchLabels: {{ include "shuffle.orborus.matchLabels" . | nindent 14 }} + {{- end }} + {{- if .Values.worker.networkPolicy.extraIngress }} + {{- include "common.tplvalues.render" ( dict "value" .Values.worker.networkPolicy.extraIngress "context" $ ) | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/shuffle/templates/orborus-worker/orborus-worker-role-binding.yaml b/charts/shuffle/templates/orborus-worker/orborus-worker-role-binding.yaml new file mode 100644 index 00000000..71479700 --- /dev/null +++ b/charts/shuffle/templates/orborus-worker/orborus-worker-role-binding.yaml @@ -0,0 +1,20 @@ +{{ if .Values.worker.rbac.create }} +kind: RoleBinding +apiVersion: {{ include "common.capabilities.rbac.apiVersion" . }} +metadata: + name: {{ template "shuffle.worker.name" . }} + namespace: {{ include "common.names.namespace" . | quote }} + labels: {{- include "shuffle.worker.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- if .Values.commonAnnotations }} + annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +# Deployed shuffle workers will use the default service account. There is currently no way to change that. +# https://github.com/Shuffle/Shuffle/pull/1421#issuecomment-2382623260 +subjects: + - kind: ServiceAccount + name: default +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ template "shuffle.worker.name" . }} +{{- end }} diff --git a/charts/shuffle/templates/orborus-worker/orborus-worker-role.yaml b/charts/shuffle/templates/orborus-worker/orborus-worker-role.yaml new file mode 100644 index 00000000..fdef5855 --- /dev/null +++ b/charts/shuffle/templates/orborus-worker/orborus-worker-role.yaml @@ -0,0 +1,21 @@ +{{ if .Values.worker.rbac.create }} +kind: Role +apiVersion: {{ include "common.capabilities.rbac.apiVersion" . }} +metadata: + name: {{ template "shuffle.worker.name" . }} + namespace: {{ include "common.names.namespace" . | quote }} + labels: {{- include "shuffle.worker.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- if .Values.commonAnnotations }} + annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +rules: + - apiGroups: [""] + resources: ["pods"] + verbs: ["list", "delete"] + - apiGroups: [""] + resources: ["services"] + verbs: ["create"] + - apiGroups: ["apps"] + resources: ["deployments"] + verbs: ["list", "create"] +{{- end }} diff --git a/charts/shuffle/templates/orborus-worker/orborus-worker-service-account.yaml b/charts/shuffle/templates/orborus-worker/orborus-worker-service-account.yaml new file mode 100644 index 00000000..e27395f0 --- /dev/null +++ b/charts/shuffle/templates/orborus-worker/orborus-worker-service-account.yaml @@ -0,0 +1,13 @@ +{{- if .Values.worker.serviceAccount.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "shuffle.worker.serviceAccountName" . }} + namespace: {{ include "common.names.namespace" . | quote }} + labels: {{- include "shuffle.worker.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- if or .Values.worker.serviceAccount.annotations .Values.commonAnnotations }} + {{- $annotations := include "common.tplvalues.merge" (dict "values" (list .Values.worker.serviceAccount.annotations .Values.commonAnnotations) "context" .) }} + annotations: {{- include "common.tplvalues.render" ( dict "value" $annotations "context" $ ) | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.worker.serviceAccount.automountServiceAccountToken }} +{{- end }} diff --git a/charts/shuffle/templates/orborus/orborus-cm-env.yaml b/charts/shuffle/templates/orborus/orborus-cm-env.yaml new file mode 100644 index 00000000..b49ce510 --- /dev/null +++ b/charts/shuffle/templates/orborus/orborus-cm-env.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "shuffle.orborus.name" . }}-env + namespace: {{ include "common.names.namespace" . | quote }} + labels: {{- include "shuffle.orborus.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- if .Values.commonAnnotations }} + annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +data: + ENVIRONMENT_NAME: "{{ .Values.shuffle.org }}" + ORG_ID: "{{ .Values.shuffle.org }}" + TZ: "{{ .Values.shuffle.timezone }}" + BASE_URL: "http://{{ include "shuffle.backend.name" . }}.{{ .Release.Namespace }}.svc.cluster.local:5001" + KUBERNETES_NAMESPACE: "{{ .Release.Namespace }}" + KUBERNETES_SERVICE_ACCOUNT: {{ include "shuffle.orborus.serviceAccountName" . }} + SHUFFLE_WORKER_IMAGE: "{{ include "shuffle.worker.image" . }}" + REGISTRY_URL: "{{ .Values.shuffle.appRegistry }}" + SHUFFLE_SWARM_CONFIG: run diff --git a/charts/shuffle/templates/orborus/orborus-dpl.yaml b/charts/shuffle/templates/orborus/orborus-dpl.yaml new file mode 100644 index 00000000..b63c7a8b --- /dev/null +++ b/charts/shuffle/templates/orborus/orborus-dpl.yaml @@ -0,0 +1,155 @@ +apiVersion: {{ include "common.capabilities.deployment.apiVersion" . }} +kind: Deployment +metadata: + name: {{ template "shuffle.orborus.name" . }} + namespace: {{ include "common.names.namespace" . | quote }} + labels: {{- include "shuffle.orborus.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- if or .Values.orborus.deploymentAnnotations .Values.commonAnnotations }} + {{- $annotations := include "common.tplvalues.merge" (dict "values" (list .Values.orborus.deploymentAnnotations .Values.commonAnnotations) "context" .) }} + annotations: {{- include "common.tplvalues.render" ( dict "value" $annotations "context" $ ) | nindent 4 }} + {{- end }} +spec: + {{- if not .Values.orborus.autoscaling.hpa.enabled }} + replicas: {{ .Values.orborus.replicaCount }} + {{- end }} + {{- if .Values.orborus.updateStrategy }} + strategy: {{- toYaml .Values.orborus.updateStrategy | nindent 4 }} + {{- end }} + {{- $podLabels := include "common.tplvalues.merge" (dict "values" (list .Values.orborus.podLabels .Values.commonLabels) "context" .) }} + selector: + matchLabels: {{- include "shuffle.orborus.matchLabels" ( dict "customLabels" $podLabels "context" $ ) | nindent 6 }} + template: + metadata: + {{- if .Values.orborus.podAnnotations }} + annotations: {{- include "common.tplvalues.render" (dict "value" .Values.orborus.podAnnotations "context" $) | nindent 8 }} + {{- end }} + labels: {{- include "shuffle.orborus.labels" ( dict "customLabels" $podLabels "context" $ ) | nindent 8 }} + spec: + {{- include "shuffle.imagePullSecrets" . | nindent 6 }} + serviceAccountName: {{ template "shuffle.orborus.serviceAccountName" . }} + automountServiceAccountToken: {{ .Values.orborus.automountServiceAccountToken }} + {{- if .Values.orborus.hostAliases }} + hostAliases: {{- include "common.tplvalues.render" (dict "value" .Values.orborus.hostAliases "context" $) | nindent 8 }} + {{- end }} + {{- if .Values.orborus.affinity }} + affinity: {{- include "common.tplvalues.render" ( dict "value" .Values.orborus.affinity "context" $) | nindent 8 }} + {{- else }} + affinity: + podAffinity: {{- include "common.affinities.pods" (dict "type" .Values.orborus.podAffinityPreset "component" "orborus" "customLabels" $podLabels "context" $) | nindent 10 }} + podAntiAffinity: {{- include "common.affinities.pods" (dict "type" .Values.orborus.podAntiAffinityPreset "component" "orborus" "customLabels" $podLabels "context" $) | nindent 10 }} + nodeAffinity: {{- include "common.affinities.nodes" (dict "type" .Values.orborus.nodeAffinityPreset.type "key" .Values.orborus.nodeAffinityPreset.key "values" .Values.orborus.nodeAffinityPreset.values) | nindent 10 }} + {{- end }} + {{- if .Values.orborus.nodeSelector }} + nodeSelector: {{- include "common.tplvalues.render" ( dict "value" .Values.orborus.nodeSelector "context" $) | nindent 8 }} + {{- end }} + {{- if .Values.orborus.tolerations }} + tolerations: {{- include "common.tplvalues.render" (dict "value" .Values.orborus.tolerations "context" .) | nindent 8 }} + {{- end }} + {{- if .Values.orborus.priorityClassName }} + priorityClassName: {{ .Values.orborus.priorityClassName | quote }} + {{- end }} + {{- if .Values.orborus.schedulerName }} + schedulerName: {{ .Values.orborus.schedulerName | quote }} + {{- end }} + {{- if .Values.orborus.topologySpreadConstraints }} + topologySpreadConstraints: {{- include "common.tplvalues.render" (dict "value" .Values.orborus.topologySpreadConstraints "context" .) | nindent 8 }} + {{- end }} + {{- if .Values.orborus.podSecurityContext.enabled }} + securityContext: {{- omit .Values.orborus.podSecurityContext "enabled" | toYaml | nindent 8 }} + {{- end }} + {{- if .Values.orborus.terminationGracePeriodSeconds }} + terminationGracePeriodSeconds: {{ .Values.orborus.terminationGracePeriodSeconds }} + {{- end }} + initContainers: + {{- if .Values.orborus.initContainers }} + {{- include "common.tplvalues.render" (dict "value" .Values.orborus.initContainers "context" $) | nindent 8 }} + {{- end }} + containers: + - name: orborus + image: {{ template "shuffle.orborus.image" . }} + imagePullPolicy: {{ .Values.orborus.image.pullPolicy }} + {{- if .Values.orborus.containerSecurityContext.enabled }} + securityContext: {{- include "common.compatibility.renderSecurityContext" (dict "secContext" .Values.orborus.containerSecurityContext "context" $) | nindent 12 }} + {{- end }} + {{- if .Values.diagnosticMode.enabled }} + command: {{- include "common.tplvalues.render" (dict "value" .Values.diagnosticMode.command "context" $) | nindent 12 }} + {{- else if .Values.orborus.command }} + command: {{- include "common.tplvalues.render" (dict "value" .Values.orborus.command "context" $) | nindent 12 }} + {{- end }} + {{- if .Values.diagnosticMode.enabled }} + args: {{- include "common.tplvalues.render" (dict "value" .Values.diagnosticMode.args "context" $) | nindent 12 }} + {{- else if .Values.orborus.args }} + args: {{- include "common.tplvalues.render" (dict "value" .Values.orborus.args "context" $) | nindent 12 }} + {{- end }} + env: + - name: RUNNING_MODE + value: kubernetes + - name: IS_KUBERNETES + value: "true" + {{- if .Values.orborus.extraEnvVars }} + {{- include "common.tplvalues.render" (dict "value" .Values.orborus.extraEnvVars "context" $) | nindent 12 }} + {{- end }} + envFrom: + - configMapRef: + name: {{ include "shuffle.orborus.name" . }}-env + {{- if .Values.orborus.extraEnvVarsCM }} + - configMapRef: + name: {{ include "common.tplvalues.render" (dict "value" .Values.orborus.extraEnvVarsCM "context" $) }} + {{- end }} + {{- if .Values.orborus.extraEnvVarsSecret }} + - secretRef: + name: {{ include "common.tplvalues.render" (dict "value" .Values.orborus.extraEnvVarsSecret "context" $) }} + {{- end }} + {{- if .Values.orborus.resources }} + resources: {{- toYaml .Values.orborus.resources | nindent 12 }} + {{- else if ne .Values.orborus.resourcesPreset "none" }} + resources: {{- include "common.resources.preset" (dict "type" .Values.orborus.resourcesPreset) | nindent 12 }} + {{- end }} + ports: + - name: http + containerPort: 8080 + {{- if not .Values.diagnosticMode.enabled }} + {{- if .Values.orborus.customLivenessProbe }} + livenessProbe: {{- include "common.tplvalues.render" (dict "value" .Values.orborus.customLivenessProbe "context" $) | nindent 12 }} + {{- else if .Values.orborus.livenessProbe.enabled }} + livenessProbe: {{- include "common.tplvalues.render" (dict "value" (omit .Values.orborus.livenessProbe "enabled") "context" $) | nindent 12 }} + httpGet: + path: / + port: 8080 + {{- end }} + {{- if .Values.orborus.customReadinessProbe }} + readinessProbe: {{- include "common.tplvalues.render" (dict "value" .Values.orborus.customReadinessProbe "context" $) | nindent 12 }} + {{- else if .Values.orborus.readinessProbe.enabled }} + readinessProbe: {{- include "common.tplvalues.render" (dict "value" (omit .Values.orborus.readinessProbe "enabled") "context" $) | nindent 12 }} + httpGet: + path: / + port: 8080 + {{- end }} + {{- if .Values.orborus.customStartupProbe }} + startupProbe: {{- include "common.tplvalues.render" (dict "value" .Values.orborus.customStartupProbe "context" $) | nindent 12 }} + {{- else if .Values.orborus.startupProbe.enabled }} + startupProbe: {{- include "common.tplvalues.render" (dict "value" (omit .Values.orborus.startupProbe "enabled") "context" $) | nindent 12 }} + httpGet: + path: / + port: 8080 + {{- end }} + {{- end }} + {{- if .Values.orborus.lifecycleHooks }} + lifecycle: {{- include "common.tplvalues.render" (dict "value" .Values.orborus.lifecycleHooks "context" $) | nindent 12 }} + {{- end }} + volumeMounts: + - name: empty-dir + mountPath: /tmp + subPath: tmp-dir + {{- if .Values.orborus.extraVolumeMounts }} + {{- include "common.tplvalues.render" (dict "value" .Values.orborus.extraVolumeMounts "context" $) | nindent 12 }} + {{- end }} + {{- if .Values.orborus.sidecars }} + {{- include "common.tplvalues.render" ( dict "value" .Values.orborus.sidecars "context" $) | nindent 8 }} + {{- end }} + volumes: + - name: empty-dir + emptyDir: {} + {{- if .Values.orborus.extraVolumes }} + {{- include "common.tplvalues.render" (dict "value" .Values.orborus.extraVolumes "context" $) | nindent 8 }} + {{- end }} diff --git a/charts/shuffle/templates/orborus/orborus-hpa.yaml b/charts/shuffle/templates/orborus/orborus-hpa.yaml new file mode 100644 index 00000000..12bf073f --- /dev/null +++ b/charts/shuffle/templates/orborus/orborus-hpa.yaml @@ -0,0 +1,43 @@ +{{- if .Values.orborus.autoscaling.hpa.enabled }} +apiVersion: {{ include "common.capabilities.hpa.apiVersion" . }} +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "shuffle.orborus.name" . }} + namespace: {{ include "common.names.namespace" . | quote }} + labels: {{- include "shuffle.orborus.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- if .Values.commonAnnotations }} + annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +spec: + scaleTargetRef: + apiVersion: {{ include "common.capabilities.deployment.apiVersion" . }} + kind: Deployment + name: {{ include "shuffle.orborus.name" . }} + minReplicas: {{ .Values.orborus.autoscaling.hpa.minReplicas }} + maxReplicas: {{ .Values.orborus.autoscaling.hpa.maxReplicas }} + metrics: + {{- if .Values.orborus.autoscaling.hpa.targetMemory }} + - type: Resource + resource: + name: memory + {{- if semverCompare "<1.23-0" (include "common.capabilities.kubeVersion" .) }} + targetAverageUtilization: {{ .Values.orborus.autoscaling.hpa.targetMemory }} + {{- else }} + target: + type: Utilization + averageUtilization: {{ .Values.worker.autoscaling.hpa.targetMemory }} + {{- end }} + {{- end }} + {{- if .Values.orborus.autoscaling.hpa.targetCPU }} + - type: Resource + resource: + name: cpu + {{- if semverCompare "<1.23-0" (include "common.capabilities.kubeVersion" .) }} + targetAverageUtilization: {{ .Values.orborus.autoscaling.hpa.targetCPU }} + {{- else }} + target: + type: Utilization + averageUtilization: {{ .Values.worker.autoscaling.hpa.targetCPU }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/shuffle/templates/orborus/orborus-network-policy.yaml b/charts/shuffle/templates/orborus/orborus-network-policy.yaml new file mode 100644 index 00000000..6e03b9e3 --- /dev/null +++ b/charts/shuffle/templates/orborus/orborus-network-policy.yaml @@ -0,0 +1,72 @@ +{{- if .Values.orborus.networkPolicy.enabled }} +kind: NetworkPolicy +apiVersion: {{ include "common.capabilities.networkPolicy.apiVersion" . }} +metadata: + name: {{ template "shuffle.orborus.name" . }} + namespace: {{ include "common.names.namespace" . | quote }} + labels: {{- include "shuffle.orborus.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- if .Values.commonAnnotations }} + annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +spec: + {{- $podLabels := include "common.tplvalues.merge" ( dict "values" ( list .Values.orborus.podLabels .Values.commonLabels ) "context" . ) }} + podSelector: + matchLabels: {{- include "shuffle.orborus.matchLabels" ( dict "customLabels" $podLabels "context" $ ) | nindent 6 }} + policyTypes: + - Ingress + - Egress + egress: + {{- if .Values.orborus.networkPolicy.allowExternalEgress }} + - {} + {{- else }} + # Allow DNS resolution with an in-cluster DNS server + - ports: + - port: 53 + protocol: UDP + - port: 53 + protocol: TCP + to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: kube-system + # Allow access to backend + - ports: + - port: 5001 + protocol: TCP + to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: {{ .Release.Namespace }} + podSelector: + matchLabels: {{ include "shuffle.backend.matchLabels" . | nindent 14 }} + # Allow access to workers + - ports: + - port: 33333 + protocol: TCP + to: + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: {{ .Release.Namespace }} + podSelector: + matchLabels: {{ include "shuffle.worker.matchLabels" . | nindent 14 }} + {{- if .Values.orborus.networkPolicy.extraEgress }} + {{- include "common.tplvalues.render" ( dict "value" .Values.orborus.networkPolicy.extraEgress "context" $ ) | nindent 4 }} + {{- end }} + {{- end }} + ingress: + - ports: + - port: 8080 + protocol: TCP + {{- if not .Values.orborus.networkPolicy.allowExternal }} + from: + # Allow traffic from workers + - namespaceSelector: + matchLabels: + kubernetes.io/metadata.name: {{ .Release.Namespace }} + podSelector: + matchLabels: {{ include "shuffle.worker.matchLabels" . | nindent 14 }} + {{- end }} + {{- if .Values.orborus.networkPolicy.extraIngress }} + {{- include "common.tplvalues.render" ( dict "value" .Values.orborus.networkPolicy.extraIngress "context" $ ) | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/shuffle/templates/orborus/orborus-pdb.yaml b/charts/shuffle/templates/orborus/orborus-pdb.yaml new file mode 100644 index 00000000..827b01bc --- /dev/null +++ b/charts/shuffle/templates/orborus/orborus-pdb.yaml @@ -0,0 +1,21 @@ +{{- if .Values.orborus.pdb.create }} +apiVersion: {{ include "common.capabilities.policy.apiVersion" . }} +kind: PodDisruptionBudget +metadata: + name: {{ include "shuffle.orborus.name" . }} + namespace: {{ include "common.names.namespace" . | quote }} + labels: {{- include "shuffle.orborus.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- if .Values.commonAnnotations }} + annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +spec: + {{- if .Values.orborus.pdb.minAvailable }} + minAvailable: {{ .Values.orborus.pdb.minAvailable }} + {{- end }} + {{- if or .Values.orborus.pdb.maxUnavailable ( not .Values.orborus.pdb.minAvailable ) }} + maxUnavailable: {{ .Values.orborus.pdb.maxUnavailable | default 1 }} + {{- end }} + {{- $podLabels := include "common.tplvalues.merge" ( dict "values" ( list .Values.orborus.podLabels .Values.commonLabels ) "context" . ) }} + selector: + matchLabels: {{- include "shuffle.orborus.matchLabels" ( dict "customLabels" $podLabels "context" $ ) | nindent 6 }} +{{- end }} diff --git a/charts/shuffle/templates/orborus/orborus-role-binding.yaml b/charts/shuffle/templates/orborus/orborus-role-binding.yaml new file mode 100644 index 00000000..f4c2689e --- /dev/null +++ b/charts/shuffle/templates/orborus/orborus-role-binding.yaml @@ -0,0 +1,18 @@ +{{ if .Values.orborus.rbac.create }} +kind: RoleBinding +apiVersion: {{ include "common.capabilities.rbac.apiVersion" . }} +metadata: + name: {{ include "shuffle.orborus.name" . }} + namespace: {{ include "common.names.namespace" . | quote }} + labels: {{- include "shuffle.orborus.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- if .Values.commonAnnotations }} + annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +subjects: + - kind: ServiceAccount + name: {{ include "shuffle.orborus.serviceAccountName" . }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: {{ include "shuffle.orborus.name" . }} +{{- end }} diff --git a/charts/shuffle/templates/orborus/orborus-role.yaml b/charts/shuffle/templates/orborus/orborus-role.yaml new file mode 100644 index 00000000..90041d89 --- /dev/null +++ b/charts/shuffle/templates/orborus/orborus-role.yaml @@ -0,0 +1,29 @@ +{{ if .Values.orborus.rbac.create }} +kind: Role +apiVersion: {{ include "common.capabilities.rbac.apiVersion" . }} +metadata: + name: {{ include "shuffle.orborus.name" . }} + namespace: {{ include "common.names.namespace" . | quote }} + labels: {{- include "shuffle.orborus.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- if .Values.commonAnnotations }} + annotations: {{- include "common.tplvalues.render" ( dict "value" .Values.commonAnnotations "context" $ ) | nindent 4 }} + {{- end }} +rules: + - verbs: + - list + - create + - delete + apiGroups: + - '' + resources: + - pods + - services + - verbs: + - list + - create + - delete + apiGroups: + - apps + resources: + - deployments +{{- end }} diff --git a/charts/shuffle/templates/orborus/orborus-service-account.yaml b/charts/shuffle/templates/orborus/orborus-service-account.yaml new file mode 100644 index 00000000..4a5c7f3a --- /dev/null +++ b/charts/shuffle/templates/orborus/orborus-service-account.yaml @@ -0,0 +1,13 @@ +{{- if .Values.orborus.serviceAccount.create }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "shuffle.orborus.serviceAccountName" . }} + namespace: {{ include "common.names.namespace" . | quote }} + labels: {{- include "shuffle.orborus.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- if or .Values.orborus.serviceAccount.annotations .Values.commonAnnotations }} + {{- $annotations := include "common.tplvalues.merge" (dict "values" (list .Values.orborus.serviceAccount.annotations .Values.commonAnnotations) "context" .) }} + annotations: {{- include "common.tplvalues.render" ( dict "value" $annotations "context" $ ) | nindent 4 }} + {{- end }} +automountServiceAccountToken: {{ .Values.orborus.serviceAccount.automountServiceAccountToken }} +{{- end }} diff --git a/charts/shuffle/templates/orborus/orborus-vpa.yaml b/charts/shuffle/templates/orborus/orborus-vpa.yaml new file mode 100644 index 00000000..a1a7c386 --- /dev/null +++ b/charts/shuffle/templates/orborus/orborus-vpa.yaml @@ -0,0 +1,38 @@ +{{- if and (.Capabilities.APIVersions.Has "autoscaling.k8s.io/v1/VerticalPodAutoscaler") .Values.orborus.autoscaling.vpa.enabled }} +apiVersion: autoscaling.k8s.io/v1 +kind: VerticalPodAutoscaler +metadata: + name: {{ include "shuffle.orborus.name" . }} + namespace: {{ include "common.names.namespace" . | quote }} + labels: {{- include "shuffle.orborus.labels" ( dict "customLabels" .Values.commonLabels "context" $ ) | nindent 4 }} + {{- if or .Values.orborus.autoscaling.vpa.annotations .Values.commonAnnotations }} + {{- $annotations := include "common.tplvalues.merge" ( dict "values" ( list .Values.orborus.autoscaling.vpa.annotations .Values.commonAnnotations ) "context" . ) }} + annotations: {{- include "common.tplvalues.render" ( dict "value" $annotations "context" $) | nindent 4 }} + {{- end }} +spec: + resourcePolicy: + containerPolicies: + - containerName: orborus + {{- with .Values.orborus.autoscaling.vpa.controlledResources }} + controlledResources: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.orborus.autoscaling.vpa.maxAllowed }} + maxAllowed: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.orborus.autoscaling.vpa.minAllowed }} + minAllowed: + {{- toYaml . | nindent 8 }} + {{- end }} + targetRef: + apiVersion: {{ include "common.capabilities.deployment.apiVersion" . }} + kind: Deployment + name: {{ include "orborus.names.name" . }} + {{- if .Values.orborus.autoscaling.vpa.updatePolicy }} + updatePolicy: + {{- with .Values.orborus.autoscaling.vpa.updatePolicy.updateMode }} + updateMode: {{ . }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/shuffle/templates/vault-secrets.yaml b/charts/shuffle/templates/vault-secrets.yaml new file mode 100644 index 00000000..0755f20a --- /dev/null +++ b/charts/shuffle/templates/vault-secrets.yaml @@ -0,0 +1,19 @@ +{{ range $index, $element := .Values.vault.secrets }} +{{- $vaultRole := $element.vaultRole | default $.Values.vault.role }} +--- +apiVersion: ricoberger.de/v1alpha1 +kind: VaultSecret +metadata: + name: {{ include "common.names.fullname" $ }}-{{ $element.name }} +spec: + type: {{ $element.type }} + path: {{ $element.path }} + {{- if $vaultRole }} + vaultRole: {{ $vaultRole }} + {{- end }} + {{- range $k, $v := $element }} + {{- if (not (has $k (list "name" "type" "path" "vaultRole"))) }} + {{ $k }}: {{ $v }} + {{- end }} + {{- end }} +{{- end }} diff --git a/charts/shuffle/values.schema.json b/charts/shuffle/values.schema.json new file mode 100644 index 00000000..62e2dff2 --- /dev/null +++ b/charts/shuffle/values.schema.json @@ -0,0 +1,2562 @@ +{ + "title": "Chart Values", + "type": "object", + "properties": { + "global": { + "type": "object", + "properties": { + "imageRegistry": { + "type": "string", + "description": "Global Docker image registry", + "default": "" + }, + "imagePullSecrets": { + "type": "array", + "description": "Global Docker registry secret names as an array", + "default": [], + "items": {} + }, + "defaultStorageClass": { + "type": "string", + "description": "Global default StorageClass for Persistent Volume(s)", + "default": "" + }, + "compatibility": { + "type": "object", + "properties": { + "openshift": { + "type": "object", + "properties": { + "adaptSecurityContext": { + "type": "string", + "description": "Adapt the securityContext sections of the deployment to make them compatible with Openshift restricted-v2 SCC: remove runAsUser, runAsGroup and fsGroup and let the platform use their allowed default IDs. Possible values: auto (apply if the detected running cluster is Openshift), force (perform the adaptation always), disabled (do not perform adaptation)", + "default": "auto" + } + } + }, + "omitEmptySeLinuxOptions": { + "type": "boolean", + "description": "If set to true, removes the seLinuxOptions from the securityContexts when it is set to an empty object", + "default": false + } + } + } + } + }, + "kubeVersion": { + "type": "string", + "description": "Override Kubernetes version", + "default": "" + }, + "nameOverride": { + "type": "string", + "description": "String to partially override common.names.name", + "default": "" + }, + "fullnameOverride": { + "type": "string", + "description": "String to fully override common.names.fullname", + "default": "" + }, + "namespaceOverride": { + "type": "string", + "description": "String to fully override common.names.namespace", + "default": "" + }, + "commonLabels": { + "type": "object", + "description": "Labels to add to all deployed objects", + "default": {} + }, + "commonAnnotations": { + "type": "object", + "description": "Annotations to add to all deployed objects", + "default": {} + }, + "clusterDomain": { + "type": "string", + "description": "Kubernetes cluster domain name", + "default": "cluster.local" + }, + "extraDeploy": { + "type": "array", + "description": "Array of extra objects to deploy with the release", + "default": [], + "items": {} + }, + "diagnosticMode": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable diagnostic mode (all probes will be disabled and the command will be overridden)", + "default": false + }, + "command": { + "type": "array", + "description": "Command to override all containers in the chart release", + "default": [ + "sleep" + ], + "items": { + "type": "string" + } + }, + "args": { + "type": "array", + "description": "Args to override all containers in the chart release", + "default": [ + "infinity" + ], + "items": { + "type": "string" + } + } + } + }, + "shuffle": { + "type": "object", + "properties": { + "baseUrl": { + "type": "string", + "description": "The external base URL under which Shuffle is reachable.", + "default": "" + }, + "org": { + "type": "string", + "description": "Default shuffle organization", + "default": "Shuffle" + }, + "appRegistry": { + "type": "string", + "description": "The registry from / to which shuffle apps are pulled / pushed", + "default": "" + }, + "timezone": { + "type": "string", + "description": "The timezone used by Shuffle", + "default": "Europe/Berlin" + } + } + }, + "backend": { + "type": "object", + "properties": { + "image": { + "type": "object", + "properties": { + "registry": { + "type": "string", + "description": "backend image registry", + "default": "ghcr.io" + }, + "repository": { + "type": "string", + "description": "backend image repository", + "default": "shuffle/shuffle-backend" + }, + "digest": { + "type": "string", + "description": "backend image digest in the way sha256:aa.... Please note this parameter, if set, will override the tag image tag (immutable tags are recommended)", + "default": "" + }, + "pullPolicy": { + "type": "string", + "description": "backend image pull policy", + "default": "IfNotPresent" + }, + "pullSecrets": { + "type": "array", + "description": "backend image pull secrets", + "default": [], + "items": {} + } + } + }, + "replicaCount": { + "type": "number", + "description": "Number of backend replicas to deploy", + "default": 1 + }, + "livenessProbe": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable livenessProbe on backend containers", + "default": false + }, + "initialDelaySeconds": { + "type": "number", + "description": "Initial delay seconds for livenessProbe", + "default": 0 + }, + "periodSeconds": { + "type": "number", + "description": "Period seconds for livenessProbe", + "default": 15 + }, + "timeoutSeconds": { + "type": "number", + "description": "Timeout seconds for livenessProbe", + "default": 1 + }, + "failureThreshold": { + "type": "number", + "description": "Failure threshold for livenessProbe", + "default": 4 + }, + "successThreshold": { + "type": "number", + "description": "Success threshold for livenessProbe", + "default": 1 + } + } + }, + "readinessProbe": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable readinessProbe on backend containers", + "default": false + }, + "initialDelaySeconds": { + "type": "number", + "description": "Initial delay seconds for readinessProbe", + "default": 0 + }, + "periodSeconds": { + "type": "number", + "description": "Period seconds for readinessProbe", + "default": 5 + }, + "timeoutSeconds": { + "type": "number", + "description": "Timeout seconds for readinessProbe", + "default": 1 + }, + "failureThreshold": { + "type": "number", + "description": "Failure threshold for readinessProbe", + "default": 3 + }, + "successThreshold": { + "type": "number", + "description": "Success threshold for readinessProbe", + "default": 1 + } + } + }, + "startupProbe": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable startupProbe on backend containers", + "default": false + }, + "initialDelaySeconds": { + "type": "number", + "description": "Initial delay seconds for startupProbe", + "default": 0 + }, + "periodSeconds": { + "type": "number", + "description": "Period seconds for startupProbe", + "default": 1 + }, + "timeoutSeconds": { + "type": "number", + "description": "Timeout seconds for startupProbe", + "default": 1 + }, + "failureThreshold": { + "type": "number", + "description": "Failure threshold for startupProbe", + "default": 60 + }, + "successThreshold": { + "type": "number", + "description": "Success threshold for startupProbe", + "default": 1 + } + } + }, + "customLivenessProbe": { + "type": "object", + "description": "Custom livenessProbe that overrides the default one", + "default": {} + }, + "customReadinessProbe": { + "type": "object", + "description": "Custom readinessProbe that overrides the default one", + "default": {} + }, + "customStartupProbe": { + "type": "object", + "description": "Custom startupProbe that overrides the default one", + "default": {} + }, + "resourcesPreset": { + "type": "string", + "description": "Set backend container resources according to one common preset (allowed values: none, nano, small, medium, large, xlarge, 2xlarge). This is ignored if backend.resources is set (backend.resources is recommended for production).", + "default": "small" + }, + "resources": { + "type": "object", + "description": "Set backend container requests and limits for different resources like CPU or memory (essential for production workloads)", + "default": {} + }, + "podSecurityContext": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable backend pods' Security Context", + "default": true + }, + "fsGroupChangePolicy": { + "type": "string", + "description": "Set filesystem group change policy for backend pods", + "default": "Always" + }, + "sysctls": { + "type": "array", + "description": "Set kernel settings using the sysctl interface for backend pods", + "default": [], + "items": {} + }, + "supplementalGroups": { + "type": "array", + "description": "Set filesystem extra groups for backend pods", + "default": [], + "items": {} + }, + "fsGroup": { + "type": "number", + "description": "Set fsGroup in backend pods' Security Context", + "default": 1001 + } + } + }, + "containerSecurityContext": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enabled backend container' Security Context", + "default": true + }, + "runAsUser": { + "type": "number", + "description": "Set runAsUser in backend container' Security Context", + "default": 1000 + }, + "runAsGroup": { + "type": "number", + "description": "Set runAsGroup in backend container' Security Context", + "default": 1000 + }, + "runAsNonRoot": { + "type": "boolean", + "description": "Set runAsNonRoot in backend container' Security Context", + "default": true + }, + "readOnlyRootFilesystem": { + "type": "boolean", + "description": "Set readOnlyRootFilesystem in backend container' Security Context", + "default": true + }, + "privileged": { + "type": "boolean", + "description": "Set privileged in backend container' Security Context", + "default": false + }, + "allowPrivilegeEscalation": { + "type": "boolean", + "description": "Set allowPrivilegeEscalation in backend container' Security Context", + "default": false + }, + "capabilities": { + "type": "object", + "properties": { + "drop": { + "type": "array", + "description": "List of capabilities to be dropped in backend container", + "default": [ + "ALL" + ], + "items": { + "type": "string" + } + } + } + }, + "seccompProfile": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Set seccomp profile in backend container", + "default": "RuntimeDefault" + } + } + } + } + }, + "command": { + "type": "array", + "description": "Override default backend container command (useful when using custom images)", + "default": [], + "items": {} + }, + "args": { + "type": "array", + "description": "Override default backend container args (useful when using custom images)", + "default": [], + "items": {} + }, + "automountServiceAccountToken": { + "type": "boolean", + "description": "Mount Service Account token in backend pods", + "default": true + }, + "hostAliases": { + "type": "array", + "description": "backend pods host aliases", + "default": [], + "items": {} + }, + "daemonsetAnnotations": { + "type": "object", + "description": "Annotations for backend daemonset", + "default": {} + }, + "deploymentAnnotations": { + "type": "object", + "description": "Annotations for backend deployment", + "default": {} + }, + "statefulsetAnnotations": { + "type": "object", + "description": "Annotations for backend statefulset", + "default": {} + }, + "podLabels": { + "type": "object", + "description": "Extra labels for backend pods", + "default": {} + }, + "podAnnotations": { + "type": "object", + "description": "Annotations for backend pods", + "default": {} + }, + "podAffinityPreset": { + "type": "string", + "description": "Pod affinity preset. Ignored if `backend.affinity` is set. Allowed values: `soft` or `hard`", + "default": "" + }, + "podAntiAffinityPreset": { + "type": "string", + "description": "Pod anti-affinity preset. Ignored if `backend.affinity` is set. Allowed values: `soft` or `hard`", + "default": "soft" + }, + "nodeAffinityPreset": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Node affinity preset type. Ignored if `backend.affinity` is set. Allowed values: `soft` or `hard`", + "default": "" + }, + "key": { + "type": "string", + "description": "Node label key to match. Ignored if `backend.affinity` is set", + "default": "" + }, + "values": { + "type": "array", + "description": "Node label values to match. Ignored if `backend.affinity` is set", + "default": [], + "items": {} + } + } + }, + "affinity": { + "type": "object", + "description": "Affinity for backend pods assignment", + "default": {} + }, + "nodeSelector": { + "type": "object", + "description": "Node labels for backend pods assignment", + "default": {} + }, + "tolerations": { + "type": "array", + "description": "Tolerations for backend pods assignment", + "default": [], + "items": {} + }, + "updateStrategy": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "backend statefulset strategy type", + "default": "RollingUpdate" + } + } + }, + "podManagementPolicy": { + "type": "string", + "description": "Pod management policy for backend statefulset", + "default": "OrderedReady" + }, + "priorityClassName": { + "type": "string", + "description": "backend pods' priorityClassName", + "default": "" + }, + "topologySpreadConstraints": { + "type": "array", + "description": "Topology Spread Constraints for backend pod assignment spread across your cluster among failure-domains", + "default": [], + "items": {} + }, + "schedulerName": { + "type": "string", + "description": "Name of the k8s scheduler (other than default) for backend pods", + "default": "" + }, + "terminationGracePeriodSeconds": { + "type": "string", + "description": "Seconds backend pods need to terminate gracefully", + "default": "" + }, + "lifecycleHooks": { + "type": "object", + "description": "for backend containers to automate configuration before or after startup", + "default": {} + }, + "extraEnvVars": { + "type": "array", + "description": "Array with extra environment variables to add to backend containers", + "default": [], + "items": {} + }, + "extraEnvVarsCM": { + "type": "string", + "description": "Name of existing ConfigMap containing extra env vars for backend containers", + "default": "" + }, + "extraEnvVarsSecret": { + "type": "string", + "description": "Name of existing Secret containing extra env vars for backend containers", + "default": "" + }, + "extraVolumes": { + "type": "array", + "description": "Optionally specify extra list of additional volumes for the backend pods", + "default": [], + "items": {} + }, + "extraVolumeMounts": { + "type": "array", + "description": "Optionally specify extra list of additional volumeMounts for the backend containers", + "default": [], + "items": {} + }, + "sidecars": { + "type": "array", + "description": "Add additional sidecar containers to the backend pods", + "default": [], + "items": {} + }, + "initContainers": { + "type": "array", + "description": "Add additional init containers to the backend pods", + "default": [], + "items": {} + }, + "pdb": { + "type": "object", + "properties": { + "create": { + "type": "boolean", + "description": "Enable/disable a Pod Disruption Budget creation", + "default": true + }, + "minAvailable": { + "type": "string", + "description": "Minimum number/percentage of pods that should remain scheduled", + "default": "" + }, + "maxUnavailable": { + "type": "string", + "description": "Maximum number/percentage of pods that may be made unavailable. Defaults to `1` if both `backend.pdb.minAvailable` and `backend.pdb.maxUnavailable` are empty.", + "default": "" + } + } + }, + "autoscaling": { + "type": "object", + "properties": { + "vpa": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable VPA for backend pods", + "default": false + }, + "annotations": { + "type": "object", + "description": "Annotations for VPA resource", + "default": {} + }, + "controlledResources": { + "type": "array", + "description": "VPA List of resources that the vertical pod autoscaler can control. Defaults to cpu and memory", + "default": [], + "items": {} + }, + "maxAllowed": { + "type": "object", + "description": "VPA Max allowed resources for the pod", + "default": {} + }, + "minAllowed": { + "type": "object", + "description": "VPA Min allowed resources for the pod", + "default": {} + }, + "updatePolicy": { + "type": "object", + "properties": { + "updateMode": { + "type": "string", + "description": "Autoscaling update policy", + "default": "Auto" + } + } + } + } + }, + "hpa": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable HPA for backend pods", + "default": false + }, + "minReplicas": { + "type": "string", + "description": "Minimum number of replicas", + "default": "" + }, + "maxReplicas": { + "type": "string", + "description": "Maximum number of replicas", + "default": "" + }, + "targetCPU": { + "type": "string", + "description": "Target CPU utilization percentage", + "default": "" + }, + "targetMemory": { + "type": "string", + "description": "Target Memory utilization percentage", + "default": "" + } + } + } + } + }, + "serviceAccount": { + "type": "object", + "properties": { + "create": { + "type": "boolean", + "description": "Specifies whether a ServiceAccount should be created", + "default": true + }, + "name": { + "type": "string", + "description": "The name of the ServiceAccount to use.", + "default": "" + }, + "annotations": { + "type": "object", + "description": "Additional Service Account annotations (evaluated as a template)", + "default": {} + }, + "automountServiceAccountToken": { + "type": "boolean", + "description": "Automount service account token for the server service account", + "default": true + } + } + }, + "rbac": { + "type": "object", + "properties": { + "create": { + "type": "boolean", + "description": "Specifies whether RBAC resources should be created", + "default": true + } + } + }, + "networkPolicy": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Specifies whether a NetworkPolicy should be created", + "default": true + }, + "allowExternal": { + "type": "boolean", + "description": "Don't require server label for connections", + "default": true + }, + "allowExternalEgress": { + "type": "boolean", + "description": "Allow the pod to access any range of port and all destinations.", + "default": true + }, + "extraIngress": { + "type": "array", + "description": "Add extra ingress rules to the NetworkPolicy", + "default": [], + "items": {} + }, + "extraEgress": { + "type": "array", + "description": "Add extra ingress rules to the NetworkPolicy (ignored if allowExternalEgress=true)", + "default": [], + "items": {} + } + } + }, + "cleanupSchedule": { + "type": "number", + "description": "The interval in seconds at which the cleanup job runs", + "default": 300 + }, + "openSearch": { + "type": "object", + "properties": { + "url": { + "type": "string", + "description": "The URL at which OpenSearch is available", + "default": "http://{{ .Release.Name }}-opensearch:9200" + }, + "username": { + "type": "string", + "description": "The username that is used for authenticating with OpenSearch", + "default": "admin" + }, + "certificateFile": { + "type": "string", + "description": "The path to a custom OpenSearch certificate file", + "default": "" + }, + "skipSSLVerify": { + "type": "boolean", + "description": "Skip SSL verification", + "default": false + }, + "indexPrefix": { + "type": "string", + "description": "A prefix for OpenSearch indices", + "default": "" + } + } + }, + "apps": { + "type": "object", + "properties": { + "downloadLocation": { + "type": "string", + "description": "The location to a git repository from which default appps are downloaded on startup.", + "default": "https://github.com/shuffle/python-apps" + }, + "downloadBranch": { + "type": "string", + "description": "The branch from which apps should be downloaded on startup.", + "default": "master" + }, + "forceUpdate": { + "type": "boolean", + "description": "Force an update of apps on startup.", + "default": false + } + } + } + } + }, + "frontend": { + "type": "object", + "properties": { + "image": { + "type": "object", + "properties": { + "registry": { + "type": "string", + "description": "frontend image registry", + "default": "ghcr.io" + }, + "repository": { + "type": "string", + "description": "frontend image repository", + "default": "shuffle/shuffle-frontend" + }, + "digest": { + "type": "string", + "description": "frontend image digest in the way sha256:aa.... Please note this parameter, if set, will override the tag image tag (immutable tags are recommended)", + "default": "" + }, + "pullPolicy": { + "type": "string", + "description": "frontend image pull policy", + "default": "IfNotPresent" + }, + "pullSecrets": { + "type": "array", + "description": "frontend image pull secrets", + "default": [], + "items": {} + } + } + }, + "replicaCount": { + "type": "number", + "description": "Number of frontend replicas to deploy", + "default": 1 + }, + "livenessProbe": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable livenessProbe on frontend containers", + "default": false + }, + "initialDelaySeconds": { + "type": "number", + "description": "Initial delay seconds for livenessProbe", + "default": 0 + }, + "periodSeconds": { + "type": "number", + "description": "Period seconds for livenessProbe", + "default": 15 + }, + "timeoutSeconds": { + "type": "number", + "description": "Timeout seconds for livenessProbe", + "default": 1 + }, + "failureThreshold": { + "type": "number", + "description": "Failure threshold for livenessProbe", + "default": 4 + }, + "successThreshold": { + "type": "number", + "description": "Success threshold for livenessProbe", + "default": 1 + } + } + }, + "readinessProbe": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable readinessProbe on frontend containers", + "default": false + }, + "initialDelaySeconds": { + "type": "number", + "description": "Initial delay seconds for readinessProbe", + "default": 0 + }, + "periodSeconds": { + "type": "number", + "description": "Period seconds for readinessProbe", + "default": 5 + }, + "timeoutSeconds": { + "type": "number", + "description": "Timeout seconds for readinessProbe", + "default": 1 + }, + "failureThreshold": { + "type": "number", + "description": "Failure threshold for readinessProbe", + "default": 3 + }, + "successThreshold": { + "type": "number", + "description": "Success threshold for readinessProbe", + "default": 1 + } + } + }, + "startupProbe": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable startupProbe on frontend containers", + "default": false + }, + "initialDelaySeconds": { + "type": "number", + "description": "Initial delay seconds for startupProbe", + "default": 0 + }, + "periodSeconds": { + "type": "number", + "description": "Period seconds for startupProbe", + "default": 1 + }, + "timeoutSeconds": { + "type": "number", + "description": "Timeout seconds for startupProbe", + "default": 1 + }, + "failureThreshold": { + "type": "number", + "description": "Failure threshold for startupProbe", + "default": 60 + }, + "successThreshold": { + "type": "number", + "description": "Success threshold for startupProbe", + "default": 1 + } + } + }, + "customLivenessProbe": { + "type": "object", + "description": "Custom livenessProbe that overrides the default one", + "default": {} + }, + "customReadinessProbe": { + "type": "object", + "description": "Custom readinessProbe that overrides the default one", + "default": {} + }, + "customStartupProbe": { + "type": "object", + "description": "Custom startupProbe that overrides the default one", + "default": {} + }, + "resourcesPreset": { + "type": "string", + "description": "Set frontend container resources according to one common preset (allowed values: none, nano, small, medium, large, xlarge, 2xlarge). This is ignored if frontend.resources is set (frontend.resources is recommended for production).", + "default": "nano" + }, + "resources": { + "type": "object", + "description": "Set frontend container requests and limits for different resources like CPU or memory (essential for production workloads)", + "default": {} + }, + "podSecurityContext": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable frontend pods' Security Context", + "default": true + }, + "fsGroupChangePolicy": { + "type": "string", + "description": "Set filesystem group change policy for frontend pods", + "default": "Always" + }, + "sysctls": { + "type": "array", + "description": "Set kernel settings using the sysctl interface for frontend pods", + "default": [], + "items": {} + }, + "supplementalGroups": { + "type": "array", + "description": "Set filesystem extra groups for frontend pods", + "default": [], + "items": {} + }, + "fsGroup": { + "type": "number", + "description": "Set fsGroup in frontend pods' Security Context", + "default": 1001 + } + } + }, + "containerSecurityContext": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enabled frontend container' Security Context", + "default": true + }, + "runAsUser": { + "type": "number", + "description": "Set runAsUser in frontend container' Security Context", + "default": 101 + }, + "runAsGroup": { + "type": "number", + "description": "Set runAsGroup in frontend container' Security Context", + "default": 101 + }, + "runAsNonRoot": { + "type": "boolean", + "description": "Set runAsNonRoot in frontend container' Security Context", + "default": true + }, + "readOnlyRootFilesystem": { + "type": "boolean", + "description": "Set readOnlyRootFilesystem in frontend container' Security Context", + "default": true + }, + "privileged": { + "type": "boolean", + "description": "Set privileged in frontend container' Security Context", + "default": false + }, + "allowPrivilegeEscalation": { + "type": "boolean", + "description": "Set allowPrivilegeEscalation in frontend container' Security Context", + "default": false + }, + "capabilities": { + "type": "object", + "properties": { + "drop": { + "type": "array", + "description": "List of capabilities to be dropped in frontend container", + "default": [ + "ALL" + ], + "items": { + "type": "string" + } + } + } + }, + "seccompProfile": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Set seccomp profile in frontend container", + "default": "RuntimeDefault" + } + } + } + } + }, + "command": { + "type": "array", + "description": "Override default frontend container command (useful when using custom images)", + "default": [], + "items": {} + }, + "args": { + "type": "array", + "description": "Override default frontend container args (useful when using custom images)", + "default": [], + "items": {} + }, + "automountServiceAccountToken": { + "type": "boolean", + "description": "Mount Service Account token in frontend pods", + "default": false + }, + "hostAliases": { + "type": "array", + "description": "frontend pods host aliases", + "default": [], + "items": {} + }, + "daemonsetAnnotations": { + "type": "object", + "description": "Annotations for frontend daemonset", + "default": {} + }, + "deploymentAnnotations": { + "type": "object", + "description": "Annotations for frontend deployment", + "default": {} + }, + "statefulsetAnnotations": { + "type": "object", + "description": "Annotations for frontend statefulset", + "default": {} + }, + "podLabels": { + "type": "object", + "description": "Extra labels for frontend pods", + "default": {} + }, + "podAnnotations": { + "type": "object", + "description": "Annotations for frontend pods", + "default": {} + }, + "podAffinityPreset": { + "type": "string", + "description": "Pod affinity preset. Ignored if `frontend.affinity` is set. Allowed values: `soft` or `hard`", + "default": "" + }, + "podAntiAffinityPreset": { + "type": "string", + "description": "Pod anti-affinity preset. Ignored if `frontend.affinity` is set. Allowed values: `soft` or `hard`", + "default": "soft" + }, + "nodeAffinityPreset": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Node affinity preset type. Ignored if `frontend.affinity` is set. Allowed values: `soft` or `hard`", + "default": "" + }, + "key": { + "type": "string", + "description": "Node label key to match. Ignored if `frontend.affinity` is set", + "default": "" + }, + "values": { + "type": "array", + "description": "Node label values to match. Ignored if `frontend.affinity` is set", + "default": [], + "items": {} + } + } + }, + "affinity": { + "type": "object", + "description": "Affinity for frontend pods assignment", + "default": {} + }, + "nodeSelector": { + "type": "object", + "description": "Node labels for frontend pods assignment", + "default": {} + }, + "tolerations": { + "type": "array", + "description": "Tolerations for frontend pods assignment", + "default": [], + "items": {} + }, + "updateStrategy": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "frontend statefulset strategy type", + "default": "RollingUpdate" + } + } + }, + "podManagementPolicy": { + "type": "string", + "description": "Pod management policy for frontend statefulset", + "default": "OrderedReady" + }, + "priorityClassName": { + "type": "string", + "description": "frontend pods' priorityClassName", + "default": "" + }, + "topologySpreadConstraints": { + "type": "array", + "description": "Topology Spread Constraints for frontend pod assignment spread across your cluster among failure-domains", + "default": [], + "items": {} + }, + "schedulerName": { + "type": "string", + "description": "Name of the k8s scheduler (other than default) for frontend pods", + "default": "" + }, + "terminationGracePeriodSeconds": { + "type": "string", + "description": "Seconds frontend pods need to terminate gracefully", + "default": "" + }, + "lifecycleHooks": { + "type": "object", + "description": "for frontend containers to automate configuration before or after startup", + "default": {} + }, + "extraEnvVars": { + "type": "array", + "description": "Array with extra environment variables to add to frontend containers", + "default": [], + "items": {} + }, + "extraEnvVarsCM": { + "type": "string", + "description": "Name of existing ConfigMap containing extra env vars for frontend containers", + "default": "" + }, + "extraEnvVarsSecret": { + "type": "string", + "description": "Name of existing Secret containing extra env vars for frontend containers", + "default": "" + }, + "extraVolumes": { + "type": "array", + "description": "Optionally specify extra list of additional volumes for the frontend pods", + "default": [], + "items": {} + }, + "extraVolumeMounts": { + "type": "array", + "description": "Optionally specify extra list of additional volumeMounts for the frontend containers", + "default": [], + "items": {} + }, + "sidecars": { + "type": "array", + "description": "Add additional sidecar containers to the frontend pods", + "default": [], + "items": {} + }, + "initContainers": { + "type": "array", + "description": "Add additional init containers to the frontend pods", + "default": [], + "items": {} + }, + "pdb": { + "type": "object", + "properties": { + "create": { + "type": "boolean", + "description": "Enable/disable a Pod Disruption Budget creation", + "default": true + }, + "minAvailable": { + "type": "string", + "description": "Minimum number/percentage of pods that should remain scheduled", + "default": "" + }, + "maxUnavailable": { + "type": "string", + "description": "Maximum number/percentage of pods that may be made unavailable. Defaults to `1` if both `frontend.pdb.minAvailable` and `frontend.pdb.maxUnavailable` are empty.", + "default": "" + } + } + }, + "autoscaling": { + "type": "object", + "properties": { + "vpa": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable VPA for frontend pods", + "default": false + }, + "annotations": { + "type": "object", + "description": "Annotations for VPA resource", + "default": {} + }, + "controlledResources": { + "type": "array", + "description": "VPA List of resources that the vertical pod autoscaler can control. Defaults to cpu and memory", + "default": [], + "items": {} + }, + "maxAllowed": { + "type": "object", + "description": "VPA Max allowed resources for the pod", + "default": {} + }, + "minAllowed": { + "type": "object", + "description": "VPA Min allowed resources for the pod", + "default": {} + }, + "updatePolicy": { + "type": "object", + "properties": { + "updateMode": { + "type": "string", + "description": "Autoscaling update policy", + "default": "Auto" + } + } + } + } + }, + "hpa": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable HPA for frontend pods", + "default": false + }, + "minReplicas": { + "type": "string", + "description": "Minimum number of replicas", + "default": "" + }, + "maxReplicas": { + "type": "string", + "description": "Maximum number of replicas", + "default": "" + }, + "targetCPU": { + "type": "string", + "description": "Target CPU utilization percentage", + "default": "" + }, + "targetMemory": { + "type": "string", + "description": "Target Memory utilization percentage", + "default": "" + } + } + } + } + }, + "serviceAccount": { + "type": "object", + "properties": { + "create": { + "type": "boolean", + "description": "Specifies whether a ServiceAccount should be created", + "default": true + }, + "name": { + "type": "string", + "description": "The name of the ServiceAccount to use.", + "default": "" + }, + "annotations": { + "type": "object", + "description": "Additional Service Account annotations (evaluated as a template)", + "default": {} + }, + "automountServiceAccountToken": { + "type": "boolean", + "description": "Automount service account token for the server service account", + "default": true + } + } + }, + "networkPolicy": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Specifies whether a NetworkPolicy should be created", + "default": true + }, + "allowExternal": { + "type": "boolean", + "description": "Don't require server label for connections", + "default": true + }, + "allowExternalEgress": { + "type": "boolean", + "description": "Allow the pod to access any range of port and all destinations.", + "default": true + }, + "extraIngress": { + "type": "array", + "description": "Add extra ingress rules to the NetworkPolicy", + "default": [], + "items": {} + }, + "extraEgress": { + "type": "array", + "description": "Add extra ingress rules to the NetworkPolicy (ignored if allowExternalEgress=true)", + "default": [], + "items": {} + } + } + } + } + }, + "orborus": { + "type": "object", + "properties": { + "image": { + "type": "object", + "properties": { + "registry": { + "type": "string", + "description": "orborus image registry", + "default": "ghcr.io" + }, + "repository": { + "type": "string", + "description": "orborus image repository", + "default": "shuffle/shuffle-orborus" + }, + "digest": { + "type": "string", + "description": "orborus image digest in the way sha256:aa.... Please note this parameter, if set, will override the tag image tag (immutable tags are recommended)", + "default": "" + }, + "pullPolicy": { + "type": "string", + "description": "orborus image pull policy", + "default": "IfNotPresent" + }, + "pullSecrets": { + "type": "array", + "description": "orborus image pull secrets", + "default": [], + "items": {} + } + } + }, + "replicaCount": { + "type": "number", + "description": "Number of orborus replicas to deploy", + "default": 1 + }, + "livenessProbe": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable livenessProbe on orborus containers", + "default": false + }, + "initialDelaySeconds": { + "type": "number", + "description": "Initial delay seconds for livenessProbe", + "default": 0 + }, + "periodSeconds": { + "type": "number", + "description": "Period seconds for livenessProbe", + "default": 15 + }, + "timeoutSeconds": { + "type": "number", + "description": "Timeout seconds for livenessProbe", + "default": 1 + }, + "failureThreshold": { + "type": "number", + "description": "Failure threshold for livenessProbe", + "default": 4 + }, + "successThreshold": { + "type": "number", + "description": "Success threshold for livenessProbe", + "default": 1 + } + } + }, + "readinessProbe": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable readinessProbe on orborus containers", + "default": false + }, + "initialDelaySeconds": { + "type": "number", + "description": "Initial delay seconds for readinessProbe", + "default": 0 + }, + "periodSeconds": { + "type": "number", + "description": "Period seconds for readinessProbe", + "default": 5 + }, + "timeoutSeconds": { + "type": "number", + "description": "Timeout seconds for readinessProbe", + "default": 1 + }, + "failureThreshold": { + "type": "number", + "description": "Failure threshold for readinessProbe", + "default": 3 + }, + "successThreshold": { + "type": "number", + "description": "Success threshold for readinessProbe", + "default": 1 + } + } + }, + "startupProbe": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable startupProbe on orborus containers", + "default": false + }, + "initialDelaySeconds": { + "type": "number", + "description": "Initial delay seconds for startupProbe", + "default": 0 + }, + "periodSeconds": { + "type": "number", + "description": "Period seconds for startupProbe", + "default": 1 + }, + "timeoutSeconds": { + "type": "number", + "description": "Timeout seconds for startupProbe", + "default": 1 + }, + "failureThreshold": { + "type": "number", + "description": "Failure threshold for startupProbe", + "default": 60 + }, + "successThreshold": { + "type": "number", + "description": "Success threshold for startupProbe", + "default": 1 + } + } + }, + "customLivenessProbe": { + "type": "object", + "description": "Custom livenessProbe that overrides the default one", + "default": {} + }, + "customReadinessProbe": { + "type": "object", + "description": "Custom readinessProbe that overrides the default one", + "default": {} + }, + "customStartupProbe": { + "type": "object", + "description": "Custom startupProbe that overrides the default one", + "default": {} + }, + "resourcesPreset": { + "type": "string", + "description": "Set orborus container resources according to one common preset (allowed values: none, nano, small, medium, large, xlarge, 2xlarge). This is ignored if orborus.resources is set (orborus.resources is recommended for production).", + "default": "nano" + }, + "resources": { + "type": "object", + "description": "Set orborus container requests and limits for different resources like CPU or memory (essential for production workloads)", + "default": {} + }, + "podSecurityContext": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable orborus pods' Security Context", + "default": true + }, + "fsGroupChangePolicy": { + "type": "string", + "description": "Set filesystem group change policy for orborus pods", + "default": "Always" + }, + "sysctls": { + "type": "array", + "description": "Set kernel settings using the sysctl interface for orborus pods", + "default": [], + "items": {} + }, + "supplementalGroups": { + "type": "array", + "description": "Set filesystem extra groups for orborus pods", + "default": [], + "items": {} + }, + "fsGroup": { + "type": "number", + "description": "Set fsGroup in orborus pods' Security Context", + "default": 1001 + } + } + }, + "containerSecurityContext": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enabled orborus container' Security Context", + "default": true + }, + "runAsUser": { + "type": "number", + "description": "Set runAsUser in orborus container' Security Context", + "default": 101 + }, + "runAsGroup": { + "type": "number", + "description": "Set runAsGroup in orborus container' Security Context", + "default": 101 + }, + "runAsNonRoot": { + "type": "boolean", + "description": "Set runAsNonRoot in orborus container' Security Context", + "default": true + }, + "readOnlyRootFilesystem": { + "type": "boolean", + "description": "Set readOnlyRootFilesystem in orborus container' Security Context", + "default": true + }, + "privileged": { + "type": "boolean", + "description": "Set privileged in orborus container' Security Context", + "default": false + }, + "allowPrivilegeEscalation": { + "type": "boolean", + "description": "Set allowPrivilegeEscalation in orborus container' Security Context", + "default": false + }, + "capabilities": { + "type": "object", + "properties": { + "drop": { + "type": "array", + "description": "List of capabilities to be dropped in orborus container", + "default": [ + "ALL" + ], + "items": { + "type": "string" + } + } + } + }, + "seccompProfile": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Set seccomp profile in orborus container", + "default": "RuntimeDefault" + } + } + } + } + }, + "command": { + "type": "array", + "description": "Override default orborus container command (useful when using custom images)", + "default": [], + "items": {} + }, + "args": { + "type": "array", + "description": "Override default orborus container args (useful when using custom images)", + "default": [], + "items": {} + }, + "automountServiceAccountToken": { + "type": "boolean", + "description": "Mount Service Account token in orborus pods", + "default": true + }, + "hostAliases": { + "type": "array", + "description": "orborus pods host aliases", + "default": [], + "items": {} + }, + "daemonsetAnnotations": { + "type": "object", + "description": "Annotations for orborus daemonset", + "default": {} + }, + "deploymentAnnotations": { + "type": "object", + "description": "Annotations for orborus deployment", + "default": {} + }, + "statefulsetAnnotations": { + "type": "object", + "description": "Annotations for orborus statefulset", + "default": {} + }, + "podLabels": { + "type": "object", + "description": "Extra labels for orborus pods", + "default": {} + }, + "podAnnotations": { + "type": "object", + "description": "Annotations for orborus pods", + "default": {} + }, + "podAffinityPreset": { + "type": "string", + "description": "Pod affinity preset. Ignored if `orborus.affinity` is set. Allowed values: `soft` or `hard`", + "default": "" + }, + "podAntiAffinityPreset": { + "type": "string", + "description": "Pod anti-affinity preset. Ignored if `orborus.affinity` is set. Allowed values: `soft` or `hard`", + "default": "soft" + }, + "nodeAffinityPreset": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "Node affinity preset type. Ignored if `orborus.affinity` is set. Allowed values: `soft` or `hard`", + "default": "" + }, + "key": { + "type": "string", + "description": "Node label key to match. Ignored if `orborus.affinity` is set", + "default": "" + }, + "values": { + "type": "array", + "description": "Node label values to match. Ignored if `orborus.affinity` is set", + "default": [], + "items": {} + } + } + }, + "affinity": { + "type": "object", + "description": "Affinity for orborus pods assignment", + "default": {} + }, + "nodeSelector": { + "type": "object", + "description": "Node labels for orborus pods assignment", + "default": {} + }, + "tolerations": { + "type": "array", + "description": "Tolerations for orborus pods assignment", + "default": [], + "items": {} + }, + "updateStrategy": { + "type": "object", + "properties": { + "type": { + "type": "string", + "description": "orborus statefulset strategy type", + "default": "RollingUpdate" + } + } + }, + "podManagementPolicy": { + "type": "string", + "description": "Pod management policy for orborus statefulset", + "default": "OrderedReady" + }, + "priorityClassName": { + "type": "string", + "description": "orborus pods' priorityClassName", + "default": "" + }, + "topologySpreadConstraints": { + "type": "array", + "description": "Topology Spread Constraints for orborus pod assignment spread across your cluster among failure-domains", + "default": [], + "items": {} + }, + "schedulerName": { + "type": "string", + "description": "Name of the k8s scheduler (other than default) for orborus pods", + "default": "" + }, + "terminationGracePeriodSeconds": { + "type": "string", + "description": "Seconds orborus pods need to terminate gracefully", + "default": "" + }, + "lifecycleHooks": { + "type": "object", + "description": "for orborus containers to automate configuration before or after startup", + "default": {} + }, + "extraEnvVars": { + "type": "array", + "description": "Array with extra environment variables to add to orborus containers", + "default": [], + "items": {} + }, + "extraEnvVarsCM": { + "type": "string", + "description": "Name of existing ConfigMap containing extra env vars for orborus containers", + "default": "" + }, + "extraEnvVarsSecret": { + "type": "string", + "description": "Name of existing Secret containing extra env vars for orborus containers", + "default": "" + }, + "extraVolumes": { + "type": "array", + "description": "Optionally specify extra list of additional volumes for the orborus pods", + "default": [], + "items": {} + }, + "extraVolumeMounts": { + "type": "array", + "description": "Optionally specify extra list of additional volumeMounts for the orborus containers", + "default": [], + "items": {} + }, + "sidecars": { + "type": "array", + "description": "Add additional sidecar containers to the orborus pods", + "default": [], + "items": {} + }, + "initContainers": { + "type": "array", + "description": "Add additional init containers to the orborus pods", + "default": [], + "items": {} + }, + "pdb": { + "type": "object", + "properties": { + "create": { + "type": "boolean", + "description": "Enable/disable a Pod Disruption Budget creation", + "default": true + }, + "minAvailable": { + "type": "string", + "description": "Minimum number/percentage of pods that should remain scheduled", + "default": "" + }, + "maxUnavailable": { + "type": "string", + "description": "Maximum number/percentage of pods that may be made unavailable. Defaults to `1` if both `orborus.pdb.minAvailable` and `orborus.pdb.maxUnavailable` are empty.", + "default": "" + } + } + }, + "autoscaling": { + "type": "object", + "properties": { + "vpa": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable VPA for orborus pods", + "default": false + }, + "annotations": { + "type": "object", + "description": "Annotations for VPA resource", + "default": {} + }, + "controlledResources": { + "type": "array", + "description": "VPA List of resources that the vertical pod autoscaler can control. Defaults to cpu and memory", + "default": [], + "items": {} + }, + "maxAllowed": { + "type": "object", + "description": "VPA Max allowed resources for the pod", + "default": {} + }, + "minAllowed": { + "type": "object", + "description": "VPA Min allowed resources for the pod", + "default": {} + }, + "updatePolicy": { + "type": "object", + "properties": { + "updateMode": { + "type": "string", + "description": "Autoscaling update policy", + "default": "Auto" + } + } + } + } + }, + "hpa": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable HPA for orborus pods", + "default": false + }, + "minReplicas": { + "type": "string", + "description": "Minimum number of replicas", + "default": "" + }, + "maxReplicas": { + "type": "string", + "description": "Maximum number of replicas", + "default": "" + }, + "targetCPU": { + "type": "string", + "description": "Target CPU utilization percentage", + "default": "" + }, + "targetMemory": { + "type": "string", + "description": "Target Memory utilization percentage", + "default": "" + } + } + } + } + }, + "serviceAccount": { + "type": "object", + "properties": { + "create": { + "type": "boolean", + "description": "Specifies whether a ServiceAccount should be created", + "default": true + }, + "name": { + "type": "string", + "description": "The name of the ServiceAccount to use.", + "default": "" + }, + "annotations": { + "type": "object", + "description": "Additional Service Account annotations (evaluated as a template)", + "default": {} + }, + "automountServiceAccountToken": { + "type": "boolean", + "description": "Automount service account token for the server service account", + "default": true + } + } + }, + "rbac": { + "type": "object", + "properties": { + "create": { + "type": "boolean", + "description": "Specifies whether RBAC resources should be created", + "default": true + } + } + }, + "networkPolicy": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Specifies whether a NetworkPolicy should be created", + "default": true + }, + "allowExternal": { + "type": "boolean", + "description": "Don't require server label for connections", + "default": true + }, + "allowExternalEgress": { + "type": "boolean", + "description": "Allow the pod to access any range of port and all destinations.", + "default": true + }, + "extraIngress": { + "type": "array", + "description": "Add extra ingress rules to the NetworkPolicy", + "default": [], + "items": {} + }, + "extraEgress": { + "type": "array", + "description": "Add extra ingress rules to the NetworkPolicy (ignored if allowExternalEgress=true)", + "default": [], + "items": {} + } + } + } + } + }, + "worker": { + "type": "object", + "properties": { + "image": { + "type": "object", + "properties": { + "registry": { + "type": "string", + "description": "worker image registry", + "default": "ghcr.io" + }, + "repository": { + "type": "string", + "description": "worker image repository", + "default": "shuffle/shuffle-worker" + }, + "digest": { + "type": "string", + "description": "worker image digest in the way sha256:aa.... Please note this parameter, if set, will override the tag image tag (immutable tags are recommended)", + "default": "" + } + } + }, + "serviceAccount": { + "type": "object", + "properties": { + "create": { + "type": "boolean", + "description": "Specifies whether a ServiceAccount should be created", + "default": true + }, + "name": { + "type": "string", + "description": "The name of the ServiceAccount to use.", + "default": "" + }, + "annotations": { + "type": "object", + "description": "Additional Service Account annotations (evaluated as a template)", + "default": {} + }, + "automountServiceAccountToken": { + "type": "boolean", + "description": "Automount service account token for the server service account", + "default": true + } + } + }, + "rbac": { + "type": "object", + "properties": { + "create": { + "type": "boolean", + "description": "Specifies whether RBAC resources should be created", + "default": true + } + } + }, + "networkPolicy": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Specifies whether a NetworkPolicy should be created", + "default": true + }, + "allowExternal": { + "type": "boolean", + "description": "Don't require server label for connections", + "default": true + }, + "allowExternalEgress": { + "type": "boolean", + "description": "Allow the pod to access any range of port and all destinations.", + "default": true + }, + "extraIngress": { + "type": "array", + "description": "Add extra ingress rules to the NetworkPolicy", + "default": [], + "items": {} + }, + "extraEgress": { + "type": "array", + "description": "Add extra ingress rules to the NetworkPolicy (ignored if allowExternalEgress=true)", + "default": [], + "items": {} + } + } + } + } + }, + "app": { + "type": "object", + "properties": { + "serviceAccount": { + "type": "object", + "properties": { + "create": { + "type": "boolean", + "description": "Specifies whether a ServiceAccount should be created", + "default": true + }, + "name": { + "type": "string", + "description": "The name of the ServiceAccount to use.", + "default": "" + }, + "annotations": { + "type": "object", + "description": "Additional Service Account annotations (evaluated as a template)", + "default": {} + }, + "automountServiceAccountToken": { + "type": "boolean", + "description": "Automount service account token for the server service account", + "default": true + } + } + }, + "rbac": { + "type": "object", + "properties": { + "create": { + "type": "boolean", + "description": "Specifies whether RBAC resources should be created", + "default": true + } + } + }, + "networkPolicy": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Specifies whether a NetworkPolicy should be created", + "default": true + }, + "allowExternal": { + "type": "boolean", + "description": "Don't require server label for connections", + "default": true + }, + "allowExternalEgress": { + "type": "boolean", + "description": "Allow the pod to access any range of port and all destinations.", + "default": true + }, + "extraIngress": { + "type": "array", + "description": "Add extra ingress rules to the NetworkPolicy", + "default": [], + "items": {} + }, + "extraEgress": { + "type": "array", + "description": "Add extra ingress rules to the NetworkPolicy (ignored if allowExternalEgress=true)", + "default": [], + "items": {} + } + } + } + } + }, + "ingress": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable ingress record generation for frontend and backend", + "default": false + }, + "pathType": { + "type": "string", + "description": "Ingress path type for the frontend path", + "default": "Prefix" + }, + "backendPathType": { + "type": "string", + "description": "Ingress path type for the backend path", + "default": "Prefix" + }, + "apiVersion": { + "type": "string", + "description": "Force Ingress API version (automatically detected if not set)", + "default": "" + }, + "hostname": { + "type": "string", + "description": "Default host for the ingress record", + "default": "shuffle.local" + }, + "ingressClassName": { + "type": "string", + "description": "IngressClass that will be be used to implement the Ingress (Kubernetes 1.18+)", + "default": "nginx" + }, + "path": { + "type": "string", + "description": "Ingress path for Shuffle frontend", + "default": "\"/\"" + }, + "backendPath": { + "type": "string", + "description": "Ingress path for Shuffle backend", + "default": "\"/api/\"" + }, + "annotations": { + "type": "object", + "description": "Additional annotations for the Ingress resource.", + "default": {} + }, + "tls": { + "type": "boolean", + "description": "Enable TLS configuration for the host defined at `ingress.hostname` parameter", + "default": false + }, + "selfSigned": { + "type": "boolean", + "description": "Create a TLS secret for this ingress record using self-signed certificates generated by Helm", + "default": false + }, + "extraHosts": { + "type": "array", + "description": "An array with additional hostname(s) to be covered with the ingress record", + "default": [], + "items": {} + }, + "extraPaths": { + "type": "array", + "description": "An array with additional arbitrary paths that may need to be added to the ingress under the main host", + "default": [], + "items": {} + }, + "extraTls": { + "type": "array", + "description": "TLS configuration for additional hostname(s) to be covered with this ingress record", + "default": [], + "items": {} + }, + "secrets": { + "type": "array", + "description": "Custom TLS certificates as secrets", + "default": [], + "items": {} + }, + "extraRules": { + "type": "array", + "description": "Additional rules to be covered with this ingress record", + "default": [], + "items": {} + } + } + }, + "istio": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable creation of an Istio Gateway and VirtualService for frontend and backend", + "default": false + }, + "apiVersion": { + "type": "string", + "description": "The istio apiVersion to use for Gateway and VirtualService resources", + "default": "networking.istio.io/v1" + }, + "hosts": { + "type": "array", + "description": "One or more hosts exposed by Istio", + "default": [], + "items": {} + }, + "gateway": { + "type": "object", + "properties": { + "annotations": { + "type": "object", + "description": "Additional annotations for the Gateway resource", + "default": {} + }, + "http": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable HTTP server port 80", + "default": true + }, + "httpsRedirect": { + "type": "boolean", + "description": "If set to true, a 301 redirect is send for all HTTP connections", + "default": false + } + } + }, + "https": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable HTTPS server on port 443", + "default": false + }, + "tlsCredentialName": { + "type": "string", + "description": "The name of the secret that holds the TLS certs including the CA certificates.", + "default": "" + } + } + }, + "extraServers": { + "type": "array", + "description": "Additional servers for the Gateway resource", + "default": [], + "items": {} + } + } + }, + "virtualService": { + "type": "object", + "properties": { + "annotations": { + "type": "object", + "description": "Additional annotations for the VirtualService resource.", + "default": {} + } + } + } + } + }, + "persistence": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable persistence using Persistent Volume Claims", + "default": true + }, + "apps": { + "type": "object", + "properties": { + "existingClaim": { + "type": "string", + "description": "Name of an existing PVC to use", + "default": "" + }, + "storageClass": { + "type": "string", + "description": "PVC Storage Class for shuffle-apps volume", + "default": "" + }, + "subPath": { + "type": "string", + "description": "The sub path used in the volume", + "default": "" + }, + "accessModes": { + "type": "array", + "description": "The access mode of the volume", + "default": [ + "ReadWriteOnce" + ], + "items": { + "type": "string" + } + }, + "size": { + "type": "string", + "description": "The size of the volume", + "default": "5Gi" + }, + "annotations": { + "type": "object", + "description": "Annotations for the PVC", + "default": {} + }, + "selector": { + "type": "object", + "description": "Selector to match an existing Persistent Volume", + "default": {} + } + } + }, + "appBuilder": { + "type": "object", + "properties": { + "storageClass": { + "type": "string", + "description": "PVC Storage Class for backend-apps-claim volume", + "default": "" + }, + "accessModes": { + "type": "array", + "description": "The access mode of the volume", + "default": [ + "ReadWriteOnce" + ], + "items": { + "type": "string" + } + }, + "size": { + "type": "string", + "description": "The size of the volume", + "default": "5Gi" + }, + "annotations": { + "type": "object", + "description": "Annotations for the PVC", + "default": {} + }, + "selector": { + "type": "object", + "description": "Selector to match an existing Persistent Volume", + "default": {} + } + } + }, + "files": { + "type": "object", + "properties": { + "existingClaim": { + "type": "string", + "description": "Name of an existing PVC to use", + "default": "" + }, + "storageClass": { + "type": "string", + "description": "PVC Storage Class for shuffle-files volume", + "default": "" + }, + "subPath": { + "type": "string", + "description": "The sub path used in the volume", + "default": "" + }, + "accessModes": { + "type": "array", + "description": "The access mode of the volume", + "default": [ + "ReadWriteOnce" + ], + "items": { + "type": "string" + } + }, + "size": { + "type": "string", + "description": "The size of the volume", + "default": "5Gi" + }, + "annotations": { + "type": "object", + "description": "Annotations for the PVC", + "default": {} + }, + "selector": { + "type": "object", + "description": "Selector to match an existing Persistent Volume", + "default": {} + } + } + } + } + }, + "volumePermissions": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable init container that changes the owner/group of the PV mount point to `runAsUser:fsGroup`", + "default": false + }, + "image": { + "type": "object", + "properties": { + "registry": { + "type": "string", + "description": "OS Shell + Utility image registry", + "default": "docker.io" + }, + "repository": { + "type": "string", + "description": "OS Shell + Utility image repository", + "default": "bitnami/os-shell" + }, + "pullPolicy": { + "type": "string", + "description": "OS Shell + Utility image pull policy", + "default": "IfNotPresent" + }, + "pullSecrets": { + "type": "array", + "description": "OS Shell + Utility image pull secrets", + "default": [], + "items": {} + } + } + }, + "resourcesPreset": { + "type": "string", + "description": "Set init container resources according to one common preset (allowed values: none, nano, small, medium, large, xlarge, 2xlarge). This is ignored if volumePermissions.resources is set (volumePermissions.resources is recommended for production).", + "default": "nano" + }, + "resources": { + "type": "object", + "description": "Set init container requests and limits for different resources like CPU or memory (essential for production workloads)", + "default": {} + }, + "containerSecurityContext": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enabled init container' Security Context", + "default": true + }, + "runAsUser": { + "type": "number", + "description": "Set init container's Security Context runAsUser", + "default": 0 + } + } + } + } + }, + "opensearch": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Switch to enable or disable the opensearch helm chart", + "default": true + } + } + }, + "vault": { + "type": "object", + "properties": { + "role": { + "type": "string", + "description": "Specify the Vault role, which should be used to get the secret from Vault.", + "default": "" + }, + "secrets": { + "type": "array", + "description": "A list of VaultSecrets to create", + "default": [], + "items": {} + } + } + } + } +} \ No newline at end of file diff --git a/charts/shuffle/values.yaml b/charts/shuffle/values.yaml new file mode 100644 index 00000000..0e551a45 --- /dev/null +++ b/charts/shuffle/values.yaml @@ -0,0 +1,1772 @@ +--- +## @section Global parameters +## Global Docker image parameters +## Please, note that this will override the image parameters, including dependencies, configured to use the global value +## Current available global Docker image parameters: imageRegistry, imagePullSecrets and storageClass +## + +## @param global.imageRegistry Global Docker image registry +## @param global.imagePullSecrets Global Docker registry secret names as an array +## @param global.defaultStorageClass Global default StorageClass for Persistent Volume(s) +## +global: + imageRegistry: "" + ## e.g: + ## imagePullSecrets: + ## - myRegistryKeySecretName + ## + imagePullSecrets: [] + defaultStorageClass: "" + ## Compatibility adaptations for Kubernetes platforms + ## + compatibility: + ## Compatibility adaptations for Openshift + ## + openshift: + ## @param global.compatibility.openshift.adaptSecurityContext Adapt the securityContext sections of the deployment to make them compatible with Openshift restricted-v2 SCC: remove runAsUser, runAsGroup and fsGroup and let the platform use their allowed default IDs. Possible values: auto (apply if the detected running cluster is Openshift), force (perform the adaptation always), disabled (do not perform adaptation) + ## + adaptSecurityContext: auto + ## @param global.compatibility.omitEmptySeLinuxOptions If set to true, removes the seLinuxOptions from the securityContexts when it is set to an empty object + ## + omitEmptySeLinuxOptions: false + +## @section Common parameters +## + +## @param kubeVersion Override Kubernetes version +## +kubeVersion: "" +## @param nameOverride String to partially override common.names.name +## +nameOverride: "" +## @param fullnameOverride String to fully override common.names.fullname +## +fullnameOverride: "" +## @param namespaceOverride String to fully override common.names.namespace +## +namespaceOverride: "" +## @param commonLabels Labels to add to all deployed objects +## +commonLabels: {} +## @param commonAnnotations Annotations to add to all deployed objects +## +commonAnnotations: {} +## @param clusterDomain Kubernetes cluster domain name +## +clusterDomain: cluster.local +## @param extraDeploy Array of extra objects to deploy with the release +## +extraDeploy: [] +## Diagnostic mode +## @param diagnosticMode.enabled Enable diagnostic mode (all probes will be disabled and the command will be overridden) +## @param diagnosticMode.command Command to override all containers in the chart release +## @param diagnosticMode.args Args to override all containers in the chart release +## +diagnosticMode: + enabled: false + command: + - sleep + args: + - infinity + +## @section Shared Shuffle Parameters +## +shuffle: + ## @param shuffle.baseUrl The external base URL under which Shuffle is reachable. + ## + baseUrl: "" + + ## ref: https://shuffler.io/docs/organizations + ## This chart only supports single-tenant deployments at the moment + ## @param shuffle.org Default shuffle organization + ## + org: Shuffle + + ## @param shuffle.appRegistry The registry from / to which shuffle apps are pulled / pushed + ## + appRegistry: "" + + ## @param shuffle.timezone The timezone used by Shuffle + ## + timezone: Europe/Berlin + +## @section backend Parameters +## +backend: + ## backend image + ## @param backend.image.registry backend image registry + ## @param backend.image.repository backend image repository + ## @skip backend.image.tag backend image tag (immutable tags are recommended) + ## @param backend.image.digest backend image digest in the way sha256:aa.... Please note this parameter, if set, will override the tag image tag (immutable tags are recommended) + ## @param backend.image.pullPolicy backend image pull policy + ## @param backend.image.pullSecrets backend image pull secrets + ## + image: + registry: ghcr.io + repository: shuffle/shuffle-backend + tag: nightly + digest: "" + ## Specify a imagePullPolicy + ## Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent' + ## ref: https://kubernetes.io/docs/concepts/containers/images/#pre-pulled-images + ## + pullPolicy: IfNotPresent + ## Optionally specify an array of imagePullSecrets. + ## Secrets must be manually created in the namespace. + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ + ## e.g: + ## pullSecrets: + ## - myRegistryKeySecretName + ## + pullSecrets: [] + ## @param backend.replicaCount Number of backend replicas to deploy + ## + replicaCount: 1 + ## Configure extra options for backend containers' liveness and readiness probes + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/#configure-probes + ## @param backend.livenessProbe.enabled Enable livenessProbe on backend containers + ## @param backend.livenessProbe.initialDelaySeconds Initial delay seconds for livenessProbe + ## @param backend.livenessProbe.periodSeconds Period seconds for livenessProbe + ## @param backend.livenessProbe.timeoutSeconds Timeout seconds for livenessProbe + ## @param backend.livenessProbe.failureThreshold Failure threshold for livenessProbe + ## @param backend.livenessProbe.successThreshold Success threshold for livenessProbe + ## + livenessProbe: + enabled: false + initialDelaySeconds: 0 + periodSeconds: 15 + timeoutSeconds: 1 + failureThreshold: 4 + successThreshold: 1 + ## @param backend.readinessProbe.enabled Enable readinessProbe on backend containers + ## @param backend.readinessProbe.initialDelaySeconds Initial delay seconds for readinessProbe + ## @param backend.readinessProbe.periodSeconds Period seconds for readinessProbe + ## @param backend.readinessProbe.timeoutSeconds Timeout seconds for readinessProbe + ## @param backend.readinessProbe.failureThreshold Failure threshold for readinessProbe + ## @param backend.readinessProbe.successThreshold Success threshold for readinessProbe + ## + readinessProbe: + enabled: false + initialDelaySeconds: 0 + periodSeconds: 5 + timeoutSeconds: 1 + failureThreshold: 3 + successThreshold: 1 + ## @param backend.startupProbe.enabled Enable startupProbe on backend containers + ## @param backend.startupProbe.initialDelaySeconds Initial delay seconds for startupProbe + ## @param backend.startupProbe.periodSeconds Period seconds for startupProbe + ## @param backend.startupProbe.timeoutSeconds Timeout seconds for startupProbe + ## @param backend.startupProbe.failureThreshold Failure threshold for startupProbe + ## @param backend.startupProbe.successThreshold Success threshold for startupProbe + ## + startupProbe: + enabled: false + initialDelaySeconds: 0 + periodSeconds: 1 + timeoutSeconds: 1 + failureThreshold: 60 + successThreshold: 1 + ## @param backend.customLivenessProbe Custom livenessProbe that overrides the default one + ## + customLivenessProbe: {} + ## @param backend.customReadinessProbe Custom readinessProbe that overrides the default one + ## + customReadinessProbe: {} + ## @param backend.customStartupProbe Custom startupProbe that overrides the default one + ## + customStartupProbe: {} + ## backend resource requests and limits + ## ref: http://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/ + ## @param backend.resourcesPreset Set backend container resources according to one common preset (allowed values: none, nano, small, medium, large, xlarge, 2xlarge). This is ignored if backend.resources is set (backend.resources is recommended for production). + ## More information: https://github.com/bitnami/charts/blob/main/bitnami/common/templates/_resources.tpl#L15 + ## Shuffle gets OOM killed with 256M memory during startup. Up to 360MiB of memory usage were observed during testing. + ## The small preset grants 512M. + ## + resourcesPreset: "small" + ## @param backend.resources Set backend container requests and limits for different resources like CPU or memory (essential for production workloads) + ## Example: + ## resources: + ## requests: + ## cpu: 2 + ## memory: 512Mi + ## limits: + ## cpu: 3 + ## memory: 1024Mi + ## + resources: {} + ## Configure Pods Security Context + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-pod + ## @param backend.podSecurityContext.enabled Enable backend pods' Security Context + ## @param backend.podSecurityContext.fsGroupChangePolicy Set filesystem group change policy for backend pods + ## @param backend.podSecurityContext.sysctls Set kernel settings using the sysctl interface for backend pods + ## @param backend.podSecurityContext.supplementalGroups Set filesystem extra groups for backend pods + ## @param backend.podSecurityContext.fsGroup Set fsGroup in backend pods' Security Context + ## + podSecurityContext: + enabled: true + fsGroupChangePolicy: Always + sysctls: [] + supplementalGroups: [] + fsGroup: 1001 + ## Configure Container Security Context + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-container + ## @param backend.containerSecurityContext.enabled Enabled backend container' Security Context + ## @param backend.containerSecurityContext.seLinuxOptions [object,nullable] Set SELinux options in backend container + ## @param backend.containerSecurityContext.runAsUser Set runAsUser in backend container' Security Context + ## @param backend.containerSecurityContext.runAsGroup Set runAsGroup in backend container' Security Context + ## @param backend.containerSecurityContext.runAsNonRoot Set runAsNonRoot in backend container' Security Context + ## @param backend.containerSecurityContext.readOnlyRootFilesystem Set readOnlyRootFilesystem in backend container' Security Context + ## @param backend.containerSecurityContext.privileged Set privileged in backend container' Security Context + ## @param backend.containerSecurityContext.allowPrivilegeEscalation Set allowPrivilegeEscalation in backend container' Security Context + ## @param backend.containerSecurityContext.capabilities.drop List of capabilities to be dropped in backend container + ## @param backend.containerSecurityContext.seccompProfile.type Set seccomp profile in backend container + ## + containerSecurityContext: + enabled: true + seLinuxOptions: {} + runAsUser: 1000 + runAsGroup: 1000 + runAsNonRoot: true + readOnlyRootFilesystem: true + privileged: false + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + seccompProfile: + type: "RuntimeDefault" + + ## @param backend.command Override default backend container command (useful when using custom images) + ## + command: [] + ## @param backend.args Override default backend container args (useful when using custom images) + ## + args: [] + ## @param backend.automountServiceAccountToken Mount Service Account token in backend pods + ## NOTE: backend requires the service account credentials to be mounted + ## + automountServiceAccountToken: true + ## @param backend.hostAliases backend pods host aliases + ## https://kubernetes.io/docs/concepts/services-networking/add-entries-to-pod-etc-hosts-with-host-aliases/ + ## + hostAliases: [] + ## @param backend.daemonsetAnnotations Annotations for backend daemonset + ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ + ## + daemonsetAnnotations: {} + ## @param backend.deploymentAnnotations Annotations for backend deployment + ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ + ## + deploymentAnnotations: {} + ## @param backend.statefulsetAnnotations Annotations for backend statefulset + ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ + ## + statefulsetAnnotations: {} + ## @param backend.podLabels Extra labels for backend pods + ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ + ## + podLabels: {} + ## @param backend.podAnnotations Annotations for backend pods + ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ + ## + podAnnotations: {} + ## @param backend.podAffinityPreset Pod affinity preset. Ignored if `backend.affinity` is set. Allowed values: `soft` or `hard` + ## ref: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#inter-pod-affinity-and-anti-affinity + ## + podAffinityPreset: "" + ## @param backend.podAntiAffinityPreset Pod anti-affinity preset. Ignored if `backend.affinity` is set. Allowed values: `soft` or `hard` + ## ref: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#inter-pod-affinity-and-anti-affinity + ## + podAntiAffinityPreset: soft + ## Node backend.affinity preset + ## ref: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#node-affinity + ## + nodeAffinityPreset: + ## @param backend.nodeAffinityPreset.type Node affinity preset type. Ignored if `backend.affinity` is set. Allowed values: `soft` or `hard` + ## + type: "" + ## @param backend.nodeAffinityPreset.key Node label key to match. Ignored if `backend.affinity` is set + ## + key: "" + ## @param backend.nodeAffinityPreset.values Node label values to match. Ignored if `backend.affinity` is set + ## E.g. + ## values: + ## - e2e-az1 + ## - e2e-az2 + ## + values: [] + ## @param backend.affinity Affinity for backend pods assignment + ## ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity + ## NOTE: `backend.podAffinityPreset`, `backend.podAntiAffinityPreset`, and `backend.nodeAffinityPreset` will be ignored when it's set + ## + affinity: {} + ## @param backend.nodeSelector Node labels for backend pods assignment + ## ref: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/ + ## + nodeSelector: {} + ## @param backend.tolerations Tolerations for backend pods assignment + ## ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ + ## + tolerations: [] + ## ONLY FOR DEPLOYMENTS: + ## @param backend.updateStrategy.type backend deployment strategy type + ## ref: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#strategy + ## ONLY FOR STATEFULSETS: + ## @param backend.updateStrategy.type backend statefulset strategy type + ## ref: https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#update-strategies + ## + updateStrategy: + ## ONLY FOR DEPLOYMENTS: + ## Can be set to RollingUpdate or Recreate + ## ONLY FOR STATEFULSETS: + ## Can be set to RollingUpdate or OnDelete + ## + type: RollingUpdate + ## ONLY FOR STATEFULSETS: + ## @param backend.podManagementPolicy Pod management policy for backend statefulset + ## Ref: https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#pod-management-policies + ## + podManagementPolicy: OrderedReady + ## @param backend.priorityClassName backend pods' priorityClassName + ## + priorityClassName: "" + ## @param backend.topologySpreadConstraints Topology Spread Constraints for backend pod assignment spread across your cluster among failure-domains + ## Ref: https://kubernetes.io/docs/concepts/workloads/pods/pod-topology-spread-constraints/#spread-constraints-for-pods + ## + topologySpreadConstraints: [] + ## @param backend.schedulerName Name of the k8s scheduler (other than default) for backend pods + ## ref: https://kubernetes.io/docs/tasks/administer-cluster/configure-multiple-schedulers/ + ## + schedulerName: "" + ## @param backend.terminationGracePeriodSeconds Seconds backend pods need to terminate gracefully + ## ref: https://kubernetes.io/docs/concepts/workloads/pods/pod/#termination-of-pods + ## + terminationGracePeriodSeconds: "" + ## @param backend.lifecycleHooks for backend containers to automate configuration before or after startup + ## + lifecycleHooks: {} + ## @param backend.extraEnvVars Array with extra environment variables to add to backend containers + ## e.g: + ## extraEnvVars: + ## - name: FOO + ## value: "bar" + ## + extraEnvVars: [] + ## @param backend.extraEnvVarsCM Name of existing ConfigMap containing extra env vars for backend containers + ## + extraEnvVarsCM: "" + ## @param backend.extraEnvVarsSecret Name of existing Secret containing extra env vars for backend containers + ## + extraEnvVarsSecret: "" + ## @param backend.extraVolumes Optionally specify extra list of additional volumes for the backend pods + ## + extraVolumes: [] + ## @param backend.extraVolumeMounts Optionally specify extra list of additional volumeMounts for the backend containers + ## + extraVolumeMounts: [] + ## @param backend.sidecars Add additional sidecar containers to the backend pods + ## e.g: + ## sidecars: + ## - name: your-image-name + ## image: your-image + ## imagePullPolicy: Always + ## ports: + ## - name: portname + ## containerPort: 1234 + ## + sidecars: [] + ## @param backend.initContainers Add additional init containers to the backend pods + ## ref: https://kubernetes.io/docs/concepts/workloads/pods/init-containers/ + ## e.g: + ## initContainers: + ## - name: your-image-name + ## image: your-image + ## imagePullPolicy: Always + ## command: ['sh', '-c', 'echo "hello world"'] + ## + initContainers: [] + ## Pod Disruption Budget configuration + ## ref: https://kubernetes.io/docs/tasks/run-application/configure-pdb + ## @param backend.pdb.create Enable/disable a Pod Disruption Budget creation + ## @param backend.pdb.minAvailable Minimum number/percentage of pods that should remain scheduled + ## @param backend.pdb.maxUnavailable Maximum number/percentage of pods that may be made unavailable. Defaults to `1` if both `backend.pdb.minAvailable` and `backend.pdb.maxUnavailable` are empty. + ## + pdb: + create: true + minAvailable: "" + maxUnavailable: "" + ## Autoscaling configuration + ## ref: https://kubernetes.io/docs/concepts/workloads/autoscaling/ + ## + autoscaling: + ## @param backend.autoscaling.vpa.enabled Enable VPA for backend pods + ## @param backend.autoscaling.vpa.annotations Annotations for VPA resource + ## @param backend.autoscaling.vpa.controlledResources VPA List of resources that the vertical pod autoscaler can control. Defaults to cpu and memory + ## @param backend.autoscaling.vpa.maxAllowed VPA Max allowed resources for the pod + ## @param backend.autoscaling.vpa.minAllowed VPA Min allowed resources for the pod + ## + vpa: + enabled: false + annotations: {} + controlledResources: [] + maxAllowed: {} + minAllowed: {} + ## @param backend.autoscaling.vpa.updatePolicy.updateMode Autoscaling update policy + ## Specifies whether recommended updates are applied when a Pod is started and whether recommended updates are applied during the life of a Pod + ## Possible values are "Off", "Initial", "Recreate", and "Auto". + ## + updatePolicy: + updateMode: Auto + ## @param backend.autoscaling.hpa.enabled Enable HPA for backend pods + ## @param backend.autoscaling.hpa.minReplicas Minimum number of replicas + ## @param backend.autoscaling.hpa.maxReplicas Maximum number of replicas + ## @param backend.autoscaling.hpa.targetCPU Target CPU utilization percentage + ## @param backend.autoscaling.hpa.targetMemory Target Memory utilization percentage + ## + hpa: + enabled: false + minReplicas: "" + maxReplicas: "" + targetCPU: "" + targetMemory: "" + + ## ServiceAccount configuration + ## + serviceAccount: + ## @param backend.serviceAccount.create Specifies whether a ServiceAccount should be created + ## + create: true + ## @param backend.serviceAccount.name The name of the ServiceAccount to use. + ## If not set and create is true, a name is generated using the common.names.fullname template + ## + name: "" + ## @param backend.serviceAccount.annotations Additional Service Account annotations (evaluated as a template) + ## + annotations: {} + ## @param backend.serviceAccount.automountServiceAccountToken Automount service account token for the server service account + ## + automountServiceAccountToken: true + + ## RBAC configuration + ## + rbac: + ## @param backend.rbac.create Specifies whether RBAC resources should be created + create: true + + ## Network Policies + ## Ref: https://kubernetes.io/docs/concepts/services-networking/network-policies/ + ## + networkPolicy: + ## @param backend.networkPolicy.enabled Specifies whether a NetworkPolicy should be created + ## + enabled: true + ## @param backend.networkPolicy.allowExternal Don't require server label for connections + ## The Policy model to apply. When set to false, only pods with the correct + ## server label will have network access to the ports server is listening + ## on. When true, server will accept connections from any source + ## (with the correct destination port). + ## + allowExternal: true + ## @param backend.networkPolicy.allowExternalEgress Allow the pod to access any range of port and all destinations. + ## + allowExternalEgress: true + ## @param backend.networkPolicy.extraIngress Add extra ingress rules to the NetworkPolicy + ## NOTE: You likely want to allow access from your ingress, e.g.: + ## extraIngress: + ## - ports: + ## - protocol: TCP + ## port: 5001 + ## from: + ## - namespaceSelector: + ## matchLabels: + ## kubernetes.io/metadata.name: istio-ingress + ## podSelector: + ## matchLabels: + ## istio: ingress + ## + extraIngress: [] + ## @param backend.networkPolicy.extraEgress Add extra ingress rules to the NetworkPolicy (ignored if allowExternalEgress=true) + ## NOTE: You likely want to allow access to OpenSearch and cluster-proxies, e.g: + ## extraEgress: + ## - to: + ## - namespaceSelector: + ## matchLabels: + ## kubernetes.io/metadata.name: istio-system + ## podSelector: + ## matchLabels: + ## istio: pilot + ## - ports: + ## - protocol: TCP + ## port: 9200 + ## - protocol: TCP + ## port: 9300 + ## to: + ## - namespaceSelector: + ## matchLabels: + ## kubernetes.io/metadata.name: shuffle + ## podSelector: + ## matchLabels: + ## app.kubernetes.io/name: opensearch + ## + extraEgress: [] + + ## @param backend.cleanupSchedule The interval in seconds at which the cleanup job runs + ## + cleanupSchedule: 300 + + ## OpenSearch configuration + ## + openSearch: + ## @param backend.openSearch.url The URL at which OpenSearch is available + ## + url: "http://{{ .Release.Name }}-opensearch:9200" + ## @param backend.openSearch.username The username that is used for authenticating with OpenSearch + ## + username: admin + ## @param backend.openSearch.certificateFile The path to a custom OpenSearch certificate file + ## + certificateFile: "" + ## @param backend.openSearch.skipSSLVerify Skip SSL verification + ## + skipSSLVerify: false + ## @param backend.openSearch.indexPrefix A prefix for OpenSearch indices + ## + indexPrefix: "" + + ## App configuration + ## + apps: + ## @param backend.apps.downloadLocation The location to a git repository from which default appps are downloaded on startup. + ## + downloadLocation: https://github.com/shuffle/python-apps + ## @param backend.apps.downloadBranch The branch from which apps should be downloaded on startup. + ## + downloadBranch: master + ## @param backend.apps.forceUpdate Force an update of apps on startup. + ## + forceUpdate: false + +## @section frontend Parameters +## +frontend: + ## frontend image + ## @param frontend.image.registry frontend image registry + ## @param frontend.image.repository frontend image repository + ## @skip frontend.image.tag frontend image tag (immutable tags are recommended) + ## @param frontend.image.digest frontend image digest in the way sha256:aa.... Please note this parameter, if set, will override the tag image tag (immutable tags are recommended) + ## @param frontend.image.pullPolicy frontend image pull policy + ## @param frontend.image.pullSecrets frontend image pull secrets + ## + image: + registry: ghcr.io + repository: shuffle/shuffle-frontend + tag: nightly + digest: "" + ## Specify a imagePullPolicy + ## Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent' + ## ref: https://kubernetes.io/docs/concepts/containers/images/#pre-pulled-images + ## + pullPolicy: IfNotPresent + ## Optionally specify an array of imagePullSecrets. + ## Secrets must be manually created in the namespace. + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ + ## e.g: + ## pullSecrets: + ## - myRegistryKeySecretName + ## + pullSecrets: [] + ## @param frontend.replicaCount Number of frontend replicas to deploy + ## + replicaCount: 1 + ## Configure extra options for frontend containers' liveness and readiness probes + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/#configure-probes + ## @param frontend.livenessProbe.enabled Enable livenessProbe on frontend containers + ## @param frontend.livenessProbe.initialDelaySeconds Initial delay seconds for livenessProbe + ## @param frontend.livenessProbe.periodSeconds Period seconds for livenessProbe + ## @param frontend.livenessProbe.timeoutSeconds Timeout seconds for livenessProbe + ## @param frontend.livenessProbe.failureThreshold Failure threshold for livenessProbe + ## @param frontend.livenessProbe.successThreshold Success threshold for livenessProbe + ## + livenessProbe: + enabled: false + initialDelaySeconds: 0 + periodSeconds: 15 + timeoutSeconds: 1 + failureThreshold: 4 + successThreshold: 1 + ## @param frontend.readinessProbe.enabled Enable readinessProbe on frontend containers + ## @param frontend.readinessProbe.initialDelaySeconds Initial delay seconds for readinessProbe + ## @param frontend.readinessProbe.periodSeconds Period seconds for readinessProbe + ## @param frontend.readinessProbe.timeoutSeconds Timeout seconds for readinessProbe + ## @param frontend.readinessProbe.failureThreshold Failure threshold for readinessProbe + ## @param frontend.readinessProbe.successThreshold Success threshold for readinessProbe + ## + readinessProbe: + enabled: false + initialDelaySeconds: 0 + periodSeconds: 5 + timeoutSeconds: 1 + failureThreshold: 3 + successThreshold: 1 + ## @param frontend.startupProbe.enabled Enable startupProbe on frontend containers + ## @param frontend.startupProbe.initialDelaySeconds Initial delay seconds for startupProbe + ## @param frontend.startupProbe.periodSeconds Period seconds for startupProbe + ## @param frontend.startupProbe.timeoutSeconds Timeout seconds for startupProbe + ## @param frontend.startupProbe.failureThreshold Failure threshold for startupProbe + ## @param frontend.startupProbe.successThreshold Success threshold for startupProbe + ## + startupProbe: + enabled: false + initialDelaySeconds: 0 + periodSeconds: 1 + timeoutSeconds: 1 + failureThreshold: 60 + successThreshold: 1 + ## @param frontend.customLivenessProbe Custom livenessProbe that overrides the default one + ## + customLivenessProbe: {} + ## @param frontend.customReadinessProbe Custom readinessProbe that overrides the default one + ## + customReadinessProbe: {} + ## @param frontend.customStartupProbe Custom startupProbe that overrides the default one + ## + customStartupProbe: {} + ## frontend resource requests and limits + ## ref: http://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/ + ## @param frontend.resourcesPreset Set frontend container resources according to one common preset (allowed values: none, nano, small, medium, large, xlarge, 2xlarge). This is ignored if frontend.resources is set (frontend.resources is recommended for production). + ## More information: https://github.com/bitnami/charts/blob/main/bitnami/common/templates/_resources.tpl#L15 + ## + resourcesPreset: "nano" + ## @param frontend.resources Set frontend container requests and limits for different resources like CPU or memory (essential for production workloads) + ## Example: + ## resources: + ## requests: + ## cpu: 2 + ## memory: 512Mi + ## limits: + ## cpu: 3 + ## memory: 1024Mi + ## + resources: {} + ## Configure Pods Security Context + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-pod + ## @param frontend.podSecurityContext.enabled Enable frontend pods' Security Context + ## @param frontend.podSecurityContext.fsGroupChangePolicy Set filesystem group change policy for frontend pods + ## @param frontend.podSecurityContext.sysctls Set kernel settings using the sysctl interface for frontend pods + ## @param frontend.podSecurityContext.supplementalGroups Set filesystem extra groups for frontend pods + ## @param frontend.podSecurityContext.fsGroup Set fsGroup in frontend pods' Security Context + ## + podSecurityContext: + enabled: true + fsGroupChangePolicy: Always + sysctls: [] + supplementalGroups: [] + fsGroup: 1001 + ## Configure Container Security Context + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-container + ## @param frontend.containerSecurityContext.enabled Enabled frontend container' Security Context + ## @param frontend.containerSecurityContext.seLinuxOptions [object,nullable] Set SELinux options in frontend container + ## @param frontend.containerSecurityContext.runAsUser Set runAsUser in frontend container' Security Context + ## @param frontend.containerSecurityContext.runAsGroup Set runAsGroup in frontend container' Security Context + ## @param frontend.containerSecurityContext.runAsNonRoot Set runAsNonRoot in frontend container' Security Context + ## @param frontend.containerSecurityContext.readOnlyRootFilesystem Set readOnlyRootFilesystem in frontend container' Security Context + ## @param frontend.containerSecurityContext.privileged Set privileged in frontend container' Security Context + ## @param frontend.containerSecurityContext.allowPrivilegeEscalation Set allowPrivilegeEscalation in frontend container' Security Context + ## @param frontend.containerSecurityContext.capabilities.drop List of capabilities to be dropped in frontend container + ## @param frontend.containerSecurityContext.seccompProfile.type Set seccomp profile in frontend container + ## + containerSecurityContext: + enabled: true + seLinuxOptions: {} + runAsUser: 101 + runAsGroup: 101 + runAsNonRoot: true + readOnlyRootFilesystem: true + privileged: false + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + seccompProfile: + type: "RuntimeDefault" + + ## @param frontend.command Override default frontend container command (useful when using custom images) + ## + command: [] + ## @param frontend.args Override default frontend container args (useful when using custom images) + ## + args: [] + ## @param frontend.automountServiceAccountToken Mount Service Account token in frontend pods + ## + automountServiceAccountToken: false + ## @param frontend.hostAliases frontend pods host aliases + ## https://kubernetes.io/docs/concepts/services-networking/add-entries-to-pod-etc-hosts-with-host-aliases/ + ## + hostAliases: [] + ## @param frontend.daemonsetAnnotations Annotations for frontend daemonset + ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ + ## + daemonsetAnnotations: {} + ## @param frontend.deploymentAnnotations Annotations for frontend deployment + ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ + ## + deploymentAnnotations: {} + ## @param frontend.statefulsetAnnotations Annotations for frontend statefulset + ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ + ## + statefulsetAnnotations: {} + ## @param frontend.podLabels Extra labels for frontend pods + ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ + ## + podLabels: {} + ## @param frontend.podAnnotations Annotations for frontend pods + ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ + ## + podAnnotations: {} + ## @param frontend.podAffinityPreset Pod affinity preset. Ignored if `frontend.affinity` is set. Allowed values: `soft` or `hard` + ## ref: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#inter-pod-affinity-and-anti-affinity + ## + podAffinityPreset: "" + ## @param frontend.podAntiAffinityPreset Pod anti-affinity preset. Ignored if `frontend.affinity` is set. Allowed values: `soft` or `hard` + ## ref: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#inter-pod-affinity-and-anti-affinity + ## + podAntiAffinityPreset: soft + ## Node frontend.affinity preset + ## ref: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#node-affinity + ## + nodeAffinityPreset: + ## @param frontend.nodeAffinityPreset.type Node affinity preset type. Ignored if `frontend.affinity` is set. Allowed values: `soft` or `hard` + ## + type: "" + ## @param frontend.nodeAffinityPreset.key Node label key to match. Ignored if `frontend.affinity` is set + ## + key: "" + ## @param frontend.nodeAffinityPreset.values Node label values to match. Ignored if `frontend.affinity` is set + ## E.g. + ## values: + ## - e2e-az1 + ## - e2e-az2 + ## + values: [] + ## @param frontend.affinity Affinity for frontend pods assignment + ## ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity + ## NOTE: `frontend.podAffinityPreset`, `frontend.podAntiAffinityPreset`, and `frontend.nodeAffinityPreset` will be ignored when it's set + ## + affinity: {} + ## @param frontend.nodeSelector Node labels for frontend pods assignment + ## ref: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/ + ## + nodeSelector: {} + ## @param frontend.tolerations Tolerations for frontend pods assignment + ## ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ + ## + tolerations: [] + ## ONLY FOR DEPLOYMENTS: + ## @param frontend.updateStrategy.type frontend deployment strategy type + ## ref: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#strategy + ## ONLY FOR STATEFULSETS: + ## @param frontend.updateStrategy.type frontend statefulset strategy type + ## ref: https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#update-strategies + ## + updateStrategy: + ## ONLY FOR DEPLOYMENTS: + ## Can be set to RollingUpdate or Recreate + ## ONLY FOR STATEFULSETS: + ## Can be set to RollingUpdate or OnDelete + ## + type: RollingUpdate + ## ONLY FOR STATEFULSETS: + ## @param frontend.podManagementPolicy Pod management policy for frontend statefulset + ## Ref: https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#pod-management-policies + ## + podManagementPolicy: OrderedReady + ## @param frontend.priorityClassName frontend pods' priorityClassName + ## + priorityClassName: "" + ## @param frontend.topologySpreadConstraints Topology Spread Constraints for frontend pod assignment spread across your cluster among failure-domains + ## Ref: https://kubernetes.io/docs/concepts/workloads/pods/pod-topology-spread-constraints/#spread-constraints-for-pods + ## + topologySpreadConstraints: [] + ## @param frontend.schedulerName Name of the k8s scheduler (other than default) for frontend pods + ## ref: https://kubernetes.io/docs/tasks/administer-cluster/configure-multiple-schedulers/ + ## + schedulerName: "" + ## @param frontend.terminationGracePeriodSeconds Seconds frontend pods need to terminate gracefully + ## ref: https://kubernetes.io/docs/concepts/workloads/pods/pod/#termination-of-pods + ## + terminationGracePeriodSeconds: "" + ## @param frontend.lifecycleHooks for frontend containers to automate configuration before or after startup + ## + lifecycleHooks: {} + ## @param frontend.extraEnvVars Array with extra environment variables to add to frontend containers + ## e.g: + ## extraEnvVars: + ## - name: FOO + ## value: "bar" + ## + extraEnvVars: [] + ## @param frontend.extraEnvVarsCM Name of existing ConfigMap containing extra env vars for frontend containers + ## + extraEnvVarsCM: "" + ## @param frontend.extraEnvVarsSecret Name of existing Secret containing extra env vars for frontend containers + ## + extraEnvVarsSecret: "" + ## @param frontend.extraVolumes Optionally specify extra list of additional volumes for the frontend pods + ## + extraVolumes: [] + ## @param frontend.extraVolumeMounts Optionally specify extra list of additional volumeMounts for the frontend containers + ## + extraVolumeMounts: [] + ## @param frontend.sidecars Add additional sidecar containers to the frontend pods + ## e.g: + ## sidecars: + ## - name: your-image-name + ## image: your-image + ## imagePullPolicy: Always + ## ports: + ## - name: portname + ## containerPort: 1234 + ## + sidecars: [] + ## @param frontend.initContainers Add additional init containers to the frontend pods + ## ref: https://kubernetes.io/docs/concepts/workloads/pods/init-containers/ + ## e.g: + ## initContainers: + ## - name: your-image-name + ## image: your-image + ## imagePullPolicy: Always + ## command: ['sh', '-c', 'echo "hello world"'] + ## + initContainers: [] + ## Pod Disruption Budget configuration + ## ref: https://kubernetes.io/docs/tasks/run-application/configure-pdb + ## @param frontend.pdb.create Enable/disable a Pod Disruption Budget creation + ## @param frontend.pdb.minAvailable Minimum number/percentage of pods that should remain scheduled + ## @param frontend.pdb.maxUnavailable Maximum number/percentage of pods that may be made unavailable. Defaults to `1` if both `frontend.pdb.minAvailable` and `frontend.pdb.maxUnavailable` are empty. + ## + pdb: + create: true + minAvailable: "" + maxUnavailable: "" + ## Autoscaling configuration + ## ref: https://kubernetes.io/docs/concepts/workloads/autoscaling/ + ## + autoscaling: + ## @param frontend.autoscaling.vpa.enabled Enable VPA for frontend pods + ## @param frontend.autoscaling.vpa.annotations Annotations for VPA resource + ## @param frontend.autoscaling.vpa.controlledResources VPA List of resources that the vertical pod autoscaler can control. Defaults to cpu and memory + ## @param frontend.autoscaling.vpa.maxAllowed VPA Max allowed resources for the pod + ## @param frontend.autoscaling.vpa.minAllowed VPA Min allowed resources for the pod + ## + vpa: + enabled: false + annotations: {} + controlledResources: [] + maxAllowed: {} + minAllowed: {} + ## @param frontend.autoscaling.vpa.updatePolicy.updateMode Autoscaling update policy + ## Specifies whether recommended updates are applied when a Pod is started and whether recommended updates are applied during the life of a Pod + ## Possible values are "Off", "Initial", "Recreate", and "Auto". + ## + updatePolicy: + updateMode: Auto + ## @param frontend.autoscaling.hpa.enabled Enable HPA for frontend pods + ## @param frontend.autoscaling.hpa.minReplicas Minimum number of replicas + ## @param frontend.autoscaling.hpa.maxReplicas Maximum number of replicas + ## @param frontend.autoscaling.hpa.targetCPU Target CPU utilization percentage + ## @param frontend.autoscaling.hpa.targetMemory Target Memory utilization percentage + ## + hpa: + enabled: false + minReplicas: "" + maxReplicas: "" + targetCPU: "" + targetMemory: "" + + ## ServiceAccount configuration + ## + serviceAccount: + ## @param frontend.serviceAccount.create Specifies whether a ServiceAccount should be created + ## + create: true + ## @param frontend.serviceAccount.name The name of the ServiceAccount to use. + ## If not set and create is true, a name is generated using the common.names.fullname template + ## + name: "" + ## @param frontend.serviceAccount.annotations Additional Service Account annotations (evaluated as a template) + ## + annotations: {} + ## @param frontend.serviceAccount.automountServiceAccountToken Automount service account token for the server service account + ## + automountServiceAccountToken: true + + ## Network Policies + ## Ref: https://kubernetes.io/docs/concepts/services-networking/network-policies/ + ## + networkPolicy: + ## @param frontend.networkPolicy.enabled Specifies whether a NetworkPolicy should be created + ## + enabled: true + ## @param frontend.networkPolicy.allowExternal Don't require server label for connections + ## The Policy model to apply. When set to false, only pods with the correct + ## server label will have network access to the ports server is listening + ## on. When true, server will accept connections from any source + ## (with the correct destination port). + ## + allowExternal: true + ## @param frontend.networkPolicy.allowExternalEgress Allow the pod to access any range of port and all destinations. + ## + allowExternalEgress: true + ## @param frontend.networkPolicy.extraIngress Add extra ingress rules to the NetworkPolicy + ## NOTE: You likely want to allow access from your ingress, e.g.: + ## extraIngress: + ## - ports: + ## - protocol: TCP + ## port: 5001 + ## from: + ## - namespaceSelector: + ## matchLabels: + ## kubernetes.io/metadata.name: istio-ingress + ## podSelector: + ## matchLabels: + ## istio: ingress + ## + extraIngress: [] + ## @param frontend.networkPolicy.extraEgress Add extra ingress rules to the NetworkPolicy (ignored if allowExternalEgress=true) + ## + extraEgress: [] + +## @section orborus Parameters +## +orborus: + ## orborus image + ## @param orborus.image.registry orborus image registry + ## @param orborus.image.repository orborus image repository + ## @skip orborus.image.tag orborus image tag (immutable tags are recommended) + ## @param orborus.image.digest orborus image digest in the way sha256:aa.... Please note this parameter, if set, will override the tag image tag (immutable tags are recommended) + ## @param orborus.image.pullPolicy orborus image pull policy + ## @param orborus.image.pullSecrets orborus image pull secrets + ## + image: + registry: ghcr.io + repository: shuffle/shuffle-orborus + tag: nightly + digest: "" + ## Specify a imagePullPolicy + ## Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent' + ## ref: https://kubernetes.io/docs/concepts/containers/images/#pre-pulled-images + ## + pullPolicy: IfNotPresent + ## Optionally specify an array of imagePullSecrets. + ## Secrets must be manually created in the namespace. + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ + ## e.g: + ## pullSecrets: + ## - myRegistryKeySecretName + ## + pullSecrets: [] + ## @param orborus.replicaCount Number of orborus replicas to deploy + ## + replicaCount: 1 + ## Configure extra options for orborus containers' liveness and readiness probes + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/#configure-probes + ## @param orborus.livenessProbe.enabled Enable livenessProbe on orborus containers + ## @param orborus.livenessProbe.initialDelaySeconds Initial delay seconds for livenessProbe + ## @param orborus.livenessProbe.periodSeconds Period seconds for livenessProbe + ## @param orborus.livenessProbe.timeoutSeconds Timeout seconds for livenessProbe + ## @param orborus.livenessProbe.failureThreshold Failure threshold for livenessProbe + ## @param orborus.livenessProbe.successThreshold Success threshold for livenessProbe + ## + livenessProbe: + enabled: false + initialDelaySeconds: 0 + periodSeconds: 15 + timeoutSeconds: 1 + failureThreshold: 4 + successThreshold: 1 + ## @param orborus.readinessProbe.enabled Enable readinessProbe on orborus containers + ## @param orborus.readinessProbe.initialDelaySeconds Initial delay seconds for readinessProbe + ## @param orborus.readinessProbe.periodSeconds Period seconds for readinessProbe + ## @param orborus.readinessProbe.timeoutSeconds Timeout seconds for readinessProbe + ## @param orborus.readinessProbe.failureThreshold Failure threshold for readinessProbe + ## @param orborus.readinessProbe.successThreshold Success threshold for readinessProbe + ## + readinessProbe: + enabled: false + initialDelaySeconds: 0 + periodSeconds: 5 + timeoutSeconds: 1 + failureThreshold: 3 + successThreshold: 1 + ## @param orborus.startupProbe.enabled Enable startupProbe on orborus containers + ## @param orborus.startupProbe.initialDelaySeconds Initial delay seconds for startupProbe + ## @param orborus.startupProbe.periodSeconds Period seconds for startupProbe + ## @param orborus.startupProbe.timeoutSeconds Timeout seconds for startupProbe + ## @param orborus.startupProbe.failureThreshold Failure threshold for startupProbe + ## @param orborus.startupProbe.successThreshold Success threshold for startupProbe + ## + startupProbe: + enabled: false + initialDelaySeconds: 0 + periodSeconds: 1 + timeoutSeconds: 1 + failureThreshold: 60 + successThreshold: 1 + ## @param orborus.customLivenessProbe Custom livenessProbe that overrides the default one + ## + customLivenessProbe: {} + ## @param orborus.customReadinessProbe Custom readinessProbe that overrides the default one + ## + customReadinessProbe: {} + ## @param orborus.customStartupProbe Custom startupProbe that overrides the default one + ## + customStartupProbe: {} + ## orborus resource requests and limits + ## ref: http://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/ + ## @param orborus.resourcesPreset Set orborus container resources according to one common preset (allowed values: none, nano, small, medium, large, xlarge, 2xlarge). This is ignored if orborus.resources is set (orborus.resources is recommended for production). + ## More information: https://github.com/bitnami/charts/blob/main/bitnami/common/templates/_resources.tpl#L15 + ## + resourcesPreset: "nano" + ## @param orborus.resources Set orborus container requests and limits for different resources like CPU or memory (essential for production workloads) + ## Example: + ## resources: + ## requests: + ## cpu: 2 + ## memory: 512Mi + ## limits: + ## cpu: 3 + ## memory: 1024Mi + ## + resources: {} + ## Configure Pods Security Context + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-pod + ## @param orborus.podSecurityContext.enabled Enable orborus pods' Security Context + ## @param orborus.podSecurityContext.fsGroupChangePolicy Set filesystem group change policy for orborus pods + ## @param orborus.podSecurityContext.sysctls Set kernel settings using the sysctl interface for orborus pods + ## @param orborus.podSecurityContext.supplementalGroups Set filesystem extra groups for orborus pods + ## @param orborus.podSecurityContext.fsGroup Set fsGroup in orborus pods' Security Context + ## + podSecurityContext: + enabled: true + fsGroupChangePolicy: Always + sysctls: [] + supplementalGroups: [] + fsGroup: 1001 + ## Configure Container Security Context + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-container + ## @param orborus.containerSecurityContext.enabled Enabled orborus container' Security Context + ## @param orborus.containerSecurityContext.seLinuxOptions [object,nullable] Set SELinux options in orborus container + ## @param orborus.containerSecurityContext.runAsUser Set runAsUser in orborus container' Security Context + ## @param orborus.containerSecurityContext.runAsGroup Set runAsGroup in orborus container' Security Context + ## @param orborus.containerSecurityContext.runAsNonRoot Set runAsNonRoot in orborus container' Security Context + ## @param orborus.containerSecurityContext.readOnlyRootFilesystem Set readOnlyRootFilesystem in orborus container' Security Context + ## @param orborus.containerSecurityContext.privileged Set privileged in orborus container' Security Context + ## @param orborus.containerSecurityContext.allowPrivilegeEscalation Set allowPrivilegeEscalation in orborus container' Security Context + ## @param orborus.containerSecurityContext.capabilities.drop List of capabilities to be dropped in orborus container + ## @param orborus.containerSecurityContext.seccompProfile.type Set seccomp profile in orborus container + ## + containerSecurityContext: + enabled: true + seLinuxOptions: {} + runAsUser: 101 + runAsGroup: 101 + runAsNonRoot: true + readOnlyRootFilesystem: true + privileged: false + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + seccompProfile: + type: "RuntimeDefault" + + ## @param orborus.command Override default orborus container command (useful when using custom images) + ## + command: [] + ## @param orborus.args Override default orborus container args (useful when using custom images) + ## + args: [] + ## @param orborus.automountServiceAccountToken Mount Service Account token in orborus pods + ## NOTE: orborus requires the service account credentials to be mounted + ## + automountServiceAccountToken: true + ## @param orborus.hostAliases orborus pods host aliases + ## https://kubernetes.io/docs/concepts/services-networking/add-entries-to-pod-etc-hosts-with-host-aliases/ + ## + hostAliases: [] + ## @param orborus.daemonsetAnnotations Annotations for orborus daemonset + ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ + ## + daemonsetAnnotations: {} + ## @param orborus.deploymentAnnotations Annotations for orborus deployment + ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ + ## + deploymentAnnotations: {} + ## @param orborus.statefulsetAnnotations Annotations for orborus statefulset + ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ + ## + statefulsetAnnotations: {} + ## @param orborus.podLabels Extra labels for orborus pods + ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ + ## + podLabels: {} + ## @param orborus.podAnnotations Annotations for orborus pods + ## ref: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ + ## + podAnnotations: {} + ## @param orborus.podAffinityPreset Pod affinity preset. Ignored if `orborus.affinity` is set. Allowed values: `soft` or `hard` + ## ref: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#inter-pod-affinity-and-anti-affinity + ## + podAffinityPreset: "" + ## @param orborus.podAntiAffinityPreset Pod anti-affinity preset. Ignored if `orborus.affinity` is set. Allowed values: `soft` or `hard` + ## ref: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#inter-pod-affinity-and-anti-affinity + ## + podAntiAffinityPreset: soft + ## Node orborus.affinity preset + ## ref: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#node-affinity + ## + nodeAffinityPreset: + ## @param orborus.nodeAffinityPreset.type Node affinity preset type. Ignored if `orborus.affinity` is set. Allowed values: `soft` or `hard` + ## + type: "" + ## @param orborus.nodeAffinityPreset.key Node label key to match. Ignored if `orborus.affinity` is set + ## + key: "" + ## @param orborus.nodeAffinityPreset.values Node label values to match. Ignored if `orborus.affinity` is set + ## E.g. + ## values: + ## - e2e-az1 + ## - e2e-az2 + ## + values: [] + ## @param orborus.affinity Affinity for orborus pods assignment + ## ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity + ## NOTE: `orborus.podAffinityPreset`, `orborus.podAntiAffinityPreset`, and `orborus.nodeAffinityPreset` will be ignored when it's set + ## + affinity: {} + ## @param orborus.nodeSelector Node labels for orborus pods assignment + ## ref: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/ + ## + nodeSelector: {} + ## @param orborus.tolerations Tolerations for orborus pods assignment + ## ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ + ## + tolerations: [] + ## ONLY FOR DEPLOYMENTS: + ## @param orborus.updateStrategy.type orborus deployment strategy type + ## ref: https://kubernetes.io/docs/concepts/workloads/controllers/deployment/#strategy + ## ONLY FOR STATEFULSETS: + ## @param orborus.updateStrategy.type orborus statefulset strategy type + ## ref: https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#update-strategies + ## + updateStrategy: + ## ONLY FOR DEPLOYMENTS: + ## Can be set to RollingUpdate or Recreate + ## ONLY FOR STATEFULSETS: + ## Can be set to RollingUpdate or OnDelete + ## + type: RollingUpdate + ## ONLY FOR STATEFULSETS: + ## @param orborus.podManagementPolicy Pod management policy for orborus statefulset + ## Ref: https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#pod-management-policies + ## + podManagementPolicy: OrderedReady + ## @param orborus.priorityClassName orborus pods' priorityClassName + ## + priorityClassName: "" + ## @param orborus.topologySpreadConstraints Topology Spread Constraints for orborus pod assignment spread across your cluster among failure-domains + ## Ref: https://kubernetes.io/docs/concepts/workloads/pods/pod-topology-spread-constraints/#spread-constraints-for-pods + ## + topologySpreadConstraints: [] + ## @param orborus.schedulerName Name of the k8s scheduler (other than default) for orborus pods + ## ref: https://kubernetes.io/docs/tasks/administer-cluster/configure-multiple-schedulers/ + ## + schedulerName: "" + ## @param orborus.terminationGracePeriodSeconds Seconds orborus pods need to terminate gracefully + ## ref: https://kubernetes.io/docs/concepts/workloads/pods/pod/#termination-of-pods + ## + terminationGracePeriodSeconds: "" + ## @param orborus.lifecycleHooks for orborus containers to automate configuration before or after startup + ## + lifecycleHooks: {} + ## @param orborus.extraEnvVars Array with extra environment variables to add to orborus containers + ## e.g: + ## extraEnvVars: + ## - name: FOO + ## value: "bar" + ## + extraEnvVars: [] + ## @param orborus.extraEnvVarsCM Name of existing ConfigMap containing extra env vars for orborus containers + ## + extraEnvVarsCM: "" + ## @param orborus.extraEnvVarsSecret Name of existing Secret containing extra env vars for orborus containers + ## + extraEnvVarsSecret: "" + ## @param orborus.extraVolumes Optionally specify extra list of additional volumes for the orborus pods + ## + extraVolumes: [] + ## @param orborus.extraVolumeMounts Optionally specify extra list of additional volumeMounts for the orborus containers + ## + extraVolumeMounts: [] + ## @param orborus.sidecars Add additional sidecar containers to the orborus pods + ## e.g: + ## sidecars: + ## - name: your-image-name + ## image: your-image + ## imagePullPolicy: Always + ## ports: + ## - name: portname + ## containerPort: 1234 + ## + sidecars: [] + ## @param orborus.initContainers Add additional init containers to the orborus pods + ## ref: https://kubernetes.io/docs/concepts/workloads/pods/init-containers/ + ## e.g: + ## initContainers: + ## - name: your-image-name + ## image: your-image + ## imagePullPolicy: Always + ## command: ['sh', '-c', 'echo "hello world"'] + ## + initContainers: [] + ## Pod Disruption Budget configuration + ## ref: https://kubernetes.io/docs/tasks/run-application/configure-pdb + ## @param orborus.pdb.create Enable/disable a Pod Disruption Budget creation + ## @param orborus.pdb.minAvailable Minimum number/percentage of pods that should remain scheduled + ## @param orborus.pdb.maxUnavailable Maximum number/percentage of pods that may be made unavailable. Defaults to `1` if both `orborus.pdb.minAvailable` and `orborus.pdb.maxUnavailable` are empty. + ## + pdb: + create: true + minAvailable: "" + maxUnavailable: "" + ## Autoscaling configuration + ## ref: https://kubernetes.io/docs/concepts/workloads/autoscaling/ + ## + autoscaling: + ## @param orborus.autoscaling.vpa.enabled Enable VPA for orborus pods + ## @param orborus.autoscaling.vpa.annotations Annotations for VPA resource + ## @param orborus.autoscaling.vpa.controlledResources VPA List of resources that the vertical pod autoscaler can control. Defaults to cpu and memory + ## @param orborus.autoscaling.vpa.maxAllowed VPA Max allowed resources for the pod + ## @param orborus.autoscaling.vpa.minAllowed VPA Min allowed resources for the pod + ## + vpa: + enabled: false + annotations: {} + controlledResources: [] + maxAllowed: {} + minAllowed: {} + ## @param orborus.autoscaling.vpa.updatePolicy.updateMode Autoscaling update policy + ## Specifies whether recommended updates are applied when a Pod is started and whether recommended updates are applied during the life of a Pod + ## Possible values are "Off", "Initial", "Recreate", and "Auto". + ## + updatePolicy: + updateMode: Auto + ## @param orborus.autoscaling.hpa.enabled Enable HPA for orborus pods + ## @param orborus.autoscaling.hpa.minReplicas Minimum number of replicas + ## @param orborus.autoscaling.hpa.maxReplicas Maximum number of replicas + ## @param orborus.autoscaling.hpa.targetCPU Target CPU utilization percentage + ## @param orborus.autoscaling.hpa.targetMemory Target Memory utilization percentage + ## + hpa: + enabled: false + minReplicas: "" + maxReplicas: "" + targetCPU: "" + targetMemory: "" + + ## ServiceAccount configuration + ## + serviceAccount: + ## @param orborus.serviceAccount.create Specifies whether a ServiceAccount should be created + ## + create: true + ## @param orborus.serviceAccount.name The name of the ServiceAccount to use. + ## If not set and create is true, a name is generated using the common.names.fullname template + ## + name: "" + ## @param orborus.serviceAccount.annotations Additional Service Account annotations (evaluated as a template) + ## + annotations: {} + ## @param orborus.serviceAccount.automountServiceAccountToken Automount service account token for the server service account + ## + automountServiceAccountToken: true + + ## RBAC configuration + ## + rbac: + ## @param orborus.rbac.create Specifies whether RBAC resources should be created + create: true + + ## Network Policies + ## Ref: https://kubernetes.io/docs/concepts/services-networking/network-policies/ + ## + networkPolicy: + ## @param orborus.networkPolicy.enabled Specifies whether a NetworkPolicy should be created + ## + enabled: true + ## @param orborus.networkPolicy.allowExternal Don't require server label for connections + ## The Policy model to apply. When set to false, only pods with the correct + ## server label will have network access to the ports server is listening + ## on. When true, server will accept connections from any source + ## (with the correct destination port). + ## + allowExternal: true + ## @param orborus.networkPolicy.allowExternalEgress Allow the pod to access any range of port and all destinations. + ## + allowExternalEgress: true + ## @param orborus.networkPolicy.extraIngress Add extra ingress rules to the NetworkPolicy + ## + extraIngress: [] + ## @param orborus.networkPolicy.extraEgress Add extra ingress rules to the NetworkPolicy (ignored if allowExternalEgress=true) + ## NOTE: You likely want to allow access to cluster-proxies, e.g: + ## extraEgress: + ## - to: + ## - namespaceSelector: + ## matchLabels: + ## kubernetes.io/metadata.name: istio-system + ## podSelector: + ## matchLabels: + ## istio: pilot + ## + extraEgress: [] + +## @section worker Parameters +## +worker: + ## worker image + ## @param worker.image.registry worker image registry + ## @param worker.image.repository worker image repository + ## @skip worker.image.tag worker image tag (immutable tags are recommended) + ## @param worker.image.digest worker image digest in the way sha256:aa.... Please note this parameter, if set, will override the tag image tag (immutable tags are recommended) + ## + image: + registry: ghcr.io + repository: shuffle/shuffle-worker + tag: nightly + digest: "" + + ## ServiceAccount configuration + ## + serviceAccount: + ## @param worker.serviceAccount.create Specifies whether a ServiceAccount should be created + ## + create: true + ## @param worker.serviceAccount.name The name of the ServiceAccount to use. + ## If not set and create is true, a name is generated using the common.names.fullname template + ## + name: "" + ## @param worker.serviceAccount.annotations Additional Service Account annotations (evaluated as a template) + ## + annotations: {} + ## @param worker.serviceAccount.automountServiceAccountToken Automount service account token for the server service account + ## + automountServiceAccountToken: true + + ## RBAC configuration + ## + rbac: + ## @param worker.rbac.create Specifies whether RBAC resources should be created + create: true + + ## Network Policies + ## Ref: https://kubernetes.io/docs/concepts/services-networking/network-policies/ + ## + networkPolicy: + ## @param worker.networkPolicy.enabled Specifies whether a NetworkPolicy should be created + ## + enabled: true + ## @param worker.networkPolicy.allowExternal Don't require server label for connections + ## The Policy model to apply. When set to false, only pods with the correct + ## server label will have network access to the ports server is listening + ## on. When true, server will accept connections from any source + ## (with the correct destination port). + ## + allowExternal: true + ## @param worker.networkPolicy.allowExternalEgress Allow the pod to access any range of port and all destinations. + ## + allowExternalEgress: true + ## @param worker.networkPolicy.extraIngress Add extra ingress rules to the NetworkPolicy + ## + extraIngress: [] + ## @param worker.networkPolicy.extraEgress Add extra ingress rules to the NetworkPolicy (ignored if allowExternalEgress=true) + ## NOTE: You likely want to allow access to cluster-proxies, e.g: + ## extraEgress: + ## - to: + ## - namespaceSelector: + ## matchLabels: + ## kubernetes.io/metadata.name: istio-system + ## podSelector: + ## matchLabels: + ## istio: pilot + ## + extraEgress: [] + +## @section app Parameters +## +app: + ## ServiceAccount configuration + ## + serviceAccount: + ## @param app.serviceAccount.create Specifies whether a ServiceAccount should be created + ## + create: true + ## @param app.serviceAccount.name The name of the ServiceAccount to use. + ## If not set and create is true, a name is generated using the common.names.fullname template + ## + name: "" + ## @param app.serviceAccount.annotations Additional Service Account annotations (evaluated as a template) + ## + annotations: {} + ## @param app.serviceAccount.automountServiceAccountToken Automount service account token for the server service account + ## NOTE: You likely want to allow access to cluster-proxies, e.g: + ## extraEgress: + ## - to: + ## - namespaceSelector: + ## matchLabels: + ## kubernetes.io/metadata.name: istio-system + ## podSelector: + ## matchLabels: + ## istio: pilot + ## + automountServiceAccountToken: true + + ## RBAC configuration + ## + rbac: + ## @param app.rbac.create Specifies whether RBAC resources should be created + create: true + + ## Network Policies + ## Ref: https://kubernetes.io/docs/concepts/services-networking/network-policies/ + ## + networkPolicy: + ## @param app.networkPolicy.enabled Specifies whether a NetworkPolicy should be created + ## + enabled: true + ## @param app.networkPolicy.allowExternal Don't require server label for connections + ## The Policy model to apply. When set to false, only pods with the correct + ## server label will have network access to the ports server is listening + ## on. When true, server will accept connections from any source + ## (with the correct destination port). + ## + allowExternal: true + ## @param app.networkPolicy.allowExternalEgress Allow the pod to access any range of port and all destinations. + ## + allowExternalEgress: true + ## @param app.networkPolicy.extraIngress Add extra ingress rules to the NetworkPolicy + ## + extraIngress: [] + ## @param app.networkPolicy.extraEgress Add extra ingress rules to the NetworkPolicy (ignored if allowExternalEgress=true) + ## + extraEgress: [] + +## @section Traffic Exposure Parameters +## + +## ingress parameters +## ref: http://kubernetes.io/docs/concepts/services-networking/ingress/ +## +ingress: + ## @param ingress.enabled Enable ingress record generation for frontend and backend + ## + enabled: false + ## @param ingress.pathType Ingress path type for the frontend path + ## + pathType: Prefix + ## @param ingress.backendPathType Ingress path type for the backend path + ## + backendPathType: Prefix + ## @param ingress.apiVersion Force Ingress API version (automatically detected if not set) + ## + apiVersion: "" + ## @param ingress.hostname Default host for the ingress record + ## + hostname: shuffle.local + ## @param ingress.ingressClassName [default: nginx] IngressClass that will be be used to implement the Ingress (Kubernetes 1.18+) + ## This is supported in Kubernetes 1.18+ and required if you have more than one IngressClass marked as the default for your cluster . + ## ref: https://kubernetes.io/blog/2020/04/02/improvements-to-the-ingress-api-in-kubernetes-1.18/ + ## + ingressClassName: "" + ## @param ingress.path [default: "/"] Ingress path for Shuffle frontend + ## NOTE: The shuffle frontend currently does not support using base paths + ## + path: / + ## @param ingress.backendPath [default: "/api/"] Ingress path for Shuffle backend + ## NOTE: The shuffle backend is currently required to be reachable at shuffle-frontend.example.com/api/ + ## + backendPath: /api/ + ## @param ingress.annotations Additional annotations for the Ingress resource. + ## + annotations: {} + ## @param ingress.tls Enable TLS configuration for the host defined at `ingress.hostname` parameter + ## TLS certificates will be retrieved from a TLS secret with name: `{{- printf "%s-tls" .Values.ingress.hostname }}` + ## You can: + ## - Use the `ingress.secrets` parameter to create this TLS secret + ## - Rely on cert-manager to create it by setting the corresponding annotations + ## - Rely on Helm to create self-signed certificates by setting `ingress.selfSigned=true` + ## + tls: false + ## @param ingress.selfSigned Create a TLS secret for this ingress record using self-signed certificates generated by Helm + ## + selfSigned: false + ## @param ingress.extraHosts An array with additional hostname(s) to be covered with the ingress record + ## e.g: + ## extraHosts: + ## - name: example.local + ## path: / + ## + extraHosts: [] + ## @param ingress.extraPaths An array with additional arbitrary paths that may need to be added to the ingress under the main host + ## e.g: + ## extraPaths: + ## - path: /* + ## backend: + ## serviceName: ssl-redirect + ## servicePort: use-annotation + ## + extraPaths: [] + ## @param ingress.extraTls TLS configuration for additional hostname(s) to be covered with this ingress record + ## ref: https://kubernetes.io/docs/concepts/services-networking/ingress/#tls + ## e.g: + ## extraTls: + ## - hosts: + ## - example.local + ## secretName: example.local-tls + ## + extraTls: [] + ## @param ingress.secrets Custom TLS certificates as secrets + ## NOTE: 'key' and 'certificate' are expected in PEM format + ## NOTE: 'name' should line up with a 'secretName' set further up + ## If it is not set and you're using cert-manager, this is unneeded, as it will create a secret for you with valid certificates + ## If it is not set and you're NOT using cert-manager either, self-signed certificates will be created valid for 365 days + ## It is also possible to create and manage the certificates outside of this helm chart + ## Please see README.md for more information + ## e.g: + ## secrets: + ## - name: example.local-tls + ## key: |- + ## -----BEGIN RSA PRIVATE KEY----- + ## ... + ## -----END RSA PRIVATE KEY----- + ## certificate: |- + ## -----BEGIN CERTIFICATE----- + ## ... + ## -----END CERTIFICATE----- + ## + secrets: [] + ## @param ingress.extraRules Additional rules to be covered with this ingress record + ## ref: https://kubernetes.io/docs/concepts/services-networking/ingress/#ingress-rules + ## e.g: + ## extraRules: + ## - host: example.local + ## http: + ## path: / + ## backend: + ## service: + ## name: example-svc + ## port: + ## name: http + ## + extraRules: [] + +## @section Istio Parameters +## +istio: + ## @param istio.enabled Enable creation of an Istio Gateway and VirtualService for frontend and backend + ## + enabled: false + + ## @param istio.apiVersion The istio apiVersion to use for Gateway and VirtualService resources + ## + apiVersion: networking.istio.io/v1 + + ## @param istio.hosts One or more hosts exposed by Istio + ## + hosts: [] + + gateway: + ## @param istio.gateway.annotations Additional annotations for the Gateway resource + ## + annotations: {} + ## @param istio.gateway.selector [object, default: { istio: ingress }] The selector matches the ingress gateway pod labels + ## + selector: + istio: ingress + ## @param istio.gateway.http.enabled Enable HTTP server port 80 + ## @param istio.gateway.http.httpsRedirect If set to true, a 301 redirect is send for all HTTP connections + ## + http: + enabled: true + httpsRedirect: false + ## @param istio.gateway.https.enabled Enable HTTPS server on port 443 + ## @param istio.gateway.https.tlsCredentialName The name of the secret that holds the TLS certs including the CA certificates. + ## NOTE: The secret must exist in the namespace of the istio gateway pod + https: + enabled: false + tlsCredentialName: "" + ## @param istio.gateway.extraServers Additional servers for the Gateway resource + ## ref: https://istio.io/latest/docs/reference/config/networking/gateway/#Server + extraServers: [] + + virtualService: + ## @param istio.virtualService.annotations Additional annotations for the VirtualService resource. + ## + annotations: {} + +## @section Persistence Parameters +## + +## Enable persistence using Persistent Volume Claims +## ref: https://kubernetes.io/docs/concepts/storage/persistent-volumes/ +## +persistence: + ## @param persistence.enabled Enable persistence using Persistent Volume Claims + ## + enabled: true + + ## @param persistence.apps.existingClaim Name of an existing PVC to use + ## @param persistence.apps.storageClass PVC Storage Class for shuffle-apps volume + ## Note: The default StorageClass will be used if not defined. Set it to `-` to disable dynamic provisioning + ## @param persistence.apps.subPath The sub path used in the volume + ## @param persistence.apps.accessModes The access mode of the volume + ## @param persistence.apps.size The size of the volume + ## @param persistence.apps.annotations Annotations for the PVC + ## @param persistence.apps.selector Selector to match an existing Persistent Volume + apps: + existingClaim: "" + storageClass: "" + subPath: "" + accessModes: + - ReadWriteOnce + size: 5Gi + annotations: {} + selector: {} + + ## @param persistence.appBuilder.storageClass PVC Storage Class for backend-apps-claim volume + ## Note: The default StorageClass will be used if not defined. Set it to `-` to disable dynamic provisioning + ## @param persistence.appBuilder.accessModes The access mode of the volume + ## @param persistence.appBuilder.size The size of the volume + ## @param persistence.appBuilder.annotations Annotations for the PVC + ## @param persistence.appBuilder.selector Selector to match an existing Persistent Volume + appBuilder: + storageClass: "" + accessModes: + - ReadWriteOnce + size: 5Gi + annotations: {} + selector: {} + + ## @param persistence.files.existingClaim Name of an existing PVC to use + ## @param persistence.files.storageClass PVC Storage Class for shuffle-files volume + ## Note: The default StorageClass will be used if not defined. Set it to `-` to disable dynamic provisioning + ## @param persistence.files.subPath The sub path used in the volume + ## @param persistence.files.accessModes The access mode of the volume + ## @param persistence.files.size The size of the volume + ## @param persistence.files.annotations Annotations for the PVC + ## @param persistence.files.selector Selector to match an existing Persistent Volume + files: + existingClaim: "" + storageClass: "" + subPath: "" + accessModes: + - ReadWriteOnce + size: 5Gi + annotations: {} + selector: {} + +## @section Init Container Parameters +## + +## 'volumePermissions' init container parameters +## Changes the owner and group of the persistent volume mount point to runAsUser:fsGroup values +## based on the *podSecurityContext/*containerSecurityContext parameters +## +volumePermissions: + ## @param volumePermissions.enabled Enable init container that changes the owner/group of the PV mount point to `runAsUser:fsGroup` + ## + enabled: false + ## OS Shell + Utility image + ## ref: https://hub.docker.com/r/bitnami/os-shell/tags/ + ## @param volumePermissions.image.registry OS Shell + Utility image registry + ## @param volumePermissions.image.repository OS Shell + Utility image repository + ## @skip volumePermissions.image.tag OS Shell + Utility image tag (immutable tags are recommended) + ## @param volumePermissions.image.pullPolicy OS Shell + Utility image pull policy + ## @param volumePermissions.image.pullSecrets OS Shell + Utility image pull secrets + ## + image: + registry: docker.io + repository: bitnami/os-shell + tag: 12-debian-12-r30 + pullPolicy: IfNotPresent + ## Optionally specify an array of imagePullSecrets. + ## Secrets must be manually created in the namespace. + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ + ## e.g: + ## pullSecrets: + ## - myRegistryKeySecretName + ## + pullSecrets: [] + ## Init container's resource requests and limits + ## ref: http://kubernetes.io/docs/concepts/configuration/manage-compute-resources-container/ + ## @param volumePermissions.resourcesPreset Set init container resources according to one common preset (allowed values: none, nano, small, medium, large, xlarge, 2xlarge). This is ignored if volumePermissions.resources is set (volumePermissions.resources is recommended for production). + ## More information: https://github.com/bitnami/charts/blob/main/bitnami/common/templates/_resources.tpl#L15 + ## + resourcesPreset: "nano" + ## @param volumePermissions.resources Set init container requests and limits for different resources like CPU or memory (essential for production workloads) + ## Example: + ## resources: + ## requests: + ## cpu: 2 + ## memory: 512Mi + ## limits: + ## cpu: 3 + ## memory: 1024Mi + ## + resources: {} + ## Init container Container Security Context + ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/#set-the-security-context-for-a-container + ## @param volumePermissions.containerSecurityContext.enabled Enabled init container' Security Context + ## @param volumePermissions.containerSecurityContext.seLinuxOptions [object,nullable] Set SELinux options in init container + ## @param volumePermissions.containerSecurityContext.runAsUser Set init container's Security Context runAsUser + ## NOTE: when runAsUser is set to special value "auto", init container will try to chown the + ## data folder to auto-determined user&group, using commands: `id -u`:`id -G | cut -d" " -f2` + ## "auto" is especially useful for OpenShift which has scc with dynamic user ids (and 0 is not allowed) + ## + containerSecurityContext: + enabled: true + seLinuxOptions: {} + runAsUser: 0 + +## @section OpenSearch Parameters +## + +## OpenSearch chart configuration +## ref: https://github.com/bitnami/charts/blob/main/bitnami/opensearch/values.yaml +## @param opensearch.enabled Switch to enable or disable the opensearch helm chart +## +opensearch: + enabled: true + +## @section Vault Parameters +## + +vault: + ## @param vault.role Specify the Vault role, which should be used to get the secret from Vault. + ## NOTE: This value is used as a default for all secrets and can be overwritten for individual secrets + ## with the vaultRole property. + ## + role: "" + + ## @param vault.secrets A list of VaultSecrets to create + ## NOTE: 'type', 'name' and 'path' must be set + ## type is the type of the Kubernetes secret + ## name is the suffix of the name of the resulting (Vault)Secret + ## path is the path of the corresponding secret in Vault + ## Additional VaultSecret parameters can optionally be set. + ## Ref: https://github.com/ricoberger/vault-secrets-operator/blob/0409d56beb36ab95c4582a0cc35c0a2b517961e7/api/v1alpha1/vaultsecret_types.go#L9-L59 + ## e.g: + ## secrets: + ## - type: Opaque + ## name: "example" + ## path: "example/secret" + ## + secrets: [] +## @section Other Parameters +## From 00cfa0bb4494ade5fd50d255da3fad68badfc1ec Mon Sep 17 00:00:00 2001 From: Pascal Sthamer <10992664+P4sca1@users.noreply.github.com> Date: Thu, 2 Jan 2025 09:07:18 +0100 Subject: [PATCH 02/67] disable security contexts for frontend by default Incompatible with official shuffle frontend image Signed-off-by: Pascal Sthamer <10992664+P4sca1@users.noreply.github.com> --- charts/shuffle/values.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/charts/shuffle/values.yaml b/charts/shuffle/values.yaml index 0e551a45..daa4ec9a 100644 --- a/charts/shuffle/values.yaml +++ b/charts/shuffle/values.yaml @@ -656,7 +656,7 @@ frontend: ## @param frontend.podSecurityContext.fsGroup Set fsGroup in frontend pods' Security Context ## podSecurityContext: - enabled: true + enabled: false fsGroupChangePolicy: Always sysctls: [] supplementalGroups: [] @@ -675,7 +675,7 @@ frontend: ## @param frontend.containerSecurityContext.seccompProfile.type Set seccomp profile in frontend container ## containerSecurityContext: - enabled: true + enabled: false seLinuxOptions: {} runAsUser: 101 runAsGroup: 101 From 95949415f399cd770dfb0920a7928a030179f2f3 Mon Sep 17 00:00:00 2001 From: Pascal Sthamer <10992664+P4sca1@users.noreply.github.com> Date: Thu, 2 Jan 2025 09:07:45 +0100 Subject: [PATCH 03/67] update README Signed-off-by: Pascal Sthamer <10992664+P4sca1@users.noreply.github.com> --- charts/shuffle/README.md | 60 ++++++++++++++++++++++------------------ 1 file changed, 33 insertions(+), 27 deletions(-) diff --git a/charts/shuffle/README.md b/charts/shuffle/README.md index 5b5ebe55..2dd62129 100644 --- a/charts/shuffle/README.md +++ b/charts/shuffle/README.md @@ -17,45 +17,52 @@ SPDX-License-Identifier: APACHE-2.0 ## Usage ```sh -# Lint chart -helm lint . - -# Package chart -helm package . - # Install (the shuffle namespace is hardcoded into the shuffle source code) -helm install shuffle oci://TODO -n shuffle +helm install shuffle oci://TODO --namespace shuffle --create-namespace ``` ## Secret Parameters The helm chart was designed to not contain any secret data and does not allow configuring secret data using helm values. -Instead, secret values must be passed to services using `extraEnvVarsSecret`. The secrets need to be manually created. +Instead, secret values must be passed to services using `extraEnvVarsSecret` or `extraEnvVars`. -### Creating secrets using vault-secrets-operator - -If you are using [vault-secret-operator by Rico Berger](https://github.com/ricoberger/vault-secrets-operator), -then you can create VaultSecret resources via Helm. -Note that the resulting (Vault)Secret is prefixed with the release name of the chart. - -```yaml -vault: - secrets: - - name: backend-env - type: Opaque - path: shuffle/backend/env -``` +The secrets need to be manually created. It is possible to run this helm chart without specifying any secrets. +You will be prompted to create an admin user when visiting the shuffle dashboard for the first time. +Note that information will not be encrypted without specifying the `SHUFFLE_ENCRYPTION_MODIFIER` value. ### Mounting env variables into a service -After creating a secret which holds the environment variables (either manually or via a VaultSecret), you can then -use that secret to mount environment variables into a service via the `extraEnvVarsSecret` value. - -You can use helm templates for generating the secret name as shown in the example below. +After creating secrets which hold sensitive information, you can mount them as environment variables into a +service via the `extraEnvVarsSecret` or `extraEnvVars` values. ```yaml backend: - extraEnvVarsSecret: "{{ include \"common.names.fullname\" . }}-backend-env" + # Use a single secret, which holds environment variables. + # Remember that the secret keys must exactly match the environment variable names. + extraEnvVarsSecret: shuffle-backend-env + + # Or mount each value explicitly + extraEnvVars: + - name: SHUFFLE_DEFAULT_USERNAME + valueFrom: + secretKeyRef: + name: "shuffle-initial-user" + key: username + - name: SHUFFLE_DEFAULT_PASSWORD + valueFrom: + secretKeyRef: + name: "shuffle-initial-user" + key: password + - name: SHUFFLE_DEFAULT_APIKEY + valueFrom: + secretKeyRef: + name: "shuffle-initial-user" + key: apikey + - name: SHUFFLE_ENCRYPTION_MODIFIER + valueFrom: + secretKeyRef: + name: "shuffle-encryption" + key: modifier ``` ### Backend @@ -567,4 +574,3 @@ SHUFFLE_ENCRYPTION_MODIFIER: "MyShuffleEncryptionModifier" | `vault.secrets` | A list of VaultSecrets to create | `[]` | ### Other Parameters - From 2980fba8c7d8e3d03b55e7a8a04aa45ad3e3e50d Mon Sep 17 00:00:00 2001 From: Pascal Sthamer <10992664+P4sca1@users.noreply.github.com> Date: Thu, 2 Jan 2025 09:09:36 +0100 Subject: [PATCH 04/67] allow to change container ports, fix default frontend ports Signed-off-by: Pascal Sthamer <10992664+P4sca1@users.noreply.github.com> --- charts/shuffle/README.md | 12 +++- .../templates/backend/backend-dpl.yaml | 11 ++-- .../backend/backend-network-policy.yaml | 2 +- .../templates/backend/backend-svc.yaml | 2 +- .../templates/frontend/frontend-dpl.yaml | 15 +++-- .../frontend/frontend-network-policy.yaml | 5 +- .../templates/frontend/frontend-svc.yaml | 8 ++- .../templates/istio/virtual-service.yaml | 4 +- .../orborus-worker-network-policy.yaml | 4 +- .../templates/orborus/orborus-dpl.yaml | 11 ++-- .../orborus/orborus-network-policy.yaml | 4 +- charts/shuffle/values.schema.json | 57 ++++++++++++++++++- charts/shuffle/values.yaml | 35 ++++++++++++ 13 files changed, 144 insertions(+), 26 deletions(-) diff --git a/charts/shuffle/README.md b/charts/shuffle/README.md index 2dd62129..3c713a6c 100644 --- a/charts/shuffle/README.md +++ b/charts/shuffle/README.md @@ -137,6 +137,8 @@ SHUFFLE_ENCRYPTION_MODIFIER: "MyShuffleEncryptionModifier" | `backend.image.pullPolicy` | backend image pull policy | `IfNotPresent` | | `backend.image.pullSecrets` | backend image pull secrets | `[]` | | `backend.replicaCount` | Number of backend replicas to deploy | `1` | +| `backend.containerPorts.http` | backend HTTP container port | `5001` | +| `backend.extraContainerPorts` | Optionally specify extra list of additional ports for backend containers | `[]` | | `backend.livenessProbe.enabled` | Enable livenessProbe on backend containers | `false` | | `backend.livenessProbe.initialDelaySeconds` | Initial delay seconds for livenessProbe | `0` | | `backend.livenessProbe.periodSeconds` | Period seconds for livenessProbe | `15` | @@ -251,6 +253,9 @@ SHUFFLE_ENCRYPTION_MODIFIER: "MyShuffleEncryptionModifier" | `frontend.image.pullPolicy` | frontend image pull policy | `IfNotPresent` | | `frontend.image.pullSecrets` | frontend image pull secrets | `[]` | | `frontend.replicaCount` | Number of frontend replicas to deploy | `1` | +| `frontend.containerPorts.http` | frontend HTTP container port | `80` | +| `frontend.containerPorts.https` | frontend HTTPS container port | `443` | +| `frontend.extraContainerPorts` | Optionally specify extra list of additional ports for frontend containers | `[]` | | `frontend.livenessProbe.enabled` | Enable livenessProbe on frontend containers | `false` | | `frontend.livenessProbe.initialDelaySeconds` | Initial delay seconds for livenessProbe | `0` | | `frontend.livenessProbe.periodSeconds` | Period seconds for livenessProbe | `15` | @@ -274,12 +279,12 @@ SHUFFLE_ENCRYPTION_MODIFIER: "MyShuffleEncryptionModifier" | `frontend.customStartupProbe` | Custom startupProbe that overrides the default one | `{}` | | `frontend.resourcesPreset` | Set frontend container resources according to one common preset (allowed values: none, nano, small, medium, large, xlarge, 2xlarge). This is ignored if frontend.resources is set (frontend.resources is recommended for production). | `nano` | | `frontend.resources` | Set frontend container requests and limits for different resources like CPU or memory (essential for production workloads) | `{}` | -| `frontend.podSecurityContext.enabled` | Enable frontend pods' Security Context | `true` | +| `frontend.podSecurityContext.enabled` | Enable frontend pods' Security Context | `false` | | `frontend.podSecurityContext.fsGroupChangePolicy` | Set filesystem group change policy for frontend pods | `Always` | | `frontend.podSecurityContext.sysctls` | Set kernel settings using the sysctl interface for frontend pods | `[]` | | `frontend.podSecurityContext.supplementalGroups` | Set filesystem extra groups for frontend pods | `[]` | | `frontend.podSecurityContext.fsGroup` | Set fsGroup in frontend pods' Security Context | `1001` | -| `frontend.containerSecurityContext.enabled` | Enabled frontend container' Security Context | `true` | +| `frontend.containerSecurityContext.enabled` | Enabled frontend container' Security Context | `false` | | `frontend.containerSecurityContext.seLinuxOptions` | Set SELinux options in frontend container | `{}` | | `frontend.containerSecurityContext.runAsUser` | Set runAsUser in frontend container' Security Context | `101` | | `frontend.containerSecurityContext.runAsGroup` | Set runAsGroup in frontend container' Security Context | `101` | @@ -355,6 +360,8 @@ SHUFFLE_ENCRYPTION_MODIFIER: "MyShuffleEncryptionModifier" | `orborus.image.pullPolicy` | orborus image pull policy | `IfNotPresent` | | `orborus.image.pullSecrets` | orborus image pull secrets | `[]` | | `orborus.replicaCount` | Number of orborus replicas to deploy | `1` | +| `orborus.containerPorts.http` | orborus HTTP container port | `8080` | +| `orborus.extraContainerPorts` | Optionally specify extra list of additional ports for orborus containers | `[]` | | `orborus.livenessProbe.enabled` | Enable livenessProbe on orborus containers | `false` | | `orborus.livenessProbe.initialDelaySeconds` | Initial delay seconds for livenessProbe | `0` | | `orborus.livenessProbe.periodSeconds` | Period seconds for livenessProbe | `15` | @@ -574,3 +581,4 @@ SHUFFLE_ENCRYPTION_MODIFIER: "MyShuffleEncryptionModifier" | `vault.secrets` | A list of VaultSecrets to create | `[]` | ### Other Parameters + diff --git a/charts/shuffle/templates/backend/backend-dpl.yaml b/charts/shuffle/templates/backend/backend-dpl.yaml index 8a4cfc36..ecf52e20 100644 --- a/charts/shuffle/templates/backend/backend-dpl.yaml +++ b/charts/shuffle/templates/backend/backend-dpl.yaml @@ -144,7 +144,10 @@ spec: {{- end }} ports: - name: http - containerPort: 5001 + containerPort: {{ .Values.backend.containerPorts.http }} + {{- if .Values.backend.extraContainerPorts }} + {{- include "common.tplvalues.render" (dict "value" .Values.backend.extraContainerPorts "context" $) | nindent 12 }} + {{- end }} {{- if not .Values.diagnosticMode.enabled }} {{- if .Values.backend.customLivenessProbe }} livenessProbe: {{- include "common.tplvalues.render" (dict "value" .Values.backend.customLivenessProbe "context" $) | nindent 12 }} @@ -152,7 +155,7 @@ spec: livenessProbe: {{- include "common.tplvalues.render" (dict "value" (omit .Values.backend.livenessProbe "enabled") "context" $) | nindent 12 }} httpGet: path: /api/v1/health - port: 5001 + port: {{ .Values.backend.containerPorts.http }} {{- end }} {{- if .Values.backend.customReadinessProbe }} readinessProbe: {{- include "common.tplvalues.render" (dict "value" .Values.backend.customReadinessProbe "context" $) | nindent 12 }} @@ -160,7 +163,7 @@ spec: readinessProbe: {{- include "common.tplvalues.render" (dict "value" (omit .Values.backend.readinessProbe "enabled") "context" $) | nindent 12 }} httpGet: path: /api/v1/health - port: 5001 + port: {{ .Values.backend.containerPorts.http }} {{- end }} {{- if .Values.backend.customStartupProbe }} startupProbe: {{- include "common.tplvalues.render" (dict "value" .Values.backend.customStartupProbe "context" $) | nindent 12 }} @@ -168,7 +171,7 @@ spec: startupProbe: {{- include "common.tplvalues.render" (dict "value" (omit .Values.backend.startupProbe "enabled") "context" $) | nindent 12 }} httpGet: path: /api/v1/health - port: 5001 + port: {{ .Values.backend.containerPorts.http }} {{- end }} {{- end }} {{- if .Values.backend.lifecycleHooks }} diff --git a/charts/shuffle/templates/backend/backend-network-policy.yaml b/charts/shuffle/templates/backend/backend-network-policy.yaml index 47d7bd3b..48010706 100644 --- a/charts/shuffle/templates/backend/backend-network-policy.yaml +++ b/charts/shuffle/templates/backend/backend-network-policy.yaml @@ -35,7 +35,7 @@ spec: {{- end }} ingress: - ports: - - port: 5001 + - port: {{ .Values.backend.containerPorts.http }} protocol: TCP {{- if not .Values.backend.networkPolicy.allowExternal }} from: diff --git a/charts/shuffle/templates/backend/backend-svc.yaml b/charts/shuffle/templates/backend/backend-svc.yaml index c41aa3bb..18328899 100644 --- a/charts/shuffle/templates/backend/backend-svc.yaml +++ b/charts/shuffle/templates/backend/backend-svc.yaml @@ -11,7 +11,7 @@ spec: type: ClusterIP ports: - name: http - port: 5001 + port: {{ .Values.backend.containerPorts.http }} targetPort: http protocol: TCP {{- $podLabels := include "common.tplvalues.merge" (dict "values" (list .Values.backend.podLabels .Values.commonLabels) "context" .) }} diff --git a/charts/shuffle/templates/frontend/frontend-dpl.yaml b/charts/shuffle/templates/frontend/frontend-dpl.yaml index 659167df..b3a68dbb 100644 --- a/charts/shuffle/templates/frontend/frontend-dpl.yaml +++ b/charts/shuffle/templates/frontend/frontend-dpl.yaml @@ -103,7 +103,14 @@ spec: {{- end }} ports: - name: http - containerPort: 8080 + containerPort: {{ .Values.frontend.containerPorts.http }} + {{- if .Values.frontend.containerPorts.https }} + - name: https + containerPort: {{ .Values.frontend.containerPorts.https }} + {{- end }} + {{- if .Values.frontend.extraContainerPorts }} + {{- include "common.tplvalues.render" (dict "value" .Values.frontend.extraContainerPorts "context" $) | nindent 12 }} + {{- end }} {{- if not .Values.diagnosticMode.enabled }} {{- if .Values.frontend.customLivenessProbe }} livenessProbe: {{- include "common.tplvalues.render" (dict "value" .Values.frontend.customLivenessProbe "context" $) | nindent 12 }} @@ -111,7 +118,7 @@ spec: livenessProbe: {{- include "common.tplvalues.render" (dict "value" (omit .Values.frontend.livenessProbe "enabled") "context" $) | nindent 12 }} httpGet: path: / - port: 8080 + port: {{ .Values.frontend.containerPorts.http }} {{- end }} {{- if .Values.frontend.customReadinessProbe }} readinessProbe: {{- include "common.tplvalues.render" (dict "value" .Values.frontend.customReadinessProbe "context" $) | nindent 12 }} @@ -119,7 +126,7 @@ spec: readinessProbe: {{- include "common.tplvalues.render" (dict "value" (omit .Values.frontend.readinessProbe "enabled") "context" $) | nindent 12 }} httpGet: path: / - port: 8080 + port: {{ .Values.frontend.containerPorts.http }} {{- end }} {{- if .Values.frontend.customStartupProbe }} startupProbe: {{- include "common.tplvalues.render" (dict "value" .Values.frontend.customStartupProbe "context" $) | nindent 12 }} @@ -127,7 +134,7 @@ spec: startupProbe: {{- include "common.tplvalues.render" (dict "value" (omit .Values.frontend.startupProbe "enabled") "context" $) | nindent 12 }} httpGet: path: / - port: 8080 + port: {{ .Values.frontend.containerPorts.http }} {{- end }} {{- end }} {{- if .Values.frontend.lifecycleHooks }} diff --git a/charts/shuffle/templates/frontend/frontend-network-policy.yaml b/charts/shuffle/templates/frontend/frontend-network-policy.yaml index b8143312..9082535b 100644 --- a/charts/shuffle/templates/frontend/frontend-network-policy.yaml +++ b/charts/shuffle/templates/frontend/frontend-network-policy.yaml @@ -36,7 +36,10 @@ spec: ingress: {{ if .Values.frontend.networkPolicy.allowExternal }} - ports: - - port: 8080 + - port: {{ .Values.frontend.containerPorts.http }} + {{- if .Values.frontend.containerPorts.https }} + - port: {{ .Values.frontend.containerPorts.https }} + {{- end }} {{- end }} {{- if .Values.frontend.networkPolicy.extraIngress }} {{- include "common.tplvalues.render" ( dict "value" .Values.frontend.networkPolicy.extraIngress "context" $ ) | nindent 4 }} diff --git a/charts/shuffle/templates/frontend/frontend-svc.yaml b/charts/shuffle/templates/frontend/frontend-svc.yaml index 8a5ca84c..76851c0a 100644 --- a/charts/shuffle/templates/frontend/frontend-svc.yaml +++ b/charts/shuffle/templates/frontend/frontend-svc.yaml @@ -11,8 +11,14 @@ spec: type: ClusterIP ports: - name: http - port: 8080 + port: {{ .Values.frontend.containerPorts.http }} targetPort: http protocol: TCP + {{- if .Values.frontend.containerPorts.https }} + - name: https + port: {{ .Values.frontend.containerPorts.https }} + targetPort: https + protocol: TCP + {{- end }} {{- $podLabels := include "common.tplvalues.merge" (dict "values" (list .Values.frontend.podLabels .Values.commonLabels) "context" .) }} selector: {{- include "shuffle.frontend.matchLabels" (dict "customLabels" $podLabels "context" $) | nindent 4 }} diff --git a/charts/shuffle/templates/istio/virtual-service.yaml b/charts/shuffle/templates/istio/virtual-service.yaml index ed74004b..902a1583 100644 --- a/charts/shuffle/templates/istio/virtual-service.yaml +++ b/charts/shuffle/templates/istio/virtual-service.yaml @@ -21,10 +21,10 @@ spec: - destination: host: {{ include "shuffle.backend.name" . }} port: - number: 5001 + number: {{ .Values.backend.containerPorts.http }} - route: - destination: host: {{ include "shuffle.frontend.name" . }} port: - number: 8080 + number: {{ .Values.frontend.containerPorts.http }} {{- end }} diff --git a/charts/shuffle/templates/orborus-worker/orborus-worker-network-policy.yaml b/charts/shuffle/templates/orborus-worker/orborus-worker-network-policy.yaml index f4ee4aba..1b07d9a9 100644 --- a/charts/shuffle/templates/orborus-worker/orborus-worker-network-policy.yaml +++ b/charts/shuffle/templates/orborus-worker/orborus-worker-network-policy.yaml @@ -28,10 +28,10 @@ spec: to: - namespaceSelector: matchLabels: - kubernetes.io/metadata.name: kube-system + kubernetes.io/metadata.name: kube-system # Allow access to orborus - ports: - - port: 8080 + - port: {{ .Values.orborus.containerPorts.http }} protocol: TCP to: - namespaceSelector: diff --git a/charts/shuffle/templates/orborus/orborus-dpl.yaml b/charts/shuffle/templates/orborus/orborus-dpl.yaml index b63c7a8b..411db680 100644 --- a/charts/shuffle/templates/orborus/orborus-dpl.yaml +++ b/charts/shuffle/templates/orborus/orborus-dpl.yaml @@ -107,7 +107,10 @@ spec: {{- end }} ports: - name: http - containerPort: 8080 + containerPort: {{ .Values.orborus.containerPorts.http }} + {{- if .Values.orborus.extraContainerPorts }} + {{- include "common.tplvalues.render" (dict "value" .Values.orborus.extraContainerPorts "context" $) | nindent 12 }} + {{- end }} {{- if not .Values.diagnosticMode.enabled }} {{- if .Values.orborus.customLivenessProbe }} livenessProbe: {{- include "common.tplvalues.render" (dict "value" .Values.orborus.customLivenessProbe "context" $) | nindent 12 }} @@ -115,7 +118,7 @@ spec: livenessProbe: {{- include "common.tplvalues.render" (dict "value" (omit .Values.orborus.livenessProbe "enabled") "context" $) | nindent 12 }} httpGet: path: / - port: 8080 + port: {{ .Values.orborus.containerPorts.http }} {{- end }} {{- if .Values.orborus.customReadinessProbe }} readinessProbe: {{- include "common.tplvalues.render" (dict "value" .Values.orborus.customReadinessProbe "context" $) | nindent 12 }} @@ -123,7 +126,7 @@ spec: readinessProbe: {{- include "common.tplvalues.render" (dict "value" (omit .Values.orborus.readinessProbe "enabled") "context" $) | nindent 12 }} httpGet: path: / - port: 8080 + port: {{ .Values.orborus.containerPorts.http }} {{- end }} {{- if .Values.orborus.customStartupProbe }} startupProbe: {{- include "common.tplvalues.render" (dict "value" .Values.orborus.customStartupProbe "context" $) | nindent 12 }} @@ -131,7 +134,7 @@ spec: startupProbe: {{- include "common.tplvalues.render" (dict "value" (omit .Values.orborus.startupProbe "enabled") "context" $) | nindent 12 }} httpGet: path: / - port: 8080 + port: {{ .Values.orborus.containerPorts.http }} {{- end }} {{- end }} {{- if .Values.orborus.lifecycleHooks }} diff --git a/charts/shuffle/templates/orborus/orborus-network-policy.yaml b/charts/shuffle/templates/orborus/orborus-network-policy.yaml index 6e03b9e3..f6a22339 100644 --- a/charts/shuffle/templates/orborus/orborus-network-policy.yaml +++ b/charts/shuffle/templates/orborus/orborus-network-policy.yaml @@ -31,7 +31,7 @@ spec: kubernetes.io/metadata.name: kube-system # Allow access to backend - ports: - - port: 5001 + - port: {{ .Values.backend.containerPorts.http }} protocol: TCP to: - namespaceSelector: @@ -55,7 +55,7 @@ spec: {{- end }} ingress: - ports: - - port: 8080 + - port: {{ .Values.orborus.containerPorts.http }} protocol: TCP {{- if not .Values.orborus.networkPolicy.allowExternal }} from: diff --git a/charts/shuffle/values.schema.json b/charts/shuffle/values.schema.json index 62e2dff2..d70c16cb 100644 --- a/charts/shuffle/values.schema.json +++ b/charts/shuffle/values.schema.json @@ -178,6 +178,22 @@ "description": "Number of backend replicas to deploy", "default": 1 }, + "containerPorts": { + "type": "object", + "properties": { + "http": { + "type": "number", + "description": "backend HTTP container port", + "default": 5001 + } + } + }, + "extraContainerPorts": { + "type": "array", + "description": "Optionally specify extra list of additional ports for backend containers", + "default": [], + "items": {} + }, "livenessProbe": { "type": "object", "properties": { @@ -840,6 +856,27 @@ "description": "Number of frontend replicas to deploy", "default": 1 }, + "containerPorts": { + "type": "object", + "properties": { + "http": { + "type": "number", + "description": "frontend HTTP container port", + "default": 80 + }, + "https": { + "type": "number", + "description": "frontend HTTPS container port", + "default": 443 + } + } + }, + "extraContainerPorts": { + "type": "array", + "description": "Optionally specify extra list of additional ports for frontend containers", + "default": [], + "items": {} + }, "livenessProbe": { "type": "object", "properties": { @@ -976,7 +1013,7 @@ "enabled": { "type": "boolean", "description": "Enable frontend pods' Security Context", - "default": true + "default": false }, "fsGroupChangePolicy": { "type": "string", @@ -1008,7 +1045,7 @@ "enabled": { "type": "boolean", "description": "Enabled frontend container' Security Context", - "default": true + "default": false }, "runAsUser": { "type": "number", @@ -1437,6 +1474,22 @@ "description": "Number of orborus replicas to deploy", "default": 1 }, + "containerPorts": { + "type": "object", + "properties": { + "http": { + "type": "number", + "description": "orborus HTTP container port", + "default": 8080 + } + } + }, + "extraContainerPorts": { + "type": "array", + "description": "Optionally specify extra list of additional ports for orborus containers", + "default": [], + "items": {} + }, "livenessProbe": { "type": "object", "properties": { diff --git a/charts/shuffle/values.yaml b/charts/shuffle/values.yaml index daa4ec9a..b9d47913 100644 --- a/charts/shuffle/values.yaml +++ b/charts/shuffle/values.yaml @@ -122,6 +122,17 @@ backend: ## @param backend.replicaCount Number of backend replicas to deploy ## replicaCount: 1 + ## @param backend.containerPorts.http backend HTTP container port + ## + containerPorts: + http: 5001 + ## @param backend.extraContainerPorts Optionally specify extra list of additional ports for backend containers + ## e.g: + ## extraContainerPorts: + ## - name: myservice + ## containerPort: 9090 + ## + extraContainerPorts: [] ## Configure extra options for backend containers' liveness and readiness probes ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/#configure-probes ## @param backend.livenessProbe.enabled Enable livenessProbe on backend containers @@ -577,6 +588,19 @@ frontend: ## @param frontend.replicaCount Number of frontend replicas to deploy ## replicaCount: 1 + ## @param frontend.containerPorts.http frontend HTTP container port + ## @param frontend.containerPorts.https frontend HTTPS container port + ## + containerPorts: + http: 80 + https: 443 + ## @param frontend.extraContainerPorts Optionally specify extra list of additional ports for frontend containers + ## e.g: + ## extraContainerPorts: + ## - name: myservice + ## containerPort: 9090 + ## + extraContainerPorts: [] ## Configure extra options for frontend containers' liveness and readiness probes ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/#configure-probes ## @param frontend.livenessProbe.enabled Enable livenessProbe on frontend containers @@ -966,6 +990,17 @@ orborus: ## @param orborus.replicaCount Number of orborus replicas to deploy ## replicaCount: 1 + ## @param orborus.containerPorts.http orborus HTTP container port + ## + containerPorts: + http: 8080 + ## @param orborus.extraContainerPorts Optionally specify extra list of additional ports for orborus containers + ## e.g: + ## extraContainerPorts: + ## - name: myservice + ## containerPort: 9090 + ## + extraContainerPorts: [] ## Configure extra options for orborus containers' liveness and readiness probes ## ref: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-probes/#configure-probes ## @param orborus.livenessProbe.enabled Enable livenessProbe on orborus containers From 577462b4208fe381492082d1744d65d6cd445e04 Mon Sep 17 00:00:00 2001 From: Pascal Sthamer <10992664+P4sca1@users.noreply.github.com> Date: Thu, 2 Jan 2025 09:32:08 +0100 Subject: [PATCH 05/67] add github workflow to package helm chart Signed-off-by: Pascal Sthamer <10992664+P4sca1@users.noreply.github.com> --- .github/workflows/.github/workflows/helm.yml | 41 ++++++++++++++++++++ charts/shuffle/Chart.yaml | 2 +- charts/shuffle/README.md | 5 +-- 3 files changed, 44 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/.github/workflows/helm.yml diff --git a/.github/workflows/.github/workflows/helm.yml b/.github/workflows/.github/workflows/helm.yml new file mode 100644 index 00000000..c3a6657a --- /dev/null +++ b/.github/workflows/.github/workflows/helm.yml @@ -0,0 +1,41 @@ + +name: helm + +on: + workflow_dispatch: + push: + branches: + - main + paths: + - "charts/**" + +permissions: + contents: read + packages: write + +jobs: + main: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install apt dependencies + run: | + curl https://baltocdn.com/helm/signing.asc | gpg --dearmor | sudo tee /usr/share/keyrings/helm.gpg > /dev/null + sudo apt-get install apt-transport-https -y --no-install-recommends + echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/helm.gpg] https://baltocdn.com/helm/stable/debian/ all main" | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list + sudo apt-get update + sudo apt-get install helm -y --no-install-recommends + + - name: Update helm dependencies + run: helm dependency update ./charts/shuffle + + - name: Package Helm chart + run: helm package ./charts/shuffle --destination ./charts + + - name: Login to OCI registry (ghcr.io) + run: helm registry login ghcr.io --username ${{ github.actor }} --password ${{ secrets.GITHUB_TOKEN }} + + - name: Push helm chart + run: helm push ./charts/shuffle-*.tgz oci://ghcr.io/shuffle/shuffle/charts diff --git a/charts/shuffle/Chart.yaml b/charts/shuffle/Chart.yaml index 3d74f418..67d1de83 100644 --- a/charts/shuffle/Chart.yaml +++ b/charts/shuffle/Chart.yaml @@ -2,7 +2,7 @@ apiVersion: v2 name: shuffle description: A Helm chart for deploying Shuffle on Kubernetes type: application -version: 0.0.0 +version: 0.1.0 appVersion: nightly dependencies: - name: common diff --git a/charts/shuffle/README.md b/charts/shuffle/README.md index 3c713a6c..8667ed8d 100644 --- a/charts/shuffle/README.md +++ b/charts/shuffle/README.md @@ -17,8 +17,8 @@ SPDX-License-Identifier: APACHE-2.0 ## Usage ```sh -# Install (the shuffle namespace is hardcoded into the shuffle source code) -helm install shuffle oci://TODO --namespace shuffle --create-namespace +# Install shuffle via helm (the shuffle namespace is hardcoded into the shuffle source code) +helm install shuffle oci://ghcr.io/shuffle/shuffle/charts/shuffle --namespace shuffle --create-namespace ``` ## Secret Parameters @@ -581,4 +581,3 @@ SHUFFLE_ENCRYPTION_MODIFIER: "MyShuffleEncryptionModifier" | `vault.secrets` | A list of VaultSecrets to create | `[]` | ### Other Parameters - From 63fac95c51f9b26e9ead5128000b4568b8ae2d61 Mon Sep 17 00:00:00 2001 From: Pascal Sthamer <10992664+P4sca1@users.noreply.github.com> Date: Thu, 2 Jan 2025 09:40:28 +0100 Subject: [PATCH 06/67] add note on how to access shuffle Signed-off-by: Pascal Sthamer <10992664+P4sca1@users.noreply.github.com> --- charts/shuffle/templates/NOTES.txt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/charts/shuffle/templates/NOTES.txt b/charts/shuffle/templates/NOTES.txt index 1091d443..10e38886 100644 --- a/charts/shuffle/templates/NOTES.txt +++ b/charts/shuffle/templates/NOTES.txt @@ -20,6 +20,8 @@ Access the pod you want to debug by executing {{- end }} -TODO +To access shuffle using port-forwarding: + +1. Run `kubectl port-forward -n shuffle svc/shuffle-frontend 8080:http` +2. Visit http://localhost:8080 with your browser -{{- include "common.warnings.rollingTag" .Values.backend.image }} From 3fa36200043bf05098ee77b246952271fbfbac42 Mon Sep 17 00:00:00 2001 From: Frikky Date: Mon, 20 Jan 2025 01:05:07 +0100 Subject: [PATCH 07/67] Fixed Tenzir issues for Orborus --- functions/onprem/orborus/go.mod | 55 +++---- functions/onprem/orborus/go.sum | 111 +++++++------- functions/onprem/orborus/orborus.go | 222 +++++++++++++--------------- 3 files changed, 187 insertions(+), 201 deletions(-) diff --git a/functions/onprem/orborus/go.mod b/functions/onprem/orborus/go.mod index 38a4d2e2..e03a0ec2 100644 --- a/functions/onprem/orborus/go.mod +++ b/functions/onprem/orborus/go.mod @@ -1,27 +1,29 @@ module orborus -go 1.22.0 +go 1.22.7 -toolchain go1.22.2 +toolchain go1.22.11 -// replace github.com/shuffle/shuffle-shared => ../../../../shuffle-shared +//replace github.com/shuffle/shuffle-shared => ../../../../shuffle-shared require ( - github.com/docker/docker v27.0.2+incompatible + github.com/docker/docker v27.5.0+incompatible github.com/docker/go-connections v0.5.0 github.com/satori/go.uuid v1.2.0 - github.com/shuffle/shuffle-shared v0.6.93 + github.com/shuffle/shuffle-shared v0.6.97 k8s.io/api v0.30.2 k8s.io/apimachinery v0.30.2 ) require ( cloud.google.com/go v0.110.2 // indirect - cloud.google.com/go/compute/metadata v0.3.0 // indirect + cloud.google.com/go/compute/metadata v0.5.0 // indirect cloud.google.com/go/datastore v1.11.0 // indirect cloud.google.com/go/iam v0.13.0 // indirect + cloud.google.com/go/scheduler v1.9.0 // indirect cloud.google.com/go/storage v1.29.0 // indirect dario.cat/mergo v1.0.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/Masterminds/semver v1.5.0 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect @@ -29,8 +31,8 @@ require ( github.com/algolia/algoliasearch-client-go/v3 v3.18.1 // indirect github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect github.com/bradfitz/slice v0.0.0-20180809154707-2b758aa73013 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cloudflare/circl v1.3.7 // indirect - github.com/containerd/log v0.1.0 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/distribution/reference v0.6.0 // indirect @@ -67,10 +69,8 @@ require ( github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect - github.com/moby/term v0.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/morikuni/aec v1.0.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.0 // indirect @@ -83,41 +83,42 @@ require ( github.com/sendgrid/rest v2.6.9+incompatible // indirect github.com/sendgrid/sendgrid-go v3.14.0+incompatible // indirect github.com/sergi/go-diff v1.1.0 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect github.com/skeema/knownhosts v1.2.1 // indirect github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 // indirect - go.opentelemetry.io/otel v1.30.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0 // indirect - go.opentelemetry.io/otel/metric v1.30.0 // indirect - go.opentelemetry.io/otel/sdk v1.30.0 // indirect - go.opentelemetry.io/otel/trace v1.30.0 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect + go.opentelemetry.io/otel v1.33.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 // indirect + go.opentelemetry.io/otel/metric v1.33.0 // indirect + go.opentelemetry.io/otel/trace v1.33.0 // indirect + go.opentelemetry.io/proto/otlp v1.4.0 // indirect go4.org v0.0.0-20201209231011-d4a079459e60 // indirect - golang.org/x/crypto v0.27.0 // indirect + golang.org/x/crypto v0.31.0 // indirect golang.org/x/mod v0.17.0 // indirect - golang.org/x/net v0.29.0 // indirect - golang.org/x/oauth2 v0.21.0 // indirect - golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.25.0 // indirect - golang.org/x/term v0.24.0 // indirect - golang.org/x/text v0.18.0 // indirect + golang.org/x/net v0.33.0 // indirect + golang.org/x/oauth2 v0.24.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.28.0 // indirect + golang.org/x/term v0.27.0 // indirect + golang.org/x/text v0.21.0 // indirect golang.org/x/time v0.3.0 // indirect golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/api v0.126.0 // indirect google.golang.org/appengine v1.6.8 // indirect google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect - google.golang.org/grpc v1.66.1 // indirect - google.golang.org/protobuf v1.34.2 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 // indirect + google.golang.org/grpc v1.68.1 // indirect + google.golang.org/protobuf v1.35.2 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - gotest.tools/v3 v3.5.1 // indirect k8s.io/client-go v0.30.2 // indirect k8s.io/klog/v2 v2.120.1 // indirect k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect diff --git a/functions/onprem/orborus/go.sum b/functions/onprem/orborus/go.sum index 91eea63b..0bca5775 100644 --- a/functions/onprem/orborus/go.sum +++ b/functions/onprem/orborus/go.sum @@ -11,8 +11,8 @@ cloud.google.com/go v0.110.2 h1:sdFPBr6xG9/wkBbfhmUz/JmZC7X6LavQgcrVINrKiVA= cloud.google.com/go v0.110.2/go.mod h1:k04UEeEtb6ZBRTv3dZz4CeJC3jKGxyhl0sAiVVquxiw= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= -cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY= +cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.11.0 h1:iF6I/HaLs3Ado8uRKMvZRvF/ZLkWaWE9i8AiHzbC774= cloud.google.com/go/datastore v1.11.0/go.mod h1:TvGxBIHCS50u8jzG+AW/ppf87v1of8nwzFNgEZU1D3c= @@ -20,6 +20,8 @@ cloud.google.com/go/iam v0.13.0 h1:+CmB+K0J/33d0zSQ9SlFWUeCCEn5XJA0ZMZ3pHE9u8k= cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= +cloud.google.com/go/scheduler v1.9.0 h1:NpQAHtx3sulByTLe2dMwWmah8PWgeoieFPpJpArwFV0= +cloud.google.com/go/scheduler v1.9.0/go.mod h1:yexg5t+KSmqu+njTIh3b7oYPheFtBWGcbVUYF1GGMIc= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.29.0 h1:6weCgzRvMg7lzuUurI4697AqIRPU1SvzHhynwpW31jI= @@ -27,8 +29,8 @@ cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjp dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= -github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= +github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= @@ -93,8 +95,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/docker v27.0.2+incompatible h1:mNhCtgXNV1fIRns102grG7rdzIsGGCq1OlOD0KunZos= -github.com/docker/docker v27.0.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v27.5.0+incompatible h1:um++2NcQtGRTz5eEgO6aJimo6/JxrTXC941hd05JO6U= +github.com/docker/docker v27.5.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= @@ -219,8 +221,8 @@ github.com/googleapis/gax-go/v2 v2.11.0 h1:9V9PWXEsWnPpQhu/PeQIkS4eGzMlTLGgt80cU github.com/googleapis/gax-go/v2 v2.11.0/go.mod h1:DxmR61SGKkGLa2xigwuZIQpkCI2S5iydzRfb3peWZJI= github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -254,8 +256,8 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= -github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= +github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= +github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -288,8 +290,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= github.com/sashabaranov/go-openai v1.19.2 h1:+dkuCADSnwXV02YVJkdphY8XD9AyHLUWwk6V7LB6EL8= github.com/sashabaranov/go-openai v1.19.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= @@ -301,8 +303,6 @@ github.com/sendgrid/sendgrid-go v3.14.0+incompatible h1:KDSasSTktAqMJCYClHVE94Fc github.com/sendgrid/sendgrid-go v3.14.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/shuffle/shuffle-shared v0.6.90 h1:FzIYtEt44eWgEsW/9tj2ki7qq8FEm/HWXUok+THp72M= -github.com/shuffle/shuffle-shared v0.6.90/go.mod h1:RAJiSFjmuKmijKTbbEf9A6Ojb+3/te7g71lED7JjPus= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= @@ -324,8 +324,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -337,23 +337,25 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 h1:9l89oX4ba9kHbBol3Xin3leYJ+252h0zszDtBwyKe2A= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0/go.mod h1:XLZfZboOJWHNKUv7eH0inh0E9VV6eWDFB/9yJyTLPp0= -go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts= -go.opentelemetry.io/otel v1.30.0/go.mod h1:tFw4Br9b7fOS+uEao81PJjVMjW/5fvNCbpsDIXqP0pc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0 h1:lsInsfvhVIfOI6qHVyysXMNDnjO9Npvl7tlDPJFBVd4= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.30.0/go.mod h1:KQsVNh4OjgjTG0G6EiNi1jVpnaeeKsKMRwbLN+f1+8M= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0 h1:umZgi92IyxfXd/l4kaDhnKgY8rnN/cZcF1LKc6I8OQ8= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.30.0/go.mod h1:4lVs6obhSVRb1EW5FhOuBTyiQhtRtAnnva9vD3yRfq8= -go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w= -go.opentelemetry.io/otel/metric v1.30.0/go.mod h1:aXTfST94tswhWEb+5QjlSqG+cZlmyXy/u8jFpor3WqQ= -go.opentelemetry.io/otel/sdk v1.30.0 h1:cHdik6irO49R5IysVhdn8oaiR9m8XluDaJAs4DfOrYE= -go.opentelemetry.io/otel/sdk v1.30.0/go.mod h1:p14X4Ok8S+sygzblytT1nqG98QG2KYKv++HE0LY/mhg= -go.opentelemetry.io/otel/trace v1.30.0 h1:7UBkkYzeg3C7kQX8VAidWh2biiQbtAKjyIML8dQ9wmc= -go.opentelemetry.io/otel/trace v1.30.0/go.mod h1:5EyKqTzzmyqB9bwtCCq6pDLktPK6fmGf/Dph+8VI02o= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= +go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= +go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0 h1:wpMfgF8E1rkrT1Z6meFh1NDtownE9Ii3n3X2GJYjsaU= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.33.0/go.mod h1:wAy0T/dUbs468uOlkT31xjvqQgEVXv58BRFWEgn5v/0= +go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= +go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= +go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM= +go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM= +go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= +go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= -go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= +go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg= +go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY= go4.org v0.0.0-20201209231011-d4a079459e60 h1:iqAGo78tVOJXELHQFRjR6TMwItrvXH4hrGJ32I/NFF8= go4.org v0.0.0-20201209231011-d4a079459e60/go.mod h1:CIiUVy99QCPfoE13bO4EZaz5GZMZXMSBGhxRdsvzbkg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -366,8 +368,8 @@ golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -425,15 +427,15 @@ golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= -golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= +golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= +golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= -golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= +golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -443,8 +445,8 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= -golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -465,6 +467,7 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -473,16 +476,16 @@ golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= -golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= +golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= -golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= +golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= +golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -494,8 +497,8 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= -golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= @@ -570,10 +573,10 @@ google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc h1:8DyZCyvI8mE1IdLy/60bS+52xfymkE72wv1asokgtao= google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:xZnkP7mREFX5MORlOPEzLMr+90PPZQ2QWzrVTWfAq64= -google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1 h1:hjSy6tcFQZ171igDaN5QHOw2n6vx40juYbC/x67CEhc= -google.golang.org/genproto/googleapis/api v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:qpvKtACPCQhAdu3PyQgV4l3LMXZEtft7y8QcarRsp9I= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q= +google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576 h1:8ZmaLZE4XWrtU3MyClkYqqtl6Oegr3235h7jxsDyqCY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241209162323-e6fa225c2576/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -586,8 +589,8 @@ google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTp google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= -google.golang.org/grpc v1.66.1 h1:hO5qAXR19+/Z44hmvIM4dQFMSYX9XcWsByfoxutBpAM= -google.golang.org/grpc v1.66.1/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/grpc v1.68.1 h1:oI5oTa11+ng8r8XMMN7jAOmWfPZWbYpCFaMUTACxkM0= +google.golang.org/grpc v1.68.1/go.mod h1:+q1XYFJjShcqn0QZHvCyeR4CXPA+llXIeUIfIe00waw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -599,8 +602,8 @@ google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpAD google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= +google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/functions/onprem/orborus/orborus.go b/functions/onprem/orborus/orborus.go index 10573709..63712c08 100755 --- a/functions/onprem/orborus/orborus.go +++ b/functions/onprem/orborus/orborus.go @@ -108,7 +108,7 @@ var queuePerMinute = os.Getenv("SHUFFLE_EXECUTION_PER_MINIUTE") var queuePerMinuteInt int // For it to download from Sigma? -var apiKey = os.Getenv("AUTH_FOR_ORBORUS") +var pipelineApikey = os.Getenv("SHUFFLE_PIPELINE_AUTH") var pipelineUrl = os.Getenv("SHUFFLE_PIPELINE_URL") var executionIds = []string{} @@ -133,6 +133,15 @@ func init() { } getThisContainerId() + + if len(pipelineApikey) == 0 { + if len(os.Getenv("SHUFFLE_AUTHORIZATION")) > 0 { + log.Printf("[DEBUG] No pipeline API key found. Overriding with api key from SHUFFLE_AUTHORIZATION") + + pipelineApikey = os.Getenv("SHUFFLE_AUTHORIZATION") + os.Setenv("SHUFFLE_PIPELINE_AUTH", pipelineApikey) + } + } } // form id of current running container @@ -2214,10 +2223,13 @@ func main() { // Looking for specific jobs if incRequest.Type == "PIPELINE_CREATE" || incRequest.Type == "PIPELINE_START" || incRequest.Type == "PIPELINE_STOP" || incRequest.Type == "PIPELINE_DELETE" { + os.Setenv("SHUFFLE_SKIP_PIPELINES", "false") + tenzirDisabled = false + + // Running NEW or editing pipelines err := handlePipeline(incRequest) if err != nil { - - log.Printf("[ERROR] Failed handling pipeline (%s %s): %s. Deleting job anyway.", incRequest.Type, incRequest.ExecutionSource, err) + log.Printf("[ERROR] Failed handling pipeline ('%s' '%s'): %s. Deleting job anyway.", incRequest.Type, incRequest.ExecutionSource, err) } toBeRemoved.Data = append(toBeRemoved.Data, incRequest) @@ -2236,65 +2248,47 @@ func main() { toBeRemoved.Data = append(toBeRemoved.Data, incRequest) } else if incRequest.Type == "CATEGORY_UPDATE" { - - err := deployTenzirNode() - if err != nil { - log.Printf("[ERROR] Failed to run CATEGORY UPDATE, reason: %s", err) - } else { - continue - } + os.Setenv("SHUFFLE_SKIP_PIPELINES", "false") + tenzirDisabled = false err = handleFileCategoryChange() if err != nil { log.Printf("[ERROR] Failed to download the file category: %s", err) - } else { - toBeRemoved.Data = append(toBeRemoved.Data, incRequest) } + toBeRemoved.Data = append(toBeRemoved.Data, incRequest) + + } else if incRequest.Type == "DISABLE_SIGMA_FOLDER" { + log.Printf("[INFO] Got job to disable sigma rules") + + err = removeFileCategory() + if err != nil { + log.Printf("[ERROR] Failed to disable the sigma rules: %s", err) + } + + toBeRemoved.Data = append(toBeRemoved.Data, incRequest) + } else if incRequest.Type == "DISABLE_SIGMA_FILE" { fileName := incRequest.ExecutionArgument - err := deployTenzirNode() - if err != nil { - log.Printf("[ERROR] Failed to run DISABLE SIGMA FILE, reason: %s", err) - } else { - continue - } + log.Printf("[INFO] Got job to disable sigma file %s", fileName) err = disableRule(fileName) if err != nil { log.Printf("[ERROR] Failed to disable the sigma file %s, reason: %s", fileName, err) - } else { - toBeRemoved.Data = append(toBeRemoved.Data, incRequest) } + toBeRemoved.Data = append(toBeRemoved.Data, incRequest) + } else if incRequest.Type == "ENABLE_SIGMA_FILE" { fileName := incRequest.ExecutionArgument - err := deployTenzirNode() - if err != nil { - log.Printf("[ERROR] Failed to run ENABLE SIGMA FILE, reason: %s", err) - } else { - continue - } + log.Printf("[INFO] Got job to enable sigma file %s", fileName) err = enableRule(fileName) if err != nil { log.Printf("[ERROR] Failed to disable the sigma file %s, reason: %s", fileName, err) - } else { - toBeRemoved.Data = append(toBeRemoved.Data, incRequest) } - } else if incRequest.Type == "DISABLE_SIGMA_FOLDER" { - err := deployTenzirNode() - if err != nil { - log.Printf("[ERROR] Failed to run DISABLE SIGMA FOLDER, reason: %s", err) - } - - err = removeAllFiles() - if err != nil { - log.Printf("[ERROR] Failed to disable the sigma rules: %s", err) - } else { - toBeRemoved.Data = append(toBeRemoved.Data, incRequest) - } + toBeRemoved.Data = append(toBeRemoved.Data, incRequest) } else if incRequest.Type == "START_TENZIR" { log.Printf("[INFO] Got job to start tenzir") @@ -2532,7 +2526,7 @@ func main() { // docker run tenzir/tenzir:latest 'from http://192.168.86.44:5002/api/v1/orgs/7e9b9007-5df2-4b47-bca5-c4d267ef2943/cache/CIDR%20ranges?type=text&authorization=cec9d01f-09b2-4419-8a0a-76c6046e3fef read lines | to http://192.168.86.44:5002/api/v1/hooks/webhook_665ace5f-f27b-496a-a365-6e07eb61078c write lines' func handlePipeline(incRequest shuffle.ExecutionRequest) error { - log.Printf("[INFO] Pipeline: %s to %s", incRequest.Type, incRequest.ExecutionSource) + log.Printf("[INFO] Pipeline: '%s' with source '%s'", incRequest.Type, incRequest.ExecutionSource) err := deployTenzirNode() if err != nil { @@ -2600,7 +2594,7 @@ func handlePipeline(incRequest shuffle.ExecutionRequest) error { pipelineId, err := searchPipeline(identifier) if err != nil { if err.Error() == "no existing pipeline found with name" { - log.Printf("[WARNING] No pipeline found for '%s', creating a new one", identifier) + log.Printf("[INFO] Starting a new pipeline with command '%s' and identifier '%s'", command, identifier) _, CreateErr := createPipeline(command, identifier) return CreateErr } @@ -2777,7 +2771,7 @@ func createAndStartTenzirNode(ctx context.Context, containerName, imageName stri } } else { tenzirStorageFolder = "/tmp/" - log.Printf("[DEBUG] Using folder %s for Tenzir storage. Change it using SHUFFLE_STORAGE_FOLDER", tenzirStorageFolder) + log.Printf("[DEBUG] Using base folder %s for Tenzir storage. Change it using environment variable SHUFFLE_STORAGE_FOLDER=/filepath/", tenzirStorageFolder) } if !anyFound { @@ -2873,7 +2867,7 @@ func createAndStartTenzirNode(ctx context.Context, containerName, imageName stri time.Sleep(20 * time.Second) err = checkTenzirNode() if err != nil { - log.Printf("[ERROR] Tenzir node is not available during deployment: %s", err) + log.Printf("[ERROR] Tenzir connection not available: %s. IF the URL seems wrong, set SHUFFLE_PIPELINE_URL=http://:5160", err) return err } @@ -3041,8 +3035,19 @@ func createPipeline(command, identifier string) (string, error) { log.Printf("[ERROR] Failed to send HTTP request: %s", err) return "", err } - defer resp.Body.Close() + + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Printf("[ERROR] Failed reading response body: %s", err) + return "", err + } + + if strings.Contains(string(body), "error") { + log.Printf("[ERROR] Pipeline creation response (%d): %s", resp.StatusCode, string(body)) + } + + defer resp.Body.Close() if resp.StatusCode != 200 { log.Printf("[DEBUG] status code is %d instead of 200", resp.StatusCode) return "", fmt.Errorf("got the status code %d instead of 200", resp.StatusCode) @@ -3050,25 +3055,22 @@ func createPipeline(command, identifier string) (string, error) { type PipelineResponse struct { ID string `json:"id"` + Message string `json:"message"` + Severity string `json:"severity"` } var response PipelineResponse - if err := json.NewDecoder(resp.Body).Decode(&response); err != nil { - log.Printf("[ERROR] decoding response: %s", err) + if err := json.Unmarshal(body, &response); err != nil { + log.Printf("[ERROR] Failed unmarshalling response: %s", err) return "", err } if response.ID == "" { - log.Println("[DEBUG] ID not found or empty in response") - return "", errors.New("pipeline ID not found or empty in the response") + log.Printf("[ERROR] ID not found or empty in response. Severity: %#v, Message: %#v", response.Severity, response.Message) + return "", errors.New("Pipeline ID not found or empty in the response. See error logs.") } id := response.ID - - //if toBeDeleted { - // go deletePipeline(pipelineId) - //} - return id, nil } @@ -3236,17 +3238,24 @@ func handleFileCategoryChange() error { return err } - req.Header.Add("Authorization", "Bearer "+apiKey) + if len(pipelineApikey) == 0 { + return errors.New("Shuffle API-key not set for Pipelines: SHUFFLE_PIPELINE_AUTH=") + } - client := &http.Client{} + req.Header.Add("Authorization", "Bearer "+pipelineApikey) + if len(org) > 0 { + req.Header.Add("Org-Id", org) + } + + client := shuffle.GetExternalClient(apiEndpoint) resp, err := client.Do(req) if err != nil { return err } - defer resp.Body.Close() + defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return fmt.Errorf("received non-200 response: %s", resp.Status) + return fmt.Errorf("Received non-200 response '%d' from backend URL %s. ", resp.StatusCode, apiEndpoint) } out, err := os.Create("files.zip") @@ -3256,64 +3265,29 @@ func handleFileCategoryChange() error { defer out.Close() defer os.Remove("files.zip") - _, err = io.Copy(out, resp.Body) if err != nil { + log.Printf("[ERROR] Failed to io.Copy ZIP file content: %s", err) return err } - log.Println("ZIP file downloaded successfully.") + //log.Println("[DEBUG] ZIP file downloaded successfully.") - err = extractZIP("files.zip", "sigma_rules") + tenzirStorageFolder := os.Getenv("SHUFFLE_STORAGE_FOLDER") + if len(tenzirStorageFolder) == 0 { + tenzirStorageFolder = "/tmp/" + } + + tenzirStorageFolder = strings.TrimRight(tenzirStorageFolder, "/") + sigmaPath := fmt.Sprintf("%s/sigma_rules", tenzirStorageFolder) + err = extractZIP("files.zip", sigmaPath) if err != nil { + log.Printf("[ERROR] Failed to extract ZIP file: %s", err) return err } - destPath := "/var/lib/tenzir/sigma_rules" - err = copyToTenzir("sigma_rules", destPath) - if err != nil { - return err - } - - log.Println("Files copied to container successfully.") - - checkDisabledDirCmd := exec.Command("docker", "exec", "tenzir-node", "sh", "-c", "test -d /var/lib/tenzir/disabled_rules") - if err := checkDisabledDirCmd.Run(); err != nil { - if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 { - // Directory does not exist, nothing to do - log.Println("[DEBUG] /var/lib/tenzir/disabled_rules does not exist.") - return nil - } - - return fmt.Errorf("error checking disabled rules directory: %v", err) - } - - // List files in /var/lib/tenzir/disabled_rules - listFilesCmd := exec.Command("docker", "exec", "tenzir-node", "sh", "-c", "ls /var/lib/tenzir/disabled_rules") - output, err := listFilesCmd.CombinedOutput() - if err != nil { - return fmt.Errorf("error listing files in disabled rules directory: %v, output: %s", err, output) - } - - files := strings.Split(strings.TrimSpace(string(output)), "\n") - for _, file := range files { - disabledFilePath := fmt.Sprintf("/var/lib/tenzir/sigma_rules/%s", file) - checkFileCmd := exec.Command("docker", "exec", "tenzir-node", "sh", "-c", fmt.Sprintf("test -f %s", disabledFilePath)) - if err := checkFileCmd.Run(); err != nil { - if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 { - log.Printf("[ERROR] File does not exist: %s, moving on.\n", disabledFilePath) - continue - } - return fmt.Errorf("error checking file: %v", err) - } - - deleteFileCmd := exec.Command("docker", "exec", "-u", "root", "tenzir-node", "sh", "-c", fmt.Sprintf("rm -f %s", disabledFilePath)) - if err := deleteFileCmd.Run(); err != nil { - return fmt.Errorf("error deleting file: %v", err) - } - log.Printf("[INFO] Deleted file: %s\n", disabledFilePath) - } + log.Printf("[DEBUG] Detection files copied to '%s' successfully.", sigmaPath) return nil } @@ -3323,8 +3297,16 @@ func extractZIP(zipFile, destDir string) error { if err != nil { return err } - defer r.Close() + // FInd size of the zip + var totalSize uint64 + for _, f := range r.File { + totalSize += f.UncompressedSize64 + } + + log.Printf("[DEBUG] Total size of the ZIP file: %d bytes", totalSize) + + defer r.Close() if err := os.MkdirAll(destDir, 0755); err != nil { return err } @@ -3382,24 +3364,24 @@ func copyToTenzir(srcPath, destPath string) error { return nil } -func removeAllFiles() error { - containerName := "tenzir-node" - sigmaPath := "/var/lib/tenzir/sigma_rules/*" - - checkCmd := exec.Command("docker", "exec", "-u", "root", containerName, "sh", "-c", fmt.Sprintf("ls %s", sigmaPath)) - checkOutput, checkErr := checkCmd.CombinedOutput() - if checkErr != nil { - if strings.Contains(string(checkOutput), "No such file or directory") { - return nil // nothing to delete - } - return fmt.Errorf("error checking files: %v, output: %s", checkErr, checkOutput) +func removeFileCategory() error { + tenzirStorageFolder := os.Getenv("SHUFFLE_STORAGE_FOLDER") + if len(tenzirStorageFolder) == 0 { + tenzirStorageFolder = "/tmp/" } - cmd := exec.Command("docker", "exec", "-u", "root", containerName, "sh", "-c", fmt.Sprintf("rm -rf %s", sigmaPath)) - output, err := cmd.CombinedOutput() + tenzirStorageFolder = strings.TrimRight(tenzirStorageFolder, "/") + + //sigmaPath := "/var/lib/tenzir/sigma_rules/*" + sigmaPath := fmt.Sprintf("%s/sigma_rules", tenzirStorageFolder) + + err := os.RemoveAll(sigmaPath) if err != nil { - return fmt.Errorf("error removing files: %v, output: %s", err, output) + return fmt.Errorf("Error removing category files in %s: %v", sigmaPath, err) } + + log.Printf("[INFO] Removed all local category data in %s", sigmaPath) + return nil } From 09c9a9a7160240c9eda781e5fc1b6611f4ae05d2 Mon Sep 17 00:00:00 2001 From: Frikky Date: Mon, 20 Jan 2025 01:05:57 +0100 Subject: [PATCH 08/67] Filesync w 2.0.0-rc3 --- frontend/src/components/ApiExplorer.jsx | 31 +- frontend/src/components/Billing.jsx | 8 +- frontend/src/components/CacheView.jsx | 283 ++++++++++++++++- frontend/src/components/DetectionExplorer.jsx | 14 +- frontend/src/components/DetectionRuleCard.jsx | 4 +- frontend/src/components/EditWorkflow.jsx | 6 +- frontend/src/components/Files.jsx | 2 +- frontend/src/components/LicencePopup.jsx | 6 +- frontend/src/components/NewHeader.jsx | 4 +- frontend/src/components/ParsedAction.jsx | 146 +++------ frontend/src/components/SearchData.jsx | 2 +- .../src/components/ShuffleCodeEditor1.jsx | 206 +++++++++++- frontend/src/views/Admin2.jsx | 23 +- frontend/src/views/AngularWorkflow.jsx | 299 ++++++++++++------ frontend/src/views/ApiExplorerWrapper.jsx | 23 +- frontend/src/views/Docs.jsx | 107 +------ frontend/src/views/GettingStarted.jsx | 10 - 17 files changed, 797 insertions(+), 377 deletions(-) diff --git a/frontend/src/components/ApiExplorer.jsx b/frontend/src/components/ApiExplorer.jsx index c2dfaeb3..7e3092ea 100644 --- a/frontend/src/components/ApiExplorer.jsx +++ b/frontend/src/components/ApiExplorer.jsx @@ -100,6 +100,8 @@ const ApiExplorer = memo(({ openapi, globalUrl, userdata, HandleApiExecution, se const [ExampleBody, setExampleBody] = useState({}); const [filteredActions, setFilteredActions] = useState([]); + const [firstSendDone, setFirstSendDone] = useState(false) + const getJsonObject = (properties) => { let jsonObject = {}; @@ -1425,18 +1427,20 @@ const ActionsList = memo(({ }; const handleSearch = (e) => { - const query = e.target.value; - setSearchQuery(query); + const query = e?.target?.value?.toLowerCase().replaceAll("_", " "); + + setSearchQuery(query) if (query.length === 0) { - setVisibleActions(actions); + setVisibleActions(actions) } else { setVisibleActions( - actions.filter((action) => - action.name.toLowerCase().includes(searchQuery.toLowerCase()) + actions?.filter((action) => + action?.name?.toLowerCase()?.replaceAll("_", " ")?.includes(searchQuery) ) - ); + ) } - }; + } + return (
@@ -1465,7 +1469,7 @@ const ActionsList = memo(({
- {action.name} + {actionname}
{ + /* + if (!firstSendDone) { + setFirstSendDone(true) + setCurTab(2) + } + */ + if (actionUrl.length === 0) { toast.error("URL cannot be empty"); return; diff --git a/frontend/src/components/Billing.jsx b/frontend/src/components/Billing.jsx index 1bafe19b..577d249c 100644 --- a/frontend/src/components/Billing.jsx +++ b/frontend/src/components/Billing.jsx @@ -215,8 +215,8 @@ const Billing = memo((props) => { var priceItem = "price_1MROFrDzMUgUjxHShcSxgHO1" - const successUrl = `${window.location.origin}/admin?admin_tab=billing&payment=success` - const failUrl = `${window.location.origin}/admin?admin_tab=billing&payment=failure` + const successUrl = `${window.location.origin}/admin?admin_tab=billingstats&payment=success` + const failUrl = `${window.location.origin}/admin?admin_tab=billingstats&payment=failure` var checkoutObject = { lineItems: [ { @@ -2361,7 +2361,8 @@ const Billing = memo((props) => {
)} -
+ {isCloud ? ( +
@@ -2545,6 +2546,7 @@ const Billing = memo((props) => {
+ ): null}
{ - const { globalUrl, userdata, serverside, orgId, isSelectedDataStore } = props; + const { globalUrl, userdata, serverside, orgId, isSelectedDataStore, selectedOrganization } = props; const [orgCache, setOrgCache] = React.useState(""); const [listCache, setListCache] = React.useState([]); const [addCache, setAddCache] = React.useState(""); @@ -82,12 +88,16 @@ const CacheView = memo((props) => { const [editCache, setEditCache] = React.useState(false); const [cachedLoaded, setCachedLoaded] = React.useState(false); const [show, setShow] = useState({}); + const [showDistributionPopup, setShowDistributionPopup] = useState(false); + const [selectedSubOrg, setSelectedSubOrg] = useState([]); + const [selectedCacheKey, setSelectedCacheKey] = useState(""); useEffect(() => { if(orgId?.length >0){ listOrgCache(orgId); } }, [orgId]); + const listOrgCache = (orgId) => { fetch(globalUrl + `/api/v1/orgs/${orgId}/list_cache`, { method: "GET", @@ -214,7 +224,7 @@ const CacheView = memo((props) => { }) .then((responseJson) => { setAddCache(responseJson); - toast("New Cache Added Successfully!"); + toast("New key Added Successfully!"); listOrgCache(orgId); setModalOpen(false); }) @@ -296,7 +306,7 @@ const CacheView = memo((props) => { > - { editCache ? "Edit Cache" : "Add Cache" } + { editCache ? "Edit Key" : "Add Key" }
@@ -368,6 +378,7 @@ const CacheView = memo((props) => { style={{ borderRadius: "2px", fontSize: 16, color: "#ff8544", textTransform:"none" }} onClick={() => { setModalOpen(false) + setKey("") setValue("") setDataValue({}) }} @@ -380,7 +391,7 @@ const CacheView = memo((props) => { style={{ borderRadius: "2px", backgroundColor: "#ff8544",color: "#1a1a1a", textTransform:"none" }} onClick={() => { {editCache ? editOrgCache(orgId) : addOrgCache(orgId)} - + setKey("") setValue("") setDataValue({}) }} @@ -392,9 +403,175 @@ const CacheView = memo((props) => { ); + const handleSelectSubOrg = (id, action) => { + if (action === "all") { + const childOrgs = userdata.orgs.filter( + (data) => data.creator_org === userdata.active_org.id + ); + setSelectedSubOrg((prev) => { + if (prev.length === childOrgs.length) { + // If all child orgs are already selected, clear the selection + return []; + } else { + // Otherwise, select all child org IDs + return childOrgs.map((data) => data.id); + } + }); + } else if (action === "none") { + setSelectedSubOrg([]); + } else { + setSelectedSubOrg((prev) => { + if (prev.includes(id)) { + return prev.filter((data) => data !== id); + } else { + return [...prev, id]; + } + }); + } + }; + + const changeDistribution = (id, selectedSubOrg) => { + + editFileConfig(id, [...new Set(selectedSubOrg)]) + } + + const editFileConfig = (id, selectedSubOrg, cacheKey) => { + const data = { + Key: id, + action: "suborg_distribute", + selected_suborgs: selectedSubOrg, + } + console.log("data: ", data); + + const url = `${globalUrl}/api/v1/orgs/${orgId}/cache/config`; + + fetch(url, { + mode: "cors", + method: "POST", + body: JSON.stringify(data), + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then((response) => + response.json().then((responseJson) => { + if (responseJson["success"] === false) { + toast("Failed overwriting datastore"); + } else { + toast("Successfully updated datastore!"); + setTimeout(() => { + listOrgCache(orgId); + setShowDistributionPopup(false); + }, 1000); + } + }) + ) + .catch((error) => { + toast("Err: " + error.toString()); + }); + }; + + + const cacheDistributionModal = showDistributionPopup ? ( + setShowDistributionPopup(false)} + PaperProps={{ + sx: { + borderRadius: theme?.palette?.DialogStyle?.borderRadius, + border: theme?.palette?.DialogStyle?.border, + fontFamily: theme?.typography?.fontFamily, + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + zIndex: 1000, + minWidth: "600px", + minHeight: "320px", + overflow: "auto", + '& .MuiDialogContent-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogTitle-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogActions-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + }, + }} + > + +
+ Select sub-org to distribute files +
+
+ + {handleSelectSubOrg(null, "none")}}>None + {handleSelectSubOrg(null, "all")}}>All + {userdata.orgs.map((data, index) => { + if (data.creator_org !== userdata.active_org.id) { + return null; + } + + const imagesize = 22; + const imageStyle = { + width: imagesize, + height: imagesize, + pointerEvents: "none", + marginRight: 10, + marginLeft: data.id === userdata.active_org.id ? 0 : 20, + }; + + const image = data.image === "" ? ( + {data.name} + ) : ( + {data.name} + ); + + return ( + handleSelectSubOrg(data.id)} + style={{ display: "flex", alignItems: "center" }} + > + + {image} + {data.name} + + ); + })} + +
+ + +
+
+
+ ) : null; + return (
{modalView} + {cacheDistributionModal}
@@ -422,7 +599,7 @@ const CacheView = memo((props) => { setValue("") }} > - Add Cache + Add Key - } + {/**/} {detectionInfo?.category === "SIGMA" || detectionInfo?.category === "SIEM" ? - + 0 ? green : red}} /> diff --git a/frontend/src/components/DetectionRuleCard.jsx b/frontend/src/components/DetectionRuleCard.jsx index 986b227f..1a2e9760 100644 --- a/frontend/src/components/DetectionRuleCard.jsx +++ b/frontend/src/components/DetectionRuleCard.jsx @@ -137,6 +137,8 @@ const RuleCard = ({ ruleName, description, file_id, globalUrl, folderDisabled, i setResponseValue(e.target.value) + toast.error("The automatic response system is NOT available for you yet. Please contact support@shuffler.io if you want to try this feature.") + // FIXME: Handle: // 1. Get the current cache for the detection // 2. Create a new mapping for Detection -> Response @@ -187,7 +189,7 @@ const RuleCard = ({ ruleName, description, file_id, globalUrl, folderDisabled, i
diff --git a/frontend/src/components/EditWorkflow.jsx b/frontend/src/components/EditWorkflow.jsx index a7694562..296612b8 100644 --- a/frontend/src/components/EditWorkflow.jsx +++ b/frontend/src/components/EditWorkflow.jsx @@ -647,7 +647,7 @@ const EditWorkflow = (props) => { userdata.orgs.filter(org => org.creator_org === userdata.active_org.id).length === 0 ? userdata.active_org.creator_org === undefined || userdata.active_org.creator_org === null || userdata.active_org.creator_org === "" ? - Your organization does not have any suborgs yet OR your user may not have access to available suborgs. Please make one or get access to suborgs by another admin, then try again. + Your organization does not have any suborgs yet OR your user may not have access to available suborgs. Please make one or get access to suborgs by another admin, then try again. : @@ -738,7 +738,7 @@ const EditWorkflow = (props) => { })} : - + Create a sub-org to distribute workflows to suborgs. @@ -753,7 +753,7 @@ const EditWorkflow = (props) => { Git Backup Repository - Decide where this workflow is backed up in a Git repository. Will create logs and notifications if upload fails. The repository and branch must already have been initialized. Files will show up in the root folder in the format 'orgid/workflow status/workflow id.json' without images. Overrides your default backup repository. Credentials are encrypted. Creates notifications if it fails. + Decide where this workflow is backed up in a Git repository. Will create logs and notifications if upload fails. The repository and branch must already have been initialized. Files will show up in the root folder in the format 'orgid/workflow status/workflow id.json' without images. Overrides your default backup repository. Credentials are encrypted. Creates notifications if it fails. diff --git a/frontend/src/components/Files.jsx b/frontend/src/components/Files.jsx index 378bc532..6db9124b 100644 --- a/frontend/src/components/Files.jsx +++ b/frontend/src/components/Files.jsx @@ -1567,7 +1567,7 @@ const Files = memo((props) => { placement="top" > { : shuffleVariant === 0 ? "price_1PZPSSEJjT17t98NLJoTMYja" : "price_1PZPQuEJjT17t98N3yORUtd9" - const successUrl = `${window.location.origin}/admin?admin_tab=billing&payment=success` - const failUrl = `${window.location.origin}/pricing?admin_tab=billing&payment=failure` + const successUrl = `${window.location.origin}/admin?admin_tab=billingstats&payment=success` + const failUrl = `${window.location.origin}/admin?admin_tab=billingstats&payment=failure` console.log("Priceitem: ", priceItem, shuffleVariant) var checkoutObject = { @@ -888,7 +888,7 @@ const LicencePopup = (props) => { } if (stripe === undefined || stripe === null || stripe.redirectToCheckout === undefined) { - window.open("https://shuffler.io/admin?admin_tab=billing&payment=stripe_error", "_self") + window.open("https://shuffler.io/admin?admin_tab=billingstats&payment=stripe_error", "_self") } stripe.redirectToCheckout(checkoutObject) diff --git a/frontend/src/components/NewHeader.jsx b/frontend/src/components/NewHeader.jsx index e193460c..ed8ef8a1 100644 --- a/frontend/src/components/NewHeader.jsx +++ b/frontend/src/components/NewHeader.jsx @@ -465,7 +465,7 @@ const Header = (props) => { - + { handleClose(); @@ -1107,7 +1107,7 @@ const Header = (props) => { ); })} - + { expansionModalOpen, setExpansionModalOpen, + fixExample, listCache, setActiveDialog, @@ -258,7 +260,7 @@ const ParsedAction = (props) => { if (param.required === false && param.name.startsWith("${") && param.name.endsWith("}")) { // Check if it's a required param - param.autocompleted = false + param.autocompleted = true if (selectedAction.required_body_fields !== undefined && selectedAction.required_body_fields !== null && selectedAction.required_body_fields.length > 0) { if (selectedAction.required_body_fields.includes(param.name)) { param.required = true @@ -281,6 +283,7 @@ const ParsedAction = (props) => { } if (param.field_active === true) { + param.autocompleted = true generated_optional.push(param) continue } @@ -298,8 +301,8 @@ const ParsedAction = (props) => { const newparams = auth .concat(bodyfield) .concat(required) - .concat(special_optional) .concat(generated_optional) + .concat(special_optional) .concat(optional) var newkeyorder = [] @@ -1492,6 +1495,7 @@ const ParsedAction = (props) => { newAppname = newAppname.replaceAll("_", " ") } + var optionalFound = false return (
@@ -3122,91 +3126,6 @@ const ParsedAction = (props) => { }} /> - {/* - - - - - - - - - */}
) @@ -3551,7 +3470,7 @@ const ParsedAction = (props) => { }} /> - ); + ) // Finds headers from a string to be used for autocompletion const findHeaders = (inputdata) => { @@ -4087,9 +4006,24 @@ const ParsedAction = (props) => { }; var parsedPaths = []; - if (typeof innerdata.example === "object") { - parsedPaths = GetParsedPaths(innerdata.example, ""); - } + if (innerdata.type === "workflow_variable") { + // Try to parse the value if it's a string that could be JSON + if (typeof innerdata.value === "string") { + try { + const parsedValue = JSON.parse(innerdata.value) + if (typeof parsedValue === "object") { + parsedPaths = GetParsedPaths(parsedValue, ""); + } + } catch (e) { + // Not valid JSON, use the value directly + parsedPaths = GetParsedPaths(innerdata.value, ""); + } + } else if (typeof innerdata.value === "object") { + parsedPaths = GetParsedPaths(innerdata.value, ""); + } + } else if (typeof innerdata.example === "object") { + parsedPaths = GetParsedPaths(innerdata.example, ""); + } const coverColor = "#82ccc3" //menuPosition.left -= 50 @@ -4270,8 +4204,14 @@ const ParsedAction = (props) => { data.variant = "STATIC_VALUE" } + const isFirstOptional = optionalFound === false && data.configuration === false && data.required === false ? true : false + if (optionalFound === false && data.configuration === false && data.required === false) { + optionalFound = true + } + return ( -
+
+ {isFirstOptional ? : null} {showButtonField === true ? hideBodyButtonValue : null}
{ ) : null} {hasAutocomplete === true ? + data.field_active === true ? + + + + + : { title={"Explore your keys in Datastore"} placement="top" > - + { parsedvalue = "" } + //console.log("Required fields: ", selectedActionParameters[count]) + setEditorData({ "name": data.name, - "value": parsedvalue, + "value": fixExample(parsedvalue), "field_number": count, "actionlist": actionlist, "field_id": clickedFieldId, - "example": selectedActionParameters[count].example, + "example": fixExample(selectedActionParameters[count].example), }) }} /> diff --git a/frontend/src/components/SearchData.jsx b/frontend/src/components/SearchData.jsx index dacab40f..1e6403fd 100644 --- a/frontend/src/components/SearchData.jsx +++ b/frontend/src/components/SearchData.jsx @@ -387,7 +387,7 @@ const SearchData = props => { if (responseJson.success === false) { toast(`Failed to ${type} the app for your organization. Please try again or contact support@shuffler.io for more info`) } else { - toast(`App successfully ${type}d. Please refresh the page to use it.`) + toast(`App successfully ${type}d. It may now be used in your workflows.`) } }) .catch(error => { diff --git a/frontend/src/components/ShuffleCodeEditor1.jsx b/frontend/src/components/ShuffleCodeEditor1.jsx index f28668fa..b2c684a5 100644 --- a/frontend/src/components/ShuffleCodeEditor1.jsx +++ b/frontend/src/components/ShuffleCodeEditor1.jsx @@ -114,7 +114,9 @@ const CodeEditor = (props) => { editorData, setAiQueryModalOpen, - fullScreenMode + fullScreenMode, + environment, + fixExample, } = props const [localcodedata, setlocalcodedata] = React.useState(codedata === undefined || codedata === null || codedata.length === 0 ? "" : codedata); @@ -189,8 +191,27 @@ const CodeEditor = (props) => { tmpVariables.push('$' + actionlist[i].autocomplete.toLowerCase()) var parsedPaths = [] - if (typeof actionlist[i].example === "object") { - parsedPaths = GetParsedPaths(actionlist[i].example, ""); + if (actionlist[i].type === "workflow_variable") { + // Try to parse the value if it's a string that could be JSON + if (typeof actionlist[i].value === "string") { + try { + const parsedValue = JSON.parse(actionlist[i].value) + if (typeof parsedValue === "object") { + parsedPaths = GetParsedPaths(parsedValue, ""); + } + } catch (e) { + // Not valid JSON, skip parsing + continue + } + } else if (typeof actionlist[i].value === "object") { + // Direct object/array value + parsedPaths = GetParsedPaths(actionlist[i].value, ""); + } + } else { + // Handle regular action results + if (typeof actionlist[i].example === "object") { + parsedPaths = GetParsedPaths(actionlist[i].example, ""); + } } for (var key in parsedPaths) { @@ -596,7 +617,7 @@ const CodeEditor = (props) => { var code_lines = value.split('\n') for (var i = 0; i < code_lines.length; i++) { var current_code_line = code_lines[i] - var variable_occurence = current_code_line.match(/[\\]{0,1}[$]{1}([a-zA-Z0-9_@-]+\.?){1}([a-zA-Z0-9#_@-]+\.?){0,}/g); + var variable_occurence = current_code_line.match(/[\\]{0,1}[$]{1}([a-zA-Z0-9_@-]+\.?){1}([a-zA-Z0-9#_@-]+\.?){0,}/g) if (!variable_occurence) { continue; @@ -664,6 +685,75 @@ const CodeEditor = (props) => { } } + var code_lines = value.split('\n') + for (var i = 0; i < code_lines.length; i++) { + var current_code_line = code_lines[i] + + // Look for REPLACE_ME + var variable_occurence = current_code_line.match(/REPLACE_ME/g) + + if (!variable_occurence) { + continue; + } + + var new_occurences = variable_occurence.filter((occurrence) => occurrence[0]) + variable_occurence = new_occurences + + // Find the start position of REPLACE_ME and highlight it + var dollar_occurence = [] + for (let ch = 0; ch < current_code_line.length; ch++) { + // Not allowing it then lol + if (ch + 9 >= current_code_line.length) { + continue + } + + // Rofl - at least it is specific + if (current_code_line[ch] === 'R' && current_code_line[ch + 1] === 'E' && current_code_line[ch + 2] === 'P' && current_code_line[ch + 3] === 'L' && current_code_line[ch + 4] === 'A' && current_code_line[ch + 5] === 'C' && current_code_line[ch + 6] === 'E' && current_code_line[ch + 7] === '_' && current_code_line[ch + 8] === 'M' && current_code_line[ch + 9] === 'E') { + dollar_occurence.push(ch) + } + } + + try { + if (variable_occurence.length === 0) { + //value.markText({line:i, ch:0}, {line:i, ch:code_lines[i].length-1}, {"css": "background-color: #282828; border-radius: 0px; color: #b8bb26"}) + //value.markText({line:i, ch:0}, {line:i, ch:code_lines[i].length-1}, {"css": "background-color: #; border-radius: 0px; color: inherit"}) + } + + for (let occ = 0; occ < variable_occurence.length; occ++) { + const fixedVariable = variable_occurence[occ] + + var startCh = dollar_occurence[occ] + var endCh = dollar_occurence[occ] + 10 + try { + newMarkers.push({ + startRow: i, + startCol: startCh, + endRow: i, + endCol: endCh, + className: "bad-marker", + type: "text", + }) + } catch (e) { + console.log("Error in color highlighting: ", e); + newMarkers.push({ + startRow: i, + startCol: startCh, + endRow: i, + endCol: endCh, + className: "bad-marker", + type: "text", + }) + } + + setMarkers(newMarkers) + } + + + } catch (e) { + console.log("Error in color highlighting: ", e); + } + } + setMarkers(newMarkers) } @@ -720,6 +810,34 @@ const CodeEditor = (props) => { const fixedVariable = fixVariable(found[i]) var valuefound = false + + // First check if it's a workflow variable + if (actionlist !== undefined && actionlist.length > 0) { + const workflowVar = actionlist?.find(item => + item.type === "workflow_variable" && + `$${item.autocomplete.toLowerCase()}` === fixedVariable.toLowerCase() + ) + + if (workflowVar && workflowVar.example) { + valuefound = true + try { + // Try to parse the example value if it's stored as a JSON string + if (typeof workflowVar.example === "string" && + (workflowVar.example.startsWith("[") || workflowVar.example.startsWith("{"))) { + const parsedExample = JSON.parse(workflowVar.example) + input = input.replace(found[i], JSON.stringify(parsedExample), -1) + } else { + input = input.replace(found[i], workflowVar.example, -1) + } + continue + } catch (e) { + console.log("Error parsing workflow variable:", e) + input = input.replace(found[i], workflowVar.example, -1) + continue + } + } + } + for (var j = 0; j < actionlist.length; j++) { if (fixedVariable.slice(1,).toLowerCase() !== actionlist[j].autocomplete.toLowerCase()) { continue @@ -750,9 +868,26 @@ const CodeEditor = (props) => { var shouldbreak = false for (var k = 0; k < actionlist.length; k++) { var parsedPaths = [] - if (typeof actionlist[k].example === "object") { - parsedPaths = GetParsedPaths(actionlist[k].example, ""); - } + + // Handle both workflow variables and regular actions + if (actionlist[k].type === "workflow_variable") { + // Try to parse the value if it's a string that could be JSON + if (typeof actionlist[k].value === "string") { + try { + const parsedValue = JSON.parse(actionlist[k].value) + if (typeof parsedValue === "object") { + parsedPaths = GetParsedPaths(parsedValue, ""); + } + } catch (e) { + // Not valid JSON, use the value directly + parsedPaths = GetParsedPaths(actionlist[k].value, ""); + } + } else if (typeof actionlist[k].value === "object") { + parsedPaths = GetParsedPaths(actionlist[k].value, ""); + } + } else if (typeof actionlist[k].example === "object") { + parsedPaths = GetParsedPaths(actionlist[k].example, ""); + } for (var key in parsedPaths) { const fullpath = "$" + actionlist[k].autocomplete.toLowerCase() + parsedPaths[key].autocomplete.toLowerCase() @@ -766,7 +901,22 @@ const CodeEditor = (props) => { var new_input = "" try { - new_input = FindJsonPath(fullpath, actionlist[k].example) + const sourceData = actionlist[k].type === "workflow_variable" ? + (() => { + // Try to parse the value if it's a JSON string + if (typeof actionlist[k].value === "string") { + try { + return JSON.parse(actionlist[k].value); + } catch (e) { + // If parsing fails, return the original string value + return actionlist[k].value; + } + } + return actionlist[k].value; + })() : + actionlist[k].example; + + new_input = FindJsonPath(fullpath, sourceData) } catch (e) { console.log("ERR IN INPUT: ", e) } @@ -789,8 +939,9 @@ const CodeEditor = (props) => { } } - input = input.replace(fixedVariable, new_input, -1) - input = input.replace(found[i], new_input, -1) + // Replace both the fixed and original variable to handle both #0 and # cases + input = input.replace(found[i], new_input) + input = input.replace(fixedVariable, new_input) shouldbreak = true break @@ -876,8 +1027,12 @@ const CodeEditor = (props) => { } if (edited === false) { - if (!item.value.includes("{%") && !item.value.includes("{{")) { - setlocalcodedata(localcodedata + " | " + item.value + " }}") + if (item.value.includes("{%") || item.value.includes("{{")) { + if (!item.value.includes("}}") && !item.value.includes("%}")) { + setlocalcodedata(localcodedata + " | " + item.value + " }}") + } else { + setlocalcodedata(localcodedata + item.value) + } } else { setlocalcodedata(localcodedata + item.value) } @@ -899,7 +1054,7 @@ const CodeEditor = (props) => { const actionname = selectedAction.name === "execute_python" && !inputdata.replaceAll(" ", "").includes("{%python%}") ? "execute_python" : selectedAction.name === "execute_bash" ? "execute_bash" : "repeat_back_to_me" const params = actionname === "execute_python" ? [{ "name": "code", "value": inputdata }] : actionname === "execute_bash" ? [{ "name": "code", "value": inputdata }, { "name": "shuffle_input", "value": "", }] : [{ "name": "call", "value": inputdata }] - const actiondata = { "description": "Repeats the call parameter", "id": "", "name": actionname, "label": "", "node_type": "", "environment": "", "sharing": false, "private_id": "", "public_id": "", "app_id": appid, "tags": null, "authentication": [], "tested": false, "parameters": params, "execution_variable": { "description": "", "id": "", "name": "", "value": "" }, "returns": { "description": "", "example": "", "id": "", "schema": { "type": "string" } }, "authentication_id": "", "example": "", "auth_not_required": false, "source_workflow": "", "run_magic_output": false, "run_magic_input": false, "execution_delay": 0, "app_name": "Shuffle Tools", "app_version": "1.2.0", "selectedAuthentication": {} } + const actiondata = { "description": "Repeats the call parameter", "id": "", "name": actionname, "label": "", "node_type": "", "environment": environment?.Name, "sharing": false, "private_id": "", "public_id": "", "app_id": appid, "tags": null, "authentication": [], "tested": false, "parameters": params, "execution_variable": { "description": "", "id": "", "name": "", "value": "" }, "returns": { "description": "", "example": "", "id": "", "schema": { "type": "string" } }, "authentication_id": "", "example": "", "auth_not_required": false, "source_workflow": "", "run_magic_output": false, "run_magic_input": false, "execution_delay": 0, "app_name": "Shuffle Tools", "app_version": "1.2.0", "selectedAuthentication": {} } setExecutionResult({ "valid": false, @@ -1396,7 +1551,22 @@ const CodeEditor = (props) => { }; var parsedPaths = []; - if (typeof innerdata.example === "object") { + if (innerdata.type === "workflow_variable") { + // Try to parse the value if it's a string that could be JSON + if (typeof innerdata.value === "string") { + try { + const parsedValue = JSON.parse(innerdata.value) + if (typeof parsedValue === "object") { + parsedPaths = GetParsedPaths(parsedValue, ""); + } + } catch (e) { + // Not valid JSON, use the value directly + parsedPaths = GetParsedPaths(innerdata.value, ""); + } + } else if (typeof innerdata.value === "object") { + parsedPaths = GetParsedPaths(innerdata.value, ""); + } + } else if (typeof innerdata.example === "object") { parsedPaths = GetParsedPaths(innerdata.example, ""); } @@ -1585,7 +1755,13 @@ const CodeEditor = (props) => { }} disabled={editorData === undefined || editorData.example === undefined || editorData.example === null || editorData.example.length === 0} onClick={() => { - setlocalcodedata(editorData.example) + if (fixExample !== undefined) { + const newExample = fixExample(editorData.example) + setlocalcodedata(newExample) + } else { + console.log("No fix example available!") + setlocalcodedata(editorData.example) + } }} color="secondary" > diff --git a/frontend/src/views/Admin2.jsx b/frontend/src/views/Admin2.jsx index 2e68cef6..2d630fbb 100644 --- a/frontend/src/views/Admin2.jsx +++ b/frontend/src/views/Admin2.jsx @@ -11,29 +11,8 @@ const Admin2 = (props) => { const [organizationFeatures, setOrganizationFeatures] = useState({}); const [orgRequest, setOrgRequest] = React.useState(true); const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; + const handleGetOrg = (orgId) => { - // if ( - // serverside !== true && - // window.location.search !== undefined && - // window.location.search !== null - // ) { - // const urlSearchParams = new URLSearchParams(window.location.search); - // const params = Object.fromEntries(urlSearchParams.entries()); - // const foundorgid = params["org_id"]; - // if (foundorgid !== undefined && foundorgid !== null) { - // orgId = foundorgid; - // } - // } - console.log("getting organization details for: ", orgId); - - // if (orgId === undefined) { - // toast( - // "Organization ID not defined. Please contact us on https://shuffler.io if this persists logout.", - // ); - // return; - // } - - // Just use this one? fetch(`${globalUrl}/api/v1/orgs/${orgId}`, { method: "GET", diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index bfeff035..516a1a60 100755 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -568,6 +568,9 @@ const AngularWorkflow = (defaultprops) => { } }, [editWorkflowModalOpen]) + const dragRef = React.useRef(false); + + // New for generated stuff const releaseToConnectLabel = "Release to Connect" const integrationApps = [{ @@ -696,11 +699,32 @@ const releaseToConnectLabel = "Release to Connect" } if (loadedApps.includes(appId)) { - return + console.log("App already loaded: ", appId) + + // 1. Find the app and check if it has actions + // 2. If it doesn't have actions, reload once again + var should_reload = false + for (var i = 0; i < apps.length; i++) { + const curapp = apps[i] + if (curapp.id !== appId) { + continue + } + + if (curapp.actions === undefined || curapp.actions === null || curapp.actions.length === 0 || curapp.actions.length === 1) { + should_reload = true + break + } + } + + if (!should_reload) { + return + } } - loadedApps.push(appId) - setLoadedApps(loadedApps) + if (!loadedApps.includes(appId)) { + loadedApps.push(appId) + setLoadedApps(loadedApps) + } const appUrl = `${globalUrl}/api/v1/apps/${appId}/config?openapi=false` fetch(appUrl, { @@ -2331,6 +2355,35 @@ const releaseToConnectLabel = "Release to Connect" return success }; + const fixExample = (input, required) => { + if (input === undefined || input === null || input.length === 0) { + return "" + } + + + // 1. Find anything matching ${variable} + // 2. If it is required, replace it with REQUIRED + // 3. If it is not required, replace it with empty + + // Check if it is a string or not + var newExample = input + if (typeof newExample !== "string") { + return newExample + } + + const found = newExample.match(/\${(.*?)}/g) + if (found === null || found === undefined || found.length === 0) { + return newExample + } + + for (var i = 0; i < found.length; i++) { + //newExample = newExample.replace(found[i], "REQUIRED") + newExample = newExample.replace(found[i], "REPLACE_ME") + } + + return newExample + } + const monitorUpdates = () => { var firstnode = cy.getElementById(workflow.start); if (firstnode.length === 0) { @@ -3656,7 +3709,7 @@ const releaseToConnectLabel = "Release to Connect" setWorkflows([responseJson]) } else { getAppAuthentication(); - getEnvironments(); + getEnvironments(responseJson.org_id) getSettings(); getFiles() @@ -6864,7 +6917,9 @@ const releaseToConnectLabel = "Release to Connect" } } - if (showEnvCnt > 1) { + // Always showing for now + //if (showEnvCnt > 1) { + if (showEnvCnt > 0) { setShowEnvironment(true) } @@ -10018,15 +10073,37 @@ const releaseToConnectLabel = "Release to Connect" const ParsedAppPaper = (props) => { const app = props.app - const small = props.small - const actionString = props.action + const small = props.small + const actionString = props.action + const [hover, setHover] = React.useState(false); - React.useEffect(() => { - if(app.name === "Shuffle Tools"){ - if (app.actions !== undefined && (app.actions === null || app.actions.length === 1)) { - loadAppConfig(app.id, false) + + // Prevent hover effects during drag + const handleMouseMove = React.useCallback((e) => { + if (dragRef.current) { + setHover(false); + } + }, []); + + React.useEffect(() => { + // Add mousemove listener to track dragging + document.addEventListener('mousemove', handleMouseMove); + return () => { + document.removeEventListener('mousemove', handleMouseMove); + }; + }, [handleMouseMove]); + + + React.useEffect(() => { + if (props.skip_load === true) { + return + } + + if (app.name === "Shuffle Tools"){ + if (app.actions !== undefined && (app.actions === null || app.actions.length === 1)) { + loadAppConfig(app.id, false) + } } - } }, []) if (app === undefined || app === null) { @@ -10115,13 +10192,20 @@ const releaseToConnectLabel = "Release to Connect" } newAppStyle.backgroundColor = theme.palette.backgroundColor - return ( { + dragRef.current = true; + newAppStyle.zIndex = 9999 + + }} onDrag={(e) => { + newAppStyle.zIndex = 9999 handleAppDrag(e, app) }} onStop={(e) => { + dragRef.current = false; + newAppStyle.zIndex = "none" handleDragStop(e, app) }} key={app.id} @@ -10132,18 +10216,18 @@ const releaseToConnectLabel = "Release to Connect" { - e.preventDefault() - e.stopPropagation() - - setHover(true) - - if (app.actions !== undefined && (app.actions === null || app.actions.length === 1)) { - loadAppConfig(app.id, false) - } - + onMouseEnter={(e) => { + if (!dragRef.current) { + e.preventDefault(); + e.stopPropagation(); + setHover(true); + + if (app.actions !== undefined && (app.actions === null || app.actions.length === 1)) { + loadAppConfig(app.id, false); + } + } }} - onMouseOut={() => { + onMouseLeave={() => { setHover(false); }} onClick={() => { @@ -10586,6 +10670,27 @@ const releaseToConnectLabel = "Release to Connect" } var viewedApps = [] + + const QuickAccessSection = ({title, items, renderItem}) => ( +
+ + {title} + +
+ {items.map((item, index) => renderItem(item, index))} +
+
+ ); + + // Popular Shuffle Tools actions + const popularActions = [ + ["repeat_back_to_me", "filter_list", "execute_python", "parse_ioc"], + ["set_cache_value", "get_file_meta", "merge_lists", "send_sms_shuffle"] + ]; return (
@@ -10628,59 +10733,37 @@ const releaseToConnectLabel = "Release to Connect" }} /> - {shuffleToolsApp !== undefined && shuffleToolsApp !== null && document?.getElementById("appsearch")?.value?.length === 0 ? -
- - Popular Actions - -
- -
- -
- -
- -
-
- -
- -
- -
- -
-
- : null} - {document?.getElementById("appsearch")?.value?.length === 0 ? -
- - Triggers - -
- {triggers.map((trigger, index) => { - if (trigger.trigger_type === "PIPELINE") { - return null - } - - - return( -
- -
- ) - })} -
-
- : null} + {shuffleToolsApp && !document?.getElementById("appsearch")?.value?.length && ( + ( + + )} + /> + )} + + {!document?.getElementById("appsearch")?.value?.length && ( + t.trigger_type !== "PIPELINE")} + renderItem={(trigger, index) => ( + + )} + /> + )} Your Apps @@ -13942,8 +14025,23 @@ const releaseToConnectLabel = "Release to Connect" }; var parsedPaths = []; - console.log("Found example data: ", innerdata.example) - if (typeof innerdata.example === "object") { + + if (innerdata.type === "workflow_variable") { + // Try to parse the value if it's a string that could be JSON + if (typeof innerdata.value === "string") { + try { + const parsedValue = JSON.parse(innerdata.value) + if (typeof parsedValue === "object") { + parsedPaths = GetParsedPaths(parsedValue, ""); + } + } catch (e) { + // Not valid JSON, use the value directly + parsedPaths = GetParsedPaths(innerdata.value, ""); + } + } else if (typeof innerdata.value === "object") { + parsedPaths = GetParsedPaths(innerdata.value, ""); + } + } else if (typeof innerdata.example === "object") { parsedPaths = GetParsedPaths(innerdata.example, ""); } @@ -16531,7 +16629,7 @@ const releaseToConnectLabel = "Release to Connect" }
- {showEnvironment === true && environments.length > 1 && selectedActionEnvironment !== undefined && selectedActionEnvironment !== null && selectedActionEnvironment.Name !== undefined && selectedActionEnvironment.Name !== null ? + {showEnvironment === true && environments.length > 0 && selectedActionEnvironment !== undefined && selectedActionEnvironment !== null && selectedActionEnvironment.Name !== undefined && selectedActionEnvironment.Name !== null ? { + console.log("CLICK!") + getEnvironments(workflow.org_id) + }} onChange={(e) => { setLastSaved(false) const env = environments.find((a) => a.Name === e.target.value); @@ -16600,8 +16702,9 @@ const releaseToConnectLabel = "Release to Connect" > {data.Name === "cloud" || data.Name === "Cloud" ? null : !isRunning ? +
- + { e.preventDefault() e.stopPropagation() + window.open(`/admin?tab=locations&env=${data.Name}`, "_blank", "noopener,noreferrer") }} @@ -16617,7 +16721,21 @@ const releaseToConnectLabel = "Release to Connect" /> - : null} + : + { + //handleChipClick + }} + variant="outlined" + color="primary" + /> + } {data.default === true ? { e.preventDefault() e.stopPropagation() - window.open(`/admin?admin_tab=priorities&workflow=${data.workflow.id}&execution_id=${data.execution_id}`, "_blank") + window.open(`/admin?admin_tab=notifications&workflow=${data.workflow.id}&execution_id=${data.execution_id}`, "_blank") }} /> @@ -19075,7 +19193,7 @@ const releaseToConnectLabel = "Release to Connect" )}
) : ( -
+
-
+

Details

{ e.preventDefault() e.stopPropagation() - window.open(`/admin?admin_tab=priorities&workflow=${executionData.workflow.id}&execution_id=${executionData.execution_id}`, "_blank") + window.open(`/admin?admin_tab=notifications&workflow=${executionData.workflow.id}&execution_id=${executionData.execution_id}`, "_blank") }} /> @@ -20190,7 +20308,7 @@ const releaseToConnectLabel = "Release to Connect" } if (stringjson.includes("kms/")) { - return "KMS authentication most likely failed. Check your notifications for more details on this page: /admin?admin_tab=priorities. If you need help with KMS, please contact support@shuffler.io" + return "KMS authentication most likely failed. Check your notifications for more details on this page: /admin?admin_tab=notifications. If you need help with KMS, please contact support@shuffler.io" } if (stringjson.includes("invalidurl")) { @@ -20208,7 +20326,7 @@ const releaseToConnectLabel = "Release to Connect" } if (isCloud && stringjson.toLowerCase().includes("timeout error")) { - return "Run this workflow in a local environment to increase the timeout. Go to https://shuffler.io/admin?tab=locationsto create an environment to connect to" + return "Run this workflow in a local environment to increase the timeout. Go to https://shuffler.io/admin?tab=locations to create an environment to connect to" } if (stringjson.toLowerCase().includes("invalid header")) { @@ -20218,7 +20336,7 @@ const releaseToConnectLabel = "Release to Connect" if (stringjson.includes("connectionerror")) { if (stringjson.includes("kms")) { - return "KMS authentication most likely failed (2). Check your notifications for more details on this page: /admin?admin_tab=priorities&kms=true. If you need help with KMS, please contact support@shuffler.io" + return "KMS authentication most likely failed (2). Check your notifications for more details on this page: /admin?admin_tab=notifications&kms=true. If you need help with KMS, please contact support@shuffler.io" } return "The URL is incorrect, or Shuffle can't reach it. Set up a Shuffle Environment in the same VLAN, or whitelist Shuffle's IPs." @@ -20696,6 +20814,7 @@ const releaseToConnectLabel = "Release to Connect" setExpansionModalOpen={setCodeEditorModalOpen} setEditorData={setEditorData} setAiQueryModalOpen={setAiQueryModalOpen} + fixExample={fixExample} />
@@ -22583,6 +22702,8 @@ const releaseToConnectLabel = "Release to Connect" changeActionParameterCodeMirror={changeActionParameterCodeMirror} activeDialog={activeDialog} setActiveDialog={setActiveDialog} + environment={selectedActionEnvironment} + setAiQueryModalOpen={setAiQueryModalOpen} /> diff --git a/frontend/src/views/ApiExplorerWrapper.jsx b/frontend/src/views/ApiExplorerWrapper.jsx index 8de63fb9..197e0d08 100644 --- a/frontend/src/views/ApiExplorerWrapper.jsx +++ b/frontend/src/views/ApiExplorerWrapper.jsx @@ -101,8 +101,8 @@ const ApiExplorerWrapper = (props) => { }, [selectedAppData, openapi]) useEffect(() => { + getAppData(appid) if (appid !== undefined && appid !== null && appid.length !== 0) { - getAppData(appid) HandleGetLocations() } @@ -157,13 +157,17 @@ const ApiExplorerWrapper = (props) => { } if (!found) { - toast.error("Failed to get app data or App doesn't exist (1). Redirecting.."); + toast.error(`Failed to get API data for '${appname}' (1). Contact support@shuffler.io if this persists.`, { + "autoClose": 10000, + }) setTimeout(()=>{ navigate("/search?tab=apps"); },3000) } } else { - toast.error("Failed to get app data or App doesn't exist (2). Redirecting.."); + toast.error(`Failed to get API data for '${appname}' (2). Contact support@shuffler.io if this persists.`, { + "autoClose": 10000, + }) setTimeout(()=>{ navigate("/search?tab=apps"); },3000) @@ -177,6 +181,11 @@ const ApiExplorerWrapper = (props) => { // Fetch data when appid is available const getAppData = useCallback((appid) => { if (appid === undefined || appid === null || appid.length === 0) { + toast.error("No API data to load (4). Please contact support@shuffler.io if this persists.") + + setTimeout(() => { + navigate("/search?tab=apps") + }, 3000) return } @@ -197,9 +206,9 @@ const ApiExplorerWrapper = (props) => { .then((response) => { if (response.status !== 200) { toast.error("Failed to get app data or App doesn't exist (3). Redirecting.."); - setTimeout(()=>{ - navigate("/search?tab=apps"); - },3000) + setTimeout(() => { + navigate("/search?tab=apps") + }, 3000) return; } return response.json(); @@ -1790,7 +1799,7 @@ const Wrapper = ({children, isLoaded,isLoggedIn})=>{ return( -
+
{children}
) diff --git a/frontend/src/views/Docs.jsx b/frontend/src/views/Docs.jsx index 84be501b..0af12280 100755 --- a/frontend/src/views/Docs.jsx +++ b/frontend/src/views/Docs.jsx @@ -383,6 +383,7 @@ const Docs = (defaultprops) => { if (hash.includes('?')) { hash = hash.split('?')[0] } + if (hash) { const element = document.getElementById(hash.toLowerCase()) if (element) { @@ -526,7 +527,7 @@ const Docs = (defaultprops) => { backgroundColor: theme.palette.inputColor, padding: 15, borderRadius: theme.palette?.borderRadius, - marginBottom: 30, + marginBottom: 25, display: "flex", }} > @@ -618,7 +619,8 @@ const Docs = (defaultprops) => { @@ -664,6 +666,8 @@ const Docs = (defaultprops) => { minHeight: "93vh", maxHeight: "93vh", marginTop: 70, + maxWidth: 250, + overflow: "hidden", } const fetchDocList = () => { @@ -705,8 +709,7 @@ const Docs = (defaultprops) => { // Find tags and translate them into ![]() format const imgRegex = / { fetchDocs(props.match.params.key); } - // const parseElementScroll = () => { - // const offset = 45; - // var parent = document.getElementById("markdown_wrapper_outer"); - // if (parent !== null) { - // //console.log("IN PARENT") - // var elements = parent.getElementsByTagName("h2"); - // - // const name = window.location.hash - // .slice(1, window.location.hash.length) - // .toLowerCase() - // .split("%20") - // .join(" ") - // .split("_") - // .join(" ") - // .split("-") - // .join(" ") - // .split("?")[0] - // - // //console.log(name) - // var found = false; - // for (var key in elements) { - // const element = elements[key]; - // if (element.innerHTML === undefined) { - // continue; - // } - // - // // Fix location.. - // if (element.innerHTML.toLowerCase() === name) { - // //console.log(element.offsetTop) - // element.scrollIntoView({ behavior: "smooth" }); - // //element.scrollTo({ - // // top: element.offsetTop+offset, - // // behavior: "smooth" - // //}) - // found = true; - // //element.scrollTo({ - // // top: element.offsetTop-100, - // // behavior: "smooth" - // //}) - // } - // } - // - // // H# - // if (!found) { - // elements = parent.getElementsByTagName("h3"); - // //console.log("NAMe: ", name) - // found = false; - // for (key in elements) { - // const element = elements[key]; - // if (element.innerHTML === undefined) { - // continue; - // } - // - // // Fix location.. - // if (element.innerHTML.toLowerCase() === name) { - // element.scrollIntoView({ behavior: "smooth" }); - // //element.scrollTo({ - // // top: element.offsetTop-offset, - // // behavior: "smooth" - // //}) - // found = true; - // //element.scrollTo({ - // // top: element.offsetTop-100, - // // behavior: "smooth" - // //}) - // } - // } - // } - // } - // //console.log(element) - // - // //console.log("NAME: ", name) - // //console.log(document.body.innerHTML) - // // parent = document.getElementById(parent); - // - // //var descendants = parent.getElementsByTagName(tagname); - // - // // this.scrollDiv.current.scrollIntoView({ behavior: 'smooth' }); - // - // //$(".parent").find("h2:contains('Statistics')").parent(); - // }; - const markdownStyle = { color: "rgba(255, 255, 255, 0.90)", overflow: "hidden", @@ -872,7 +793,7 @@ const Docs = (defaultprops) => { maxWidth: "100%", minWidth: "100%", overflow: "hidden", - fontSize: isMobile ? "1.3rem" : "1.1rem", + fontSize: isMobile ? "1.3rem" : "0.8rem", }; const alertNote = { @@ -1084,25 +1005,23 @@ const Docs = (defaultprops) => {
{tocLines.length > 0 ? ( -

Table Of Content

+

Table of Content

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

    Suggestions

    + +

    Suggestions

    Suggestions are tasks identified by Shuffle to help you discover ways to protect your and customers' company.
    These range from simple configurations in Shuffle to Usecases you may have missed.  { return ( { if (item.disabled) { - toast.error("This feature may not work in your environment yet, and is awaiting updates to the Shuffle python execution environment.", { autoClose: 10000 }) + toast.error("This feature may not work in your environment until you update your Shuffle Tools app.", { autoClose: 10000 }) } if (selectedAction.name !== "execute_python") { diff --git a/frontend/src/components/WorkflowTemplatePopup2.jsx b/frontend/src/components/WorkflowTemplatePopup2.jsx index 2e666637..3696e026 100644 --- a/frontend/src/components/WorkflowTemplatePopup2.jsx +++ b/frontend/src/components/WorkflowTemplatePopup2.jsx @@ -26,6 +26,7 @@ import { Close as CloseIcon, East as EastIcon, Interests as InterestsIcon, + OpenInNew as OpenInNewIcon, } from '@mui/icons-material'; import { @@ -35,7 +36,8 @@ import { grey, } from "../views/AngularWorkflow.jsx" -import WorkflowTemplatePopup2 from "./WorkflowTemplatePopup.jsx"; +//import WorkflowTemplatePopup2 from "./WorkflowTemplatePopup2.jsx"; +import WorkflowTemplatePopup2 from "./WorkflowTemplatePopup2.jsx"; import ConfigureWorkflow from "../components/ConfigureWorkflow.jsx"; import WorkflowValidationTimeline from "../components/WorkflowValidationTimeline.jsx"; import FixWorkflowValidationErrors from "../components/FixWorkflowValidationErrors.jsx"; @@ -47,9 +49,11 @@ const WorkflowTemplatePopup = (props) => { isModalOpenDefault, setIsClicked, inputWorkflowId, + inputWorkflow, } = props; - const [isActive, setIsActive] = useState(workflowBuilt === true); + const [isActive, setIsActive] = useState(workflowBuilt === true || (workflowBuilt !== undefined && workflowBuilt !== null && workflowBuilt?.length > 0) || (inputWorkflow !== undefined && inputWorkflow !== null && inputWorkflow.id !== undefined && inputWorkflow.id !== null && inputWorkflow.id !== "") ? true : false) + const [isHovered, setIsHovered] = useState(false); const [modalOpen, setModalOpen] = useState(isModalOpenDefault === true ? true : false) const [errorMessage, setErrorMessage] = useState(""); @@ -65,7 +69,7 @@ const WorkflowTemplatePopup = (props) => { const [showTryitOut, setShowTryitout] = React.useState(showTryit === true ? true : false) const [loadingWorkflow, setLoadingWorkflow] = React.useState(false) - const [workflow, setWorkflow] = useState({}); + const [workflow, setWorkflow] = useState(inputWorkflow !== undefined && inputWorkflow !== null && inputWorkflow.id !== undefined && inputWorkflow.id !== null && inputWorkflow.id !== "" ? inputWorkflow : {}) const [_, setUpdate] = useState(0) const fetchWorkflow = (id) => { @@ -455,7 +459,7 @@ const WorkflowTemplatePopup = (props) => { //console.log("Error in workflow template: ", responseJson.error); setRequestSent(false) - const defaultMessage = "Error: Failed to generate workflow the workflow - the Shuffle team has been notified. Contact support@shuffler.io if you want manual help building this usecase until the AI system is handled." + const defaultMessage = "Error: Failed to generate workflow the workflow - the Shuffle team has been notified. Contact support@shuffler.io if you want manual help building this usecase." if (responseJson.reason !== undefined && responseJson.reason !== null && responseJson.reason !== "") { setErrorMessage(defaultMessage + "\n\n" + responseJson.reason) } else { @@ -535,8 +539,8 @@ const WorkflowTemplatePopup = (props) => { style: { backgroundColor: "black", color: "white", - minWidth: isHomePage ? null : isMobile ? 300 : 850, - maxWidth: isHomePage ? null : isMobile ? 300 : 850, + minWidth: isHomePage ? null : isMobile ? 300 : 750, + maxWidth: isHomePage ? null : isMobile ? 300 : 750, paddingTop: isMobile ? null : 75, itemAlign: "center", }, @@ -564,7 +568,7 @@ const WorkflowTemplatePopup = (props) => { {title === undefined || title === null || title === "" ? null : - Selected Workflow: + Selected Usecase:
    { dstapp={dstapp} title={title} description={description} - visualOnly={true} + visualOnly={true} workflowBuilt={workflowBuilt} + inputWorkflow={workflow} shownColor={shownColor} /> @@ -585,7 +590,7 @@ const WorkflowTemplatePopup = (props) => { } -
    +
    {/* Fix the timeline when errors are fixed.. how? */} {
    {workflowLoading === true ? -
    - Generating the Workflow... +
    + Generating Workflows...
    :
    - {usecaseDetails === undefined ? null : - - {usecaseDetails?.description} + {usecaseDetails === undefined || usecaseDetails === null || workflow.id !== undefined ? null : + + {usecaseDetails?.description} } - - {errorMessage !== "" ? errorMessage : ""} - + {errorMessage !== "" ? + + {errorMessage !== "" ? errorMessage : ""} + + : null} {showLoginButton ? { variant="outlined" style={{ textTransform: "none", + marginTop: 15, }} onClick={() => { //setWorkflowLoading(true) @@ -730,11 +738,11 @@ const WorkflowTemplatePopup = (props) => { const parsedDescription = description !== undefined && description !== null ? description.replaceAll("_", " ") : "" - const boxHeight = 104 + const boxHeight = visualOnly ? 75 : 104 const highlightColor = shownColor !== undefined && shownColor !== null && shownColor !== "" ? shownColor : "#f85a3e" var hasInterest = false - if (userdata.interests !== undefined && userdata.interests !== null && userdata.interests.length > 0) { + if (userdata?.interests !== undefined && userdata?.interests !== null && userdata?.interests?.length > 0) { const comparisonTitle = title === undefined || title === null ? "" : title.trim().toLowerCase().replaceAll(" ", "_") for (var interestkey in userdata.interests) { if (userdata.interests[interestkey].name === undefined || userdata.interests[interestkey].name === null || userdata.interests[interestkey].name === "") { @@ -742,12 +750,12 @@ const WorkflowTemplatePopup = (props) => { } if (modalOpen) { - console.log("COMPARE: ", userdata.interests[interestkey].name.trim().toLowerCase().replaceAll(" ", "_"), comparisonTitle) + //console.log("COMPARE: ", userdata.interests[interestkey].name.trim().toLowerCase().replaceAll(" ", "_"), comparisonTitle) } if (userdata.interests[interestkey].name.trim().toLowerCase().replaceAll(" ", "_") === comparisonTitle) { if (modalOpen) { - console.log("FOUND: ", comparisonTitle) + //console.log("FOUND: ", comparisonTitle) } hasInterest = true @@ -791,7 +799,16 @@ const WorkflowTemplatePopup = (props) => { }} onClick={() => { if (visualOnly === true) { - console.log("Not showing more than visuals.") + console.log("Not showing more than visuals. Workflow built: ", workflowBuilt, workflow) + + if (workflowBuilt !== undefined && workflowBuilt !== null && workflowBuilt?.length > 0) { + window.open("/workflows/" + workflowBuilt, "_blank") + } else if (workflow.id !== undefined && workflow.id !== null && workflow.id !== "") { + window.open("/workflows/" + workflow.id, "_blank") + } else { + toast("Click 'Try this usecase' to generate workflows for this usecase.") + } + return } @@ -822,7 +839,7 @@ const WorkflowTemplatePopup = (props) => { : null}
    -
    +
    {img1 !== undefined && img1 !== "" && srcapp !== undefined && srcapp !== "" ? @@ -849,7 +866,7 @@ const WorkflowTemplatePopup = (props) => { }
    -
    +
    {parsedTitle} @@ -858,13 +875,19 @@ const WorkflowTemplatePopup = (props) => {
    + {isActive === true && errorMessage === "" ? - - - + visualOnly === true ? + + + + : + + + : ""} - {!isActive && hasInterest === true ? + {!isActive && hasInterest === true && !visualOnly ? @@ -872,7 +895,7 @@ const WorkflowTemplatePopup = (props) => {
    - {showTryitOut && !isActive ? + {showTryitOut && !isActive && !visualOnly ?
    " + // Inject HTML at a fixed location? + //const newHtml = "

    Do you want to add this suggestion?

    " - // Find mouse cursor position on screen - console.log("Suggestion html to be added at location: ", event) - /* - const position = { - "top": cy.pan().y, - "left": cy.pan().x, - } - */ + // Find mouse cursor position on screen + console.log("Suggestion html to be added at location: ", event) + /* + const position = { + "top": cy.pan().y, + "left": cy.pan().x, + } + */ - const position = event.target.renderedPosition(); - const container = cy.container(); - const offset = { - left: container.offsetLeft, - top: container.offsetTop - }; - - // Calculate the actual screen position for the box - const screenPosition = { - left: position.x + offset.left - 150, - top: position.y + offset.top, - }; - - // Log the position to the console - console.log('Node screen position:', screenPosition); + const position = event.target.renderedPosition(); + const container = cy.container(); + const offset = { + left: container.offsetLeft, + top: container.offsetTop + }; - const newbox = { - "position": screenPosition, - "node_position": event.target.position(), - "open": true, - "attachedTo": data.attachedTo, - } + // Calculate the actual screen position for the box + const screenPosition = { + left: position.x + offset.left - 150, + top: position.y + offset.top, + }; - console.log("Rendered position: ", newbox.node_position) + // Log the position to the console + console.log('Node screen position:', screenPosition); - setSuggestionBox(newbox) + const newbox = { + "position": screenPosition, + "node_position": event.target.position(), + "open": true, + "attachedTo": data.attachedTo, + } + + console.log("Rendered position: ", newbox.node_position) + + setSuggestionBox(newbox) + + // Unselect + event.target.unselect(); - // Unselect - event.target.unselect(); - } else if (data.buttonType === "delete") { const parentNode = cy.getElementById(data.attachedTo); if (parentNode !== null && parentNode !== undefined) { @@ -5281,9 +5267,9 @@ const releaseToConnectLabel = "Release to Connect" return } else if (data.buttonType === "set_startnode" && data.type !== "TRIGGER") { - //console.log("STARTNODE") - //event.preventDefault() - //event.stopPropagation() + //console.log("STARTNODE") + //event.preventDefault() + //event.stopPropagation() const parentNode = cy.getElementById(data.attachedTo); if (parentNode !== null && parentNode !== undefined) { @@ -5375,46 +5361,46 @@ const releaseToConnectLabel = "Release to Connect" const destinationbranches = workflow.branches.filter((foundbranch) => foundbranch.destination_id === parentNode.data("id")) - - for (var sourceBranchesKey in sourcebranches) { - var newbranch = JSON.parse(JSON.stringify(sourcebranches[sourceBranchesKey])); - newbranch.id = uuidv4() - newbranch.source_id = newNodeData.id + for (var sourceBranchesKey in sourcebranches) { + var newbranch = JSON.parse(JSON.stringify(sourcebranches[sourceBranchesKey])); - newbranch._id = newbranch.id - newbranch.source = newbranch.source_id - newbranch.target = newbranch.destination_id - cy.add({ - group: "edges", - data: newbranch, - }) + newbranch.id = uuidv4() + newbranch.source_id = newNodeData.id + + newbranch._id = newbranch.id + newbranch.source = newbranch.source_id + newbranch.target = newbranch.destination_id + cy.add({ + group: "edges", + data: newbranch, + }) } - for (var destinationBranchesKey in destinationbranches) { - var newbranch = JSON.parse(JSON.stringify(destinationbranches[destinationBranchesKey])) + for (var destinationBranchesKey in destinationbranches) { + var newbranch = JSON.parse(JSON.stringify(destinationbranches[destinationBranchesKey])) - const sourcenode = cy.getElementById(newbranch.source_id) - if (sourcenode !== null && sourcenode !== undefined) { - const sourcedata = sourcenode.data() + const sourcenode = cy.getElementById(newbranch.source_id) + if (sourcenode !== null && sourcenode !== undefined) { + const sourcedata = sourcenode.data() - if (sourcedata.trigger_type !== "SUBFLOW" && sourcedata.trigger_type !== "USERINPUT") { - continue - } + if (sourcedata.trigger_type !== "SUBFLOW" && sourcedata.trigger_type !== "USERINPUT") { + continue + } - } + } - newbranch.id = uuidv4() - newbranch.destination_id = newNodeData.id + newbranch.id = uuidv4() + newbranch.destination_id = newNodeData.id - newbranch._id = newbranch.id - newbranch.source = newbranch.source_id - newbranch.target = newbranch.destination_id - cy.add({ - group: "edges", - data: newbranch, - }) + newbranch._id = newbranch.id + newbranch.source = newbranch.source_id + newbranch.target = newbranch.destination_id + cy.add({ + group: "edges", + data: newbranch, + }) } //event.target.unselect(); @@ -5424,48 +5410,48 @@ const releaseToConnectLabel = "Release to Connect" return; } else if (data.isDescriptor) { - // Find parent + // Find parent event.target.unselect(); - if (data.attachedTo !== undefined && data.attachedTo !== null && data.attachedTo.length > 0) { - const parentNode = cy.getElementById(data.attachedTo) - if (parentNode !== null && parentNode !== undefined) { - setTimeout(() => { - parentNode.select() - }, 100) - } - } + if (data.attachedTo !== undefined && data.attachedTo !== null && data.attachedTo.length > 0) { + const parentNode = cy.getElementById(data.attachedTo) + if (parentNode !== null && parentNode !== undefined) { + setTimeout(() => { + parentNode.select() + }, 100) + } + } //console.log("Can't select descriptor"); - if (data.isTrigger) { - console.log("But maybe we can select trigger descriptor? Maybe open execution tab?") - setExecutionModalOpen(true) - } + if (data.isTrigger) { + console.log("But maybe we can select trigger descriptor? Maybe open execution tab?") + setExecutionModalOpen(true) + } return; } - if (data.type === undefined) { - console.log("No type, automatically setting to action"); - data.type = "ACTION" - } + if (data.type === undefined) { + console.log("No type, automatically setting to action"); + data.type = "ACTION" + } if (data.type === "ACTION") { setSelectedComment({}) - // FIXME: is this what is mapping it an actual action in the workflow? wtf? + // FIXME: is this what is mapping it an actual action in the workflow? wtf? var curactionIndex = workflow.actions.findIndex((a) => a.id === data.id) var curaction = undefined - if (curactionIndex >= 0) { - curaction = workflow.actions[curactionIndex] - } + if (curactionIndex >= 0) { + curaction = workflow.actions[curactionIndex] + } if (!curaction || curaction === undefined) { if (data.id !== undefined && data.app_name !== undefined) { workflow.actions.push(data) setWorkflow(workflow) - // FIXME: Is this necessary? + // FIXME: Is this necessary? //curaction = JSON.parse(JSON.stringify(data)) } else { if (workflow.public !== true) { @@ -5477,15 +5463,15 @@ const releaseToConnectLabel = "Release to Connect" } } - // FIXME: This change may have caused... something - // FIXME: Somehow there is a referencing problem between the action in - // cytoscape and the one in the "workflow.actions" state + // FIXME: This change may have caused... something + // FIXME: Somehow there is a referencing problem between the action in + // cytoscape and the one in the "workflow.actions" state curaction = data - //workflow.actions[curactionIndex] = curaction - - //const data = event.target.data() - //event.target.data(curaction) - //event.target.data(curaction) + //workflow.actions[curactionIndex] = curaction + + //const data = event.target.data() + //event.target.data(curaction) + //event.target.data(curaction) var newapps = apps if (apps === null || apps === undefined || apps.length === 0) { @@ -5516,65 +5502,65 @@ const releaseToConnectLabel = "Release to Connect" (a.loop_versions !== null && a.loop_versions.includes(curaction.app_version))) ) - } + } if (curaction.template === true && curaction.name !== undefined) { //newapps. const parsedname = curaction.name.replaceAll(" ", "_").toLowerCase() console.log("FIND AN ACTION AMONG THE APPS THAT MATCHES NAME: ", parsedname) - curaction.matching_actions = [] - for (var newAppskey in newapps) { - for (let actionsSubkey in newapps[newAppskey].actions) { - const tmpaction = newapps[newAppskey].actions[actionsSubkey] - if (tmpaction.name.replaceAll(" ", "_").toLowerCase() === parsedname) { - console.log("MATCH!: ", newapps[newAppskey]) - curaction.matching_actions.push({ - "app_name": newapps[newAppskey].name, - "app_version": newapps[newAppskey].app_version, - "app_id": newapps[newAppskey].id, - "action": tmpaction, - "large_image": newapps[newAppskey].large_image, - "app_index": newAppskey, - "action_index": actionsSubkey, - }) - } - } - } - } + curaction.matching_actions = [] + for (var newAppskey in newapps) { + for (let actionsSubkey in newapps[newAppskey].actions) { + const tmpaction = newapps[newAppskey].actions[actionsSubkey] + if (tmpaction.name.replaceAll(" ", "_").toLowerCase() === parsedname) { + console.log("MATCH!: ", newapps[newAppskey]) + curaction.matching_actions.push({ + "app_name": newapps[newAppskey].name, + "app_version": newapps[newAppskey].app_version, + "app_id": newapps[newAppskey].id, + "action": tmpaction, + "large_image": newapps[newAppskey].large_image, + "app_index": newAppskey, + "action_index": actionsSubkey, + }) + } + } + } + } - if (!curapp || curapp === undefined) { - // Check local storage has it - const foundapps = localStorage.getItem("apps") - if (foundapps !== null && foundapps !== undefined) { - try { - const parsedapps = JSON.parse(foundapps) - if (parsedapps !== null && parsedapps !== undefined && parsedapps.length > 0) { - for (let appkey in parsedapps) { - if (parsedapps[appkey].name === curaction.app_name) { - curapp = parsedapps[appkey] - break - } - } - } - } catch (e) { - console.log("Problem with parsing apps from local storage", e) - } + if (!curapp || curapp === undefined) { + // Check local storage has it + const foundapps = localStorage.getItem("apps") + if (foundapps !== null && foundapps !== undefined) { + try { + const parsedapps = JSON.parse(foundapps) + if (parsedapps !== null && parsedapps !== undefined && parsedapps.length > 0) { + for (let appkey in parsedapps) { + if (parsedapps[appkey].name === curaction.app_name) { + curapp = parsedapps[appkey] + break + } + } + } + } catch (e) { + console.log("Problem with parsing apps from local storage", e) + } - } else { - console.log("No apps found in local storage") - } - } + } else { + console.log("No apps found in local storage") + } + } - /* - if (curapp && curapp.app_id !== undefined && curapp.app_id !== null && curapp.app_id.length > 0 &&curapp.actions.length <= 1) { - toast(`Side-loading app ${curapp.name} to get actions.`) - } - */ + /* + if (curapp && curapp.app_id !== undefined && curapp.app_id !== null && curapp.app_id.length > 0 &&curapp.actions.length <= 1) { + toast(`Side-loading app ${curapp.name} to get actions.`) + } + */ - if (curapp !== undefined && curapp !== null && curapp.id !== undefined && curapp.id !== null && curapp.id.length > 0) { - loadAppConfig(curapp.id, true) - } + if (curapp !== undefined && curapp !== null && curapp.id !== undefined && curapp.id !== null && curapp.id.length > 0) { + loadAppConfig(curapp.id, true) + } if (!curapp || curapp === undefined) { const tmpapp = { @@ -5591,31 +5577,31 @@ const releaseToConnectLabel = "Release to Connect" curaction.app_id = curapp.id - if (curapp.authentication === undefined || curapp.authentication === null) { - setAuthenticationType({ - type: "", - }) + if (curapp.authentication === undefined || curapp.authentication === null) { + setAuthenticationType({ + type: "", + }) - curapp.authentication = { - type: "", - required: false, - } - } else { - setAuthenticationType( - curapp.authentication.type === "oauth2-app" || (curapp.authentication.type === "oauth2" && curapp.authentication.redirect_uri !== undefined && curapp.authentication.redirect_uri !== null) ? { - type: curapp.authentication.type, - redirect_uri: curapp.authentication.redirect_uri, - refresh_uri: curapp.authentication.refresh_uri, - token_uri: curapp.authentication.token_uri, - scope: curapp.authentication.scope, - client_id: curapp.authentication.client_id, - client_secret: curapp.authentication.client_secret, - grant_type: curapp.authentication.grant_type, - } : { - type: "", - } - ) - } + curapp.authentication = { + type: "", + required: false, + } + } else { + setAuthenticationType( + curapp.authentication.type === "oauth2-app" || (curapp.authentication.type === "oauth2" && curapp.authentication.redirect_uri !== undefined && curapp.authentication.redirect_uri !== null) ? { + type: curapp.authentication.type, + redirect_uri: curapp.authentication.redirect_uri, + refresh_uri: curapp.authentication.refresh_uri, + token_uri: curapp.authentication.token_uri, + scope: curapp.authentication.scope, + client_id: curapp.authentication.client_id, + client_secret: curapp.authentication.client_secret, + grant_type: curapp.authentication.grant_type, + } : { + type: "", + } + ) + } const requiresAuth = curapp.authentication.required; //&& ((curapp.authentication.parameters !== undefined && curapp.authentication.parameters !== null) || (curapp.authentication.type === "oauth2" && curapp.authentication.redirect_uri !== undefined && curapp.authentication.redirect_uri !== null)) setRequiresAuthentication(requiresAuth); @@ -5634,57 +5620,57 @@ const releaseToConnectLabel = "Release to Connect" const tmpAuth = JSON.parse(JSON.stringify(newAppAuth)); - const curappName = curapp.name.toLowerCase() - for (let tmpAuthKey in tmpAuth) { - var item = tmpAuth[tmpAuthKey]; + const curappName = curapp.name.toLowerCase() + for (let tmpAuthKey in tmpAuth) { + var item = tmpAuth[tmpAuthKey]; - const newfields = {}; - if (item.app.name.toLowerCase() !== curappName) { - continue - } + const newfields = {}; + if (item.app.name.toLowerCase() !== curappName) { + continue + } - // Makes list into key:value object - for (let fieldkey in item.fields) { - if (item.fields[fieldkey] === undefined) { - console.log("Problem with filterkey in Node select", fieldkey) - continue - } + // Makes list into key:value object + for (let fieldkey in item.fields) { + if (item.fields[fieldkey] === undefined) { + console.log("Problem with filterkey in Node select", fieldkey) + continue + } - const filterkey = item.fields[fieldkey]["key"] - if (filterkey === null || filterkey === undefined) { - console.log("Problem with filterkey 2. Null or undefined 3") - continue - } + const filterkey = item.fields[fieldkey]["key"] + if (filterkey === null || filterkey === undefined) { + console.log("Problem with filterkey 2. Null or undefined 3") + continue + } - newfields[filterkey] = item.fields[fieldkey]["value"]; - } + newfields[filterkey] = item.fields[fieldkey]["value"]; + } - item.fields = newfields; - if (item.app.name.toLowerCase() === curappName) { - authenticationOptions.push(item); - if (item.id === findAuthId) { - curaction.selectedAuthentication = item; - } - } - } + item.fields = newfields; + if (item.app.name.toLowerCase() === curappName) { + authenticationOptions.push(item); + if (item.id === findAuthId) { + curaction.selectedAuthentication = item; + } + } + } - // Find with authenticationOption (authenticationOptions) has the highest .edited time. In this index, set the "last_modified" to true - - var latesttime = 0 - var latestindex = -1 + // Find with authenticationOption (authenticationOptions) has the highest .edited time. In this index, set the "last_modified" to true - for (var i = 0; i < authenticationOptions.length; i++) { - const authopt = authenticationOptions[i] + var latesttime = 0 + var latestindex = -1 - if (authopt.edited > latesttime) { - latesttime = authopt.edited - latestindex = i - } - } + for (var i = 0; i < authenticationOptions.length; i++) { + const authopt = authenticationOptions[i] - if (latestindex !== -1) { - authenticationOptions[latestindex].last_modified = true - } + if (authopt.edited > latesttime) { + latesttime = authopt.edited + latestindex = i + } + } + + if (latestindex !== -1) { + authenticationOptions[latestindex].last_modified = true + } curaction.authentication = authenticationOptions if ( @@ -5701,77 +5687,77 @@ const releaseToConnectLabel = "Release to Connect" curaction.selectedAuthentication = {}; } - if ( - curaction.parameters !== undefined && - curaction.parameters !== null && - curaction.parameters.length > 0 - ) { - for (var curActionParamKey in curaction.parameters) { - if ( - curaction.parameters[curActionParamKey].options !== undefined && - curaction.parameters[curActionParamKey].options !== null && - curaction.parameters[curActionParamKey].options.length > 0 && - curaction.parameters[curActionParamKey].value === "" - ) { - curaction.parameters[curActionParamKey].value = curaction.parameters[curActionParamKey].options[0]; - } - } + if ( + curaction.parameters !== undefined && + curaction.parameters !== null && + curaction.parameters.length > 0 + ) { + for (var curActionParamKey in curaction.parameters) { + if ( + curaction.parameters[curActionParamKey].options !== undefined && + curaction.parameters[curActionParamKey].options !== null && + curaction.parameters[curActionParamKey].options.length > 0 && + curaction.parameters[curActionParamKey].value === "" + ) { + curaction.parameters[curActionParamKey].value = curaction.parameters[curActionParamKey].options[0]; + } + } - } else { - console.log("Should check APP if it has the same params as ACTION") - for (let actionKey in curapp.actions) { - const tmpaction = curapp.actions[actionKey] - if (tmpaction.name === curaction.name) { - console.log("Found action - needs change?", tmpaction) - if (tmpaction.parameters !== undefined && tmpaction.parameters !== null && tmpaction.parameters.length > 0) { - curaction.parameters = JSON.parse(JSON.stringify(tmpaction.parameters)) - } - break - } - } + } else { + console.log("Should check APP if it has the same params as ACTION") + for (let actionKey in curapp.actions) { + const tmpaction = curapp.actions[actionKey] + if (tmpaction.name === curaction.name) { + console.log("Found action - needs change?", tmpaction) + if (tmpaction.parameters !== undefined && tmpaction.parameters !== null && tmpaction.parameters.length > 0) { + curaction.parameters = JSON.parse(JSON.stringify(tmpaction.parameters)) + } + break + } + } - } + } - // Fix authentication fields that may be missing in the UI - if (curapp.authentication.required && !curapp?.authentication?.type?.includes("oauth")) { - if (curapp.authentication.parameters !== undefined && curapp.authentication.parameters !== null) { - var actionChanged = false - for (let paramKey in curapp.authentication.parameters) { - var param = curapp.authentication.parameters[paramKey] + // Fix authentication fields that may be missing in the UI + if (curapp.authentication.required && !curapp?.authentication?.type?.includes("oauth")) { + if (curapp.authentication.parameters !== undefined && curapp.authentication.parameters !== null) { + var actionChanged = false + for (let paramKey in curapp.authentication.parameters) { + var param = curapp.authentication.parameters[paramKey] - if (curaction.parameters === undefined || curaction.parameters === null) { - curaction.parameters = [] - } + if (curaction.parameters === undefined || curaction.parameters === null) { + curaction.parameters = [] + } - var found = false - for (let actionParamKey in curaction.parameters) { - if (curaction.parameters[actionParamKey].name === param.name) { - found = true - break - } - } + var found = false + for (let actionParamKey in curaction.parameters) { + if (curaction.parameters[actionParamKey].name === param.name) { + found = true + break + } + } - if (!found) { - param.configuration = true - curaction.parameters.push(param) - - actionChanged = true - } - } + if (!found) { + param.configuration = true + curaction.parameters.push(param) - if (actionChanged && workflow.actions !== undefined && workflow.actions !== null) { - // Find it in the workflow and set it - for (let wfActionKey in workflow.actions) { - if (workflow.actions[wfActionKey].id === curaction.id) { - workflow.actions[wfActionKey] = curaction - } - } + actionChanged = true + } + } - setWorkflow(workflow) - - } - } - } + if (actionChanged && workflow.actions !== undefined && workflow.actions !== null) { + // Find it in the workflow and set it + for (let wfActionKey in workflow.actions) { + if (workflow.actions[wfActionKey].id === curaction.id) { + workflow.actions[wfActionKey] = curaction + } + } + + setWorkflow(workflow) + + } + } + } setSelectedApp(curapp) setSelectedAction(curaction) @@ -5814,13 +5800,12 @@ const releaseToConnectLabel = "Release to Connect" if (data.app_name === "Shuffle Workflow" || data.app_name === "User Input") { - // Check if public workflow - if (workflow.public === true) { - setWorkflows([workflow]) - } else { - getAvailableWorkflows(trigger_index); - getSettings(); - } + // Check if public workflow + if (workflow.public === true) { + setWorkflows([workflow]) + } else { + getAvailableWorkflows(trigger_index); + } } else if (data.app_name === "Webhook") { if (workflow.triggers[trigger_index].parameters !== undefined && workflow.triggers[trigger_index].parameters !== null && workflow.triggers[trigger_index].parameters.length > 0) { workflow.triggers[trigger_index].parameters[0] = { @@ -5828,120 +5813,120 @@ const releaseToConnectLabel = "Release to Connect" value: referenceUrl + "webhook_" + workflow.triggers[trigger_index].id, }; - if (workflow.triggers[trigger_index].parameters.length < 5) { - console.log("Adding to webhook params!") - workflow.triggers[trigger_index].parameters.push({ - name: "await_response", - value: "v1," - }) - } + if (workflow.triggers[trigger_index].parameters.length < 5) { + console.log("Adding to webhook params!") + workflow.triggers[trigger_index].parameters.push({ + name: "await_response", + value: "v1," + }) + } } } else if (data.app_name === "Pipeline") { - // Check if environment is set - if (data.environment === undefined || data.environment === null || data.environment === "" || data.environment.toLowerCase() === "cloud") { - for (var envKey in environments) { - if (environments[envKey].archived === true) { - continue - } + // Check if environment is set + if (data.environment === undefined || data.environment === null || data.environment === "" || data.environment.toLowerCase() === "cloud") { + for (var envKey in environments) { + if (environments[envKey].archived === true) { + continue + } - if (environments[envKey].Name.toLowerCase() === "cloud") { - continue - } + if (environments[envKey].Name.toLowerCase() === "cloud") { + continue + } - workflow.triggers[trigger_index].environment = environments[envKey].Name - data.environment = environments[envKey].Name - //setSelectedTrigger(data) - break - } - } - } + workflow.triggers[trigger_index].environment = environments[envKey].Name + data.environment = environments[envKey].Name + //setSelectedTrigger(data) + break + } + } + } setTimeout(() => { - if (trigger_index !== -1) { - const trigger = workflow.triggers[trigger_index] - if (trigger !== undefined && trigger !== null) { + if (trigger_index !== -1) { + const trigger = workflow.triggers[trigger_index] + if (trigger !== undefined && trigger !== null) { - // Autofixer - if (trigger.trigger_type === "USERINPUT") { - const relevantparams = [ - "alertinfo", - "options", - "type", - "email", - "sms", - "subflow", - ] - var foundparams = 0 - for (var paramkey in trigger.parameters) { - if (relevantparams.includes(trigger.parameters[paramkey].name)) { - foundparams++ - } - } + // Autofixer + if (trigger.trigger_type === "USERINPUT") { + const relevantparams = [ + "alertinfo", + "options", + "type", + "email", + "sms", + "subflow", + ] + var foundparams = 0 + for (var paramkey in trigger.parameters) { + if (relevantparams.includes(trigger.parameters[paramkey].name)) { + foundparams++ + } + } - if (foundparams < 6) { - trigger.parameters = [{ - name: "alertinfo", - value: "Do you want to continue the workflow? Start parameters: $exec", - },{ - name: "options", - value: "boolean", - }, - { - name: "type", - value: "subflow", - }, - { - name: "email", - value: "test@test.com", - }, - { - name: "sms", - value: "0000000", - }, - { - name: "subflow", - value: "", - }] + if (foundparams < 6) { + trigger.parameters = [{ + name: "alertinfo", + value: "Do you want to continue the workflow? Start parameters: $exec", + }, { + name: "options", + value: "boolean", + }, + { + name: "type", + value: "subflow", + }, + { + name: "email", + value: "test@test.com", + }, + { + name: "sms", + value: "0000000", + }, + { + name: "subflow", + value: "", + }] - workflow.triggers[trigger_index].parameters = trigger.parameters - } - } - } - } + workflow.triggers[trigger_index].parameters = trigger.parameters + } + } + } + } - if (allTriggers !== undefined && allTriggers !== null) { + if (allTriggers !== undefined && allTriggers !== null) { - // Just checking all three. Could just make a new list, but meh - if (allTriggers.pipelines !== undefined && allTriggers.pipelines !== null) { - for (var pipelineKey in allTriggers.pipelines) { - if (allTriggers.pipelines[pipelineKey].id === data.id) { - data.status = allTriggers.pipelines[pipelineKey].status - } - } - } + // Just checking all three. Could just make a new list, but meh + if (allTriggers.pipelines !== undefined && allTriggers.pipelines !== null) { + for (var pipelineKey in allTriggers.pipelines) { + if (allTriggers.pipelines[pipelineKey].id === data.id) { + data.status = allTriggers.pipelines[pipelineKey].status + } + } + } - if (allTriggers.webhooks !== undefined && allTriggers.webhooks !== null) { - for (var webhookKey in allTriggers.webhooks) { - if (allTriggers.webhooks[webhookKey].id === data.id) { - data.status = allTriggers.webhooks[webhookKey].status - } - } - } + if (allTriggers.webhooks !== undefined && allTriggers.webhooks !== null) { + for (var webhookKey in allTriggers.webhooks) { + if (allTriggers.webhooks[webhookKey].id === data.id) { + data.status = allTriggers.webhooks[webhookKey].status + } + } + } - if (allTriggers.schedules !== undefined && allTriggers.schedules !== null) { - for (var scheduleKey in allTriggers.schedules) { - if (allTriggers.schedules[scheduleKey].id === data.id) { - data.status = allTriggers.schedules[scheduleKey].status - } - } - } - } + if (allTriggers.schedules !== undefined && allTriggers.schedules !== null) { + for (var scheduleKey in allTriggers.schedules) { + if (allTriggers.schedules[scheduleKey].id === data.id) { + data.status = allTriggers.schedules[scheduleKey].status + } + } + } + } - setSelectedTriggerIndex(trigger_index) - setSelectedTrigger(data) - //setSelectedActionEnvironment(data.env) - }, 25) + setSelectedTriggerIndex(trigger_index) + setSelectedTrigger(data) + //setSelectedActionEnvironment(data.env) + }, 25) } else if (data.type === "COMMENT") { setSelectedComment(data); } else { @@ -5957,14 +5942,14 @@ const releaseToConnectLabel = "Release to Connect" selected: "", }); - setSuggestionBox({ - "position": { - "top": 500, - "left": 500, - }, - "open": false, - "attachedTo": "", - }); + setSuggestionBox({ + "position": { + "top": 500, + "left": 500, + }, + "open": false, + "attachedTo": "", + }); sendStreamRequest({ "item": "node", @@ -6000,8 +5985,8 @@ const releaseToConnectLabel = "Release to Connect" toast("Failed to auto-activate the app. Go to /apps and activate it.") } else { if (refresh === true) { - setHighlightedApp(appid) - //toast("App activated for your organisation! Refresh the page to use the app.") + setHighlightedApp(appid) + //toast("App activated for your organisation! Refresh the page to use the app.") getApps() } @@ -6164,7 +6149,7 @@ const releaseToConnectLabel = "Release to Connect" selectedkey = `.${key}`; } - for (let [subitem,subitemval] in Object.entries(value)) { + for (let [subitem, subitemval] in Object.entries(value)) { toreturn = GetParamMatch( paramname, value[subitem], @@ -6251,34 +6236,34 @@ const releaseToConnectLabel = "Release to Connect" : item.label.toLowerCase().trim().replaceAll(" ", "_"); exampledata = GetExampleResult(item); - if (dstdata.parameters !== undefined && dstdata.parameters !== null) { - for (let [paramkey,paramkeyval] in Object.entries(dstdata.parameters)) { - const param = dstdata.parameters[paramkey]; - // Skip authentication params - if (param.configuration) { - continue - } + if (dstdata.parameters !== undefined && dstdata.parameters !== null) { + for (let [paramkey, paramkeyval] in Object.entries(dstdata.parameters)) { + const param = dstdata.parameters[paramkey]; + // Skip authentication params + if (param.configuration) { + continue + } - if (param.options !== undefined && param.options !== null && param.options.length > 0) { - continue - } + if (param.options !== undefined && param.options !== null && param.options.length > 0) { + continue + } - const paramname = param.name - .toLowerCase() - .trim() - .replaceAll("_", " "); + const paramname = param.name + .toLowerCase() + .trim() + .replaceAll("_", " "); - const foundresult = GetParamMatch(paramname, exampledata, ""); - if (foundresult.length > 0) { - if (dstdata.parameters[paramkey].value.length === 0) { - dstdata.parameters[paramkey].value = `$${parentlabel}${foundresult}`; - dstdata.parameters[paramkey].autocompleted = true - } else { - //dstdata.parameters[paramkey].value = `$${parentlabel}${foundresult}`; - } - } + const foundresult = GetParamMatch(paramname, exampledata, ""); + if (foundresult.length > 0) { + if (dstdata.parameters[paramkey].value.length === 0) { + dstdata.parameters[paramkey].value = `$${parentlabel}${foundresult}`; + dstdata.parameters[paramkey].autocompleted = true + } else { + //dstdata.parameters[paramkey].value = `$${parentlabel}${foundresult}`; + } + } } - } + } } } @@ -6291,7 +6276,7 @@ const releaseToConnectLabel = "Release to Connect" const edge = event.target.data(); if (edge.source === undefined && edge.target === undefined) { - //console.log("Edge added without source or target") + //console.log("Edge added without source or target") return } @@ -6307,12 +6292,12 @@ const releaseToConnectLabel = "Release to Connect" const destinationnode = cy.getElementById(edge.target) if (sourcenode === undefined || sourcenode === null || destinationnode === undefined || destinationnode === null) { - console.log("Source or destination node is undefined or null: ", sourcenode, destinationnode) + console.log("Source or destination node is undefined or null: ", sourcenode, destinationnode) } else { - if (sourcenode.data("name") === "switch") { - event.target.remove() - return - } + if (sourcenode.data("name") === "switch") { + event.target.remove() + return + } console.log("Edge added: Is it a trigger? If so, check if it already has a branch and remove it: ", sourcenode.data()) if (sourcenode.data("type") === "TRIGGER") { @@ -6343,8 +6328,8 @@ const releaseToConnectLabel = "Release to Connect" const edgeCurve = calculateEdgeCurve(sourcenode.position(), destinationnode.position()) const currentedge = cy.getElementById(edge.id) if (currentedge !== undefined && currentedge !== null) { - currentedge.style('control-point-distance', edgeCurve.distance) - currentedge.style('control-point-weight', edgeCurve.weight) + currentedge.style('control-point-distance', edgeCurve.distance) + currentedge.style('control-point-weight', edgeCurve.weight) } } @@ -6354,7 +6339,7 @@ const releaseToConnectLabel = "Release to Connect" ) if (targetnode !== -1) { if (workflow.triggers[targetnode].app_name === "User Input" || workflow.triggers[targetnode].app_name === "Shuffle Workflow" || workflow.triggers[targetnode].app_name === "Shuffle Subflow") { - console.log("User Input or Shuffle Workflow") + console.log("User Input or Shuffle Workflow") } else { toast("Can't have triggers as target of branch") event.target.remove() @@ -6400,7 +6385,7 @@ const releaseToConnectLabel = "Release to Connect" // dest == source && source == dest // dest == dest && source == source // backend: check all children? to stop recursion - // + // var found = false; for (let branchkey in workflow.branches) { if (workflow.branches[branchkey].destination_id === edge.source && workflow.branches[branchkey].source_id === edge.target) { @@ -6408,25 +6393,25 @@ const releaseToConnectLabel = "Release to Connect" event.target.remove() found = true break - } + } - if (workflow.branches[branchkey].destination_id === edge.target && workflow.branches[branchkey].source_id === edge.source) { + if (workflow.branches[branchkey].destination_id === edge.target && workflow.branches[branchkey].source_id === edge.source) { - console.log("That branch already exists: ", workflow.branches[branchkey]) - const foundbranch = cy.getElementById(workflow.branches[branchkey].id) - if (foundbranch !== undefined && foundbranch !== null && foundbranch.data() !== undefined && foundbranch.data() !== null) { - console.log("Removing branch: ", foundbranch.data()) + console.log("That branch already exists: ", workflow.branches[branchkey]) + const foundbranch = cy.getElementById(workflow.branches[branchkey].id) + if (foundbranch !== undefined && foundbranch !== null && foundbranch.data() !== undefined && foundbranch.data() !== null) { + console.log("Removing branch: ", foundbranch.data()) - event.target.remove() + event.target.remove() - found = true - break - } else { - //console.log("Old branch didn't exist afterall. Remove.") - } - } + found = true + break + } else { + //console.log("Old branch didn't exist afterall. Remove.") + } + } - if (edge.target === workflow.start) { + if (edge.target === workflow.start) { targetnode = workflow.triggers.findIndex( (data) => data.id === edge.source ); @@ -6439,9 +6424,9 @@ const releaseToConnectLabel = "Release to Connect" found = true; } - } + } - if (edge.source === workflow.branches[branchkey].source_id) { + if (edge.source === workflow.branches[branchkey].source_id) { // FIXME: Verify multi-target for triggers // 1. Check if destination exists // 2. Check if source is a trigger @@ -6545,21 +6530,21 @@ const releaseToConnectLabel = "Release to Connect" setWorkflowAsCode(true); } - if (nodedata.decorator !== true && nodedata.attachedTo === undefined) { - var newdata = JSON.parse(JSON.stringify(nodedata)) - newdata.large_image = "" - sendStreamRequest({ - "item": "node", - "type": "add", - "id": nodedata.id, - "data": nodedata, - "x": node.position("x"), - "y": node.position("y"), - }) - } + if (nodedata.decorator !== true && nodedata.attachedTo === undefined) { + var newdata = JSON.parse(JSON.stringify(nodedata)) + newdata.large_image = "" + sendStreamRequest({ + "item": "node", + "type": "add", + "id": nodedata.id, + "data": nodedata, + "x": node.position("x"), + "y": node.position("y"), + }) + } if (nodedata.type === "ACTION") { - // Should get recommendations to load in for all nodesma + // Should get recommendations to load in for all nodesma if (workflow.actions.length === 1 && workflow.actions[0].id === workflow.start) { const newEdgeUuid = uuidv4(); @@ -6602,7 +6587,7 @@ const releaseToConnectLabel = "Release to Connect" if (nodedata.parameters !== undefined && nodedata.parameters !== null && !nodedata?.label?.endsWith("_copy")) { var newparameters = []; - for (let [subkey,subkeyval] in Object.entries(nodedata.parameters)) { + for (let [subkey, subkeyval] in Object.entries(nodedata.parameters)) { var newparam = JSON.parse(JSON.stringify(nodedata.parameters[subkey])) newparam.id = uuidv4() @@ -6624,13 +6609,13 @@ const releaseToConnectLabel = "Release to Connect" workflow.actions.push(nodedata); } - // 1. Check how many actions there are. If less than three, send a toast notification with suggested workflows - //if (workflow.actions.length < 3) { - // toast("Recommendations to show??") - //} + // 1. Check how many actions there are. If less than three, send a toast notification with suggested workflows + //if (workflow.actions.length < 3) { + // toast("Recommendations to show??") + //} setWorkflow(workflow); - fetchRecommendations(workflow) + fetchRecommendations(workflow) } else if (nodedata.type === "TRIGGER") { if (nodedata.is_valid === false) { toast("This trigger is not available to you"); @@ -6661,15 +6646,15 @@ const releaseToConnectLabel = "Release to Connect" data: newcybranch, }; - if (edgeToBeAdded.data.source !== edgeToBeAdded.data.target && edgeToBeAdded.data.source !== undefined && edgeToBeAdded.data.target !== undefined) { - if (nodedata.name !== "User Input" && nodedata.name !== "Shuffle Workflow") { - console.log("NAME: ", nodedata.name) - if (workflow.actions !== undefined && workflow.actions !== null && workflow.actions.length > 0) { - console.log("Edge handle: ", edgeToBeAdded) - cy.add(edgeToBeAdded); - } - } - } + if (edgeToBeAdded.data.source !== edgeToBeAdded.data.target && edgeToBeAdded.data.source !== undefined && edgeToBeAdded.data.target !== undefined) { + if (nodedata.name !== "User Input" && nodedata.name !== "Shuffle Workflow") { + console.log("NAME: ", nodedata.name) + if (workflow.actions !== undefined && workflow.actions !== null && workflow.actions.length > 0) { + console.log("Edge handle: ", edgeToBeAdded) + cy.add(edgeToBeAdded); + } + } + } setWorkflow(workflow); } @@ -6697,42 +6682,42 @@ const releaseToConnectLabel = "Release to Connect" // Check if the source is trigger and can start //console.log("Removed: ", edge.data()) const allNodes = cy.nodes().jsons() - for (let nodekey in allNodes) { - const curnode = allNodes[nodekey] - if (curnode.data.type !== "TRIGGER") { - continue - } + for (let nodekey in allNodes) { + const curnode = allNodes[nodekey] + if (curnode.data.type !== "TRIGGER") { + continue + } - if (curnode.data.id === edge.data("source")) { - console.log("Found matching trigger source: ", curnode) - if (curnode.data.app_name !== "Shuffle Workflow" && curnode.data.app_name !== "User Input") { + if (curnode.data.id === edge.data("source")) { + console.log("Found matching trigger source: ", curnode) + if (curnode.data.app_name !== "Shuffle Workflow" && curnode.data.app_name !== "User Input") { - // If it's started, READD the edge - if (curnode.data.status === "running") { - //console.log("Edge is running - readd it: ", edge.data()) + // If it's started, READD the edge + if (curnode.data.status === "running") { + //console.log("Edge is running - readd it: ", edge.data()) - // Just making sure it's not running infinitely - var newdata = edge.data() - newdata.readded = true + // Just making sure it's not running infinitely + var newdata = edge.data() + newdata.readded = true - try { - cy.add({ - group: "edges", - data: newdata, - }) + try { + cy.add({ + group: "edges", + data: newdata, + }) - //toast.error("You must STOP the trigger before deleting its branches") - console.log("You must STOP the trigger before deleting its branches") - } catch (e) { - console.log("Failed re-adding edge: ", e) - } - } + //toast.error("You must STOP the trigger before deleting its branches") + console.log("You must STOP the trigger before deleting its branches") + } catch (e) { + console.log("Failed re-adding edge: ", e) + } + } - //status: "uninitialized", - } - } - } + //status: "uninitialized", + } + } + } workflow.branches = workflow.branches.filter( (a) => a.id !== edge.data().id @@ -6765,14 +6750,14 @@ const releaseToConnectLabel = "Release to Connect" const node = event.target; const data = node.data(); - // FIXME: This is still a bit buggy - if (data.decorator !== true && data.attachedTo === undefined) { - sendStreamRequest({ - "item": "node", - "type": "remove", - "id": data.id, - }) - } + // FIXME: This is still a bit buggy + if (data.decorator !== true && data.attachedTo === undefined) { + sendStreamRequest({ + "item": "node", + "type": "remove", + "id": data.id, + }) + } if (data.finished === false) { return @@ -6978,35 +6963,35 @@ const releaseToConnectLabel = "Release to Connect" // try { var parsedjson = JSON.parse(clipboard); - // Check if array - if (!Array.isArray(parsedjson)) { - console.log("Not array! Adding to array.") - parsedjson = [parsedjson] - } + // Check if array + if (!Array.isArray(parsedjson)) { + console.log("Not array! Adding to array.") + parsedjson = [parsedjson] + } for (let jsonkey in parsedjson) { var item = parsedjson[jsonkey]; console.log("Adding: ", item); - if (item.data === undefined || item.data === null) { - console.log("Appending from here") - const newitem = { - "data": item, - "position": { - "x": 0, - "y": 0 - }, - "group": "nodes", - } + if (item.data === undefined || item.data === null) { + console.log("Appending from here") + const newitem = { + "data": item, + "position": { + "x": 0, + "y": 0 + }, + "group": "nodes", + } - item = newitem - item.type = "ACTION" - item.isStartNode = false - item.data.type = "ACTION" - item.data.isStartNode = false - } + item = newitem + item.type = "ACTION" + item.isStartNode = false + item.data.type = "ACTION" + item.data.isStartNode = false + } - item.data.id = uuidv4() + item.data.id = uuidv4() cy.add({ group: item.group, @@ -7026,14 +7011,14 @@ const releaseToConnectLabel = "Release to Connect" }; const getEnvironments = (orgId) => { - var headers = { - "Content-Type": "application/json", - "Accept": "application/json", - } + var headers = { + "Content-Type": "application/json", + "Accept": "application/json", + } - if (orgId !== undefined && orgId !== null && orgId.length > 0) { - headers["Org-Id"] = orgId - } + if (orgId !== undefined && orgId !== null && orgId.length > 0) { + headers["Org-Id"] = orgId + } fetch(globalUrl + "/api/v1/getenvironments", { method: "GET", @@ -7069,7 +7054,7 @@ const releaseToConnectLabel = "Release to Connect" } } - // Always showing for now + // Always showing for now //if (showEnvCnt > 1) { if (showEnvCnt > 0) { setShowEnvironment(true) @@ -7094,22 +7079,22 @@ const releaseToConnectLabel = "Release to Connect" setEnvironments(responseJson) } - /* - setTimeout(() => { - console.log("ACTIONS: ", workflow.actions) - if (workflow.actions !== undefined && workflow.actions !== null && workflow.actions.length > 0) { - for (var actionkey in workflow.actions) { - if (workflow.actions[actionkey].environment !== undefined && workflow.actions[actionkey].environment !== null && workflow.actions[actionkey].environment.length > 0) { - - const env = environments.findIndex((data) => data.Name === workflow.actions[actionkey].environment) - if (env !== -1) { - setSelectedActionEnvironment(environments[env]) - } - } - } - } - }, 2500) - */ + /* + setTimeout(() => { + console.log("ACTIONS: ", workflow.actions) + if (workflow.actions !== undefined && workflow.actions !== null && workflow.actions.length > 0) { + for (var actionkey in workflow.actions) { + if (workflow.actions[actionkey].environment !== undefined && workflow.actions[actionkey].environment !== null && workflow.actions[actionkey].environment.length > 0) { + + const env = environments.findIndex((data) => data.Name === workflow.actions[actionkey].environment) + if (env !== -1) { + setSelectedActionEnvironment(environments[env]) + } + } + } + } + }, 2500) + */ }) .catch((error) => { //toast(error.toString()); @@ -7127,14 +7112,14 @@ const releaseToConnectLabel = "Release to Connect" workflow.id.length > 0 ) { - // Check if - if (distributedFromParent === "" && suborgWorkflows === []) { - toast.info("Redirecting as the workflow ID does not match the URL") + // Check if + if (distributedFromParent === "" && suborgWorkflows === []) { + toast.info("Redirecting as the workflow ID does not match the URL") - setTimeout(() => { - window.location.pathname = "/workflows/" + props.match.params.key; - }, 2500) - } + setTimeout(() => { + window.location.pathname = "/workflows/" + props.match.params.key; + }, 2500) + } } const animationDuration = 150; @@ -7146,37 +7131,37 @@ const releaseToConnectLabel = "Release to Connect" cytoscapeElement.style.cursor = "default" } - if (nodedata.finished === false) { + if (nodedata.finished === false) { - // Should just be 1, so this should be fast enough :3 - const incomingEdges = event.target.incomers("edge").jsons() - if (incomingEdges !== undefined && incomingEdges !== null) { - for (var i = 0; i < incomingEdges.length; i++) { - // Find the actual edge - const edge = cy.getElementById(incomingEdges[i].data.id) - if (edge === undefined || edge === null) { - console.log("edge is null or undefined") - continue - } + // Should just be 1, so this should be fast enough :3 + const incomingEdges = event.target.incomers("edge").jsons() + if (incomingEdges !== undefined && incomingEdges !== null) { + for (var i = 0; i < incomingEdges.length; i++) { + // Find the actual edge + const edge = cy.getElementById(incomingEdges[i].data.id) + if (edge === undefined || edge === null) { + console.log("edge is null or undefined") + continue + } - // Set the edge to be dashed - edge.style("target-arrow-color", "#555555") - edge.style("line-style", "dashed") - edge.style("line-gradient-stop-colors", ["#555555", "#555555"]) - } - } + // Set the edge to be dashed + edge.style("target-arrow-color", "#555555") + edge.style("line-style", "dashed") + edge.style("line-gradient-stop-colors", ["#555555", "#555555"]) + } + } return } - if (nodedata.name === "switch") { - return - } + if (nodedata.name === "switch") { + return + } // console.log("nodedata", nodedata); // console.log("nodedata.app_name: ", nodedata.app_name); if (nodedata.app_name !== undefined) { - + const allNodes = cy.nodes().jsons(); // console.log("allNodes: ", allNodes) for (var nodekey in allNodes) { @@ -7184,9 +7169,9 @@ const releaseToConnectLabel = "Release to Connect" // console.log("Current node: ", currentNode); if (currentNode.data.isButton && currentNode.data.attachedTo !== nodedata.id) { - if (currentNode.data.buttonType === "condition-drag") { - continue - } + if (currentNode.data.buttonType === "condition-drag") { + continue + } cy.getElementById(currentNode.data.id).remove(); } @@ -7286,148 +7271,148 @@ const releaseToConnectLabel = "Release to Connect" }; const addRunCountButton = (event) => { - // Count executions? - // Maybe it shouldn't be onclick? - } + // Count executions? + // Maybe it shouldn't be onclick? + } const addConditionDraggers = (event, allElements, branches) => { - const nodedata = event.target.data() - const position = event.target.position() + const nodedata = event.target.data() + const position = event.target.position() - var conditions = [] - const foundParam = nodedata.parameters.find((param) => param.name.toLowerCase() === "conditions") + var conditions = [] + const foundParam = nodedata.parameters.find((param) => param.name.toLowerCase() === "conditions") - try { - conditions = JSON.parse(foundParam.value) - } catch (e) { - //toast("Failed parsing conditions: ", e) - } + try { + conditions = JSON.parse(foundParam.value) + } catch (e) { + //toast("Failed parsing conditions: ", e) + } - // Test conditions - if (conditions === undefined || conditions === null || typeof conditions !== "object") { - return - } + // Test conditions + if (conditions === undefined || conditions === null || typeof conditions !== "object") { + return + } - // Look for if it has the "Else" condition or not - const elseindex = conditions.findIndex((condition) => condition.name.toLowerCase() === "else") - const parentId = nodedata.id + // Look for if it has the "Else" condition or not + const elseindex = conditions.findIndex((condition) => condition.name.toLowerCase() === "else") + const parentId = nodedata.id - // Force following of Else at the least - const newId = uuidv5(parentId, uuidv5.URL) - if (elseindex === -1) { - conditions.push({ - name: "Else", - check: "Else", - id: newId, - parent_source: parentId, - }) - } else { - conditions[elseindex].id = newId - } + // Force following of Else at the least + const newId = uuidv5(parentId, uuidv5.URL) + if (elseindex === -1) { + conditions.push({ + name: "Else", + check: "Else", + id: newId, + parent_source: parentId, + }) + } else { + conditions[elseindex].id = newId + } - // 4 conditions (with else) = 300px -> 75px each - const parentHeight = (conditions.length*75)*0.75 + // 4 conditions (with else) = 300px -> 75px each + const parentHeight = (conditions.length * 75) * 0.75 - var startheight = -parentHeight/2 - var newnodes = [] - for (let conditionkey in conditions) { - var circleId = conditions[conditionkey].id === undefined ? (newNodeId = uuidv4()) : conditions[conditionkey].id + var startheight = -parentHeight / 2 + var newnodes = [] + for (let conditionkey in conditions) { + var circleId = conditions[conditionkey].id === undefined ? (newNodeId = uuidv4()) : conditions[conditionkey].id - // Check if circleId is a valid uuid or not - if (circleId === undefined || circleId === null) { - circleId = uuidv4() - } + // Check if circleId is a valid uuid or not + if (circleId === undefined || circleId === null) { + circleId = uuidv4() + } - if (!isUUID(circleId)) { - if (conditions[circleId].name !== undefined && conditions[circleId].name !== null) { - circleId = uuidv5(conditions[circleId].name, uuidv5.URL) - } else { - circleId = uuidv4() - conditions[conditionkey].name = circleId - conditions[conditionkey].id = circleId - } - } + if (!isUUID(circleId)) { + if (conditions[circleId].name !== undefined && conditions[circleId].name !== null) { + circleId = uuidv5(conditions[circleId].name, uuidv5.URL) + } else { + circleId = uuidv4() + conditions[conditionkey].name = circleId + conditions[conditionkey].id = circleId + } + } - // Check if circleId already exists as a node - if (cy !== undefined && cy !== null) { - const existingNode = cy.getElementById(circleId) - if (existingNode !== undefined && existingNode !== null && existingNode.length > 0) { - continue - } - } + // Check if circleId already exists as a node + if (cy !== undefined && cy !== null) { + const existingNode = cy.getElementById(circleId) + if (existingNode !== undefined && existingNode !== null && existingNode.length > 0) { + continue + } + } - // 1. Create "small" nodes at each point along the section based on the amount of conditions - // 2. Make these conditions have edgehandles - // 3. Make these conditions have a "drag" handle - const px = position.x + 65 - const py = position.y + startheight + // 1. Create "small" nodes at each point along the section based on the amount of conditions + // 2. Make these conditions have edgehandles + // 3. Make these conditions have a "drag" handle + const px = position.x + 65 + const py = position.y + startheight - console.log("Y height: ", startheight) + console.log("Y height: ", startheight) - const node = { - group: "nodes", - data: { - name: conditions[conditionkey].name, - id: circleId, - buttonType: "condition-drag", - attachedTo: nodedata.id, - is_valid: true, - }, - position: { - x: px, - y: py, - }, - locked: true, - } + const node = { + group: "nodes", + data: { + name: conditions[conditionkey].name, + id: circleId, + buttonType: "condition-drag", + attachedTo: nodedata.id, + is_valid: true, + }, + position: { + x: px, + y: py, + }, + locked: true, + } - newnodes.push(node) + newnodes.push(node) - // Check if ANY of the incoming branches has the id as source - if (branches !== undefined && branches !== null && branches.length > 0) { - for (let branchkey in branches) { - const branch = branches[branchkey] - if (branch.source_id !== circleId) { - continue - } + // Check if ANY of the incoming branches has the id as source + if (branches !== undefined && branches !== null && branches.length > 0) { + for (let branchkey in branches) { + const branch = branches[branchkey] + if (branch.source_id !== circleId) { + continue + } - const branchid = uuidv4() - newnodes.push({ - group: "edges", - data: { - id: branchid, - _id: branchid, + const branchid = uuidv4() + newnodes.push({ + group: "edges", + data: { + id: branchid, + _id: branchid, - source: circleId, - target: branch.destination_id, - label: branch.label, - conditions: branch.conditions, - hasErrors: branch.has_errors, - decorator: false, - parent_source: parentId, - } - }) - } - } + source: circleId, + target: branch.destination_id, + label: branch.label, + conditions: branch.conditions, + hasErrors: branch.has_errors, + decorator: false, + parent_source: parentId, + } + }) + } + } - startheight = startheight + parentHeight/(conditions.length-1) - } + startheight = startheight + parentHeight / (conditions.length - 1) + } - if (cy !== undefined && cy !== null) { - cy.add(newnodes) - } else { - var newelements = elements - if (allElements !== undefined) { - newelements = allElements - } + if (cy !== undefined && cy !== null) { + cy.add(newnodes) + } else { + var newelements = elements + if (allElements !== undefined) { + newelements = allElements + } - for (let nodekey in newnodes) { - newelements.push(newnodes[nodekey]) - } + for (let nodekey in newnodes) { + newelements.push(newnodes[nodekey]) + } - console.log("ELEMENTS: ", newelements) - setElements(newelements) - } + console.log("ELEMENTS: ", newelements) + setElements(newelements) + } } const addCopyButton = (event) => { @@ -7468,288 +7453,288 @@ const releaseToConnectLabel = "Release to Connect" }; const addActionSuggestions = (nodedata, event) => { - if (nodedata.type !== "ACTION") { - return - } + if (nodedata.type !== "ACTION") { + return + } - var parentNode = cy.$("#" + event.target.data("id")) - if (parentNode.data("isButton") || parentNode.data("buttonId")) { - return - } + var parentNode = cy.$("#" + event.target.data("id")) + if (parentNode.data("isButton") || parentNode.data("buttonId")) { + return + } - const px = parentNode.position("x") + 0; - const py = parentNode.position("y") + 100; + const px = parentNode.position("x") + 0; + const py = parentNode.position("y") + 100; - const parentlabel = parentNode.data("label")?.toLowerCase().replace(" ", "_") - const parentname = parentNode.data("app_name")?.toLowerCase().replace(" ", "_") - if (!parentlabel?.startsWith(parentname)+"_") { - return - } + const parentlabel = parentNode.data("label")?.toLowerCase().replace(" ", "_") + const parentname = parentNode.data("app_name")?.toLowerCase().replace(" ", "_") + if (!parentlabel?.startsWith(parentname) + "_") { + return + } - // Check if action has changed - const parentAppId = parentNode.data("app_id") - const parentActionname = parentNode.data("name") - for (var appkey in apps) { - const curapp = apps[appkey] + // Check if action has changed + const parentAppId = parentNode.data("app_id") + const parentActionname = parentNode.data("name") + for (var appkey in apps) { + const curapp = apps[appkey] - if (curapp.id !== parentAppId) { - continue - } + if (curapp.id !== parentAppId) { + continue + } - if (curapp.actions === undefined || curapp.actions === null || curapp.actions.length === 0) { - continue - } + if (curapp.actions === undefined || curapp.actions === null || curapp.actions.length === 0) { + continue + } - var startIndex = curapp.actions.findIndex((action) => action.category_label !== undefined && action.category_label !== null && action.category_label.length > 0) - if (startIndex === -1) { - startIndex = 0 - } + var startIndex = curapp.actions.findIndex((action) => action.category_label !== undefined && action.category_label !== null && action.category_label.length > 0) + if (startIndex === -1) { + startIndex = 0 + } - if (curapp.actions[startIndex].name !== parentActionname) { - console.log("Return 2") - return - } + if (curapp.actions[startIndex].name !== parentActionname) { + console.log("Return 2") + return + } - break - } + break + } - console.log("CONTINUE EVEN WHEN FIELDS ARE FILLED") + console.log("CONTINUE EVEN WHEN FIELDS ARE FILLED") - const iconInfo = { - icon: "M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm-1 4l6 6v10c0 1.1-.9 2-2 2H7.99C6.89 23 6 22.1 6 21l.01-14c0-1.1.89-2 1.99-2h7zm-1 7h5.5L14 6.5V12z", - iconColor: buttonColor, - iconBackgroundColor: buttonBackgroundColor, - }; + const iconInfo = { + icon: "M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm-1 4l6 6v10c0 1.1-.9 2-2 2H7.99C6.89 23 6 22.1 6 21l.01-14c0-1.1.89-2 1.99-2h7zm-1 7h5.5L14 6.5V12z", + iconColor: buttonColor, + iconBackgroundColor: buttonBackgroundColor, + }; - const svg_pin = ``; - const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); + const svg_pin = ``; + const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); - // 1. Find the app - // 2. Loop the apps' actions - // 3. Find actions based on category label IF it exists - - var addedLabels = [] - for (let appKey in apps) { - const curapp = apps[appKey] - if (curapp.name.toLowerCase().replace(" ", "_") !== parentname) { - continue - } + // 1. Find the app + // 2. Loop the apps' actions + // 3. Find actions based on category label IF it exists - if (curapp.actions === undefined || curapp.actions === null || curapp.actions.length === 0) { - continue - } + var addedLabels = [] + for (let appKey in apps) { + const curapp = apps[appKey] + if (curapp.name.toLowerCase().replace(" ", "_") !== parentname) { + continue + } - for (let actionKey in curapp.actions) { - const curaction = curapp.actions[actionKey] - - // Check if this is the current action already - if (parentNode.data("name") == curaction.name) { - continue - } + if (curapp.actions === undefined || curapp.actions === null || curapp.actions.length === 0) { + continue + } - if (curaction.category_label !== undefined && curaction.category_label !== null && curaction.category_label.length > 0) { - if (addedLabels.includes(curaction.category_label[0])) { - continue - } + for (let actionKey in curapp.actions) { + const curaction = curapp.actions[actionKey] - if (curaction.category_label[0].replaceAll("_", " ").toLowerCase() === "no label") { - continue - } + // Check if this is the current action already + if (parentNode.data("name") == curaction.name) { + continue + } - cy.add({ - group: "nodes", - data: { - weight: 30, - id: uuidv4(), - label: curaction.category_label[0], - attachedTo: event.target.data("id"), - is_valid: true, - buttonType: "ACTIONSUGGESTION", - }, - position: { - x: px, - y: py + (addedLabels.length * 50), - }, - locked: true, - }) + if (curaction.category_label !== undefined && curaction.category_label !== null && curaction.category_label.length > 0) { + if (addedLabels.includes(curaction.category_label[0])) { + continue + } - addedLabels.push(curaction.category_label[0]) - if (addedLabels.length >= 2) { - break - } - } - } + if (curaction.category_label[0].replaceAll("_", " ").toLowerCase() === "no label") { + continue + } - break - } + cy.add({ + group: "nodes", + data: { + weight: 30, + id: uuidv4(), + label: curaction.category_label[0], + attachedTo: event.target.data("id"), + is_valid: true, + buttonType: "ACTIONSUGGESTION", + }, + position: { + x: px, + y: py + (addedLabels.length * 50), + }, + locked: true, + }) + + addedLabels.push(curaction.category_label[0]) + if (addedLabels.length >= 2) { + break + } + } + } + + break + } } const addSuggestionButtons = (nodedata, event) => { - //console.log("Skipping Adding suggestion buttons") - //return - // Skipping add for now. Should Re-enable + //console.log("Skipping Adding suggestion buttons") + //return + // Skipping add for now. Should Re-enable - // Add a button for autocompletion based on input - if (nodedata.type === "ACTION") { - /* - const color = "#34a853" + // Add a button for autocompletion based on input + if (nodedata.type === "ACTION") { + /* + const color = "#34a853" + + // Fix icon + const iconInfo = { + icon: "M7.5 5.6 10 7 8.6 4.5 10 2 7.5 3.4 5 2l1.4 2.5L5 7zm12 9.8L17 14l1.4 2.5L17 19l2.5-1.4L22 19l-1.4-2.5L22 14zM22 2l-2.5 1.4L17 2l1.4 2.5L17 7l2.5-1.4L22 7l-1.4-2.5zm-7.63 5.29a.9959.9959 0 0 0-1.41 0L1.29 18.96c-.39.39-.39 1.02 0 1.41l2.34 2.34c.39.39 1.02.39 1.41 0L16.7 11.05c.39-.39.39-1.02 0-1.41l-2.33-2.35zm-1.03 5.49-2.12-2.12 2.44-2.44 2.12 2.12-2.44 2.44z", + iconColor: buttonColor, + iconBackgroundColor: buttonBackgroundColor, + }; + + const svg_pin = ``; + const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); + + const decoratorNode = { + position: { + x: event.target.position().x + 0, + y: event.target.position().y + 65, + }, + locked: true, + data: { + isButton: true, + isValid: true, + is_valid: true, + //label: "+", + attachedTo: nodedata.id, + imageColor: color, + buttonType: "suggestion", + icon: svgpin_Url, + iconBackground: iconInfo.iconBackgroundColor, + }, + }; + + cy.add(decoratorNode); + */ + } - // Fix icon - const iconInfo = { - icon: "M7.5 5.6 10 7 8.6 4.5 10 2 7.5 3.4 5 2l1.4 2.5L5 7zm12 9.8L17 14l1.4 2.5L17 19l2.5-1.4L22 19l-1.4-2.5L22 14zM22 2l-2.5 1.4L17 2l1.4 2.5L17 7l2.5-1.4L22 7l-1.4-2.5zm-7.63 5.29a.9959.9959 0 0 0-1.41 0L1.29 18.96c-.39.39-.39 1.02 0 1.41l2.34 2.34c.39.39 1.02.39 1.41 0L16.7 11.05c.39-.39.39-1.02 0-1.41l-2.33-2.35zm-1.03 5.49-2.12-2.12 2.44-2.44 2.12 2.12-2.44 2.44z", - iconColor: buttonColor, - iconBackgroundColor: buttonBackgroundColor, - }; + if (workflowRecommendations === undefined || workflowRecommendations === null || workflowRecommendations.length === 0) { + return + } - const svg_pin = ``; - const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); + var parentNode = cy.$("#" + event.target.data("id")); + if (parentNode.data("isButton") || parentNode.data("buttonId")) return; - const decoratorNode = { - position: { - x: event.target.position().x + 0, - y: event.target.position().y + 65, - }, - locked: true, - data: { - isButton: true, - isValid: true, - is_valid: true, - //label: "+", - attachedTo: nodedata.id, - imageColor: color, - buttonType: "suggestion", - icon: svgpin_Url, - iconBackground: iconInfo.iconBackgroundColor, - }, - }; + const px = parentNode.position("x") + 0; + const py = parentNode.position("y") + 200; + const circleId = (newNodeId = uuidv4()); - cy.add(decoratorNode); - */ - } + parentNode.data("circleId", circleId); - if (workflowRecommendations === undefined || workflowRecommendations === null || workflowRecommendations.length === 0) { - return - } + var startHeight = 0 + for (let recKey in workflowRecommendations) { + const rec = workflowRecommendations[recKey] + if (rec.action_id !== nodedata.id) { + continue + } - var parentNode = cy.$("#" + event.target.data("id")); - if (parentNode.data("isButton") || parentNode.data("buttonId")) return; + if (rec.recommendations === undefined || rec.recommendations === null || rec.recommendations.length === 0) { + continue + } - const px = parentNode.position("x") + 0; - const py = parentNode.position("y") + 200; - const circleId = (newNodeId = uuidv4()); + for (let recIndex in rec.recommendations) { + const parsedRec = rec.recommendations[recIndex] + console.log("REC: ", parsedRec) - parentNode.data("circleId", circleId); + const foundVersion = parsedRec.app_version !== undefined && parsedRec.app_version !== null && parsedRec.app_version !== "" ? parsedRec.app_version : "1.1.0" + const foundApp = apps.find((app) => app.app_name === parsedRec.app_name && app.app_version === foundVersion) + // Find out if foundApp is shuffle tools, and if so, add the correct image based on name - var startHeight = 0 - for (let recKey in workflowRecommendations) { - const rec = workflowRecommendations[recKey] - if (rec.action_id !== nodedata.id) { - continue - } + const largeImage = parsedRec.large_image !== undefined && parsedRec.large_image !== null && parsedRec.large_image !== "" ? parsedRec.large_image : foundApp === undefined || foundApp === null ? theme.palette.defaultImage : foundApp.large_image - if (rec.recommendations === undefined || rec.recommendations === null || rec.recommendations.length === 0) { - continue - } + const uuid = uuidv4() + const attachedToId = event.target.data("id") + // Check if parsedRec.app_action exists already as a node under this one + const branches = cy.edges().jsons() + var found = false + for (let branchKey in branches) { + const branch = branches[branchKey] + if (branch.data.source !== attachedToId) { + continue + } - for (let recIndex in rec.recommendations) { - const parsedRec = rec.recommendations[recIndex] - console.log("REC: ", parsedRec) + const targetNode = cy.getElementById(branch.data.target) + if (targetNode === undefined || targetNode === null) { + continue + } - const foundVersion = parsedRec.app_version !== undefined && parsedRec.app_version !== null && parsedRec.app_version !== "" ? parsedRec.app_version : "1.1.0" - const foundApp = apps.find((app) => app.app_name === parsedRec.app_name && app.app_version === foundVersion) - // Find out if foundApp is shuffle tools, and if so, add the correct image based on name + if (targetNode.data("name") === parsedRec.app_action) { + console.log("Found existing node (action name): ", targetNode) + found = true + } - const largeImage = parsedRec.large_image !== undefined && parsedRec.large_image !== null && parsedRec.large_image !== "" ? parsedRec.large_image : foundApp === undefined || foundApp === null ? theme.palette.defaultImage : foundApp.large_image + // FIXME: This could potentially be removed + if (targetNode.data("app_id") === parsedRec.app_id) { + console.log("Found existing node (id): ", targetNode) + found = true + } + } - const uuid = uuidv4() - const attachedToId = event.target.data("id") - // Check if parsedRec.app_action exists already as a node under this one - const branches = cy.edges().jsons() - var found = false - for (let branchKey in branches) { - const branch = branches[branchKey] - if (branch.data.source !== attachedToId) { - continue - } + // Skip the suggestion if it already exists + if (found) { + continue + } - const targetNode = cy.getElementById(branch.data.target) - if (targetNode === undefined || targetNode === null) { - continue - } + // Checks for src/dst (e.g. trigger = src usually) + const isTarget = true - if (targetNode.data("name") === parsedRec.app_action) { - console.log("Found existing node (action name): ", targetNode) - found = true - } + var name = parsedRec.app_action + if (parsedRec.app_action === "subflow") { + name = "Shuffle Workflow" + } else if (parsedRec.app_action === "user_input") { + name = "User Input" + } - // FIXME: This could potentially be removed - if (targetNode.data("app_id") === parsedRec.app_id) { - console.log("Found existing node (id): ", targetNode) - found = true - } - } + const newaction = { + name: name, + label: parsedRec.app_action, + label_replaced: parsedRec.app_action.replace("_", " ", -1), - // Skip the suggestion if it already exists - if (found) { - continue - } + id: uuid, + app_name: parsedRec.app_name, + app_version: foundVersion, + app_id: parsedRec.app_id, + sharing: false, + private_id: "", + isStartNode: false, + large_image: largeImage, + is_valid: true, + isSuggestion: true, + isTarget: isTarget, + attachedTo: attachedToId, - // Checks for src/dst (e.g. trigger = src usually) - const isTarget = true + finished: false, + } - var name = parsedRec.app_action - if (parsedRec.app_action === "subflow") { - name = "Shuffle Workflow" - } else if (parsedRec.app_action === "user_input") { - name = "User Input" - } + cy.add({ + group: "nodes", + data: newaction, + position: { + x: px + startHeight, + y: py, + }, + locked: true, + }); - const newaction = { - name: name, - label: parsedRec.app_action, - label_replaced: parsedRec.app_action.replace("_", " ", -1), + cy.add({ + group: "edges", + data: { + source: event.target.data("id"), + target: uuid, + decorator: true, + } + }) - id: uuid, - app_name: parsedRec.app_name, - app_version: foundVersion, - app_id: parsedRec.app_id, - sharing: false, - private_id: "", - isStartNode: false, - large_image: largeImage, - is_valid: true, - isSuggestion: true, - isTarget: isTarget, - attachedTo: attachedToId, + startHeight += 100 + } - finished: false, - } - - cy.add({ - group: "nodes", - data: newaction, - position: { - x: px+startHeight, - y: py, - }, - locked: true, - }); - - cy.add({ - group: "edges", - data: { - source: event.target.data("id"), - target: uuid, - decorator: true, - } - }) - - startHeight += 100 - } - - console.log("Got Rec: ", rec) - break - } + console.log("Got Rec: ", rec) + break + } } const addDeleteButton2 = (event) => { @@ -7840,23 +7825,23 @@ const releaseToConnectLabel = "Release to Connect" if (nodedata.finished === false) { console.log("NODE UNFINISHED (hover in): ", JSON.parse(JSON.stringify(nodedata))) - // Should just be 1, so this should be fast enough :3 - const incomingEdges = event.target.incomers("edge").jsons() - if (incomingEdges !== undefined && incomingEdges !== null) { - for (var i = 0; i < incomingEdges.length; i++) { - // Find the actual edge - const edge = cy.getElementById(incomingEdges[i].data.id) - if (edge === undefined || edge === null) { - console.log("edge is null or undefined") - continue - } + // Should just be 1, so this should be fast enough :3 + const incomingEdges = event.target.incomers("edge").jsons() + if (incomingEdges !== undefined && incomingEdges !== null) { + for (var i = 0; i < incomingEdges.length; i++) { + // Find the actual edge + const edge = cy.getElementById(incomingEdges[i].data.id) + if (edge === undefined || edge === null) { + console.log("edge is null or undefined") + continue + } - // Set the edge to be dashed - edge.style("target-arrow-color", "white") - edge.style("line-style", "solid") - edge.style("line-gradient-stop-colors", ["white", "white"]) - } - } + // Set the edge to be dashed + edge.style("target-arrow-color", "white") + edge.style("line-style", "solid") + edge.style("line-gradient-stop-colors", ["white", "white"]) + } + } return } @@ -7867,58 +7852,58 @@ const releaseToConnectLabel = "Release to Connect" //if (parentNode.data("isButton") || parentNode.data("buttonId")) return; if (nodedata.app_name !== undefined && !workflow.public === true) { - const allNodes = cy.nodes().jsons(); + const allNodes = cy.nodes().jsons(); - if (nodedata.app_name === "Webhook" || nodedata.app_name === "Schedule" || nodedata.app_name === "Gmail" || nodedata.app_name === "Office365") { - var found = false; - for (let nodekey in allNodes) { - const currentNode = allNodes[nodekey]; - if ( - currentNode.data.attachedTo === nodedata.id && - currentNode.data.isDescriptor - ) { - found = true; - break; - } - } + if (nodedata.app_name === "Webhook" || nodedata.app_name === "Schedule" || nodedata.app_name === "Gmail" || nodedata.app_name === "Office365") { + var found = false; + for (let nodekey in allNodes) { + const currentNode = allNodes[nodekey]; + if ( + currentNode.data.attachedTo === nodedata.id && + currentNode.data.isDescriptor + ) { + found = true; + break; + } + } - if (!found) { - // Find how many executions it has - var executions = 0 - const matchingExecutions = workflowExecutions.filter((execution => execution.execution_source === nodedata.app_name.toLowerCase())) - const color = matchingExecutions.length > 0 ? "#34a853" : "#ea4436" - const decoratorNode = { - position: { - x: event.target.position().x + 44, - y: event.target.position().y + 44, - }, - locked: true, - data: { - isDescriptor: true, - isValid: true, - is_valid: true, - isTrigger: true, - label: `${matchingExecutions.length}`, - attachedTo: nodedata.id, - imageColor: color, - hasExecutions: true, - }, - }; + if (!found) { + // Find how many executions it has + var executions = 0 + const matchingExecutions = workflowExecutions.filter((execution => execution.execution_source === nodedata.app_name.toLowerCase())) + const color = matchingExecutions.length > 0 ? "#34a853" : "#ea4436" + const decoratorNode = { + position: { + x: event.target.position().x + 44, + y: event.target.position().y + 44, + }, + locked: true, + data: { + isDescriptor: true, + isValid: true, + is_valid: true, + isTrigger: true, + label: `${matchingExecutions.length}`, + attachedTo: nodedata.id, + imageColor: color, + hasExecutions: true, + }, + }; - cy.add(decoratorNode) - } - } + cy.add(decoratorNode) + } + } var found = false; for (var _key in allNodes) { const currentNode = allNodes[_key]; // console.log("CURRENT NODE: ", currentNode) - + if ((currentNode.data.buttonType === "ACTIONSUGGESTION" || currentNode.data.isButton || currentNode.data.isSuggestion) && currentNode.data.attachedTo !== nodedata.id) { - if (currentNode.data.buttonType === "condition-drag") { - continue - } + if (currentNode.data.buttonType === "condition-drag") { + continue + } cy.getElementById(currentNode.data.id).remove() } @@ -7931,14 +7916,14 @@ const releaseToConnectLabel = "Release to Connect" }*/ if (currentNode.data.isButton && currentNode.data.attachedTo === nodedata.id) { - found = true; + found = true; } } - if (nodedata.name === "switch") { - addConditionDraggers(event) - return - } + if (nodedata.name === "switch") { + addConditionDraggers(event) + return + } if (!found) { addDeleteButton(event) @@ -7947,31 +7932,31 @@ const releaseToConnectLabel = "Release to Connect" if (nodedata.trigger_type === "SUBFLOW" || nodedata.trigger_type === "USERINPUT") { addCopyButton(event); } else { - // Check how many executions from the source - addRunCountButton(event); - } - } else { + // Check how many executions from the source + addRunCountButton(event); + } + } else { - addCopyButton(event); - addStartnodeButton(event); - } + addCopyButton(event); + addStartnodeButton(event); + } - // autocomplete - // right click - // suggestions - addActionSuggestions(nodedata, event); + // autocomplete + // right click + // suggestions + addActionSuggestions(nodedata, event); - if (workflow.actions.length < 4) { - addSuggestionButtons(nodedata, event); - } else { - //console.log("Too many actions to suggest (for now)") - } - } + if (workflow.actions.length < 4) { + addSuggestionButtons(nodedata, event); + } else { + //console.log("Too many actions to suggest (for now)") + } + } } - if (nodedata.name === "switch") { - return - } + if (nodedata.name === "switch") { + return + } var parsedStyle = { "border-width": "7px", @@ -7980,9 +7965,9 @@ const releaseToConnectLabel = "Release to Connect" //"cursor": "pointer", } - if (nodedata.buttonType === "ACTIONSUGGESTION") { - parsedStyle["font-size"] = "18px" - } + if (nodedata.buttonType === "ACTIONSUGGESTION") { + parsedStyle["font-size"] = "18px" + } const typeIds = cy.elements('node:selected').jsons(); for (var idkey in typeIds) { @@ -7996,7 +7981,7 @@ const releaseToConnectLabel = "Release to Connect" if (nodedata.type !== "COMMENT") { parsedStyle.color = "white"; - } + } if (event.target !== undefined && event.target !== null) { event.target.animate( @@ -8029,10 +8014,10 @@ const releaseToConnectLabel = "Release to Connect" const edgeData = event.target.data(); if (edgeData.decorator === true) { - // Defaults - event.target.style("target-arrow-color", "#555555") - event.target.style("line-style", "dashed") - event.target.style("line-gradient-stop-colors", ["#555555", "#555555"]) + // Defaults + event.target.style("target-arrow-color", "#555555") + event.target.style("line-style", "dashed") + event.target.style("line-gradient-stop-colors", ["#555555", "#555555"]) return; } @@ -8053,15 +8038,15 @@ const releaseToConnectLabel = "Release to Connect" const edgeData = event.target.data(); if (edgeData.decorator === true) { - // Set color of it to white and not stripled - event.target.style("target-arrow-color", "white") - event.target.style("line-style", "solid") - event.target.style("line-gradient-stop-colors", ["white", "white"]) + // Set color of it to white and not stripled + event.target.style("target-arrow-color", "white") + event.target.style("line-style", "solid") + event.target.style("line-gradient-stop-colors", ["white", "white"]) return; } - // FIXME: Color problem. Do later + // FIXME: Color problem. Do later //sendStreamRequest({ // "item": "edge", // "type": "hover", @@ -8118,9 +8103,9 @@ const releaseToConnectLabel = "Release to Connect" if (event.target !== undefined && event.target !== null) { - // If decorator and hovered - // Set color to white - + // If decorator and hovered + // Set color to white + @@ -8188,19 +8173,19 @@ const releaseToConnectLabel = "Release to Connect" } } */ - if (isNaN(controlPointDistance[0])) { - controlPointDistance[0] = 0 - } - if (isNaN(controlPointDistance[1])) { - controlPointDistance[1] = 0 - } + if (isNaN(controlPointDistance[0])) { + controlPointDistance[0] = 0 + } + if (isNaN(controlPointDistance[1])) { + controlPointDistance[1] = 0 + } - if (isNaN(controlPointWeight[0])) { - controlPointWeight[0] = 0 - } - if (isNaN(controlPointWeight[1])) { - controlPointWeight[1] = 0 - } + if (isNaN(controlPointWeight[0])) { + controlPointWeight[0] = 0 + } + if (isNaN(controlPointWeight[1])) { + controlPointWeight[1] = 0 + } return { "distance": controlPointDistance, @@ -8209,32 +8194,32 @@ const releaseToConnectLabel = "Release to Connect" } const setupGraph = (inputworkflow) => { - // Reset cytoscape nodes and branches - if (cy !== undefined && cy !== null) { - if (inputworkflow.actions !== undefined && inputworkflow.actions !== null && inputworkflow.actions.length > 0) { - //cy.remove('*') - } - } + // Reset cytoscape nodes and branches + if (cy !== undefined && cy !== null) { + if (inputworkflow.actions !== undefined && inputworkflow.actions !== null && inputworkflow.actions.length > 0) { + //cy.remove('*') + } + } - if (inputworkflow.actions === undefined || inputworkflow.actions === null) { - inputworkflow.actions = [] - } + if (inputworkflow.actions === undefined || inputworkflow.actions === null) { + inputworkflow.actions = [] + } - if (inputworkflow.branches === undefined || inputworkflow.branches === null) { - inputworkflow.branches = [] - } + if (inputworkflow.branches === undefined || inputworkflow.branches === null) { + inputworkflow.branches = [] + } - if (inputworkflow.triggers === undefined || inputworkflow.triggers === null) { - inputworkflow.triggers = [] - } + if (inputworkflow.triggers === undefined || inputworkflow.triggers === null) { + inputworkflow.triggers = [] + } - if (inputworkflow.comments === undefined || inputworkflow.comments === null) { - inputworkflow.comments = [] - } + if (inputworkflow.comments === undefined || inputworkflow.comments === null) { + inputworkflow.comments = [] + } - if (inputworkflow.visual_branches === undefined || inputworkflow.visual_branches === null) { - inputworkflow.visual_branches = [] - } + if (inputworkflow.visual_branches === undefined || inputworkflow.visual_branches === null) { + inputworkflow.visual_branches = [] + } const actions = inputworkflow.actions.map((action) => { const node = {}; @@ -8256,13 +8241,13 @@ const releaseToConnectLabel = "Release to Connect" action.iconBackground = iconInfo.iconBackgroundColor; } } else if (action.app_name === "Integration Framework" || action.app_name === "Singul") { - const iconInfo = GetIconInfo(action) - if (iconInfo !== undefined && iconInfo !== null) { - action.fillGradient = iconInfo.fillGradient - action.iconBackground = iconInfo.iconBackgroundColor - action.fillstyle = "linear-gradient" - } - } + const iconInfo = GetIconInfo(action) + if (iconInfo !== undefined && iconInfo !== null) { + action.fillGradient = iconInfo.fillGradient + action.iconBackground = iconInfo.iconBackgroundColor + action.fillstyle = "linear-gradient" + } + } node.position = action.position; node.data = action; @@ -8272,10 +8257,10 @@ const releaseToConnectLabel = "Release to Connect" node.data.type = "ACTION"; node.isStartNode = action["id"] === inputworkflow.start; - if (node.data.errors !== undefined && node.data.errors !== null && node.data.errors.length > 0) { - node.data.is_valid = false - node.is_valid = false - } + if (node.data.errors !== undefined && node.data.errors !== null && node.data.errors.length > 0) { + node.data.is_valid = false + node.is_valid = false + } if (inputworkflow.public === true) { node.data.is_valid = true @@ -8294,150 +8279,203 @@ const releaseToConnectLabel = "Release to Connect" node.data.example = example; - if (inputworkflow?.id !== undefined && originalWorkflow?.id !== undefined && inputworkflow?.id !== originalWorkflow?.id && originalWorkflow.actions !== undefined && originalWorkflow.actions !== null && originalWorkflow.actions.length > 0) { - // Find the node in the original workflow - var inParent = false - for (var i = 0; i < originalWorkflow.actions.length; i++) { - const originalAction = originalWorkflow.actions[i] - if (originalAction.id === action.id) { - inParent = true - break - } - } + if (inputworkflow?.id !== undefined && originalWorkflow?.id !== undefined && inputworkflow?.id !== originalWorkflow?.id && originalWorkflow.actions !== undefined && originalWorkflow.actions !== null && originalWorkflow.actions.length > 0) { + // Find the node in the original workflow + var inParent = false + for (var i = 0; i < originalWorkflow.actions.length; i++) { + const originalAction = originalWorkflow.actions[i] + if (originalAction.id === action.id) { + inParent = true + break + } + } - if (inParent === true) { - setTimeout(() => { - const foundnode = cy.getElementById(action.id) - if (foundnode !== undefined && foundnode !== null) { - const parsedStyle = { - "border-width": "3px", - "border-opacity": "1", - "border-color": "#40E0D0", - "opacity": "0.4", - } + if (inParent === true) { + setTimeout(() => { + const foundnode = cy.getElementById(action.id) + if (foundnode !== undefined && foundnode !== null) { + const parsedStyle = { + "border-width": "3px", + "border-opacity": "1", + "border-color": "#40E0D0", + "opacity": "0.4", + } - const animationDuration = 150 - foundnode.animate( - { - style: parsedStyle, - }, - { - duration: animationDuration, - } - ) - } - }, 500) - } - } + const animationDuration = 150 + foundnode.animate( + { + style: parsedStyle, + }, + { + duration: animationDuration, + } + ) + } + }, 500) + } + } return node }) - // What are these again? Where are they used? - const decoratorNodes = [] + // What are these again? Where are they used? + const decoratorNodes = [] - /* - // Removed for now as it wasn't really that helpful - const decoratorNodes = inputworkflow.actions.map((action) => { - if (!action.isStartNode) { - if (action.app_name === "Testing") { - return null - } else if (action.app_name === "Shuffle Tools") { - return null - } else if (action.app_name === "Integration Framework" || action.app_name === "Singul") { - return null + /* + // Removed for now as it wasn't really that helpful + const decoratorNodes = inputworkflow.actions.map((action) => { + if (!action.isStartNode) { + if (action.app_name === "Testing") { + return null + } else if (action.app_name === "Shuffle Tools") { + return null + } else if (action.app_name === "Integration Framework" || action.app_name === "Singul") { + return null + } } + + if (action.id === undefined || action.id === null) { + return null } - - if (action.id === undefined || action.id === null) { - return null - } - - if (action.position === undefined || action.position === null || action.position.x === undefined || action.position.x === null || action.position.y === undefined || action.position.y === null) { - return null - } - - const iconInfo = GetIconInfo(action); - const svg_pin = ``; - const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); - - const offset = action.isStartNode ? 36 : 44; - - const decoratorNode = { - position: { - x: action.position.x + offset, - y: action.position.y + offset, - }, - locked: true, - data: { - isDescriptor: true, - isValid: true, - is_valid: true, - label: "", - image: svgpin_Url, - imageColor: iconInfo.iconBackgroundColor, - attachedTo: action.id, - }, + + if (action.position === undefined || action.position === null || action.position.x === undefined || action.position.x === null || action.position.y === undefined || action.position.y === null) { + return null } - return decoratorNode - }) - */ + + const iconInfo = GetIconInfo(action); + const svg_pin = ``; + const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); + + const offset = action.isStartNode ? 36 : 44; + + const decoratorNode = { + position: { + x: action.position.x + offset, + y: action.position.y + offset, + }, + locked: true, + data: { + isDescriptor: true, + isValid: true, + is_valid: true, + label: "", + image: svgpin_Url, + imageColor: iconInfo.iconBackgroundColor, + attachedTo: action.id, + }, + } + return decoratorNode + }) + */ const foundtriggers = inputworkflow.triggers.map((trigger) => { const node = {}; node.position = trigger.position; - if (trigger.large_image === undefined || trigger.large_image === null || trigger.large_image.length === 0) { - - // Search triggers array for it where the name is matching and set image - var foundTrigger = triggers.find((t) => t.name === trigger.name) - if (foundTrigger !== undefined && foundTrigger !== null) { - console.log("Autofilled missing trigger image") - trigger.large_image = foundTrigger.large_image - } - } + // Search triggers array for it where the name is matching and set image + if (trigger.large_image === undefined || trigger.large_image === null || trigger.large_image.length === 0) { + var foundTrigger = triggers.find((t) => t.name === trigger.name) + if (foundTrigger !== undefined && foundTrigger !== null) { + console.log("Autofilled missing trigger image") + trigger.large_image = foundTrigger.large_image + } + } node.data = trigger; node.data._id = trigger["id"]; node.data.id = trigger["id"]; node.data.type = "TRIGGER"; - if (inputworkflow?.id !== undefined && originalWorkflow?.id !== undefined && inputworkflow?.id !== originalWorkflow?.id && originalWorkflow.triggers !== undefined && originalWorkflow.triggers !== null && originalWorkflow.triggers.length > 0) { - // Find the node in the original workflow - var inParent = false - for (var i = 0; i < originalWorkflow.triggers.length; i++) { - const originalAction = originalWorkflow.triggers[i] - if (originalAction.id === trigger.id) { - inParent = true - break - } - } + // Adds the correct branching for same-workflow trigger + if (trigger?.trigger_type === "SUBFLOW" && trigger?.parameters !== undefined && trigger?.parameters !== null && trigger?.parameters.length > 0) { + var foundTargetNode = "" + var sameWorkflow = false + for (var key in trigger.parameters) { + if (trigger.parameters[key].name === "workflow" && trigger.parameters[key].value === inputworkflow.id) { + sameWorkflow = true + } - if (inParent === true) { - setTimeout(() => { - const foundnode = cy.getElementById(trigger.id) - if (foundnode !== undefined && foundnode !== null) { - const parsedStyle = { - "border-width": "3px", - "border-opacity": "1", - "border-color": "#40E0D0", - "opacity": "0.4", - } + if (trigger.parameters[key].name === "startnode" && trigger.parameters[key].value !== "") { + foundTargetNode = trigger.parameters[key].value + } + } - const animationDuration = 150 - foundnode.animate( - { - style: parsedStyle, - }, - { - duration: animationDuration, - } - ) - } - }, 500) - } - } + if (sameWorkflow && foundTargetNode !== "") { + const newid = uuidv4() + const newbranch = { + id: newid, + _id: newid, + source: trigger.id, + source_id: trigger.id, + target: foundTargetNode, + destination_id: foundTargetNode, + + conditions: [], + has_errors: false, + decorator: true, + label: "Subflow", + } + + if (inputworkflow.visual_branches !== undefined) { + if (inputworkflow.visual_branches === null) { + inputworkflow.visual_branches = [newbranch] + } else if (inputworkflow.visual_branches.length === 0) { + inputworkflow.visual_branches.push(newbranch) + } else { + const foundIndex = inputworkflow.visual_branches.findIndex( + (branch) => branch.source_id === newbranch.source_id + ) + + if (foundIndex !== -1) { + //console.log("Already found subflow branch") + } else { + inputworkflow.visual_branches.push(newbranch); + } + } + } else { + inputworkflow.visual_branches = [newbranch] + } + + } + + } + + if (inputworkflow?.id !== undefined && originalWorkflow?.id !== undefined && inputworkflow?.id !== originalWorkflow?.id && originalWorkflow.triggers !== undefined && originalWorkflow.triggers !== null && originalWorkflow.triggers.length > 0) { + // Find the node in the original workflow + var inParent = false + for (var i = 0; i < originalWorkflow.triggers.length; i++) { + const originalAction = originalWorkflow.triggers[i] + if (originalAction.id === trigger.id) { + inParent = true + break + } + } + + if (inParent === true) { + setTimeout(() => { + const foundnode = cy.getElementById(trigger.id) + if (foundnode !== undefined && foundnode !== null) { + const parsedStyle = { + "border-width": "3px", + "border-opacity": "1", + "border-color": "#40E0D0", + "opacity": "0.4", + } + + const animationDuration = 150 + foundnode.animate( + { + style: parsedStyle, + }, + { + duration: animationDuration, + } + ) + } + }, 500) + } + } return node; }); @@ -8477,30 +8515,30 @@ const releaseToConnectLabel = "Release to Connect" label = conditions.length + " conditions"; } - // Verify if branch.source_id and branch.destination_id exists in triggers or actions - /* - var sourceExists = false; - var destinationExists = false; - for (var i = 0; i < insertedNodes.length; i++) { - console.log("Insertednode: ", insertedNodes[i].data); - if (insertedNodes[i].data._id === branch.source_id) { - sourceExists = true; - } - if (insertedNodes[i].data._id === branch.destination_id) { - destinationExists = true; - } - } + // Verify if branch.source_id and branch.destination_id exists in triggers or actions + /* + var sourceExists = false; + var destinationExists = false; + for (var i = 0; i < insertedNodes.length; i++) { + console.log("Insertednode: ", insertedNodes[i].data); + if (insertedNodes[i].data._id === branch.source_id) { + sourceExists = true; + } + if (insertedNodes[i].data._id === branch.destination_id) { + destinationExists = true; + } + } + + if (sourceExists === false || destinationExists === false) { + console.log("Couldn't find source node for branch " + branch.id); + return null; + } + */ - if (sourceExists === false || destinationExists === false) { - console.log("Couldn't find source node for branch " + branch.id); - return null; - } - */ - - var parentcontrolled = false - if (branch.parent_controlled !== undefined && branch.parent_controlled !== null && branch.parent_controlled === true) { - parentcontrolled = true - } + var parentcontrolled = false + if (branch.parent_controlled !== undefined && branch.parent_controlled !== null && branch.parent_controlled === true) { + parentcontrolled = true + } edge.data = { id: branch.id, @@ -8511,7 +8549,7 @@ const releaseToConnectLabel = "Release to Connect" conditions: conditions, hasErrors: branch.has_errors, decorator: false, - parent_controlled: parentcontrolled, + parent_controlled: parentcontrolled, } // This is an attempt at prettier edges. The numbers are weird to work with. @@ -8595,35 +8633,35 @@ const releaseToConnectLabel = "Release to Connect" insertedNodes = insertedNodes.concat(newedges); setWorkflow(inputworkflow); - // Reset view for cytoscape - if (cy !== undefined && cy !== null) { - cy.add(insertedNodes); - cy.fit(null, 400); - } else { - setElements(insertedNodes); - } + // Reset view for cytoscape + if (cy !== undefined && cy !== null) { + cy.add(insertedNodes); + cy.fit(null, 400); + } else { + setElements(insertedNodes); + } - const additionalNodes = inputworkflow.actions.map((action) => { - // Looking for: el.data("name") != "switch" - if (action.name !== "switch") { - return null - } + const additionalNodes = inputworkflow.actions.map((action) => { + // Looking for: el.data("name") != "switch" + if (action.name !== "switch") { + return null + } - addConditionDraggers({ - target: { - // Run data() function - data: function() { - return action - }, - position: function() { - return action.position - } - } - }, - insertedNodes, - inputworkflow.branches, - ) - }) + addConditionDraggers({ + target: { + // Run data() function + data: function () { + return action + }, + position: function () { + return action.position + } + } + }, + insertedNodes, + inputworkflow.branches, + ) + }) } const removeNode = (nodeId) => { @@ -8681,11 +8719,11 @@ const releaseToConnectLabel = "Release to Connect" if (selectedNode.data().decorator === true && selectedNode.data("type") !== "COMMENT") { toast("This node can't be deleted."); } else { - selectedNode.remove(); + selectedNode.remove(); - setSelectedTrigger({}) - setSelectedEdge({}) - setSelectedAction({}) + setSelectedTrigger({}) + setSelectedEdge({}) + setSelectedAction({}) } // An attempt at NOT unselecting when removing @@ -8716,55 +8754,55 @@ const releaseToConnectLabel = "Release to Connect" } const fetchRecommendations = (inputWorkflow) => { - console.log("Disabled recommendations as they were too inaccurate") - return + console.log("Disabled recommendations as they were too inaccurate") + return - const parsedWorkflow = JSON.parse(JSON.stringify(inputWorkflow)) + const parsedWorkflow = JSON.parse(JSON.stringify(inputWorkflow)) fetch(globalUrl + "/api/v1/workflows/recommend", { method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - body: JSON.stringify(parsedWorkflow), - credentials: "include", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(parsedWorkflow), + credentials: "include", }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 for usecases"); - } + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for usecases"); + } - return response.json(); - }) - .then((responseJson) => { - if (responseJson.success !== false) { - //console.log("recommendations: ", responseJson); + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success !== false) { + //console.log("recommendations: ", responseJson); - if (responseJson.actions !== undefined && responseJson.actions !== null) { - console.log("Got recommendations: ", responseJson.actions) + if (responseJson.actions !== undefined && responseJson.actions !== null) { + console.log("Got recommendations: ", responseJson.actions) - //if (cy !== undefined && cy !== null) { - // cy.removeListener("mouseover", "node"); - //} + //if (cy !== undefined && cy !== null) { + // cy.removeListener("mouseover", "node"); + //} - setWorkflowRecommendations(responseJson.actions) + setWorkflowRecommendations(responseJson.actions) - //if (cy !== undefined && cy !== null) { - // cy.on("mouseover", "node", (e) => onNodeHover(e)); - //} - } else { - setWorkflowRecommendations([]) - } - } else { - setWorkflowRecommendations([]) - } - }) - .catch((error) => { - //toast("ERROR: " + error.toString()); - setWorkflowRecommendations([]) - console.log("ERROR getting usecases: " + error.toString()); - }) + //if (cy !== undefined && cy !== null) { + // cy.on("mouseover", "node", (e) => onNodeHover(e)); + //} + } else { + setWorkflowRecommendations([]) + } + } else { + setWorkflowRecommendations([]) + } + }) + .catch((error) => { + //toast("ERROR: " + error.toString()); + setWorkflowRecommendations([]) + console.log("ERROR getting usecases: " + error.toString()); + }) } const fetchUsecases = () => { @@ -8795,81 +8833,91 @@ const releaseToConnectLabel = "Release to Connect" }) } - const getRevisionHistory = (workflow_id) => { - fetch(`${globalUrl}/api/v1/workflows/${workflow_id}/revisions`, { - method: "GET", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - credentials: "include", - }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 for workflows :O!"); - } - - // Read text from stream - //return response.text(); - return response.json(); - }) - .then((responseJson) => { - if (responseJson === null) { - //console.log("No revisions found") - return - } - - if (responseJson.success === false) { - console.log("Error getting workflow revisions: ", responseJson) - return - } - - setAllRevisions(responseJson) - setSelectedVersion(responseJson[0]) - }) - .catch((error) => { - console.log("Error getting workflow revisions: ", error) - }); + const getRevisionHistory = (workflow_id, revisionCount = 50, turn = 0, orgId = "") => { + let headers = { + "Content-Type": "application/json", + Accept: "application/json", } - const loadTriggers = () => { - const url = `${globalUrl}/api/v1/triggers` - fetch(url, - { - method: "GET", - headers: { "content-type": "application/json" }, - credentials: "include", + if (orgId !== "") { + headers["Org-Id"] = orgId + } + + fetch(`${globalUrl}/api/v1/workflows/${workflow_id}/revisions?count=${revisionCount}`, { + method: "GET", + headers: headers, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for workflows :O!"); } - ) - .then((response) => { - if (response.status !== 200) { - throw new Error("No folders :o!"); - } - return response.json(); - }) - .then((responseJson) => { - if (responseJson.success !== false) { - setAllTriggers(responseJson) - } - }) - .catch((error) => { - console.log("Get outlook folders error: ", error.toString()); - }); - } + // Read text from stream + //return response.text(); + return response.json(); + }) + .then((responseJson) => { + if (responseJson === null) { + //console.log("No revisions found") + return + } + + if (responseJson.success === false) { + console.log("Error getting workflow revisions: ", responseJson) + return + } + + setAllRevisions(responseJson) + setSelectedVersion(responseJson[0]) + }) + .catch((error) => { + console.log("Error getting workflow revisions: ", error); + ++turn; + if (turn < 2) { + getRevisionHistory(workflow_id, 5, turn, orgId); + } + }); + } + + const loadTriggers = () => { + const url = `${globalUrl}/api/v1/triggers` + fetch(url, + { + method: "GET", + headers: { "content-type": "application/json" }, + credentials: "include", + } + ) + .then((response) => { + if (response.status !== 200) { + throw new Error("No folders :o!"); + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success !== false) { + setAllTriggers(responseJson) + } + }) + .catch((error) => { + console.log("Get outlook folders error: ", error.toString()); + }); + } // eslint-disable-next-line react-hooks/exhaustive-deps //useEffect(() => { if (firstrequest) { setFirstrequest(false); getWorkflow(props.match.params.key, {}); - getChildWorkflows(props.match.params.key) - getRevisionHistory(props.match.params.key) - loadTriggers() + getChildWorkflows(props.match.params.key) + getRevisionHistory(props.match.params.key) + loadTriggers() getApps() fetchUsecases() - setLeftSideBarOpenByClick(false) + setLeftSideBarOpenByClick(false) localStorage.setItem("expandLeftNav", false) const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; @@ -8912,7 +8960,7 @@ const releaseToConnectLabel = "Release to Connect" console.log("In graph setup") // 2nd load - configures cytoscape - //} else if (!established && cy !== undefined && ((apps !== null && apps !== undefined && apps.length > 0) || workflow.public === true) && Object.getOwnPropertyNames(workflow).length > 0 && appAuthentication !== undefined && workflowRecommendations !== undefined) { + //} else if (!established && cy !== undefined && ((apps !== null && apps !== undefined && apps.length > 0) || workflow.public === true) && Object.getOwnPropertyNames(workflow).length > 0 && appAuthentication !== undefined && workflowRecommendations !== undefined) { } else if (!established && cy !== undefined && ((apps !== null && apps !== undefined && apps.length > 0) || workflow.public === true) && Object.getOwnPropertyNames(workflow).length > 0 && appAuthentication !== undefined) { //This part has to load LAST, as it's kind of not async. @@ -8948,10 +8996,10 @@ const releaseToConnectLabel = "Release to Connect" if (cy.edgehandles !== undefined) { cy.edgehandles({ handleNodes: (el) => { - // Check of length of el.data() is 1 - if (el.data() === undefined || Object.keys(el.data()).length === 1) { - return false - } + // Check of length of el.data() is 1 + if (el.data() === undefined || Object.keys(el.data()).length === 1) { + return false + } if (el.isNode() && el.data("buttonType") != "ACTIONSUGGESTION" && @@ -9004,9 +9052,9 @@ const releaseToConnectLabel = "Release to Connect" } }); - cy.on('grab', 'edge', (e) => { - console.log("Edge grabbed: ", e.target.data()) - }) + cy.on('grab', 'edge', (e) => { + console.log("Edge grabbed: ", e.target.data()) + }) cy.on("select", "node", (e) => { onNodeSelect(e, appAuthentication); @@ -9033,7 +9081,7 @@ const releaseToConnectLabel = "Release to Connect" document.title = "Workflow - " + workflow.name; - startWorkflowStream(props.match.params.key); + startWorkflowStream(props.match.params.key); registerKeys(); } @@ -9084,7 +9132,7 @@ const releaseToConnectLabel = "Release to Connect" toast("Error: name can't be empty"); return; } - + var mappedStartnode = ""; const alledges = cy.edges().jsons(); if (alledges !== undefined && alledges !== null && alledges.length > 0) { @@ -9113,7 +9161,7 @@ const releaseToConnectLabel = "Release to Connect" if (response.status !== 200) { console.log("Status not 200 for stream results :O!"); } - + return response.json(); }) .then((responseJson) => { @@ -9123,21 +9171,22 @@ const releaseToConnectLabel = "Release to Connect" if (data.type === "create") { toast("Pipeline will be created!"); } else if (data.type === "stop") { - toast("Pipeline will be stopped!"); + toast("Pipeline will be stopped!"); } else { toast("Pipeline deleted!") return } - if (trigger.parameters){ - trigger.parameters.push({ - name: data.name, - value: data.command, - });} - + if (trigger.parameters) { + trigger.parameters.push({ + name: data.name, + value: data.command, + }); + } + if (data.type === "stop") trigger.status = "stopped"; else trigger.status = "running"; workflow.triggers[triggerindex] = trigger; - + setSelectedTrigger(trigger); setWorkflow(workflow); saveWorkflow(workflow); @@ -9147,27 +9196,27 @@ const releaseToConnectLabel = "Release to Connect" console.log("Get pipeline error: ", error.toString()); }); }; - + const submitSchedule = (trigger, triggerindex) => { if (trigger.name.length <= 0) { toast("Error: name can't be empty"); return; } - var mappedStartnode = "" - const alledges = cy.edges().jsons() + var mappedStartnode = "" + const alledges = cy.edges().jsons() if (alledges !== undefined && alledges !== null && alledges.length > 0) { - for (let edgekey in alledges) { - const tmp = alledges[edgekey] - if (tmp.data.source === trigger.id) { - mappedStartnode = tmp.data.target - break - } - } - } + for (let edgekey in alledges) { + const tmp = alledges[edgekey] + if (tmp.data.source === trigger.id) { + mappedStartnode = tmp.data.target + break + } + } + } - toast("Creating schedule") - var data = { + toast("Creating schedule") + var data = { name: trigger.name, frequency: workflow.triggers[triggerindex].parameters[0].value, execution_argument: workflow.triggers[triggerindex].parameters[1].value, @@ -9176,17 +9225,17 @@ const releaseToConnectLabel = "Release to Connect" start: mappedStartnode, } - if (data.frequency === undefined || data.frequency === null || data.frequency.length === 0) { - if (isCloud || selectedTrigger?.environment === "cloud") { - data.frequency = "*/25 * * * *" - workflow.triggers[triggerindex].parameters[0].value = "*/25 * * * *" - } else { - data.frequency = "60" - workflow.triggers[triggerindex].parameters[0].value = "60" - } + if (data.frequency === undefined || data.frequency === null || data.frequency.length === 0) { + if (isCloud || selectedTrigger?.environment === "cloud") { + data.frequency = "*/25 * * * *" + workflow.triggers[triggerindex].parameters[0].value = "*/25 * * * *" + } else { + data.frequency = "60" + workflow.triggers[triggerindex].parameters[0].value = "60" + } - setWorkflow(workflow) - } + setWorkflow(workflow) + } fetch( `${globalUrl}/api/v1/workflows/${props.match.params.key}/schedule`, @@ -9227,7 +9276,7 @@ const releaseToConnectLabel = "Release to Connect" const getSigmaInfo = () => { const url = globalUrl + "/api/v1/files/detection/sigma_rules"; - + fetch(url, { method: "GET", credentials: "include", @@ -9241,7 +9290,7 @@ const releaseToConnectLabel = "Release to Connect" toast("Failed to get sigma rules"); } else { setRules(responseJson.sigma_info); - + } }) ) @@ -9251,7 +9300,7 @@ const releaseToConnectLabel = "Release to Connect" }); }; - const parsedHeight = isMobile ? bodyHeight - appBarSize * 4 : bodyHeight - 57 + const parsedHeight = isMobile ? bodyHeight - appBarSize * 4 : bodyHeight - 57 const appViewStyle = { marginLeft: 5, marginRight: 5, @@ -9288,7 +9337,7 @@ const releaseToConnectLabel = "Release to Connect" } const VariableItem = (props) => { - const { variable, index, type } = props; + const { variable, index, type } = props; const [open, setOpen] = React.useState(false); const [anchorEl, setAnchorEl] = React.useState(null); @@ -9300,144 +9349,144 @@ const releaseToConnectLabel = "Release to Connect" const deleteVariable = (type, variableIndex) => { - console.log("Delete type: ", type, variableIndex) + console.log("Delete type: ", type, variableIndex) - if (type === "normal") { - if (workflow.workflow_variables !== undefined && workflow.workflow_variables !== null && workflow.workflow_variables.length > variableIndex) { - var vars = JSON.parse(JSON.stringify(workflow.workflow_variables)) - vars.splice(variableIndex, 1) - workflow.workflow_variables = vars + if (type === "normal") { + if (workflow.workflow_variables !== undefined && workflow.workflow_variables !== null && workflow.workflow_variables.length > variableIndex) { + var vars = JSON.parse(JSON.stringify(workflow.workflow_variables)) + vars.splice(variableIndex, 1) + workflow.workflow_variables = vars - console.log("Workflow after del: ", workflow) + console.log("Workflow after del: ", workflow) - setWorkflow(workflow); - setUpdate(Math.random()); - } - } else if (type === "exec") { - if (workflow.execution_variables !== undefined && workflow.execution_variables !== null && workflow.execution_variables.length > variableIndex) { - var vars = JSON.parse(JSON.stringify(workflow.execution_variables)) - vars.splice(variableIndex, 1) - workflow.execution_variables = vars + setWorkflow(workflow); + setUpdate(Math.random()); + } + } else if (type === "exec") { + if (workflow.execution_variables !== undefined && workflow.execution_variables !== null && workflow.execution_variables.length > variableIndex) { + var vars = JSON.parse(JSON.stringify(workflow.execution_variables)) + vars.splice(variableIndex, 1) + workflow.execution_variables = vars - console.log("Workflow after del: ", workflow) + console.log("Workflow after del: ", workflow) - setWorkflow(workflow); - setUpdate(Math.random()); - } - } + setWorkflow(workflow); + setUpdate(Math.random()); + } + } }; - return ( -
    - { }}> -
    -
    -
    { - setVariableInfo({ - "name": variable.name, - "description": variable.description, - "value": variable.value, - "index": index, - }) + return ( +
    + { }}> +
    +
    +
    { + setVariableInfo({ + "name": variable.name, + "description": variable.description, + "value": variable.value, + "index": index, + }) - if (type === "normal") { - setVariablesModalOpen(true); - } else if (type === "exec") { - setExecutionVariablesModalOpen(true); - } else { - console.log("Unknown type: ", type) - } - }} - > - {variable.name} -
    -
    - - - - { - setOpen(false); - setAnchorEl(null); - }} - > - { - setOpen(false); - setVariableInfo({ - "name": variable.name, - "description": variable.description, - "value": variable.value, - "index": index, - }) + if (type === "normal") { + setVariablesModalOpen(true); + } else if (type === "exec") { + setExecutionVariablesModalOpen(true); + } else { + console.log("Unknown type: ", type) + } + }} + > + {variable.name} +
    +
    + + + + { + setOpen(false); + setAnchorEl(null); + }} + > + { + setOpen(false); + setVariableInfo({ + "name": variable.name, + "description": variable.description, + "value": variable.value, + "index": index, + }) - if (type === "normal") { - setVariablesModalOpen(true); - } else if (type === "exec") { - setExecutionVariablesModalOpen(true); - } else { - console.log("Unknown type: ", type) - } - }} - key={"Edit"} - > - {"Edit"} - - { - deleteVariable(type, index); - setOpen(false); - }} - key={"Delete"} - > - {"Delete"} - - -
    -
    - -
    - ) - } + if (type === "normal") { + setVariablesModalOpen(true); + } else if (type === "exec") { + setExecutionVariablesModalOpen(true); + } else { + console.log("Unknown type: ", type) + } + }} + key={"Edit"} + > + {"Edit"} + + { + deleteVariable(type, index); + setOpen(false); + }} + key={"Delete"} + > + {"Delete"} + + +
    +
    + +
    + ) + } const VariablesView = () => { const variableScrollStyle = { @@ -9465,13 +9514,13 @@ const releaseToConnectLabel = "Release to Connect" ? null : workflow.workflow_variables.map((variable, varindex) => { return ( - + ); - })} + })}
    - + + - const executionArgumentModal = - { }} > - + { + e.preventDefault(); + setExecutionArgumentModalOpen(false) + }} > - { - e.preventDefault(); - setExecutionArgumentModalOpen(false) - }} - > - - - - - Provide an execution argument - - + + + + + Provide an execution argument + + - {workflow.input_questions !== undefined && workflow.input_questions !== null && workflow.input_questions.length > 0 ? -
    - {workflow.input_questions.map((question, index) => { + {workflow.input_questions !== undefined && workflow.input_questions !== null && workflow.input_questions.length > 0 ? +
    + {workflow.input_questions.map((question, index) => { - return ( -
    - {question.name} - { - var newtext = {} - if (executionText.length > 0) { - try { - newtext = JSON.parse(executionText) - // Check if list or object, then make it object only - if (Array.isArray(newtext)) { - newtext = {} - } - } catch (e) { - console.log("Error parsing JSON: ", e) - } - } + return ( +
    + {question.name} + { + var newtext = {} + if (executionText.length > 0) { + try { + newtext = JSON.parse(executionText) + // Check if list or object, then make it object only + if (Array.isArray(newtext)) { + newtext = {} + } + } catch (e) { + console.log("Error parsing JSON: ", e) + } + } - newtext[question.value] = e.target.value - setExecutionText(JSON.stringify(newtext)) - }} - /> -
    - ) - })} + newtext[question.value] = e.target.value + setExecutionText(JSON.stringify(newtext)) + }} + /> +
    + ) + })} - -
    - : -
    - - At least one node in this workflow requires an execution argument ($exec). Please select one below, or provide a custom one in the text field next to the run button. - + +
    + : +
    + + At least one node in this workflow requires an execution argument ($exec). Please select one below, or provide a custom one in the text field next to the run button. + - - {availableArguments.length > 0 ? -
    - - Previously used arguments: - - {availableArguments.map((data) => { - return ( - { - setExecutionText(data) - executeWorkflow(data, workflow.start, lastSaved); + + {availableArguments.length > 0 ? +
    + + Previously used arguments: + + {availableArguments.map((data) => { + return ( + { + setExecutionText(data) + executeWorkflow(data, workflow.start, lastSaved); - setExecutionArgumentModalOpen(false) - }} - > -
    - - {data} - - - ) - })} -
    - : null} + setExecutionArgumentModalOpen(false) + }} + > +
    + + {data} + + + ) + })} +
    + : null} - -
    - } - + +
    + } +
    const submitQueryModal = () => { - const changeActionTextfield = document.getElementById("change-action-textfield") - if (changeActionTextfield === undefined || changeActionTextfield === null) { - setAiQueryModalOpen(false) - toast.error("Failed to find textfield") - return - } + const changeActionTextfield = document.getElementById("change-action-textfield") + if (changeActionTextfield === undefined || changeActionTextfield === null) { + setAiQueryModalOpen(false) + toast.error("Failed to find textfield") + return + } - if (changeActionTextfield.value === undefined || changeActionTextfield.value === null || changeActionTextfield.value === "") { - toast("Please provide how you want formatting to happen") - return - } + if (changeActionTextfield.value === undefined || changeActionTextfield.value === null || changeActionTextfield.value === "") { + toast("Please provide how you want formatting to happen") + return + } - setAutocompleting(true) - if (codeEditorModalOpen === true) { - autoFormatCodemodal(changeActionTextfield.value) - } else { - aiSubmit(changeActionTextfield.value, undefined, undefined, selectedAction) - } + setAutocompleting(true) + if (codeEditorModalOpen === true) { + autoFormatCodemodal(changeActionTextfield.value) + } else { + aiSubmit(changeActionTextfield.value, undefined, undefined, selectedAction) + } } - const autoFormatCodemodal = (input) => { - if (codeEditorModalOpen !== true) { - toast.error("Code editor is not open") - return - } + const autoFormatCodemodal = (input) => { + if (codeEditorModalOpen !== true) { + toast.error("Code editor is not open") + return + } - if (editorData.name === undefined || editorData.name === null || editorData.name === "") { - toast.error("Failed to find editor field name") - return - } + if (editorData.name === undefined || editorData.name === null || editorData.name === "") { + toast.error("Failed to find editor field name") + return + } - const codeeditor = document.getElementById("shuffle-codeeditor") - if (codeeditor === undefined || codeeditor === null) { - toast.error("Failed to find code editor html") - return - } + const codeeditor = document.getElementById("shuffle-codeeditor") + if (codeeditor === undefined || codeeditor === null) { + toast.error("Failed to find code editor html") + return + } - const editorInstance = window?.ace?.edit("shuffle-codeeditor") - if (editorInstance === undefined || editorInstance === null) { - toast.error("Failed to find code editor instance") - return - } + const editorInstance = window?.ace?.edit("shuffle-codeeditor") + if (editorInstance === undefined || editorInstance === null) { + toast.error("Failed to find code editor instance") + return + } - //console.log("ACE data: ", editorInstance.getValue()) - //editorInstance.setValue("HELO") + //console.log("ACE data: ", editorInstance.getValue()) + //editorInstance.setValue("HELO") - // Should try to automatically fix this input - console.log("Running AI input fixer: ", selectedResult) - if (aiSubmit === undefined || selectedAction === undefined) { - toast.error("Failed to find AI submit function") - return - } - - // Should remove params from selectedAction that aren't parameterName - var tmpAction = JSON.parse(JSON.stringify(selectedAction)) - var tmpParams = tmpAction.parameters.filter((param) => param.name === editorData.name) - if (tmpParams.length !== 1) { - toast.error("Failed to find correct parameter in action") - return - } + // Should try to automatically fix this input + console.log("Running AI input fixer: ", selectedResult) + if (aiSubmit === undefined || selectedAction === undefined) { + toast.error("Failed to find AI submit function") + return + } - tmpParams[0].value = editorInstance.getValue() - tmpAction.parameters = tmpParams - aiSubmit(input, undefined, undefined, tmpAction) - } + // Should remove params from selectedAction that aren't parameterName + var tmpAction = JSON.parse(JSON.stringify(selectedAction)) + var tmpParams = tmpAction.parameters.filter((param) => param.name === editorData.name) + if (tmpParams.length !== 1) { + toast.error("Failed to find correct parameter in action") + return + } - const aiQueryModal = + tmpParams[0].value = editorInstance.getValue() + tmpAction.parameters = tmpParams + aiSubmit(input, undefined, undefined, tmpAction) + } + + const aiQueryModal = { - setAiQueryModalOpen(false) + setAiQueryModalOpen(false) }} > - + { + }} > - { - }} - > - - - - { - setAiQueryModalOpen(false) - }} - > - - - - Shuffle AI - - - What you write here will be fed to the Shuffle AI to generate a change for the selected action or field. Best used for when you are stuck with formatting. Uses your AI credits (resets monthly). Alpha feature. Please give feedback to support@shuffler.io {"<"}3 + + + + { + setAiQueryModalOpen(false) + }} + > + + + + Shuffle AI + + + What you write here will be fed to the Shuffle AI to generate a change for the selected action or field. Best used for when you are stuck with formatting. Uses your AI credits (resets monthly). Alpha feature. Please give feedback to support@shuffler.io {"<"}3 - - - - - ), - onKeyPress: (e) => { - if (e.key === "Enter" && !e.shiftKey) { - submitQueryModal() - } - }, - }} + + + + + ), + onKeyPress: (e) => { + if (e.key === "Enter" && !e.shiftKey) { + submitQueryModal() + } + }, + }} - /> + /> @@ -12431,8 +12480,8 @@ const releaseToConnectLabel = "Release to Connect" minWidth: isMobile ? "90%" : 800, border: theme.palette.defaultBorder, - borderRadius: theme.palette.borderRadius, - backgroundColor: "black", + borderRadius: theme.palette.borderRadius, + backgroundColor: "black", }, }} onClose={() => { @@ -12444,7 +12493,7 @@ const releaseToConnectLabel = "Release to Connect" bottom: 10, left: 10, color: "rgba(255,255,255,0.6)", - zIndex: 10000, + zIndex: 10000, }} > Conditions can't be used for loops [ .# ]{" "} @@ -12452,10 +12501,10 @@ const releaseToConnectLabel = "Release to Connect" rel="noopener noreferrer" target="_blank" href="/docs/workflows#conditions" - style={{ - textDecoration: "none", - color: "#FF8544", - }} + style={{ + textDecoration: "none", + color: "#FF8544", + }} > Learn more @@ -12923,25 +12972,25 @@ const releaseToConnectLabel = "Release to Connect" }); } - // Startnode = dest node - const conditionsDisabled = false + // Startnode = dest node + const conditionsDisabled = false const conditionId = uuidv4(); return (
    - +
    +

    + Conditions +

    + + What are conditions? + +
    - {/* Check if dest is the same as start */} - {conditionsDisabled ? - - Conditions are unavailable between triggers and the startnode. - - : null} + {/* Check if dest is the same as start */} + {conditionsDisabled ? + + Conditions are unavailable between triggers and the startnode. + + : null} -
    - - - {/* + {/* -
    + +
    ); }; - const handleWorkflowSelectionUpdate = (e, isUserinput) => { + const handleWorkflowSelectionUpdate = (e, isUserinput) => { - if (e.target.value === undefined || e.target.value === null || e.target.value.id === undefined) { - console.log("Returning as there's no id. Value: ", e.target.value); - return null - } + if (e.target.value === undefined || e.target.value === null || e.target.value.id === undefined) { + console.log("Returning as there's no id. Value: ", e.target.value); + return null + } - const paramIndex = isUserinput === true ? 5 : 0 + const paramIndex = isUserinput === true ? 5 : 0 - console.log("USERINPUT: ", paramIndex, workflow.triggers[selectedTriggerIndex]) - if (workflow.triggers[selectedTriggerIndex].parameters[paramIndex] === undefined || workflow.triggers[selectedTriggerIndex].parameters[paramIndex] === null) { - workflow.triggers[selectedTriggerIndex].parameters[paramIndex] = { - "name": "subflow", - "value": "", - } - } + console.log("USERINPUT: ", paramIndex, workflow.triggers[selectedTriggerIndex]) + if (workflow.triggers[selectedTriggerIndex].parameters[paramIndex] === undefined || workflow.triggers[selectedTriggerIndex].parameters[paramIndex] === null) { + workflow.triggers[selectedTriggerIndex].parameters[paramIndex] = { + "name": "subflow", + "value": "", + } + } - setUpdate(Math.random()); - workflow.triggers[selectedTriggerIndex].parameters[paramIndex].value = e.target.value.id; - setSubworkflow(e.target.value); + setUpdate(Math.random()); + workflow.triggers[selectedTriggerIndex].parameters[paramIndex].value = e.target.value.id; + setSubworkflow(e.target.value); - // Sets the startnode - if (e.target.value.id !== workflow.id && e.target.value.id.length > 0 ) { + // Sets the startnode + if (e.target.value.id !== workflow.id && e.target.value.id.length > 0) { - const startnode = e?.target?.value?.actions?.find((action) => action.id === e.target.value.start); - + const startnode = e?.target?.value?.actions?.find((action) => action.id === e.target.value.start); - if (startnode !== undefined && startnode !== null) { - setSubworkflowStartnode(startnode); - if (paramIndex === 0) { - try { - workflow.triggers[selectedTriggerIndex].parameters[3].value = startnode.id; - } catch { - workflow.triggers[selectedTriggerIndex].parameters[3] = { - name: "startnode", - value: startnode.id, - }; - } - } + if (startnode !== undefined && startnode !== null) { + setSubworkflowStartnode(startnode); - //setWorkflow(workflow); - } - } else { - console.log("WORKFLOW: ", workflow); - } + if (paramIndex === 0) { + try { + workflow.triggers[selectedTriggerIndex].parameters[3].value = startnode.id; + } catch { + workflow.triggers[selectedTriggerIndex].parameters[3] = { + name: "startnode", + value: startnode.id, + }; + } + } - setWorkflow(workflow); - } + //setWorkflow(workflow); + } + } else { + console.log("WORKFLOW: ", workflow); + } + + setWorkflow(workflow); + } // Function to transform the data const transformAuthData = (authData) => { const transformedData = {}; @@ -13170,7 +13219,7 @@ const releaseToConnectLabel = "Release to Connect" }) appIdsInWorkflow = [...new Set(appIdsInWorkflow)]; - + // loop through the authData and create transformedData which looks like: // appId: [auth1, auth2, ...] authData.forEach((auth) => { @@ -13189,7 +13238,7 @@ const releaseToConnectLabel = "Release to Connect" }); return transformedData; - + }; const AppAuthSelector = ({ appAuthData }) => { @@ -13214,7 +13263,7 @@ const releaseToConnectLabel = "Release to Connect" if (mappingWithName[appName] !== undefined) { return mappingWithName[appName]; } - + return "no-overrides"; } @@ -13223,10 +13272,10 @@ const releaseToConnectLabel = "Release to Connect" if (authId === "no-override") { // remove the override parameter - let oldValue = workflow.triggers[selectedTriggerIndex].parameters[5].value; + let oldValue = workflow.triggers[selectedTriggerIndex].parameters[5].value; // replace from appName= to the next ; let newValue = oldValue.replace(new RegExp(appName + "=[^;]*;"), ""); - + workflow.triggers[selectedTriggerIndex].parameters[5].value = newValue setSelectedAuth(""); return @@ -13249,19 +13298,19 @@ const releaseToConnectLabel = "Release to Connect" // return; // } // } - - if (workflow.triggers[selectedTriggerIndex].parameters[5] === undefined || workflow.triggers[selectedTriggerIndex].parameters[5] === null) { - workflow.triggers[selectedTriggerIndex].parameters[5] = { - name: "auth_override", - value: "", - }; - } - - let authGroupValue = workflow.triggers[selectedTriggerIndex].parameters[5].value; - if (authGroupValue === undefined || authGroupValue === null || authGroupValue === "") { + if (workflow.triggers[selectedTriggerIndex].parameters[5] === undefined || workflow.triggers[selectedTriggerIndex].parameters[5] === null) { + workflow.triggers[selectedTriggerIndex].parameters[5] = { + name: "auth_override", + value: "", + }; + } + + let authGroupValue = workflow.triggers[selectedTriggerIndex].parameters[5].value; + + if (authGroupValue === undefined || authGroupValue === null || authGroupValue === "") { workflow.triggers[selectedTriggerIndex].parameters[5].value = appName + "=" + auth.id + ";"; - } else { + } else { // check if the app is already in the list if (authGroupValue.includes(appName)) { let oldValue = workflow.triggers[selectedTriggerIndex].parameters[5].value; @@ -13270,7 +13319,7 @@ const releaseToConnectLabel = "Release to Connect" } else { workflow.triggers[selectedTriggerIndex].parameters[5].value += appName + "=" + auth.id + ";"; } - } + } // workflow.triggers[selectedTriggerIndex].parameters.push({ // name: auth.label + "_" + auth.app.id + "_override", @@ -13280,892 +13329,878 @@ const releaseToConnectLabel = "Release to Connect" } return ( -
    - {Object.entries(transformedAuthData).map(([appId, authList]) => { - if (authList === undefined || authList === null || authList.length < 2) { - return null; - } +
    + {Object.entries(transformedAuthData).map(([appId, authList]) => { + if (authList === undefined || authList === null || authList.length < 2) { + return null; + } - return ( -
    - - handleSelectChange(authList[0].app.name, authList[0].app.id, e)} + className="auth-select" style={{ + width: '100%', + padding: '10px', + fontSize: '16px', + borderRadius: '4px', + border: '1px solid #555', backgroundColor: theme.palette.inputColor, - fontSize: "1.2em", + color: '#E8E8E8', + boxShadow: '0 2px 4px rgba(0, 0, 0, 0.2)', + transition: 'border-color 0.2s, box-shadow 0.2s', }} + onFocus={(e) => e.target.style.borderColor = '#007BFF'} + onBlur={(e) => e.target.style.borderColor = '#555'} > - {auth.label} - - )} - -
    - )})} -
    + + {authList.flatMap((auth) => + + )} + +
    + ) + })} +
    ); }; - const iconStyle = { - marginRight: 15, - }; + const iconStyle = { + marginRight: 15, + }; - - if (Object.getOwnPropertyNames(selectedTrigger)?.length > 0) { - if (workflow.triggers[selectedTriggerIndex] === undefined) { - return null; - } - if ( - workflow.triggers[selectedTriggerIndex].parameters === undefined || - workflow.triggers[selectedTriggerIndex].parameters === null || - workflow.triggers[selectedTriggerIndex].parameters.length === 0 - ) { - workflow.triggers[selectedTriggerIndex].parameters = []; - workflow.triggers[selectedTriggerIndex].parameters[0] = { - name: "workflow", - value: "", - }; - workflow.triggers[selectedTriggerIndex].parameters[1] = { - name: "argument", - value: "", - }; - workflow.triggers[selectedTriggerIndex].parameters[2] = { - name: "user_apikey", - value: "", - }; - workflow.triggers[selectedTriggerIndex].parameters[3] = { - name: "startnode", - value: "", - }; - workflow.triggers[selectedTriggerIndex].parameters[4] = { - name: "check_result", - value: "false", - }; - workflow.triggers[selectedTriggerIndex].parameters[5] = { - name: "auth_override", - value: "", - }; - - /* - // API-key has been replaced by auth key for the execution. - // Parents can now automatically execute children without auth from a user, as long as the subflow in question is owned by the same org and the subflow is actually referencing it during checkin. - console.log("SETTINGS: ", userSettings); - if ( - userSettings !== undefined && - userSettings !== null && - userSettings.apikey !== null && - userSettings.apikey !== undefined && - userSettings.apikey.length > 0 - ) { - workflow.triggers[selectedTriggerIndex].parameters[2] = { - name: "user_apikey", - value: userSettings.apikey, - }; - } - */ - } - - var handleSubflowStartnodeSelection = (e) => { - setSubworkflowStartnode(e.target.value); - - if (e.target.value === null || e.target.value === undefined) { - return - } - - const branchId = uuidv4(); - const newbranch = { - source_id: workflow.triggers[selectedTriggerIndex].id, - destination_id: e.target.value.id, - source: workflow.triggers[selectedTriggerIndex].id, - target: e.target.value.id, - has_errors: false, - id: branchId, - _id: branchId, - label: "Subflow", - decorator: true, - }; - - if (workflow.visual_branches !== undefined) { - if (workflow.visual_branches === null) { - workflow.visual_branches = [newbranch]; - } else if (workflow.visual_branches.length === 0) { - workflow.visual_branches.push(newbranch); - } else { - const foundIndex = workflow.visual_branches.findIndex( - (branch) => branch.source_id === newbranch.source_id - ); - if (foundIndex !== -1) { - const currentEdge = cy.getElementById( - workflow.visual_branches[foundIndex].id - ); - if ( - currentEdge !== undefined && - currentEdge !== null - ) { - currentEdge.remove(); - } - } - - workflow.visual_branches.splice(foundIndex, 1); - workflow.visual_branches.push(newbranch); - } - } - - if (workflow.id === subworkflow.id) { - const cybranch = { - group: "edges", - source: newbranch.source_id, - target: newbranch.destination_id, - id: branchId, - data: newbranch, - }; - - cy.add(cybranch); - } - - console.log("Value to be set: ", e.target.value); - try { - workflow.triggers[ - selectedTriggerIndex - ].parameters[3].value = e.target.value.id; - } catch { - workflow.triggers[selectedTriggerIndex].parameters[3] = - { - name: "startnode", - value: e.target.value.id, - }; - } - - setWorkflow(workflow); - } + if (Object.getOwnPropertyNames(selectedTrigger)?.length > 0) { + if (workflow.triggers[selectedTriggerIndex] === undefined) { + return null; } - const handleMenuClose = () => { - setUpdate(Math.random()); - setMenuPosition(null); - }; + if ( + workflow.triggers[selectedTriggerIndex].parameters === undefined || + workflow.triggers[selectedTriggerIndex].parameters === null || + workflow.triggers[selectedTriggerIndex].parameters.length === 0 + ) { + workflow.triggers[selectedTriggerIndex].parameters = []; + workflow.triggers[selectedTriggerIndex].parameters[0] = { + name: "workflow", + value: "", + }; + workflow.triggers[selectedTriggerIndex].parameters[1] = { + name: "argument", + value: "", + }; + workflow.triggers[selectedTriggerIndex].parameters[2] = { + name: "user_apikey", + value: "", + }; + workflow.triggers[selectedTriggerIndex].parameters[3] = { + name: "startnode", + value: "", + }; + workflow.triggers[selectedTriggerIndex].parameters[4] = { + name: "check_result", + value: "false", + }; + workflow.triggers[selectedTriggerIndex].parameters[5] = { + name: "auth_override", + value: "", + }; - const handleItemClick = (values) => { - console.log("VALUES: ", values) - if (values === undefined || values === null || values.length === 0) { - return; - } + } + var handleSubflowStartnodeSelection = (e) => { + setSubworkflowStartnode(e.target.value); - /* - workflow.triggers[selectedTriggerIndex].parameters[1].value - .trim() - .endsWith("$") - ? values[0].autocomplete - : "$" + values[0].autocomplete; - - for (var key in values) { - if (key === 0 || values[key].autocomplete.length === 0) { - continue; - } - - toComplete += values[key].autocomplete - } - */ - - if (selectedTrigger.name === "Shuffle Workflow") { - const toComplete = workflow?.triggers?.[selectedTriggerIndex]?.parameters?.[1]?.value + "$" + values[0]?.autocomplete - // selectedTrigger.parameters[1].value = toComplete - workflow.triggers[selectedTriggerIndex].parameters[1].value = toComplete - const foundfield = document.getElementById("subflow_exec_field") - if (foundfield !== undefined && foundfield !== null) { - foundfield.value = toComplete - } - setWorkflow(workflow) - } - - setUpdate(Math.random()); - setShowDropdown(false); - setMenuPosition(null); - }; - - const subflowtypes = [ - { - name: "Any", - }, - { - name: "Enrich", - } - ] - - const handleSubflowParamChange = (value) => { - - if (!workflow?.triggers || !workflow.triggers[selectedTriggerIndex]?.parameters) { - console.log("Required workflow properties are undefined") + if (e.target.value === null || e.target.value === undefined) { return } - if (!workflow.triggers[selectedTriggerIndex].parameters[1]) { - workflow.triggers[selectedTriggerIndex].parameters[1] = { - name: "execution_argument", - value: "" + const branchId = uuidv4(); + const newbranch = { + source_id: workflow.triggers[selectedTriggerIndex].id, + destination_id: e.target.value.id, + source: workflow.triggers[selectedTriggerIndex].id, + target: e.target.value.id, + has_errors: false, + id: branchId, + _id: branchId, + label: "Subflow", + decorator: true, + }; + + if (workflow.visual_branches !== undefined) { + if (workflow.visual_branches === null) { + workflow.visual_branches = [newbranch]; + } else if (workflow.visual_branches.length === 0) { + workflow.visual_branches.push(newbranch); + } else { + const foundIndex = workflow.visual_branches.findIndex( + (branch) => branch.source_id === newbranch.source_id + ); + + if (foundIndex !== -1) { + const currentEdge = cy.getElementById( + workflow.visual_branches[foundIndex].id + ); + if ( + currentEdge !== undefined && + currentEdge !== null + ) { + currentEdge.remove(); + } + } + + workflow.visual_branches.splice(foundIndex, 1); + workflow.visual_branches.push(newbranch); } } - workflow.triggers[selectedTriggerIndex].parameters[1].value = value - setWorkflow(workflow) - setLastSaved(false) + if (workflow.id === subworkflow.id) { + const cybranch = { + group: "edges", + source: newbranch.source_id, + target: newbranch.destination_id, + id: branchId, + data: newbranch, + }; + + cy.add(cybranch); + } + + console.log("Value to be set: ", e.target.value); + try { + workflow.triggers[ + selectedTriggerIndex + ].parameters[3].value = e.target.value.id; + } catch { + workflow.triggers[selectedTriggerIndex].parameters[3] = + { + name: "startnode", + value: e.target.value.id, + }; + } + + setWorkflow(workflow); + } + } + + const handleMenuClose = () => { + setUpdate(Math.random()); + setMenuPosition(null); + }; + + const handleItemClick = (values) => { + console.log("VALUES: ", values) + if (values === undefined || values === null || values.length === 0) { + return; } - const SubflowSidebar = Object.getOwnPropertyNames(selectedTrigger).length === 0 || workflow.triggers[selectedTriggerIndex] === undefined || selectedTrigger.trigger_type !== "SUBFLOW" ? null : -
    - -

    - {selectedTrigger.app_name} -

    - - - -
    - - What are subflows? - - { + + if (!workflow?.triggers || !workflow.triggers[selectedTriggerIndex]?.parameters) { + console.log("Required workflow properties are undefined") + return + } + + if (!workflow.triggers[selectedTriggerIndex].parameters[1]) { + workflow.triggers[selectedTriggerIndex].parameters[1] = { + name: "execution_argument", + value: "" + } + } + + workflow.triggers[selectedTriggerIndex].parameters[1].value = value + setWorkflow(workflow) + setLastSaved(false) + } + + const SubflowSidebar = Object.getOwnPropertyNames(selectedTrigger).length === 0 || workflow.triggers[selectedTriggerIndex] === undefined || selectedTrigger.trigger_type !== "SUBFLOW" ? null : +
    + +

    + {selectedTrigger.app_name} +

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

    Comment

    - - What are comments? - +

    Comment

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

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

    - - What are webhooks? - - +

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

    + + What are webhooks? + + +
    Name
    + + {apps !== undefined && apps !== null && apps.length > 0 ? +
    + { + const lowercaseValue = inputValue.toLowerCase() + options = options.filter(x => x.name.replaceAll("_", " ").toLowerCase().includes(lowercaseValue) || x.description.toLowerCase().includes(lowercaseValue)) + + return options + }} + getOptionLabel={(option) => { + if ( + option === undefined || + option === null || + option.name === undefined || + option.name === null + ) { + return null; + } + + const newname = ( + option.name.charAt(0).toUpperCase() + option.name.substring(1) + ).replaceAll("_", " "); + + return newname; + }} + options={sortByKey(apps, "name")} + fullWidth style={{ - marginBottom: "10px", - marginTop: "10px", - height: "1px", - width: "100%", - backgroundColor: "rgb(91, 96, 100)", + backgroundColor: theme.palette.inputColor, + borderRadius: theme.palette?.borderRadius, + }} + onChange={(event, newValue) => { + // Workaround with event lol + if (newValue !== undefined && newValue !== null) { + var parsedvalue = JSON.parse(JSON.stringify(newValue)) + parsedvalue.actions = [] + parsedvalue.authentication = {} + selectedTrigger.app_association = parsedvalue + setUpdate(Math.random()); + } + }} + renderOption={(props, app, state) => { + var appname = app.name.replaceAll("_", " ") + appname = appname.charAt(0).toUpperCase() + appname.substring(1) + + return ( + + { + const newValue = app + + if (newValue !== undefined && newValue !== null) { + var parsedvalue = JSON.parse(JSON.stringify(newValue)) + parsedvalue.actions = [] + parsedvalue.authentication = {} + selectedTrigger.app_association = parsedvalue + selectedTrigger.large_image = app.large_image + + if (cy !== undefined && cy !== null) { + const foundnode = cy.getElementById(selectedTrigger.id) + if (foundnode !== undefined && foundnode !== null) { + foundnode.data("large_image", app.large_image) + } + } + + setUpdate(Math.random()); + } + document.activeElement.blur(); + }} + > +
    + + {appname} + + + {appname} + +
    +
    +
    + ) + }} + renderInput={(params) => { + return ( + + ); }} /> -
    Name
    - + : null} + + {selectedTrigger.status === "running" ? null : +
    + Environment + +
    + } + +
    +
    + Parameters +
    +
    +
    + Webhook URI +
    +
    + { + }} + helperText={ + workflow.triggers[selectedTriggerIndex].parameters[0].value !== undefined && + workflow.triggers[selectedTriggerIndex].parameters[0].value !== null && + (workflow.triggers[ + selectedTriggerIndex + ].parameters[0].value.includes("localhost") || + workflow.triggers[ + selectedTriggerIndex + ].parameters[0].value.includes("127.0.0.1")) ? ( + + PS: This does NOT work with localhost. Use your local IP + instead. + + ) : null + } + InputProps={{ + style: { + }, + endAdornment: + + { + var copyText = document.getElementById("webhook_uri_field"); + if (copyText !== undefined && copyText !== null) { + console.log("NAVIGATOR: ", navigator); + const clipboard = navigator.clipboard; + if (clipboard === undefined) { + toast("Can only copy over HTTPS (port 3443)"); + return; + } + + navigator.clipboard.writeText(copyText.value); + copyText.select(); + copyText.setSelectionRange( + 0, + 99999 + ); /* For mobile devices */ + + /* Copy the text inside the text field */ + document.execCommand("copy"); + toast("Copied Webhook URL"); + } else { + console.log("Couldn't find webhook URI field: ", copyText); + } + }} + edge="end" + > + + + + }} + fullWidth + disabled + value={ + workflow.triggers[selectedTriggerIndex].parameters[0].value + } color="primary" - placeholder={selectedTrigger.label} - onChange={selectedTriggerChange} + placeholder="10" + onBlur={(e) => { + setTriggerCronWrapper(e.target.value); + }} /> - {apps !== undefined && apps !== null && apps.length > 0 ? -
    - { - const lowercaseValue = inputValue.toLowerCase() - options = options.filter(x => x.name.replaceAll("_", " ").toLowerCase().includes(lowercaseValue) || x.description.toLowerCase().includes(lowercaseValue)) - - return options - }} - getOptionLabel={(option) => { - if ( - option === undefined || - option === null || - option.name === undefined || - option.name === null - ) { - return null; - } - - const newname = ( - option.name.charAt(0).toUpperCase() + option.name.substring(1) - ).replaceAll("_", " "); - - return newname; - }} - options={sortByKey(apps, "name")} - fullWidth - style={{ - backgroundColor: theme.palette.inputColor, - height: 50, - borderRadius: theme.palette?.borderRadius, - }} - onChange={(event, newValue) => { - // Workaround with event lol - console.log("CHANGE: ", event, newValue) - if (newValue !== undefined && newValue !== null) { - var parsedvalue = JSON.parse(JSON.stringify(newValue)) - parsedvalue.actions = [] - parsedvalue.authentication = {} - selectedTrigger.app_association = parsedvalue - setUpdate(Math.random()); - } - }} - renderOption={(props, app, state) => { - var appname = app.name.replaceAll("_", " ") - appname = appname.charAt(0).toUpperCase() + appname.substring(1) - - return ( - - { - const newValue = app - - if (newValue !== undefined && newValue !== null) { - var parsedvalue = JSON.parse(JSON.stringify(newValue)) - parsedvalue.actions = [] - parsedvalue.authentication = {} - selectedTrigger.app_association = parsedvalue - selectedTrigger.large_image = app.large_image - - if (cy !== undefined && cy !== null) { - const foundnode = cy.getElementById(selectedTrigger.id) - if (foundnode !== undefined && foundnode !== null) { - foundnode.data("large_image", app.large_image) - } - } - - setUpdate(Math.random()); - } - document.activeElement.blur(); - }} - > -
    - - {appname} - - - {appname} - -
    -
    -
    - ) - }} - renderInput={(params) => { - return ( - - ); - }} - /> - -
    - : null} - {selectedTrigger.status === "running" ? null : -
    - Environment - -
    - } +
    + + +
    -
    -
    - Parameters -
    -
    -
    - Webhook URI -
    -
    - { - }} - helperText={ - workflow.triggers[selectedTriggerIndex].parameters[0].value !== undefined && - workflow.triggers[selectedTriggerIndex].parameters[0].value !== null && - (workflow.triggers[ - selectedTriggerIndex - ].parameters[0].value.includes("localhost") || - workflow.triggers[ - selectedTriggerIndex - ].parameters[0].value.includes("127.0.0.1")) ? ( - - PS: This does NOT work with localhost. Use your local IP - instead. - - ) : null - } - InputProps={{ - style: { - }, - endAdornment: - - { - var copyText = document.getElementById("webhook_uri_field"); - if (copyText !== undefined && copyText !== null) { - console.log("NAVIGATOR: ", navigator); - const clipboard = navigator.clipboard; - if (clipboard === undefined) { - toast("Can only copy over HTTPS (port 3443)"); - return; - } - - navigator.clipboard.writeText(copyText.value); - copyText.select(); - copyText.setSelectionRange( - 0, - 99999 - ); /* For mobile devices */ - - /* Copy the text inside the text field */ - document.execCommand("copy"); - toast("Copied Webhook URL"); - } else { - console.log("Couldn't find webhook URI field: ", copyText); - } - }} - edge="end" - > - - - - }} - fullWidth - disabled - value={ - workflow.triggers[selectedTriggerIndex].parameters[0].value - } - color="primary" - placeholder="10" - onBlur={(e) => { - setTriggerCronWrapper(e.target.value); - }} - /> -
    - - -
    - -
    -
    -
    - Authentication headers -
    -
    -
    - { }} - InputProps={{ - style: { - }, - }} - fullWidth - multiline - rows="4" - defaultValue={trigger_header_auth} - color="primary" - disabled={selectedTrigger.status === "running"} - placeholder={"AUTH_HEADER=AUTH_VALUE1"} - onBlur={(e) => { - const value = e.target.value; - if (selectedTrigger.parameters === null) { - selectedTrigger.parameters = []; - } - - workflow.triggers[selectedTriggerIndex].parameters[2] = { - value: value, - name: "auth_headers", - }; - setWorkflow(workflow); - }} - /> -
    -
    -
    -
    - Custom Response -
    -
    -
    - { }} - InputProps={{ - style: { - }, - }} - fullWidth - multiline - rows="2" - color="primary" - disabled={selectedTrigger.status === "running"} - placeholder={"OK"} - onBlur={(e) => { - const value = e.target.value; - if (selectedTrigger.parameters === null) { - selectedTrigger.parameters = []; - } - - workflow.triggers[selectedTriggerIndex].parameters[3] = { - value: value, - name: "custom_response_body", - }; - setWorkflow(workflow); - }} - /> -
    - {workflow.triggers[selectedTriggerIndex].parameters.length > 4 ? - - { - if (selectedTrigger.parameters === null) { - selectedTrigger.parameters = []; - } - - // Sets the webhook to run as version 2.. kinda - var value = "v2" - if (workflow.triggers[selectedTriggerIndex].parameters[4].value.includes("v2")) { - value = "v1" - } - - workflow.triggers[selectedTriggerIndex].parameters[4] = { - name: "await_response", - value: value - } - - setWorkflow(workflow) - setUpdate(Math.random()) - }} - color="primary" - value="await_response" - /> - } - label={
    Wait For Response
    } - /> -
    - : null} -
    +
    +
    +
    + Authentication headers +
    +
    + { }} + InputProps={{ + style: { + }, + }} + fullWidth + multiline + rows="4" + defaultValue={trigger_header_auth} + color="primary" + disabled={selectedTrigger.status === "running"} + placeholder={"AUTH_HEADER=AUTH_VALUE1"} + onBlur={(e) => { + const value = e.target.value; + if (selectedTrigger.parameters === null) { + selectedTrigger.parameters = []; + } + + workflow.triggers[selectedTriggerIndex].parameters[2] = { + value: value, + name: "auth_headers", + }; + setWorkflow(workflow); + }} + /> +
    +
    +
    +
    + Custom Response +
    +
    +
    + { }} + InputProps={{ + style: { + }, + }} + fullWidth + multiline + rows="2" + color="primary" + disabled={selectedTrigger.status === "running"} + placeholder={"OK"} + onBlur={(e) => { + const value = e.target.value; + if (selectedTrigger.parameters === null) { + selectedTrigger.parameters = []; + } + + workflow.triggers[selectedTriggerIndex].parameters[3] = { + value: value, + name: "custom_response_body", + }; + setWorkflow(workflow); + }} + /> +
    + {workflow.triggers[selectedTriggerIndex].parameters.length > 4 ? + + { + if (selectedTrigger.parameters === null) { + selectedTrigger.parameters = []; + } + + // Sets the webhook to run as version 2.. kinda + var value = "v2" + if (workflow.triggers[selectedTriggerIndex].parameters[4].value.includes("v2")) { + value = "v1" + } + + workflow.triggers[selectedTriggerIndex].parameters[4] = { + name: "await_response", + value: value + } + + setWorkflow(workflow) + setUpdate(Math.random()) + }} + color="primary" + value="await_response" + /> + } + label={
    Wait For Response
    } + /> +
    + : null}
    +
    +
    const stopMailSub = (trigger, triggerindex) => { // DELETE @@ -15310,7 +15344,7 @@ const releaseToConnectLabel = "Release to Connect" } - // Version: v2 = await response for 30 sec + // Version: v2 = await response for 30 sec const await_resp = trigger.parameters.find((param) => param.name === "await_response"); var version = ""; if (await_resp !== undefined && await_resp !== null) { @@ -15334,11 +15368,11 @@ const releaseToConnectLabel = "Release to Connect" environment: trigger.environment, auth: auth, custom_response: custom_response, - version: version, - version_timeout: 15, + version: version, + version_timeout: 15, }; - console.log("Trigger data: ", data) + console.log("Trigger data: ", data) fetch(globalUrl + "/api/v1/hooks/new", { method: "POST", @@ -15370,7 +15404,7 @@ const releaseToConnectLabel = "Release to Connect" if (trigger.id === undefined) { return; } - + fetch(globalUrl + "/api/v1/hooks/" + trigger.id + "/delete", { method: "DELETE", headers: { @@ -15383,7 +15417,7 @@ const releaseToConnectLabel = "Release to Connect" if (response.status !== 200) { console.log("Status not 200 for stream results :O!"); } - + return response.json(); }) .then((responseJson) => { @@ -15402,7 +15436,7 @@ const releaseToConnectLabel = "Release to Connect" setWorkflow(workflow); saveWorkflow(workflow); setSelectedTrigger({}) - + }) .catch((error) => { //toast(error.toString()); @@ -15411,76 +15445,76 @@ const releaseToConnectLabel = "Release to Connect" ); }); }; - + // POST to /api/v1/workflows const createWorkflow = (workflow, trigger_index) => { - fetch(globalUrl + "/api/v1/workflows", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(workflow), - credentials: "include", - }) - .then((response) => { - if (response.status === 200) { - getAvailableWorkflows(trigger_index) - } + fetch(globalUrl + "/api/v1/workflows", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(workflow), + credentials: "include", + }) + .then((response) => { + if (response.status === 200) { + getAvailableWorkflows(trigger_index) + } - return response.json(); - }) - .then((responseJson) => { - if (responseJson.id !== undefined && responseJson.id !== null && responseJson.id.length > 0) { - toast("Successfully created workflow"); + return response.json(); + }) + .then((responseJson) => { + if (responseJson.id !== undefined && responseJson.id !== null && responseJson.id.length > 0) { + toast("Successfully created workflow"); - handleWorkflowSelectionUpdate({ target: { value: responseJson } }, true) - } - }) - .catch((error) => { - console.log("Create workflow error: ", error.toString()) - }) + handleWorkflowSelectionUpdate({ target: { value: responseJson } }, true) + } + }) + .catch((error) => { + console.log("Create workflow error: ", error.toString()) + }) } - const UserinputSidebar = !selectedTrigger || Object.getOwnPropertyNames(selectedTrigger)?.length === 0 || !workflow?.triggers || !workflow?.triggers[selectedTriggerIndex] || !selectedTrigger.trigger_type || selectedTrigger.trigger_type !== "USERINPUT" ? null : -
    -

    - {selectedTrigger.app_name} -

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

    + {selectedTrigger.app_name} +

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

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

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

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

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

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

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

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

    + + What are schedules? + + +
    Name
    + +
    + Environment + +
    + +
    +
    + Parameters +
    -
    Name
    + > +
    +
    + When to start: {isCloud || selectedTrigger?.environment === "cloud" ? Cron formatting : "every X second"} +
    +
    { + setTriggerCronWrapper(e.target.value); + }} /> -
    - Environment - + /> +
    + Runtime Argument: +
    + 1 ? workflow.triggers[selectedTriggerIndex].parameters[1].value : "" + } + placeholder='{"key": "value"}' + onBlur={(e) => { + setTriggerBodyWrapper(e.target.value); + }} + /> -
    -
    - Parameters -
    -
    -
    - When to start: {isCloud || selectedTrigger?.environment === "cloud" ? Cron formatting : "every X second"} -
    -
    - { - setTriggerCronWrapper(e.target.value); - }} - /> - {/*selectedTrigger.environment === "cloud" ? - - : - null - */} -
    -
    -
    - Runtime Argument: -
    -
    - 1 ? workflow.triggers[selectedTriggerIndex].parameters[1].value : "" - } - placeholder='{"key": "value"}' - onBlur={(e) => { - setTriggerBodyWrapper(e.target.value); - }} - /> - -
    - - -
    -
    +
    + +
    +
    +
    const cytoscapeViewWidths = isMobile ? 50 : 950; const bottomBarStyle = { @@ -16386,24 +16422,24 @@ const releaseToConnectLabel = "Release to Connect" marginLeft: 20, marginBottom: 30, zIndex: 10, - transform: isMobile - ? `translateX(20px)` - : `translateX(${leftBarSize}px)`, - top: isMobile ? appBarSize + 55 : undefined, + transform: isMobile + ? `translateX(20px)` + : `translateX(${leftBarSize}px)`, + top: isMobile ? appBarSize + 55 : undefined, bottom: isMobile ? undefined : 0, -}; + }; const topBarStyle = { position: "absolute", top: isMobile ? 30 : 25, - transform: isMobile ? "translateX(20px)" : `translateX(${leftBarSize}px)`, + transform: isMobile ? "translateX(20px)" : `translateX(${leftBarSize}px)`, transition: "all 0.3s ease", - zoom: 0.9, + zoom: 0.9, } const TopCytoscapeBar = (props) => { - const [hovered, setHovered] = useState(false) + const [hovered, setHovered] = useState(false) if (workflow.public === true) { return null @@ -16413,37 +16449,37 @@ const releaseToConnectLabel = "Release to Connect" return null } - const isCorrectOrg = workflow.public === true || userdata.active_org.id === undefined || userdata.active_org.id === null || workflow.org_id === null || workflow.org_id === undefined || workflow.org_id.length === 0 || userdata.active_org.id === workflow.org_id + const isCorrectOrg = workflow.public === true || userdata.active_org.id === undefined || userdata.active_org.id === null || workflow.org_id === null || workflow.org_id === undefined || workflow.org_id.length === 0 || userdata.active_org.id === workflow.org_id return (
    -
    - { - setHovered(true) - }} - onMouseLeave={() => { - setHovered(false) - }} - onClick={() => { - setEditWorkflowModalOpen(true) - setLastSaved(false) - }} - > - - {workflow.name} - +
    + { + setHovered(true) + }} + onMouseLeave={() => { + setHovered(false) + }} + onClick={() => { + setEditWorkflowModalOpen(true) + setLastSaved(false) + }} + > + + {workflow.name} + {workflowAsCode && (
    + + {showEnvironment === true && environments.length > 0 && selectedActionEnvironment !== undefined && selectedActionEnvironment !== null && selectedActionEnvironment.Name !== undefined && selectedActionEnvironment.Name !== null ? + - Select an Org + Runtime Location - { + getEnvironments(workflow.org_id) + }} + onChange={(e) => { + setLastSaved(false) + const env = environments.find((a) => a.Name === e.target.value); + setSelectedActionEnvironment(env) + selectedAction.environment = env.Name + setSelectedAction(selectedAction) - navigate(`?org_id=${e.target.value}`) + for (let actionkey in workflow.actions) { + workflow.actions[actionkey].environment = env.Name + } - // Unselect in cy - if (cy !== undefined && cy !== null) { - cy.nodes().unselect() - cy.edges().unselect() - } + setWorkflow(workflow) + //toast.success("Set execution location for ALL actions to " + env.Name) + }} + style={{ + pointerEvents: "auto", + color: "white", + maxWidth: 250, + minWidth: 250, + borderRadius: theme.palette?.borderRadius, + marginLeft: 35, - ReactDOM.unstable_batchedUpdates(() => { - getEnvironments(e.target.value) - getAppAuthentication(undefined, undefined, undefined, e.target.value) - getFiles(e.target.value) - listOrgCache(e.target.value) + backgroundColor: theme.palette.inputColor, + height: 40, + }} + > + {environments.map((data, index) => { + if (data.archived === true) { + return null + } - // FIXME: There is a timing problem here. - // For events to have the data they need, they - // need to be registered with setupGraph() - // AFTER all the APIs are done + const isRunning = data.running_ip !== "" - // Should look through childorg workflow - setTimeout(() => { - if (e.target.value === originalWorkflow.org_id) { - console.log("Original org selected. No change.") + return ( + - updateCurrentWorkflow(originalWorkflow) - return - } else { - // Load environments, auth, auth groups - //toast("Loading correct info for suborg") - } + {data.Name === "cloud" || data.Name === "Cloud" ? null : !isRunning ? - if (originalWorkflow.childorg_workflow_ids === undefined || originalWorkflow.childorg_workflow_ids === null || originalWorkflow.childorg_workflow_ids.length === 0) { - //console.log("Childorg doesn't exist (?). Suborgworkflows: ", suborgWorkflows) + + + { + e.preventDefault() + e.stopPropagation() - if (suborgWorkflows !== undefined && suborgWorkflows !== null && suborgWorkflows.length > 0) { - var found = false - for (var suborgkey in suborgWorkflows) { - const suborgWorkflow = suborgWorkflows[suborgkey] - if (suborgWorkflow.org_id === e.target.value) { - found = true - updateCurrentWorkflow(suborgWorkflow) - break - } - } - - if (!found) { - toast("(3) Creating new workflow for this org. Please wait a second while we duplicate.") - //console.log("No workflow found out of suborg workflows.") - - //saveWorkflow(originalWorkflow, undefined, undefined, e.target.value) - } - } else { - console.log("Suborgworkflows: ", suborgWorkflows) - toast("(1) Loading NEW workflow for this org (?). Please wait a second.") - saveWorkflow(originalWorkflow, undefined, undefined, e.target.value) - } - } else { - console.log("In childorg EXIST!") - - var workflowFound = false - for (var childorgidkey in originalWorkflow.childorg_workflow_ids) { - const childworkflowid = originalWorkflow.childorg_workflow_ids[childorgidkey] - for (var suborgWorkflowKey in suborgWorkflows) { - const suborgWorkflow = suborgWorkflows[suborgWorkflowKey] - if (suborgWorkflow.org_id === e.target.value) { - workflowFound = true - - updateCurrentWorkflow(suborgWorkflow) - break - } - } - - if (workflowFound) { - break - } - } - - if (!workflowFound) { - console.log("No workflow found.") - toast("(2) Creating new workflow for this org. Please wait a few seconds while we prepare it for you.") - //saveWorkflow(originalWorkflow, undefined, undefined, e.target.value) - } - } - }, 500) - }) - }} - label="Suborg Distribution" - fullWidth - > - - Parent: {userdata.active_org.large_image}{" "} - - {userdata.active_org.name} - - - - - - {originalWorkflow.suborg_distribution.map((org_id, index) => { - var data = {} - for (var key in userdata.orgs) { - if (userdata.orgs[key].id === org_id) { - data = userdata.orgs[key] - break - } - } - - if (data.id === undefined || data.id === null) { - //toast("No org found for id: " + org_id) - return null - } - - var skipOrg = false; - - const imagesize = 22 - const imageStyle = { - width: imagesize, - height: imagesize, - pointerEvents: "none", - marginRight: 10, - marginLeft: - data.creator_org !== undefined && - data.creator_org !== null && - data.creator_org.length > 0 - ? data.id === userdata.active_org.id - ? 0 - : 0 - : 0, - } - - const image = - data.image === "" ? ( - {data.name} - ) : ( - {data.name} - ) + window.open(`/admin?tab=locations&env=${data.Name}`, "_blank", "noopener,noreferrer") + }} - return ( - - {image}{" "} - - {data.name} - - - ) - })} - - - } -
    + /> + + + : + { + //handleChipClick + }} + variant="outlined" + color="primary" + /> + } - {showEnvironment === true && environments.length > 0 && selectedActionEnvironment !== undefined && selectedActionEnvironment !== null && selectedActionEnvironment.Name !== undefined && selectedActionEnvironment.Name !== null ? - + {data.default === true ? + + : null} - - Runtime Location - - + + : null} - /> - - - : - { - //handleChipClick - }} - variant="outlined" - color="primary" - /> - } + {parentWorkflows === undefined || parentWorkflows === null || parentWorkflows.length === 0 ? null : + + }
    ); @@ -17119,163 +17155,163 @@ const releaseToConnectLabel = "Release to Connect" }; const BottomAvatars = () => { - const connectedUsers = [{ - "user": "Anonymous", - "user_id": "user_id", - "color": "blue", - }] - + const connectedUsers = [{ + "user": "Anonymous", + "user_id": "user_id", + "color": "blue", + }] - if (connectedUsers === undefined || connectedUsers === null || connectedUsers.length < 2) { - return null - } - const avatarStyle = { - position: "fixed", - display: "flex", - right: isMobile ? 20 : 20, - top: isMobile ? appBarSize-100 : undefined, - bottom: isMobile ? undefined : 0, - left: isMobile ? undefined : leftBarSize, - minWidth: cytoscapeViewWidths, - maxWidth: cytoscapeViewWidths, - marginLeft: 20, - marginBottom: 20, - zIndex: 50, - } + if (connectedUsers === undefined || connectedUsers === null || connectedUsers.length < 2) { + return null + } - const HandleAvatar = (props) => { - const {user} = props - console.log("Clicked avatar: ", user) + const avatarStyle = { + position: "fixed", + display: "flex", + right: isMobile ? 20 : 20, + top: isMobile ? appBarSize - 100 : undefined, + bottom: isMobile ? undefined : 0, + left: isMobile ? undefined : leftBarSize, + minWidth: cytoscapeViewWidths, + maxWidth: cytoscapeViewWidths, + marginLeft: 20, + marginBottom: 20, + zIndex: 50, + } - const userTitle = user.user[0].toUpperCase() - return ( - - - {userTitle} - - - ) - } + const HandleAvatar = (props) => { + const { user } = props + console.log("Clicked avatar: ", user) + const userTitle = user.user[0].toUpperCase() return ( -
    - {connectedUsers.map((user) => { - return ( - - ) - })} -
    - ) + + + {userTitle} + + + ) + } + + return ( +
    + {connectedUsers.map((user) => { + return ( + + ) + })} +
    + ) } - const shownErrors = !distributedFromParent && !isMobile && workflow.errors !== undefined && workflow.errors !== null && workflow.errors.length > 0 && showErrors && (!workflow.public || userdata.support === true) ? -
    0 && showErrors && (!workflow.public || userdata.support === true) ? +
    + color: "white", + padding: 10, + borderRadius: theme.palette?.borderRadius, + transition: "left 0.3s ease, top 0.3s ease", + }} + > - + { + e.preventDefault(); + + // A temporary hider thing + setShowErrors(false) + }} > - { - e.preventDefault(); + + + - // A temporary hider thing - setShowErrors(false) - }} - > - - - + + {/**/} + {workflow.errors.length} Workflow Issue{workflow.errors.length > 1 ? "s" : ""} + + + {workflow.errors.slice(0, 3).map((error) => { + // Loop through each word, and if it matches "Action " then replace it with a link to the action + var colornext = false + const newerror = error === undefined || error == null ? "" : error.split(" ").map((word) => { + if (colornext) { + colornext = false + return ( + { + // Find it in cytoscape + if (cy === undefined || cy === null) { + return + } - - {/**/} - {workflow.errors.length} Workflow Issue{workflow.errors.length > 1 ? "s" : ""} - - - {workflow.errors.slice(0,3).map((error) => { - // Loop through each word, and if it matches "Action " then replace it with a link to the action - var colornext = false - const newerror = error === undefined || error == null ? "" : error.split(" ").map((word) => { - if (colornext) { - colornext = false - return ( - { - // Find it in cytoscape - if (cy === undefined || cy === null) { - return - } + const foundnode = cy.nodes().filter((node) => { + const nodelabel = node.data("label") + if (nodelabel === undefined || nodelabel === null) { + return false + } - const foundnode = cy.nodes().filter((node) => { - const nodelabel = node.data("label") - if (nodelabel === undefined || nodelabel === null) { - return false - } + return nodelabel.toLowerCase() === word.toLowerCase() + }) - return nodelabel.toLowerCase() === word.toLowerCase() - }) + if (foundnode === undefined || foundnode === null || foundnode.length === 0) { + return + } - if (foundnode === undefined || foundnode === null || foundnode.length === 0) { - return - } + cy.elements().unselect() + foundnode[0].select() + }} + > + {word}  + + ) + } - cy.elements().unselect() - foundnode[0].select() - }} - > - {word}  - - ) - } + if (word.toLowerCase() === "action") { + colornext = true + } - if (word.toLowerCase() === "action") { - colornext = true - } + return word + " " + }) - return word + " " - }) + if (newerror === undefined || newerror === null || newerror === "") { + return null + } - if (newerror === undefined || newerror === null || newerror === "") { - return null - } - - return ( -
    - - {newerror} -
    - ) - })} -
    -
    - : null + return ( +
    + - {newerror} +
    + ) + })} + +
    + : null const RightsideBar = () => { - const [hovered, setHovered] = useState(false) + const [hovered, setHovered] = useState(false) useEffect(() => { const handleKeyDown = (event) => { @@ -17303,33 +17339,33 @@ const releaseToConnectLabel = "Release to Connect" } } - if (( event.ctrlKey || event.metaKey ) && event.key === ";") { + if ((event.ctrlKey || event.metaKey) && event.key === ";") { if (!workflow.public && executionModalOpen) { getWorkflowExecution(props.match.params.key, ""); } } - /* - if (( event.ctrlKey || event.metaKey ) && event.shiftKey) { - console.log("Shift key pressed") - if (!workflow.public && executionModalOpen) { - setExecutionRunning(false); - stop() - const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; - const newitem = removeParam("execution_id", cursearch); - navigate(curpath + newitem) - setExecutionModalView(0); - } - } - */ + /* + if (( event.ctrlKey || event.metaKey ) && event.shiftKey) { + console.log("Shift key pressed") + if (!workflow.public && executionModalOpen) { + setExecutionRunning(false); + stop() + const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; + const newitem = removeParam("execution_id", cursearch); + navigate(curpath + newitem) + setExecutionModalView(0); + } + } + */ }; - + document.addEventListener('keydown', handleKeyDown); - + return () => { document.removeEventListener('keydown', handleKeyDown); } - }, [executeWorkflow, executionText, workflow, lastSaved, executionRequestStarted]) + }, [executeWorkflow, executionText, workflow, lastSaved, executionRequestStarted]) useEffect(() => { const handleKeyDown = (event) => { @@ -17356,147 +17392,154 @@ const releaseToConnectLabel = "Release to Connect" } } - if (( event.ctrlKey || event.metaKey ) && event.key === ";") { + if ((event.ctrlKey || event.metaKey) && event.key === ";") { if (!workflow.public && executionModalOpen) { getWorkflowExecution(props.match.params.key, ""); } } - /* - if (( event.ctrlKey || event.metaKey ) && event.shiftKey) { - console.log("Shift key pressed") - if (!workflow.public && executionModalOpen) { - setExecutionRunning(false); - stop() - const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; - const newitem = removeParam("execution_id", cursearch); - navigate(curpath + newitem) - setExecutionModalView(0); - } - } - */ + /* + if (( event.ctrlKey || event.metaKey ) && event.shiftKey) { + console.log("Shift key pressed") + if (!workflow.public && executionModalOpen) { + setExecutionRunning(false); + stop() + const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; + const newitem = removeParam("execution_id", cursearch); + navigate(curpath + newitem) + setExecutionModalView(0); + } + } + */ }; - + document.addEventListener('keydown', handleKeyDown); - + return () => { document.removeEventListener('keydown', handleKeyDown); }; - }, [executeWorkflow, executionText, workflow, lastSaved, executionRequestStarted]); + }, [executeWorkflow, executionText, workflow, lastSaved, executionRequestStarted]); - if (isMobile) { - return null - } + if (isMobile) { + return null + } - return ( -
    setHovered(true)} - onMouseLeave={() => setHovered(false)} - onClick={() => { - setExecutionModalOpen(true); - getWorkflowExecution(props.match.params.key, ""); - }} - > - - - Explore runs - -
    - ) + if (workflow.public === true) { + return null + } + + return ( +
    setHovered(true)} + onMouseLeave={() => setHovered(false)} + onClick={() => { + setExecutionModalOpen(true); + getWorkflowExecution(props.match.params.key, ""); + }} + > + + + Explore runs + +
    + ) } // Used for handling suborg workflow distribution management const updateCurrentWorkflow = (inputworkflow) => { - //setLastSaved(false) - setSelectedAction({}); - setSelectedApp({}) - setWorkflow(inputworkflow) - if (inputworkflow !== undefined && inputworkflow !== null && inputworkflow.id !== undefined && inputworkflow.id !== null) { - getRevisionHistory(inputworkflow.id) - getWorkflowExecution(inputworkflow.id) - } + setCurrentWorkflow(inputworkflow) - // Update props match key - if (inputworkflow.parentorg_workflow !== undefined && inputworkflow.parentorg_workflow !== null && inputworkflow.parentorg_workflow !== "") { - setDistributedFromParent(inputworkflow.parentorg_workflow) - } else { - setDistributedFromParent("") - } + //setLastSaved(false) + setSelectedAction({}); + setSelectedApp({}) + setWorkflow(inputworkflow) - if (cy !== undefined) { - cy.removeListener("select"); - cy.removeListener("unselect"); + if (inputworkflow !== undefined && inputworkflow !== null && inputworkflow.id !== undefined && inputworkflow.id !== null) { + getRevisionHistory(inputworkflow.id, 50, 0, inputworkflow.org_id) + getWorkflowExecution(inputworkflow.id, "", "") + } - cy.removeListener("add"); - cy.removeListener("remove"); + // Update props match key + if (inputworkflow.parentorg_workflow !== undefined && inputworkflow.parentorg_workflow !== null && inputworkflow.parentorg_workflow !== "") { + setDistributedFromParent(inputworkflow.parentorg_workflow) + } else { + setDistributedFromParent("") + } - cy.removeListener("mouseover"); - cy.removeListener("mouseout"); + if (cy !== undefined) { + cy.removeListener("select"); + cy.removeListener("unselect"); - cy.removeListener("drag"); - cy.removeListener("free"); - cy.removeListener("cxttap"); + cy.removeListener("add"); + cy.removeListener("remove"); - setElements([]) + cy.removeListener("mouseover"); + cy.removeListener("mouseout"); - // Remove all edges - cy.edges().remove() - cy.nodes().remove() - } + cy.removeListener("drag"); + cy.removeListener("free"); + cy.removeListener("cxttap"); + + setElements([]) + + // Remove all edges + cy.edges().remove() + cy.nodes().remove() + } } // Uses Org-Id referencing header to create a workflow while getting it in realtime // This further ensures the user needs access to GET the workflow properly const duplicateParentWorkflow = (inputWorkflow, org_id, setWorkflow) => { - fetch(`${globalUrl}/api/v1/workflows/${inputWorkflow.id}`, { - method: "GET", - headers: { - "Org-Id": org_id, - "Content-Type": "application/json", - }, - credentials: "include", - }) - .then((response) => { - if (response.status === 200) { - getChildWorkflows(inputWorkflow.id) - } + fetch(`${globalUrl}/api/v1/workflows/${inputWorkflow.id}`, { + method: "GET", + headers: { + "Org-Id": org_id, + "Content-Type": "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status === 200) { + getChildWorkflows(inputWorkflow.id) + } - return response.json(); - }) - .then((responseJson) => { - if (responseJson.success === false) { - //toast("Failed to duplicate workflow") - } else { - //toast("Successfully duplicated workflow. Reloading child workflows.") - if (setWorkflow === true) { - updateCurrentWorkflow(responseJson) - } - } - }) - .catch((error) => { - console.log("Dupe workflow for suborg error: ", error.toString()) - }) + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === false) { + //toast("Failed to duplicate workflow") + } else { + //toast("Successfully duplicated workflow. Reloading child workflows.") + if (setWorkflow === true) { + updateCurrentWorkflow(responseJson) + } + } + }) + .catch((error) => { + console.log("Dupe workflow for suborg error: ", error.toString()) + }) } const BottomCytoscapeBar = () => { @@ -17504,13 +17547,13 @@ const releaseToConnectLabel = "Release to Connect" return null; } - const buttonHeights = 45 + const buttonHeights = 45 const boxSize = buttonHeights const executionButton = executionRunning ? ( - + + + ) return ( @@ -17555,32 +17598,32 @@ const releaseToConnectLabel = "Release to Connect" flexDirection: isMobile ? "column" : "row", }} > - - {executionButton} - - - + + {executionButton} + + + { setExecutionText(e.target.value); }} - // Start adornment + // Start adornment /> - + {/*userdata.avatar === creatorProfile.github_avatar ? null :*/} - - - - - - - {workflow.public || userdata.support == true ? - + maxHeight: buttonHeights, + }} + > + - : null} + {workflow.public || userdata.support == true ? + + + + + + : null} + + {/* */} - - - - - - - - + + + + + - - - - - - - - + removeNode(selectedNode.data("id")) + }} + > + + + + - {workflow.configuration !== null && - workflow.configuration !== undefined && - workflow.configuration.exit_on_error !== undefined ? ( - - ) : null} + + + + + - {/* + {workflow.configuration !== null && + workflow.configuration !== undefined && + workflow.configuration.exit_on_error !== undefined ? ( + + ) : null} + + {/* */} - - - - - - + + + + + + + -
    ); @@ -17880,12 +17923,12 @@ const releaseToConnectLabel = "Release to Connect" // defaultReturn = return null; } else { - /* - console.log( - "Unable to handle invalid trigger type " + - selectedTrigger.trigger_type - ); - */ + /* + console.log( + "Unable to handle invalid trigger type " + + selectedTrigger.trigger_type + ); + */ return null; } } else if (Object.getOwnPropertyNames(selectedEdge).length > 0) { @@ -17925,32 +17968,32 @@ const releaseToConnectLabel = "Release to Connect" {defaultReturn} : - - {/**/} + + {/**/}
    {defaultReturn}
    -
    +
    ); //return null; }; const unPublishWorkflow = (data) => { - data.id = props.match.params.key - if (!isCloud) { - toast("Function only supported on cloud") - return - } + data.id = props.match.params.key + if (!isCloud) { + toast("Function only supported on cloud") + return + } - if (data.public !== true) { - toast("Workflow is not public. Can't unpublish"); - return - } + if (data.public !== true) { + toast("Workflow is not public. Can't unpublish"); + return + } // This ALWAYS talks to Shuffle cloud data = JSON.parse(JSON.stringify(data)); - const url = `${globalUrl}/api/v1/workflows/${props.match.params.key}/unpublish`; + const url = `${globalUrl}/api/v1/workflows/${props.match.params.key}/unpublish`; fetch(url, { method: "POST", headers: { @@ -17960,27 +18003,27 @@ const releaseToConnectLabel = "Release to Connect" body: JSON.stringify(data), credentials: "include", }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 for workflow publish :O!"); - } + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for workflow publish :O!"); + } - return response.json(); - }) - .then((responseJson) => { - if (responseJson.reason !== undefined) { - toast("Unpublishing: "+responseJson.reason) - } + return response.json(); + }) + .then((responseJson) => { + if (responseJson.reason !== undefined) { + toast("Unpublishing: " + responseJson.reason) + } - if (responseJson.success === true) { - workflow.public = false - setWorkflow(workflow) - } - }) - .catch((error) => { - toast("Failed publishing: is the workflow valid? Remember to save the workflow first.") - console.log(error.toString()) - }) + if (responseJson.success === true) { + workflow.public = false + setWorkflow(workflow) + } + }) + .catch((error) => { + toast("Failed publishing: is the workflow valid? Remember to save the workflow first.") + console.log(error.toString()) + }) } // This can execute a workflow with firestore. Used for test, as datastore is old and stuff @@ -17996,20 +18039,20 @@ const releaseToConnectLabel = "Release to Connect" // console.log(allowList, userdata.public_username) const leftView = workflow.public === true ? -
    +
    - - - {workflow.name} - - {workflow.validated === true ? - - - - : null} - + + + {workflow.name} + + {workflow.validated === true ? + + + + : null} + This workflow is public and { saveWorkflow(workflow) @@ -18193,7 +18236,7 @@ const releaseToConnectLabel = "Release to Connect"
    : null} - {/* + {/*
    - {userdata.support === true ? - - - Manual Verification: {workflow.validated === undefined || workflow.validated === null || workflow.validated === false ? "Not valided" : "Validated"} - -
    - - Validate Workflow: - - { - workflow.validated = event.target.checked - workflow.user_editing = true - //setUserediting(true) + {userdata.support === true ? + + + Manual Verification: {workflow.validated === undefined || workflow.validated === null || workflow.validated === false ? "Not valided" : "Validated"} + +
    + + Validate Workflow: + + { + workflow.validated = event.target.checked + workflow.user_editing = true + //setUserediting(true) - saveWorkflow(workflow) - }} - /> -
    -
    - : null} + saveWorkflow(workflow) + }} + /> +
    +
    + : null}
    : null} @@ -18335,7 +18377,7 @@ const releaseToConnectLabel = "Release to Connect" marginBottom: 10, padding: 5, backgroundColor: theme.palette.backgroundColor, - borderRadius: theme.palette.borderRadius, + borderRadius: theme.palette.borderRadius, cursor: "pointer", display: "flex", minHeight: 45, @@ -18407,12 +18449,12 @@ const releaseToConnectLabel = "Release to Connect" theme={theme.palette.jsonTheme} style={theme.palette.reactJsonStyle} collapsed={false} - shouldCollapse={(jsonField) => { - return collapseField(jsonField) - }} - iconStyle={theme.palette.jsonIconStyle} - collapseStringsAfterLength={theme.palette.jsonCollapseStringsAfterLength} - displayArrayKey={false} + shouldCollapse={(jsonField) => { + return collapseField(jsonField) + }} + iconStyle={theme.palette.jsonIconStyle} + collapseStringsAfterLength={theme.palette.jsonCollapseStringsAfterLength} + displayArrayKey={false} enableClipboard={(copy) => { handleReactJsonClipboard(copy); }} @@ -18443,42 +18485,42 @@ const releaseToConnectLabel = "Release to Connect" "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJYAAACOCAMAAADkWgEmAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAWlBMVEX4Wj69TDgmKCvkVTwlJyskJiokJikkJSkjJSn4Ykf+6+f5h3L////8xLr5alH/9fT7nYz4Wz/919H5cVn/+vr8qpv4XUL94d35e2X//v38t6v4YUbkVDy8SzcVIzHLAAAAAWJLR0QMgbNRYwAAAAlwSFlzAAARsAAAEbAByCf1VAAAAAd0SU1FB+QGGgsvBZ/GkmwAAAFKSURBVHja7dlrTgMxDEXhFgpTiukL2vLc/zbZQH5N7MmReu4KPmlGN4m9WgGzfhgtaOZxM1rQztNoQDvPowHtTKMB7WxHA2TJkiVLlixIZMmSRYgsWbIIkSVLFiGyZMkiRNZirBcma/eKZEW87ZGsOBxPRFbE+R3Jio/LlciKuH0iWfH1/UNkRSR3RRYruSvyWKldkcjK7IpUVl5X5LLSuiKbldQV6aycrihgZXRFCau/K2pY3V1RxersijJWX1cUsnq6opLV0RW1rNldUc2a2RXlrHldsQBrTlfcLwv5EZm/PLIgkHXKPHyQRzXzYoO8BjIvzcgnBvJBxny+Ih/7zNEIcpDEHLshh5TIkS5zAI5cFzCXK8hVFHNxh1xzQpfC0BV6XWTJkkWILFmyCJElSxYhsmTJIkSWLFmEyJIlixBZsmQB8stk/U3/Yb49pVcDMg4AAAAldEVYdGRhdGU6Y3JlYXRlADIwMjAtMDYtMjZUMTE6NDc6MDUrMDI6MDD8QCPmAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIwLTA2LTI2VDExOjQ3OjA1KzAyOjAwjR2bWgAAAABJRU5ErkJggg==" const size = 40; - const borderRadius = 5 + const borderRadius = 5 if (execution.execution_source === undefined || execution.execution_source === null || execution.execution_source.length === 0) { return ( default ) } if (execution.execution_source === "authgroups") { - const iconMargin = 7 - return ( -
    - -
    + const iconMargin = 7 + return ( +
    + +
    ) - } else if (execution.execution_source === "webhook") { + } else if (execution.execution_source === "webhook") { return ( {"webhook"} trigger.trigger_type === "WEBHOOK") .large_image } - style={{ - width: size, - height: size, - borderRadius: borderRadius, - }} + style={{ + width: size, + height: size, + borderRadius: borderRadius, + }} /> ); } else if (execution.execution_source === "outlook") { @@ -18501,11 +18543,11 @@ const releaseToConnectLabel = "Release to Connect" triggers.find((trigger) => trigger.trigger_type === "EMAIL") .large_image } - style={{ - width: size, - height: size, - borderRadius: borderRadius, - }} + style={{ + width: size, + height: size, + borderRadius: borderRadius, + }} /> ); } else if (execution.execution_source === "schedule") { @@ -18516,11 +18558,11 @@ const releaseToConnectLabel = "Release to Connect" triggers.find((trigger) => trigger.trigger_type === "SCHEDULE") .large_image } - style={{ - width: size, - height: size, - borderRadius: borderRadius, - }} + style={{ + width: size, + height: size, + borderRadius: borderRadius, + }} /> ); } else if (execution.execution_source === "EMAIL") { @@ -18531,19 +18573,19 @@ const releaseToConnectLabel = "Release to Connect" triggers.find((trigger) => trigger.trigger_type === "EMAIL") .large_image } - style={{ - width: size, - height: size, - borderRadius: borderRadius, - }} + style={{ + width: size, + height: size, + borderRadius: borderRadius, + }} /> ); - } else if (execution.execution_source === "ShuffleGPT") { + } else if (execution.execution_source === "ShuffleGPT") { return ( - + ); } else if (execution.execution_source === "pipeline") { return ( @@ -18556,7 +18598,7 @@ const releaseToConnectLabel = "Release to Connect" style={{ width: size, height: size }} /> ); - } + } if ( execution.execution_parent !== null && @@ -18570,11 +18612,11 @@ const releaseToConnectLabel = "Release to Connect" triggers.find((trigger) => trigger.trigger_type === "SUBFLOW") .large_image } - style={{ - width: size, - height: size, - borderRadius: borderRadius, - }} + style={{ + width: size, + height: size, + borderRadius: borderRadius, + }} /> ); } @@ -18583,11 +18625,11 @@ const releaseToConnectLabel = "Release to Connect" {execution.execution_source} ); }; @@ -18732,7 +18774,7 @@ const releaseToConnectLabel = "Release to Connect" console.log("IN useeffectt (2)" + collapsed) return; } - },[]) + }, []) /* componentWillUpdate = (nextProps, nextState) => { console.log(nextProps, nextState) @@ -18750,12 +18792,12 @@ const releaseToConnectLabel = "Release to Connect" theme={theme.palette.jsonTheme} style={theme.palette.reactJsonStyle} collapsed={parsedCollapse} - shouldCollapse={(jsonField) => { - return collapseField(jsonField) - }} - iconStyle={theme.palette.jsonIconStyle} - collapseStringsAfterLength={theme.palette.jsonCollapseStringsAfterLength} - displayArrayKey={false} + shouldCollapse={(jsonField) => { + return collapseField(jsonField) + }} + iconStyle={theme.palette.jsonIconStyle} + collapseStringsAfterLength={theme.palette.jsonCollapseStringsAfterLength} + displayArrayKey={false} enableClipboard={(copy) => { handleReactJsonClipboard(copy); }} @@ -18812,70 +18854,70 @@ const releaseToConnectLabel = "Release to Connect" ) } - const changeExecution = (data) => { - if ((data.result === undefined || data.result === null || data.result.length === 0) && data.status !== "FINISHED" && data.status !== "ABORTED") { - start() - setExecutionRunning(true) - setExecutionRequestStarted(false) - } + const changeExecution = (data) => { + if ((data.result === undefined || data.result === null || data.result.length === 0) && data.status !== "FINISHED" && data.status !== "ABORTED") { + start() + setExecutionRunning(true) + setExecutionRequestStarted(false) + } - var checkStarted = false - if (data.results !== undefined && data.results !== null && data.results.length > 0) { - if (data.execution_argument !== undefined && data.execution_argument !== null && data.execution_argument.includes("too large")) { - setExecutionData({}); - checkStarted = true - start(); - setExecutionRunning(true); - setExecutionRequestStarted(false); - } else { - if (data.results !== undefined && data.results !== null) { - for (let resultkey in data.results) { - if (data.results[resultkey].status !== "SUCCESS") { - continue - } + var checkStarted = false + if (data.results !== undefined && data.results !== null && data.results.length > 0) { + if (data.execution_argument !== undefined && data.execution_argument !== null && data.execution_argument.includes("too large")) { + setExecutionData({}); + checkStarted = true + start(); + setExecutionRunning(true); + setExecutionRequestStarted(false); + } else { + if (data.results !== undefined && data.results !== null) { + for (let resultkey in data.results) { + if (data.results[resultkey].status !== "SUCCESS") { + continue + } - if (data.results[resultkey].result.includes("too large")) { - setExecutionData({}); - checkStarted = true - start(); - setExecutionRunning(true); - setExecutionRequestStarted(false); - break - } - } - } - } - } + if (data.results[resultkey].result.includes("too large")) { + setExecutionData({}); + checkStarted = true + start(); + setExecutionRunning(true); + setExecutionRequestStarted(false); + break + } + } + } + } + } - const cur_execution = { - execution_id: data.execution_id, - authorization: data.authorization, - } + const cur_execution = { + execution_id: data.execution_id, + authorization: data.authorization, + } - setExecutionRequest(cur_execution) - setExecutionModalView(1) + setExecutionRequest(cur_execution) + setExecutionModalView(1) - if (!checkStarted) { - handleUpdateResults(data, cur_execution) + if (!checkStarted) { + handleUpdateResults(data, cur_execution) - if (cy !== undefined && cy !== null) { - cy.elements().removeClass("success-highlight failure-highlight executing-highlight"); - for (let actionKey in data.workflow.actions) { - var actionitem = data.workflow.actions[actionKey] + if (cy !== undefined && cy !== null) { + cy.elements().removeClass("success-highlight failure-highlight executing-highlight"); + for (let actionKey in data.workflow.actions) { + var actionitem = data.workflow.actions[actionKey] - handleColoring(actionitem.id, "", actionitem.label) - } + handleColoring(actionitem.id, "", actionitem.label) + } - for (let resultKey in data.results) { - var item = data.results[resultKey] + for (let resultKey in data.results) { + var item = data.results[resultKey] - handleColoring(item.action.id, item.status, item.action.label) - } - } + handleColoring(item.action.id, item.status, item.action.label) + } + } - setExecutionData(data) - } - } + setExecutionData(data) + } + } const ShowCopyingTooltip = () => { const [showCopying, setShowCopying] = React.useState(true) @@ -18909,14 +18951,14 @@ const releaseToConnectLabel = "Release to Connect" onClose={() => { setExecutionModalOpen(false) - const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; - const newitem = removeParam("execution_id", cursearch); - navigate(curpath + newitem) + const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; + const newitem = removeParam("execution_id", cursearch); + navigate(curpath + newitem) + }} + style={{ + resize: "both", + overflow: "auto", }} - style={{ - resize: "both", - overflow: "auto", - }} hideBackdrop={false} variant="temporary" BackdropProps={{ @@ -18934,8 +18976,8 @@ const releaseToConnectLabel = "Release to Connect" fontSize: 18, borderLeft: theme.palette.defaultBorder, - borderRadius: theme.palette.borderRadius, - backgroundColor: "black", + borderRadius: theme.palette.borderRadius, + backgroundColor: "black", }, }} > @@ -18958,91 +19000,92 @@ const releaseToConnectLabel = "Release to Connect" : null} {executionModalView === 0 ? (
    -
    - -

    - - All Workflow Runs -

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

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

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

    -
    - - + > + See more runs +

    +
    +
    +
    { - const skip_popup = true + const skip_popup = true executeWorkflow( executionData.execution_argument, executionData.start, lastSaved, - skip_popup, + skip_popup, ) if (executionText === undefined || executionText === null || executionText.length === 0) { @@ -19389,77 +19434,77 @@ const releaseToConnectLabel = "Release to Connect" - - - - - + changeExecution(data) + }} + > + + + + - - - - - + changeExecution(data) + }} + > + + + + {executionData.status === "EXECUTING" ? ( - ) : + ) : - { - e.preventDefault() - e.stopPropagation() - window.open(`/admin?admin_tab=notifications&workflow=${executionData.workflow.id}&execution_id=${executionData.execution_id}`, "_blank") - }} - /> + { + e.preventDefault() + e.stopPropagation() + window.open(`/admin?admin_tab=notifications&workflow=${executionData.workflow.id}&execution_id=${executionData.execution_id}`, "_blank") + }} + /> - } + } - {isCloud ? + {isCloud ? { - toast("Opening logs in a new tab") + toast("Opening logs in a new tab") - setTimeout(() => { - window.open(`/api/v1/workflows/search/${executionData.execution_id}`, "_blank") - }, 250) + setTimeout(() => { + window.open(`/api/v1/workflows/search/${executionData.execution_id}`, "_blank") + }, 250) }} > - + - : null} + : null}
    - {executionData.workflow !== undefined && executionData.workflow !== null && executionData.workflow.actions !== undefined && executionData.workflow.actions !== null && executionData.workflow.actions.length > 0 ? -
    - - - {/*envStatus === "success" ? - - - - : envStatus === "failure" ? - - - - : null*/} - - Env      - - - { - window.open("/admin?tab=locations", "_blank") - }}> - {executionData.workflow.actions[0].environment} - - -
    - : null} {executionData.status !== undefined && executionData.status.length > 0 ? (
    @@ -19576,7 +19596,7 @@ const releaseToConnectLabel = "Release to Connect" executionData.execution_source !== null && executionData.execution_source.length > 0 && executionData.execution_source !== "default" || - (executionData.authgroup !== undefined && executionData.authgroup !== null && executionData.authgroup.length > 0) ? ( + (executionData.authgroup !== undefined && executionData.authgroup !== null && executionData.authgroup.length > 0) ? (
    Source    @@ -19584,53 +19604,53 @@ const releaseToConnectLabel = "Release to Connect" - {executionData.execution_source === "authgroups" || (executionData.authgroup !== undefined && executionData.authgroup !== null && executionData.authgroup.length > 0) ? - - Auth Group '{executionData.authgroup !== undefined && executionData.authgroup !== null && executionData.authgroup.length > 0 ? `${executionData.authgroup}` : null}' - - : - executionData.execution_parent !== null && - executionData.execution_parent !== undefined && - executionData.execution_parent.length > 0 ? ( - executionData.execution_source === props.match.params.key ? - { - getWorkflowExecution( - props.match.params.key, - executionData.execution_parent - ); - }} - > - Parent Execution - - : - - Parent Workflow - + {executionData.execution_source === "authgroups" || (executionData.authgroup !== undefined && executionData.authgroup !== null && executionData.authgroup.length > 0) ? + + Auth Group '{executionData.authgroup !== undefined && executionData.authgroup !== null && executionData.authgroup.length > 0 ? `${executionData.authgroup}` : null}' + + : + executionData.execution_parent !== null && + executionData.execution_parent !== undefined && + executionData.execution_parent.length > 0 ? ( + executionData.execution_source === props.match.params.key ? + { + getWorkflowExecution( + props.match.params.key, + executionData.execution_parent + ); + }} + > + Parent Execution + + : + + Parent Workflow + ) - : - executionData.execution_source === "questions" || executionData.execution_source === "web" || executionData.execution_source === "form" || executionData.execution_source === "forms" ? - - Form - - : - executionData.execution_source + : + executionData.execution_source === "questions" || executionData.execution_source === "web" || executionData.execution_source === "form" || executionData.execution_source === "forms" ? + + Form + + : + executionData.execution_source }
    @@ -19661,27 +19681,53 @@ const releaseToConnectLabel = "Release to Connect" ) : null} - {userdata.support === true && executionData.workflow !== undefined && executionData.workflow !== null && executionData.status !== "EXECUTING" ? -
    - 0 ? +
    + - apps={apps} - workflow={executionData.workflow} - getParents={getParents} + {/*envStatus === "success" ? + + + + : envStatus === "failure" ? + + + + : null*/} - execution={executionData} - /> -
    - : null} + Location   + + + { + window.open("/admin?tab=locations", "_blank") + }}> + {executionData.workflow.actions[0].environment} + + +
    + : null} + + {userdata.support === true && executionData.workflow !== undefined && executionData.workflow !== null && executionData.status !== "EXECUTING" ? +
    + +
    + : null}
    - {executionData.execution_argument !== undefined && executionData.execution_argument !== null && + {executionData.execution_argument !== undefined && executionData.execution_argument !== null && executionData.execution_argument.length > 1 ? parsedExecutionArgument() - : - null} + : + null}
    {executionData.status !== undefined && - executionData.status !== "ABORTED" && - executionData.status !== "FINISHED" && - executionData.status !== "FAILURE" && - executionData.status !== "WAITING" && - !(executionData.results === undefined || executionData.results === null || (executionData.results.length === 0 && executionData.status === "EXECUTING")) ? ( + executionData.status !== "ABORTED" && + executionData.status !== "FINISHED" && + executionData.status !== "FAILURE" && + executionData.status !== "WAITING" && + !(executionData.results === undefined || executionData.results === null || (executionData.results.length === 0 && executionData.status === "EXECUTING")) ? (
    - { - console.log(environments, defaultEnvironmentIndex, nonskippedResults) - }} /> + { + console.log(environments, defaultEnvironmentIndex, nonskippedResults) + }} /> {environments.length > 0 && defaultEnvironmentIndex < environments.length && nonskippedResults.length === 0 && environments[defaultEnvironmentIndex].Name !== "Cloud" ? @@ -19738,93 +19784,76 @@ const releaseToConnectLabel = "Release to Connect"
    { - executionData.results === undefined || - executionData.results === null || - (executionData.results.length === 0 && executionData.status === "EXECUTING") ? ( + executionData.results === undefined || + executionData.results === null || + (executionData.results.length === 0 && executionData.status === "EXECUTING") ? ( -
    - - {environments.length > 0 && defaultEnvironmentIndex < environments.length && nonskippedResults.length === 0 && environments[defaultEnvironmentIndex].Name !== "Cloud" ? - - No results yet. Is Orborus running for the "{environments[defaultEnvironmentIndex].Name}" environment? Learn more. If the Workflow doesn't start within 30 seconds with Orborus running, contact support: support@shuffler.io - - : - null} -
    - ) : ( - executionData.results.map((data, index) => { - if (executionData.results.length !== 1 && !showSkippedActions && (data.status === "SKIPPED")) { - return null; - } - - // FIXME: The latter replace doens't really work if ' is used in a string - var showResult = data.result.trim(); - const validate = validateJson(showResult); - - const curapp = apps.find( - (a) => - a.name === data.action.app_name && - a.app_version === data.action.app_version - ); - const imgsize = 50; - const statusColor = - data.status === "FINISHED" || data.status === "SUCCESS" - ? green - : data.status === "ABORTED" || data.status === "FAILURE" - ? "red" - : yellow; - - var imgSrc = curapp === undefined ? "" : curapp.large_image; - if (imgSrc.length === 0 && workflow.actions !== undefined && workflow.actions !== null) { - // Look for the node in the workflow - const action = workflow.actions.find( - (action) => action.id === data.action.id - ) - if (action !== undefined && action !== null) { - imgSrc = action.large_image; +
    + + {environments.length > 0 && defaultEnvironmentIndex < environments.length && nonskippedResults.length === 0 && environments[defaultEnvironmentIndex].Name !== "Cloud" ? + + No results yet. Is Orborus running for the "{environments[defaultEnvironmentIndex].Name}" environment? Learn more. If the Workflow doesn't start within 30 seconds with Orborus running, contact support: support@shuffler.io + + : + null} +
    + ) : ( + executionData.results.map((data, index) => { + if (executionData.results.length !== 1 && !showSkippedActions && (data.status === "SKIPPED")) { + return null; } - } - if ((imgSrc === undefined || imgSrc === null || imgSrc.length === 0) && cy !== undefined && cy !== null) { - const foundnode = cy.getElementById(data.action.id) - if (foundnode !== undefined && foundnode !== null && foundnode.length > 0) { - // FIXME: Find image from cytoscape action - } else { - for (let actionkey in workflow.actions) { - if (workflow.actions[actionkey].app_name === data.action.app_name || workflow.actions[actionkey].id === data.action.id || workflow.actions[actionkey].label === data.action.label || workflow.actions[actionkey].name === data.action.name) { + // FIXME: The latter replace doens't really work if ' is used in a string + var showResult = data.result.trim(); + const validate = validateJson(showResult); - if (workflow.actions[actionkey].large_image !== undefined && workflow.actions[actionkey].large_image !== null && workflow.actions[actionkey].large_image.length > 0) { - imgSrc = workflow.actions[actionkey].large_image - break - } - } - } - } - } - - - var actionimg = - curapp === null ? null : ( - {data.action.app_name} + const curapp = apps.find( + (a) => + a.name === data.action.app_name && + a.app_version === data.action.app_version ); + const imgsize = 50; + const statusColor = + data.status === "FINISHED" || data.status === "SUCCESS" + ? green + : data.status === "ABORTED" || data.status === "FAILURE" + ? "red" + : yellow; - if (triggers.length > 2) { - if (data.action.app_name === "shuffle-subflow") { - const parsedImage = triggers[3].large_image; - actionimg = ( + var imgSrc = curapp === undefined ? "" : curapp.large_image; + if (imgSrc.length === 0 && workflow.actions !== undefined && workflow.actions !== null) { + // Look for the node in the workflow + const action = workflow.actions.find( + (action) => action.id === data.action.id + ) + if (action !== undefined && action !== null) { + imgSrc = action.large_image; + } + } + + if ((imgSrc === undefined || imgSrc === null || imgSrc.length === 0) && cy !== undefined && cy !== null) { + const foundnode = cy.getElementById(data.action.id) + if (foundnode !== undefined && foundnode !== null && foundnode.length > 0) { + // FIXME: Find image from cytoscape action + } else { + for (let actionkey in workflow.actions) { + if (workflow.actions[actionkey].app_name === data.action.app_name || workflow.actions[actionkey].id === data.action.id || workflow.actions[actionkey].label === data.action.label || workflow.actions[actionkey].name === data.action.name) { + + if (workflow.actions[actionkey].large_image !== undefined && workflow.actions[actionkey].large_image !== null && workflow.actions[actionkey].large_image.length > 0) { + imgSrc = workflow.actions[actionkey].large_image + break + } + } + } + } + } + + + var actionimg = + curapp === null ? null : ( {"Shuffle ); - } - if (data.action.app_name === "User Input") { - actionimg = ( - {"Shuffle - ); - } - } + if (triggers.length > 2) { + if (data.action.app_name === "shuffle-subflow") { + const parsedImage = triggers[3].large_image; + actionimg = ( + {"Shuffle + ); + } - if (data.action.app_name === "Shuffle Tools" && data.action.id !== undefined && cy !== undefined) { - const nodedata = cy.getElementById(data.action.id).data(); - //if (nodedata !== undefined && nodedata !== null && nodedata.fillstyle === "linear-gradient") { - if (nodedata !== undefined && nodedata !== null) { - var imgStyle = { - marginRight: 20, - width: imgsize, - height: imgsize, - border: `2px solid ${statusColor}`, - borderRadius: executionData.start === data.action.id ? 25 : 5, - background: `linear-gradient(to right, ${nodedata.fillGradient})`, - }; - - actionimg = ( - {nodedata.label} - ); - } else { - //console.log("Node not found: ", nodedata) - actionimg = ( - {data.action.app_name} - ) - } - } - - if (validate.valid && typeof validate.result === "string") { - validate.result = JSON.parse(validate.result); - } - - if (validate.valid && typeof validate.result === "object") { - if ( - validate.result.result !== undefined && - validate.result.result !== null - ) { - try { - validate.result.result = JSON.parse(validate.result.result); - } catch (e) { - //console.log("ERROR PARSING: ", e) + if (data.action.app_name === "User Input") { + actionimg = ( + {"Shuffle + ); } } - } + if (data.action.app_name === "Shuffle Tools" && data.action.id !== undefined && cy !== undefined) { + const nodedata = cy.getElementById(data.action.id).data(); + //if (nodedata !== undefined && nodedata !== null && nodedata.fillstyle === "linear-gradient") { + if (nodedata !== undefined && nodedata !== null) { + var imgStyle = { + marginRight: 20, + width: imgsize, + height: imgsize, + border: `2px solid ${statusColor}`, + borderRadius: executionData.start === data.action.id ? 25 : 5, + background: `linear-gradient(to right, ${nodedata.fillGradient})`, + }; - var similarActionsView = null - if (data.similar_actions !== undefined && data.similar_actions !== null) { - var minimumMatch = 85 - var matching_executions = [] - if (data.similar_actions !== undefined && data.similar_actions !== null) { - for (let [k,kval] in Object.entries(data.similar_actions)){ - if (data.similar_actions.hasOwnProperty(k)) { - if (data.similar_actions[k].similarity > minimumMatch) { - matching_executions.push(data.similar_actions[k].execution_id) - } - } - } - } - - if (matching_executions.length !== 0) { - var parsed_url = matching_executions.join(",") - - similarActionsView = - - + ); + } else { + //console.log("Node not found: ", nodedata) + actionimg = ( + {data.action.app_name} { - navigate(`?execution_highlight=${parsed_url}`) - }} - > - - - + /> + ) + } } - } - const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; - const chosenNodeId = new URLSearchParams(cursearch).get("node"); - const highlightNode = chosenNodeId !== null && chosenNodeId !== undefined && chosenNodeId !== "" && chosenNodeId === data.action.id + if (validate.valid && typeof validate.result === "string") { + validate.result = JSON.parse(validate.result); + } - return ( -
    { - if (cy == undefined || cy == null) { - return - } - - var currentnode = cy.getElementById(data.action.id); - if (currentnode !== undefined && currentnode !== null && currentnode.length !== 0) { - currentnode.addClass("shuffle-hover-highlight"); + if (validate.valid && typeof validate.result === "object") { + if ( + validate.result.result !== undefined && + validate.result.result !== null + ) { + try { + validate.result.result = JSON.parse(validate.result.result); + } catch (e) { + //console.log("ERROR PARSING: ", e) } + } + } - // Add a hover highlight - //var copyText = document.getElementById( - // "copy_element_shuffle" - //) - }} - onMouseOut={() => { - if (cy == undefined || cy == null) { - return - } - - var currentnode = cy.getElementById(data.action.id); - if (currentnode.length !== 0) { - currentnode.removeClass("shuffle-hover-highlight"); + var similarActionsView = null + if (data.similar_actions !== undefined && data.similar_actions !== null) { + var minimumMatch = 85 + var matching_executions = [] + if (data.similar_actions !== undefined && data.similar_actions !== null) { + for (let [k, kval] in Object.entries(data.similar_actions)) { + if (data.similar_actions.hasOwnProperty(k)) { + if (data.similar_actions[k].similarity > minimumMatch) { + matching_executions.push(data.similar_actions[k].execution_id) + } + } } - }} - > -
    -
    - { - if (cy !== undefined) { - const oldstartnode = cy.getElementById(data.action.id); - //console.log("FOUND NODe: ", oldstartnode) - if (oldstartnode !== undefined && oldstartnode !== null) { - const foundname = oldstartnode.data("label") - if (foundname !== undefined && foundname !== null) { - data.action.label = foundname - } - } + } - //console.log("Click data: ", data) - //data.action.label = "" - setSelectedResult(data); - setActiveDialog("result") - setCodeModalOpen(true); - } else { - toast("Please wait until the workflow is loaded and try again") - setCodeModalOpen(true) - setSelectedResult(data) + if (matching_executions.length !== 0) { + var parsed_url = matching_executions.join(",") - } - }} + similarActionsView = + - - - - - {actionimg} -
    -
    { + navigate(`?execution_highlight=${parsed_url}`) }} > - {data.action.label === undefined || data.action.label === null || data.action.label === "" ? data.action.label : data.action.label.replaceAll("_", " ")} - -
    -
    - - {data.action.name} - + + + + } + } + + const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; + const chosenNodeId = new URLSearchParams(cursearch).get("node"); + const highlightNode = chosenNodeId !== null && chosenNodeId !== undefined && chosenNodeId !== "" && chosenNodeId === data.action.id + + return ( +
    { + if (cy == undefined || cy == null) { + return + } + + var currentnode = cy.getElementById(data.action.id); + if (currentnode !== undefined && currentnode !== null && currentnode.length !== 0) { + currentnode.addClass("shuffle-hover-highlight"); + } + + // Add a hover highlight + + //var copyText = document.getElementById( + // "copy_element_shuffle" + //) + }} + onMouseOut={() => { + if (cy == undefined || cy == null) { + return + } + + var currentnode = cy.getElementById(data.action.id); + if (currentnode.length !== 0) { + currentnode.removeClass("shuffle-hover-highlight"); + } + }} + > +
    +
    + { + if (cy !== undefined) { + const oldstartnode = cy.getElementById(data.action.id); + //console.log("FOUND NODe: ", oldstartnode) + if (oldstartnode !== undefined && oldstartnode !== null) { + const foundname = oldstartnode.data("label") + if (foundname !== undefined && foundname !== null) { + data.action.label = foundname + } + } + + //console.log("Click data: ", data) + //data.action.label = "" + setSelectedResult(data); + setActiveDialog("result") + setCodeModalOpen(true); + } else { + toast("Please wait until the workflow is loaded and try again") + setCodeModalOpen(true) + setSelectedResult(data) + + } + }} + > + + + + + {actionimg} +
    +
    + {data.action.label === undefined || data.action.label === null || data.action.label === "" ? data.action.label : data.action.label.replaceAll("_", " ")} + +
    +
    + + {data.action.name} + +
    -
    - {data.action.app_name === "shuffle-subflow" && - validate.result.success !== undefined && - validate.result.success === true ? ( - - {validate.valid && - data.action.parameters !== undefined && - data.action.parameters !== null && - data.action.parameters.length > 0 ? ( - data.action.parameters[0].value === - props.match.params.key ? ( - { - getWorkflowExecution( - props.match.params.key, - validate.result.execution_id - ); - }} - > - - + {data.action.app_name === "shuffle-subflow" && + validate.result.success !== undefined && + validate.result.success === true ? ( + + {validate.valid && + data.action.parameters !== undefined && + data.action.parameters !== null && + data.action.parameters.length > 0 ? ( + data.action.parameters[0].value === + props.match.params.key ? ( + { + getWorkflowExecution( + props.match.params.key, + validate.result.execution_id + ); + }} + > + + + ) : ( + { }} + > + + + ) ) : ( - { }} - > - - - ) - ) : ( - "" - )} - - ) : null} -
    - - { data.status !== "SUCCESS" ? -
    - - Status  - - - {data.status} - - {similarActionsView} -
    - : null} - - {validate.valid ? ( - - { - return collapseField(jsonField) - }} - iconStyle={theme.palette.jsonIconStyle} - collapseStringsAfterLength={theme.palette.jsonCollapseStringsAfterLength} - displayArrayKey={false} - enableClipboard={(copy) => { - handleReactJsonClipboard(copy); - }} - displayDataTypes={false} - onSelect={(select) => { - HandleJsonCopy(showResult, select, data.action.label); - console.log("SELECTED!: ", select); - }} - name={"Results for " + data.action.label} - /> - - - ) : ( -
    - - Result  - - - {data.result} - + "" + )} + + ) : null}
    - )} -
    - ); - }) - )} + + {data.status !== "SUCCESS" ? +
    + + Status  + + + {data.status} + + {similarActionsView} +
    + : null} + + {validate.valid ? ( + + { + return collapseField(jsonField) + }} + iconStyle={theme.palette.jsonIconStyle} + collapseStringsAfterLength={theme.palette.jsonCollapseStringsAfterLength} + displayArrayKey={false} + enableClipboard={(copy) => { + handleReactJsonClipboard(copy); + }} + displayDataTypes={false} + onSelect={(select) => { + HandleJsonCopy(showResult, select, data.action.label); + console.log("SELECTED!: ", select); + }} + name={"Results for " + data.action.label} + /> + + + ) : ( +
    + + Result  + + + {data.result} + +
    + )} +
    + ); + }) + )}
    )} @@ -20203,45 +20249,45 @@ const releaseToConnectLabel = "Release to Connect" const [open, setOpen] = React.useState(false) const showVariable = data.value.length < 60 - // Check if it's valid JSON - const checked = validateJson(data.value.trim()) + // Check if it's valid JSON + const checked = validateJson(data.value.trim()) - if (data.name === "shuffle_action_logs" && data.value !== undefined && data.value !== null && data.value.length > 0 && data.value.includes("add env SHUFFLE_LOGS_DISABLED")) { - return ( -
    - - Action Logs - - - Logs for an action are not available without an onprem environment with the SHUFFLE_LOGS_DISABLED environment variable set to false: SHUFFLE_LOGS_DISABLED=false. Logs are enabled by default, except in scale mode. - -
    - ) - } + if (data.name === "shuffle_action_logs" && data.value !== undefined && data.value !== null && data.value.length > 0 && data.value.includes("add env SHUFFLE_LOGS_DISABLED")) { + return ( +
    + + Action Logs + + + Logs for an action are not available without an onprem environment with the SHUFFLE_LOGS_DISABLED environment variable set to false: SHUFFLE_LOGS_DISABLED=false. Logs are enabled by default, except in scale mode. + +
    + ) + } - var showlink = false - if (data.name.endsWith("-Url")) { - //data.name = data.name.toLowerCase().replaceAll("-", "_") - if (data.value.startsWith(", ")) { - data.value = data.value.substring(2) - } + var showlink = false + if (data.name.endsWith("-Url")) { + //data.name = data.name.toLowerCase().replaceAll("-", "_") + if (data.value.startsWith(", ")) { + data.value = data.value.substring(2) + } - if (data.value.startsWith("http") || (data.value.startsWith("/") && data.value.includes("?"))) { - showlink = true - } - } + if (data.value.startsWith("http") || (data.value.startsWith("/") && data.value.includes("?"))) { + showlink = true + } + } return (
    {data.value.length > 60 || checked.valid ? {data.name} - {checked.valid ? - - : null} - {showVariable ? data.value : null} + {checked.valid ? + + : null} + {showVariable ? data.value : null} : @@ -20277,40 +20323,40 @@ const releaseToConnectLabel = "Release to Connect" } {open ? - checked.valid ? - { - return collapseField(jsonField) - }} - iconStyle={theme.palette.jsonIconStyle} - collapseStringsAfterLength={theme.palette.jsonCollapseStringsAfterLength} - displayArrayKey={false} - displayDataTypes={false} - name={"Parsed data for variable " + data.name} - /> - : - { - if (showlink) { - e.preventDefault() - e.stopPropagation() - window.open(data.value, "_blank") - } - }} - color={showlink ? "inherit" : "textSecondary"} - > - {data.value} - + checked.valid ? + { + return collapseField(jsonField) + }} + iconStyle={theme.palette.jsonIconStyle} + collapseStringsAfterLength={theme.palette.jsonCollapseStringsAfterLength} + displayArrayKey={false} + displayDataTypes={false} + name={"Parsed data for variable " + data.name} + /> + : + { + if (showlink) { + e.preventDefault() + e.stopPropagation() + window.open(data.value, "_blank") + } + }} + color={showlink ? "inherit" : "textSecondary"} + > + {data.value} + : null}
    ) @@ -20320,258 +20366,258 @@ const releaseToConnectLabel = "Release to Connect" // Should probably put this on the backend instead when notifications are made :)) const getErrorSuggestion = (result) => { - if (result === undefined || result === null) { - return "" - } + if (result === undefined || result === null) { + return "" + } - // Check if array with json inside to handle one item at a time~ - if (typeof result === "object" && result.length !== undefined) { - if (result.length > 0) { - // Check type inside - if (typeof result[0] === "object") { - result = result[0] - } - } - } + // Check if array with json inside to handle one item at a time~ + if (typeof result === "object" && result.length !== undefined) { + if (result.length > 0) { + // Check type inside + if (typeof result[0] === "object") { + result = result[0] + } + } + } - if (result.success === true && result.status === 200) { - if (result.body !== undefined && result.body !== null) { - const stringbody = result.body.toString() - if ((stringbody.startsWith("{") && stringbody.endsWith("}")) || (stringbody.startsWith("[") && stringbody.endsWith("]"))) { - return "" - } + if (result.success === true && result.status === 200) { + if (result.body !== undefined && result.body !== null) { + const stringbody = result.body.toString() + if ((stringbody.startsWith("{") && stringbody.endsWith("}")) || (stringbody.startsWith("[") && stringbody.endsWith("]"))) { + return "" + } - if (stringbody.length > 1000) { - return "Body looks to be big in a standard format. Consider using the 'To File' parameter to automatically make it into a file." - } - } - } + if (stringbody.length > 1000) { + return "Body looks to be big in a standard format. Consider using the 'To File' parameter to automatically make it into a file." + } + } + } - if (result.status === 429) { - return "Rate limit exceeded. Consider using a different API key or wait a bit before trying again." - } + if (result.status === 429) { + return "Rate limit exceeded. Consider using a different API key or wait a bit before trying again." + } - if (result.status === 405) { - return "Method not allowed. Check the URL to ensure it has all the required parameters. If you keep getting a 405, please forward a screenshot of this to support@shuffler.io" - } + if (result.status === 405) { + return "Method not allowed. Check the URL to ensure it has all the required parameters. If you keep getting a 405, please forward a screenshot of this to support@shuffler.io" + } - if (result.status === 415) { - return "Content-Type header missing or wrong. Please add the correct Content-Type header and save the workflow." - } + if (result.status === 415) { + return "Content-Type header missing or wrong. Please add the correct Content-Type header and save the workflow." + } - if (result.status === 401) { - return "Authentication failed (401). The URL or auth key is wrong. Check the body of the result for more information." - } + if (result.status === 401) { + return "Authentication failed (401). The URL or auth key is wrong. Check the body of the result for more information." + } - if (result.status === 403) { - return "Authorization failed (403). The API user most likely doesn't have the correct permissions. Check the body of the result for more information." - } + if (result.status === 403) { + return "Authorization failed (403). The API user most likely doesn't have the correct permissions. Check the body of the result for more information." + } - if (result.status === 404) { - return "The URL, or content of the URL is incorrect. Check it and try again." - } + if (result.status === 404) { + return "The URL, or content of the URL is incorrect. Check it and try again." + } - if (result.status === 400) { - return "The queries or data sent to the API is most likely wrong (400). Check the body of the result for more information." - } + if (result.status === 400) { + return "The queries or data sent to the API is most likely wrong (400). Check the body of the result for more information." + } - if (result.status === 200 || result.status === 201 || result.status === 204) { - return "It looks like the result was successful! If it didn't work, make sure to check if the body you are sending was correct." - } + if (result.status === 200 || result.status === 201 || result.status === 204) { + return "It looks like the result was successful! If it didn't work, make sure to check if the body you are sending was correct." + } - // Validate and check for newlines - if (result.success !== false) { + // Validate and check for newlines + if (result.success !== false) { - var stringjson = result - const valid = validateJson(stringjson, true) - if (valid.valid === false) { - if (stringjson.startsWith("{") && stringjson.endsWith("}")) { - // Look for newline - if (stringjson.includes("\n") && !stringjson.includes("\n")) { - return "Looks like you have a newline problem. Consider using the | replace: '\n', '\\n' }} filter in Liquid." - } else { - return "The result looks like it should be JSON, but is invalid. Look for potential single quotes instead of double quotes, missing commas or newlines" - } - } - } + var stringjson = result + const valid = validateJson(stringjson, true) + if (valid.valid === false) { + if (stringjson.startsWith("{") && stringjson.endsWith("}")) { + // Look for newline + if (stringjson.includes("\n") && !stringjson.includes("\n")) { + return "Looks like you have a newline problem. Consider using the | replace: '\n', '\\n' }} filter in Liquid." + } else { + return "The result looks like it should be JSON, but is invalid. Look for potential single quotes instead of double quotes, missing commas or newlines" + } + } + } - //return "" - } + //return "" + } - try { - stringjson = JSON.stringify(result) - } catch (e) { - } + try { + stringjson = JSON.stringify(result) + } catch (e) { + } - stringjson = stringjson.toLowerCase() - if (stringjson.includes("localhost")) { - return "You can't use localhost in apps. Use the external ip or url of the server instead" - } + stringjson = stringjson.toLowerCase() + if (stringjson.includes("localhost")) { + return "You can't use localhost in apps. Use the external ip or url of the server instead" + } - if (stringjson.includes("manifest unknown")) { - return "The app's Docker Image is not available in the environment yet. Re-run the app to force a re-download of the app. If the problem persists, contact support" - } + if (stringjson.includes("manifest unknown")) { + return "The app's Docker Image is not available in the environment yet. Re-run the app to force a re-download of the app. If the problem persists, contact support" + } - if (result.status !== 200 && result.url !== undefined && result.url !== null && typeof result.url === "string" && (result.url.includes("192.168") || result.url.includes("172.16") || result.url.includes("10.0"))) { - return "Consider whether your Orborus environment can connect to a local IP or not." - } + if (result.status !== 200 && result.url !== undefined && result.url !== null && typeof result.url === "string" && (result.url.includes("192.168") || result.url.includes("172.16") || result.url.includes("10.0"))) { + return "Consider whether your Orborus environment can connect to a local IP or not." + } - if (stringjson.includes("kms/")) { - return "KMS authentication most likely failed. Check your notifications for more details on this page: /admin?admin_tab=notifications. If you need help with KMS, please contact support@shuffler.io" - } + if (stringjson.includes("kms/")) { + return "KMS authentication most likely failed. Check your notifications for more details on this page: /admin?admin_tab=notifications. If you need help with KMS, please contact support@shuffler.io" + } - if (stringjson.includes("invalidurl")) { - // IF count of "http" is more than one, 1, it's prolly invalid - var additionalinfo = "" - if (stringjson.includes("http") && stringjson.match(/http/g).length > 1) { - additionalinfo = "You may be using multiple 'http' in the URL. " - } + if (stringjson.includes("invalidurl")) { + // IF count of "http" is more than one, 1, it's prolly invalid + var additionalinfo = "" + if (stringjson.includes("http") && stringjson.match(/http/g).length > 1) { + additionalinfo = "You may be using multiple 'http' in the URL. " + } - return "The URL is invalid. Change the URL to a valid one, and try again. "+additionalinfo - } + return "The URL is invalid. Change the URL to a valid one, and try again. " + additionalinfo + } - if (stringjson.includes("result too large to handle")) { - return "Execution loading failed. Reload the execution by closing it and clicking it again" - } + if (stringjson.includes("result too large to handle")) { + return "Execution loading failed. Reload the execution by closing it and clicking it again" + } - if (isCloud && stringjson.toLowerCase().includes("timeout error")) { - return "Run this workflow in a local environment to increase the timeout. Go to https://shuffler.io/admin?tab=locations to create an environment to connect to" - } + if (isCloud && stringjson.toLowerCase().includes("timeout error")) { + return "Run this workflow in a local environment to increase the timeout. Go to https://shuffler.io/admin?tab=locations to create an environment to connect to" + } - if (stringjson.toLowerCase().includes("invalid header")) { - return "A header or authentication token in the app is invalid. Check the app's configuration" - } + if (stringjson.toLowerCase().includes("invalid header")) { + return "A header or authentication token in the app is invalid. Check the app's configuration" + } - if (stringjson.includes("connectionerror")) { - if (stringjson.includes("kms")) { - return "KMS authentication most likely failed (2). Check your notifications for more details on this page: /admin?admin_tab=notifications&kms=true. If you need help with KMS, please contact support@shuffler.io" - } + if (stringjson.includes("connectionerror")) { + if (stringjson.includes("kms")) { + return "KMS authentication most likely failed (2). Check your notifications for more details on this page: /admin?admin_tab=notifications&kms=true. If you need help with KMS, please contact support@shuffler.io" + } - return "The URL is incorrect, or Shuffle can't reach it. Set up a Shuffle Environment in the same VLAN, or whitelist Shuffle's IPs." - } + return "The URL is incorrect, or Shuffle can't reach it. Set up a Shuffle Environment in the same VLAN, or whitelist Shuffle's IPs." + } - return "" + return "" } const currentSuggestion = getErrorSuggestion(validate.result) const codePopoutModal = !codeModalOpen ? null : ( - setActiveDialog("result"), - style: { - pointerEvents: "auto", - color: "white", - minWidth: isMobile ? "90%" : 750, - padding: 30, - maxHeight: 550, - overflowY: "auto", - overflowX: "hidden", - border: theme.palette.defaultBorder, + setActiveDialog("result"), + style: { + pointerEvents: "auto", + color: "white", + minWidth: isMobile ? "90%" : 750, + padding: 30, + maxHeight: 550, + overflowY: "auto", + overflowX: "hidden", + border: theme.palette.defaultBorder, - borderRadius: theme.palette.borderRadius, - backgroundColor: "black", - }, - }} - > - {/* Have a sticky top bar */} - - + {/* Have a sticky top bar */} + + + { + e.preventDefault() + }} > - { - e.preventDefault() - }} - > - - - - - { - e.preventDefault() + + + + + { + e.preventDefault() - if (workflowExecutions !== null) { - for (let execkey in workflowExecutions) { - const execution = workflowExecutions[execkey]; - if (execution.execution_argument.includes("too large")) { - continue - } - - const result = execution.results.find((data) => data.status === "SUCCESS" && data.action.id === selectedResult.action.id) - - if (result !== undefined) { - const oldstartnode = cy.getElementById(selectedResult.action.id) - if (oldstartnode !== undefined && oldstartnode !== null) { - const foundname = oldstartnode.data("label") - if (foundname !== undefined && foundname !== null) { - result.action.label = foundname - } - } - - setSelectedResult(result) - setUpdate(Math.random()) - break; - } - } - } - }} - > - - - - - { - e.preventDefault(); + if (workflowExecutions !== null) { for (let execkey in workflowExecutions) { const execution = workflowExecutions[execkey]; - const result = execution.results.find( - (data) => - data.action.id === selectedResult.action.id && - data.status !== "SUCCESS" && - data.status !== "SKIPPED" && - data.status !== "WAITING" - ); + if (execution.execution_argument.includes("too large")) { + continue + } + + const result = execution.results.find((data) => data.status === "SUCCESS" && data.action.id === selectedResult.action.id) + + if (result !== undefined) { + const oldstartnode = cy.getElementById(selectedResult.action.id) + if (oldstartnode !== undefined && oldstartnode !== null) { + const foundname = oldstartnode.data("label") + if (foundname !== undefined && foundname !== null) { + result.action.label = foundname + } + } + + setSelectedResult(result) + setUpdate(Math.random()) + break; + } + } + } + }} + > + + + + + { + e.preventDefault(); + for (let execkey in workflowExecutions) { + const execution = workflowExecutions[execkey]; + const result = execution.results.find( + (data) => + data.action.id === selectedResult.action.id && + data.status !== "SUCCESS" && + data.status !== "SKIPPED" && + data.status !== "WAITING" + ); if (result !== undefined) { const oldstartnode = cy.getElementById(selectedResult.action.id); @@ -20607,15 +20653,15 @@ const releaseToConnectLabel = "Release to Connect" setExecutionModalOpen(true); setExecutionModalView(1); - if (workflowExecutions[executionIndex] !== undefined && workflowExecutions[executionIndex] !== null && workflowExecutions[executionIndex].execution_argument.includes("too large")) { - //checkStarted = true - setExecutionData({}); - start(); - setExecutionRunning(true); - setExecutionRequestStarted(false); - } else { - setExecutionData(workflowExecutions[executionIndex]); - } + if (workflowExecutions[executionIndex] !== undefined && workflowExecutions[executionIndex] !== null && workflowExecutions[executionIndex].execution_argument.includes("too large")) { + //checkStarted = true + setExecutionData({}); + start(); + setExecutionRunning(true); + setExecutionRequestStarted(false); + } else { + setExecutionData(workflowExecutions[executionIndex]); + } } }} > @@ -20629,13 +20675,13 @@ const releaseToConnectLabel = "Release to Connect" > { }} > @@ -20670,8 +20716,8 @@ const releaseToConnectLabel = "Release to Connect" width: imgsize, height: imgsize, border: `2px solid ${statusColor}`, - filter: curapp === undefined ? "grayscale(100%)" : null, - borderRadius: theme.palette?.borderRadius, + filter: curapp === undefined ? "grayscale(100%)" : null, + borderRadius: theme.palette?.borderRadius, }} /> )} @@ -20693,15 +20739,15 @@ const releaseToConnectLabel = "Release to Connect"
    - {currentSuggestion.length > 0 ? -
    - Debug: {currentSuggestion} -
    - : -
    - Status {selectedResult.status} -
    - } + {currentSuggestion.length > 0 ? +
    + Debug: {currentSuggestion} +
    + : +
    + Status {selectedResult.status} +
    + } {validate.valid ? ( { - return collapseField(jsonField) - }} - iconStyle={theme.palette.jsonIconStyle} - collapseStringsAfterLength={theme.palette.jsonCollapseStringsAfterLength} - displayArrayKey={false} + shouldCollapse={(jsonField) => { + return collapseField(jsonField) + }} + iconStyle={theme.palette.jsonIconStyle} + collapseStringsAfterLength={theme.palette.jsonCollapseStringsAfterLength} + displayArrayKey={false} enableClipboard={(copy) => { handleReactJsonClipboard(copy); }} @@ -20727,13 +20773,13 @@ const releaseToConnectLabel = "Release to Connect" ) : (
    Result -
    +
    { to_be_copied = selectedResult.result; @@ -20811,7 +20857,7 @@ const releaseToConnectLabel = "Release to Connect" ); const newView = ( -
    +
    @@ -20839,15 +20885,15 @@ const releaseToConnectLabel = "Release to Connect"
    ) : ( - - {/**/} + + {/**/} { // FIXME: There's something specific loading when // you do the first hover of a node. Why is this different? - - setCy(incy); + + setCy(incy); }} /> - + )}
    {executionModal} - + { - rightSideBarOpen && Object.getOwnPropertyNames(selectedAction).length > 0 ? - -
    - -
    -
    - : null + rightSideBarOpen && Object.getOwnPropertyNames(selectedAction).length > 0 ? + +
    + +
    +
    + : null } - {/* Looks for triggers" */} - {/* Only fixed the ones that require scrolling on a small screen */} - {/* Most important: Actions. But these are a lot more complex */} - {rightSideBarOpen && (selectedTrigger.trigger_type === "SCHEDULE" || selectedTrigger.trigger_type === "WEBHOOK" || selectedTrigger.trigger_type === "PIPELINE" || selectedTrigger.trigger_type === "USERINPUT" || selectedTrigger.trigger_type === "SUBFLOW") ? -
    - {Object.getOwnPropertyNames(selectedTrigger)?.length > 0 ? - selectedTrigger.trigger_type === "SCHEDULE" ? - ScheduleSidebar - : selectedTrigger.trigger_type === "PIPELINE" ? - PipelineSidebar - : selectedTrigger.trigger_type === "WEBHOOK" ? - WebhookSidebar - : selectedTrigger.trigger_type === "USERINPUT" ? - UserinputSidebar - : selectedTrigger.trigger_type === "SUBFLOW" ? - SubflowSidebar - : null - : null} -
    - : null} -{/* + {/* Looks for triggers" */} + {/* Only fixed the ones that require scrolling on a small screen */} + {/* Most important: Actions. But these are a lot more complex */} + {rightSideBarOpen && (selectedTrigger.trigger_type === "SCHEDULE" || selectedTrigger.trigger_type === "WEBHOOK" || selectedTrigger.trigger_type === "PIPELINE" || selectedTrigger.trigger_type === "USERINPUT" || selectedTrigger.trigger_type === "SUBFLOW") ? +
    + {Object.getOwnPropertyNames(selectedTrigger)?.length > 0 ? + selectedTrigger.trigger_type === "SCHEDULE" ? + ScheduleSidebar + : selectedTrigger.trigger_type === "PIPELINE" ? + PipelineSidebar + : selectedTrigger.trigger_type === "WEBHOOK" ? + WebhookSidebar + : selectedTrigger.trigger_type === "USERINPUT" ? + UserinputSidebar + : selectedTrigger.trigger_type === "SUBFLOW" ? + SubflowSidebar + : null + : null} +
    + : null} + {/* { rightSideBarOpen && selectedTrigger?.trigger_type === "SUBFLOW"&& Object.getOwnPropertyNames(selectedTrigger)?.length > 0 ?
    : null } */} - - {/* + + {/* */} - {showWorkflowRevisions ? null : - - {/**/} - {shownErrors} - - - - - } + {showWorkflowRevisions ? null : + + {/**/} + {shownErrors} + + + + + }
    ); @@ -21039,10 +21085,10 @@ const releaseToConnectLabel = "Release to Connect" color: "white", border: theme.palette.defaultBorder, maxWidth: isMobile ? bodyWidth - 100 : 800, - minWidth: isMobile ? bodyWidth - 100 : 800, + minWidth: isMobile ? bodyWidth - 100 : 800, - borderRadius: theme.palette.borderRadius, - backgroundColor: "black", + borderRadius: theme.palette.borderRadius, + backgroundColor: "black", }, }} > @@ -21072,7 +21118,7 @@ const releaseToConnectLabel = "Release to Connect" }, }} margin="dense" - label="Name" + label="Name" fullWidth defaultValue={newVariableName} /> @@ -21087,7 +21133,7 @@ const releaseToConnectLabel = "Release to Connect" }, }} margin="dense" - label="Default Value (optional)" + label="Default Value (optional)" fullWidth defaultValue={newVariableValue} /> @@ -21127,9 +21173,9 @@ const releaseToConnectLabel = "Release to Connect" workflow.execution_variables[found].name = newVariableName; } - if (newVariableValue.length > 0) { - workflow.execution_variables[found].value = newVariableValue; - } + if (newVariableValue.length > 0) { + workflow.execution_variables[found].value = newVariableValue; + } } else { workflow.execution_variables.push({ name: newVariableName, @@ -21220,8 +21266,8 @@ const releaseToConnectLabel = "Release to Connect" border: theme.palette.defaultBorder, maxWidth: isMobile ? bodyWidth - 100 : "100%", - borderRadius: theme.palette.borderRadius, - backgroundColor: "black", + borderRadius: theme.palette.borderRadius, + backgroundColor: "black", }, }} > @@ -21446,18 +21492,18 @@ const releaseToConnectLabel = "Release to Connect" selectedAction.authentication_id = authenticationOption.id; selectedAction.selectedAuthentication = authenticationOption; - console.log("auth option 4: ", authenticationOption) + console.log("auth option 4: ", authenticationOption) if (selectedAction.authentication === undefined || selectedAction.authentication === null) { selectedAction.authentication = [authenticationOption] } else { - try { - selectedAction.authentication.push(authenticationOption) - } catch (e) { - //console.log("Error: ", e) - } + try { + selectedAction.authentication.push(authenticationOption) + } catch (e) { + //console.log("Error: ", e) + } } setSelectedAction(selectedAction) @@ -21482,8 +21528,8 @@ const releaseToConnectLabel = "Release to Connect" setUpdate(authenticationOption.id) } - if (authenticationOption.label === null || authenticationOption.label === undefined) { - authenticationOption.label = selectedApp.name + " authentication"; + if (authenticationOption.label === null || authenticationOption.label === undefined) { + authenticationOption.label = selectedApp.name + " authentication"; } return ( @@ -21532,35 +21578,35 @@ const releaseToConnectLabel = "Release to Connect" />
    {selectedApp.authentication.parameters.map((data, index) => { - // FIXME: Look for relevant fields in the action that may already be filled in with the same name - if (data.value === "" || data.value === null || data.value === undefined || data.name === "url") { - if (selectedAction !== undefined && selectedAction !== null && selectedAction.parameters !== undefined && selectedAction.parameters !== null) { - for (var fieldkey in selectedAction.parameters) { - const field = selectedAction.parameters[fieldkey] - if (field.name !== data.name) { - continue - } + // FIXME: Look for relevant fields in the action that may already be filled in with the same name + if (data.value === "" || data.value === null || data.value === undefined || data.name === "url") { + if (selectedAction !== undefined && selectedAction !== null && selectedAction.parameters !== undefined && selectedAction.parameters !== null) { + for (var fieldkey in selectedAction.parameters) { + const field = selectedAction.parameters[fieldkey] + if (field.name !== data.name) { + continue + } - if (field.value !== undefined && field.value !== null && field.value.length > 0) { - data.value = field.value - data.autocomplete = true - break - } - } - } - } + if (field.value !== undefined && field.value !== null && field.value.length > 0) { + data.value = field.value + data.autocomplete = true + break + } + } + } + } return (
    -
    - - - {data?.name?.endsWith("_basic") ? data?.name?.replace("_basic", "") : data?.name} - -
    +
    + + + {data?.name?.endsWith("_basic") ? data?.name?.replace("_basic", "") : data?.name} + +
    {data.schema !== undefined && data.schema !== null && @@ -21631,7 +21677,7 @@ const releaseToConnectLabel = "Release to Connect" authenticationOption.fields[data.name] = event.target.value; }} - id={`${data.name}_auth`} + id={`${data.name}_auth`} /> )}
    @@ -21640,9 +21686,9 @@ const releaseToConnectLabel = "Release to Connect"
    - - + - - - + cursor: "move", + }} + > + + + - {authenticationType.type === "oauth2" || authenticationType.type === "oauth2-app" ? + {authenticationType.type === "oauth2" || authenticationType.type === "oauth2-app" ? - : + : }
    @@ -21879,51 +21925,51 @@ const releaseToConnectLabel = "Release to Connect" overflowY: "auto", overflowX: "hidden", }} - onLoad={() => { - /* - if (isCloud && ReactGA !== undefined) { - toast("Sending GA info") - // Google analytics info about what app people are looking at - ReactGA.event({ - category: "workflow", - action: `documentation_load`, - label: selectedApp.name, - }) - - } - */ - }} + onLoad={() => { + /* + if (isCloud && ReactGA !== undefined) { + toast("Sending GA info") + // Google analytics info about what app people are looking at + ReactGA.event({ + category: "workflow", + action: `documentation_load`, + label: selectedApp.name, + }) + + } + */ + }} > {selectedApp.documentation === undefined || selectedApp.documentation === null || selectedApp.documentation.length === 0 ? ( - -
    - - {selectedApp.description} - -
    + +
    + + {selectedApp.description} + +
    -
    - - There is no Shuffle-specific documentation for this app yet outside of the general description above. Documentation is written for each api, and is a community effort. We hope to see your contribution! - - -
    + setTimeout(() => { + window.open(`https://github.com/Shuffle/openapi-apps/new/master/docs?filename=${selectedApp.name.toLowerCase()}.md`, "_blank") + }, 2500) + }} + > +   Create Docs + +
    Want to help the making of, or improve this app?{" "} -
    +
    ) : ( -
    - {selectedMeta !== undefined && selectedMeta !== null && Object.getOwnPropertyNames(selectedMeta).length > 0 && selectedMeta.name !== undefined && selectedMeta.name !== null ? -
    -
    - {isMobile ? null : ( - - - - - - )} - {isMobile ? null : ( -
    - )} - - {selectedMeta.read_time} minute - {selectedMeta.read_time === 1 ? "" : "s"} to read - -
    -
    - {isMobile || - selectedMeta.contributors === undefined || - selectedMeta.contributors === null ? ( - "" - ) : ( -
    - {selectedMeta.contributors.slice(0, 7).map((data, index) => { - return ( - - - {data.url} - - - ); - })} -
    - )} -
    -
    - : null} +
    + {selectedMeta !== undefined && selectedMeta !== null && Object.getOwnPropertyNames(selectedMeta).length > 0 && selectedMeta.name !== undefined && selectedMeta.name !== null ? +
    +
    + {isMobile ? null : ( + + + + + + )} + {isMobile ? null : ( +
    + )} + + {selectedMeta.read_time} minute + {selectedMeta.read_time === 1 ? "" : "s"} to read + +
    +
    + {isMobile || + selectedMeta.contributors === undefined || + selectedMeta.contributors === null ? ( + "" + ) : ( +
    + {selectedMeta.contributors.slice(0, 7).map((data, index) => { + return ( + + + {data.url} + + + ); + })} +
    + )} +
    +
    + : null} - - {selectedApp.documentation} - -
    + + {selectedApp.documentation} + +
    )}
    ) : null; - const tenzirConfigModal = !tenzirConfigModalOpen ? null : - - -
    Run a Tenzir Pipeline
    - - Runs a Tenzir pipeline. You can use the output of the pipeline in your workflow. - -
    - -
    - Pipeline - -
    - -
    - - - - -
    + placeholder={""} + defaultValue={selectedOption} + /> +
    - const SuggestionBoxUi = () => { - const [suggestionValue, setSuggestionValue] = useState(""); - const [suggestionLoading, setSuggestionLoading] = useState(false); - const [responseMsg, setResponseMsg] = useState(""); + - if (suggestionBox === undefined || suggestionBox.open === false) { - return false - } + + + + - return ( -
    - {/* + const SuggestionBoxUi = () => { + const [suggestionValue, setSuggestionValue] = useState(""); + const [suggestionLoading, setSuggestionLoading] = useState(false); + const [responseMsg, setResponseMsg] = useState(""); + + if (suggestionBox === undefined || suggestionBox.open === false) { + return false + } + + return ( +
    + {/* */} - - { - e.preventDefault(); - setSuggestionBox({ - "position": { - "top": 500, - "left": 500, - }, - "open": false, - "value": "", - "loading": false, - }); - }} - > - - - - { - e.preventDefault(); - aiSubmit(suggestionValue, setResponseMsg, setSuggestionLoading) - }}> - { - setSuggestionValue(e.target.value) - }} - InputProps={{ - endAdornment: ( - - - { - e.preventDefault(); - aiSubmit(suggestionValue, setResponseMsg, setSuggestionLoading) - }} /> - - - ), - }} - /> - - {suggestionLoading === true ? - - : null} - {responseMsg.length > 0 ? - - {responseMsg} - - : null} -
    - ) - } + + { + e.preventDefault(); + setSuggestionBox({ + "position": { + "top": 500, + "left": 500, + }, + "open": false, + "value": "", + "loading": false, + }); + }} + > + + + +
    { + e.preventDefault(); + aiSubmit(suggestionValue, setResponseMsg, setSuggestionLoading) + }}> + { + setSuggestionValue(e.target.value) + }} + InputProps={{ + endAdornment: ( + + + { + e.preventDefault(); + aiSubmit(suggestionValue, setResponseMsg, setSuggestionLoading) + }} /> + + + ), + }} + /> + + {suggestionLoading === true ? + + : null} + {responseMsg.length > 0 ? + + {responseMsg} + + : null} +
    + ) + } - - /*else if (selectedRevision === undefined || selectedRevision === null || selectedRevision == {} && originalWorkflow !== undefined && originalWorkflow !== null && originalWorkflow !== {}) { - console.log("Setting original workflow as selected revision") - setSelectedRevision(originalWorkflow) - }*/ - const RevisionBox = (props) => { - const { revision, showBorder, } = props - if (revision === undefined || revision === null) { - return null - } + /*else if (selectedRevision === undefined || selectedRevision === null || selectedRevision == {} && originalWorkflow !== undefined && originalWorkflow !== null && originalWorkflow !== {}) { + console.log("Setting original workflow as selected revision") + setSelectedRevision(originalWorkflow) + }*/ - var newrevision = JSON.parse(JSON.stringify(revision)) - // Make unix timestamp into ISO timestamp in the format July 27th, 3:05 AM - // Format: July 27th, 3:05 AM - //console.log("Edited time: ", revision.edited) - // Convert 1692128391 to valid timestamp - const validTimestamp = newrevision.edited.toString().length === 10 ? newrevision.edited * 1000 : newrevision.edited - const translatedDate = new Date(validTimestamp).toLocaleString('en-US', { month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric', hour12: true }) + const RevisionBox = (props) => { + const { revision, showBorder, } = props + if (revision === undefined || revision === null) { + return null + } - var workflowStatus = newrevision.status !== undefined && newrevision.status !== null && newrevision.status !== "" ? newrevision.status : "test" - if (newrevision.name !== undefined && newrevision.name !== null && newrevision.name !== "") { - if (newrevision.name.toLowerCase().includes("test")) { - workflowStatus = "test" - } + var newrevision = JSON.parse(JSON.stringify(revision)) + // Make unix timestamp into ISO timestamp in the format July 27th, 3:05 AM + // Format: July 27th, 3:05 AM + //console.log("Edited time: ", revision.edited) + // Convert 1692128391 to valid timestamp + const validTimestamp = newrevision.edited.toString().length === 10 ? newrevision.edited * 1000 : newrevision.edited + const translatedDate = new Date(validTimestamp).toLocaleString('en-US', { month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric', hour12: true }) - if (newrevision.name.toLowerCase().includes("dev") || newrevision.name.toLowerCase().includes("staging") || newrevision.name.toLowerCase().includes("rollback")) { - workflowStatus = "dev" - } + var workflowStatus = newrevision.status !== undefined && newrevision.status !== null && newrevision.status !== "" ? newrevision.status : "test" + if (newrevision.name !== undefined && newrevision.name !== null && newrevision.name !== "") { + if (newrevision.name.toLowerCase().includes("test")) { + workflowStatus = "test" + } - if (newrevision.name.toLowerCase().includes("prod") || newrevision.name.toLowerCase().includes("main")) { - workflowStatus = "prod" - } - } + if (newrevision.name.toLowerCase().includes("dev") || newrevision.name.toLowerCase().includes("staging") || newrevision.name.toLowerCase().includes("rollback")) { + workflowStatus = "dev" + } - return ( - { + if (newrevision.name.toLowerCase().includes("prod") || newrevision.name.toLowerCase().includes("main")) { + workflowStatus = "prod" + } + } + + return ( + { setRightSideBarOpen(false) - if (newrevision.edited === selectedVersion.edited) { - console.log("Same revision! No setting.") - return - } + if (newrevision.edited === selectedVersion.edited) { + console.log("Same revision! No setting.") + return + } - // Should render if it's not the same as workflow.edited - console.log("Clicked revision: ", newrevision) - setLastSaved(false) - setSelectedVersion(newrevision); - setWorkflow(newrevision) - setSelectedAction({}); - setSelectedApp({}) + // Should render if it's not the same as workflow.edited + console.log("Clicked revision: ", newrevision) + setLastSaved(false) + setSelectedVersion(newrevision); + setWorkflow(newrevision) + setSelectedAction({}); + setSelectedApp({}) - // Remove all cytoscape triggers first? - if (cy !== undefined && cy !== null) { - cy.removeListener("select"); - cy.removeListener("unselect"); + // Remove all cytoscape triggers first? + if (cy !== undefined && cy !== null) { + cy.removeListener("select"); + cy.removeListener("unselect"); - cy.removeListener("add"); - cy.removeListener("remove"); + cy.removeListener("add"); + cy.removeListener("remove"); - cy.removeListener("mouseover"); - cy.removeListener("mouseout"); + cy.removeListener("mouseover"); + cy.removeListener("mouseout"); - cy.removeListener("drag"); - cy.removeListener("free"); - cy.removeListener("cxttap"); + cy.removeListener("drag"); + cy.removeListener("free"); + cy.removeListener("cxttap"); - setElements([]) + setElements([]) - cy.remove('*') - cy.edges().remove() - cy.nodes().remove() - } + cy.remove('*') + cy.edges().remove() + cy.nodes().remove() + } - // Remove all cy nodes - setTimeout(() => { - //toast("Running setupgraph with new revision. Actions: " + newrevision.actions.length) - setupGraph(newrevision) - }, 250) + // Remove all cy nodes + setTimeout(() => { + //toast("Running setupgraph with new revision. Actions: " + newrevision.actions.length) + setupGraph(newrevision) + }, 250) - // Re-adding cytoscape triggers - if (cy !== undefined && cy !== null) { - cy.on("select", "node", (e) => { - onNodeSelect(e, appAuthentication); - }); - cy.on("select", "edge", (e) => onEdgeSelect(e)); + // Re-adding cytoscape triggers + if (cy !== undefined && cy !== null) { + cy.on("select", "node", (e) => { + onNodeSelect(e, appAuthentication); + }); + cy.on("select", "edge", (e) => onEdgeSelect(e)); - cy.on("unselect", (e) => onUnselect(e)); + cy.on("unselect", (e) => onUnselect(e)); - cy.on("add", "node", (e) => onNodeAdded(e)); - cy.on("add", "edge", (e) => onEdgeAdded(e)); - cy.on("remove", "node", (e) => onNodeRemoved(e)); - cy.on("remove", "edge", (e) => onEdgeRemoved(e)); + cy.on("add", "node", (e) => onNodeAdded(e)); + cy.on("add", "edge", (e) => onEdgeAdded(e)); + cy.on("remove", "node", (e) => onNodeRemoved(e)); + cy.on("remove", "edge", (e) => onEdgeRemoved(e)); - cy.on("mouseover", "edge", (e) => onEdgeHover(e)); - cy.on("mouseout", "edge", (e) => onEdgeHoverOut(e)); - cy.on("mouseover", "node", (e) => onNodeHover(e)); - cy.on("mouseout", "node", (e) => onNodeHoverOut(e)); + cy.on("mouseover", "edge", (e) => onEdgeHover(e)); + cy.on("mouseout", "edge", (e) => onEdgeHoverOut(e)); + cy.on("mouseover", "node", (e) => onNodeHover(e)); + cy.on("mouseout", "node", (e) => onNodeHoverOut(e)); - // Handles dragging - cy.on("drag", "node", (e) => onNodeDrag(e, selectedAction)); - cy.on("free", "node", (e) => onNodeDragStop(e, selectedAction)); + // Handles dragging + cy.on("drag", "node", (e) => onNodeDrag(e, selectedAction)); + cy.on("free", "node", (e) => onNodeDragStop(e, selectedAction)); - cy.on("cxttap", "node", (e) => onCtxTap(e)); - + cy.on("cxttap", "node", (e) => onCtxTap(e)); - if (selectedAction.id !== undefined && selectedAction.id !== null && selectedAction.id !== "") { - setTimeout(() => { - const foundaction = cy.$id(selectedAction.id) - if (foundaction !== undefined && foundaction !== null) { - foundaction.select() - } - }, 250) - } - } + + if (selectedAction.id !== undefined && selectedAction.id !== null && selectedAction.id !== "") { + setTimeout(() => { + const foundaction = cy.$id(selectedAction.id) + if (foundaction !== undefined && foundaction !== null) { + foundaction.select() + } + }, 250) + } + } - // Need to run through graph setup with this one - }}> -
    - - - {translatedDate} - {/* {newrevision.edited.toString().slice(6,10)} | {newrevision.revision_id.slice(0,5)} */} - - - - - - - -
    - {/*revision.edited === originalWorkflow.edited ? + // Need to run through graph setup with this one + }}> +
    + + + {translatedDate} + {/* {newrevision.edited.toString().slice(6,10)} | {newrevision.revision_id.slice(0,5)} */} + + + + + + + +
    + {/*revision.edited === originalWorkflow.edited ? Current version : null*/} -
    - {revision.actions !== undefined && revision.actions !== null ? - - - - - {revision.actions.length} - - - - : null} - {revision.triggers !== undefined && revision.triggers !== null ? - - - - - {revision.triggers.length} - - - - : null} -
    - {revision.updated_by !== undefined && revision.updated_by !== null && revision.updated_by !== "" ? - - {revision.updated_by} - - : null} -
    - ) - } +
    + {revision.actions !== undefined && revision.actions !== null ? + + + + + {revision.actions.length} + + + + : null} + {revision.triggers !== undefined && revision.triggers !== null ? + + + + + {revision.triggers.length} + + + + : null} +
    + {revision.updated_by !== undefined && revision.updated_by !== null && revision.updated_by !== "" ? + + {revision.updated_by} + + : null} +
    + ) + } - const drawerData = originalWorkflow !== undefined && originalWorkflow !== null ? -
    - - Version History - - - Versions are stored for every change made, up to once per minute. When restoring a version, the changes will not take effect until you save the workflow. - - -
    - {/* + const drawerData = originalWorkflow !== undefined && originalWorkflow !== null ? +
    + + Version History + + + Versions are stored for every change made, up to once per minute. When restoring a version, the changes will not take effect until you save the workflow. + + +
    + {/*
    @@ -22500,85 +22547,85 @@ const releaseToConnectLabel = "Release to Connect" */} - {allRevisions.length > 0 ? -
    - { - allRevisions.map((revision, index) => { - /* - if(revision.edited === selectedVersion.edited){ - return null - } - */ + {allRevisions.length > 0 ? +
    + { + allRevisions.map((revision, index) => { + /* + if(revision.edited === selectedVersion.edited){ + return null + } + */ - return ( - - ) - }) - } -
    + showBorder={revision.edited === selectedVersion.edited} + /> + ) + }) + } +
    - : -
    - - No other revisions found. Save your workflow with changes to create a revision. - -
    - } -
    + : +
    + + No other revisions found. Save your workflow with changes to create a revision. + +
    + } +
    - : null + : null - const workflowRevisions = !showWorkflowRevisions ? null : -
    - { - //setShowWorkflowRevisions(false) - }} - style={{ - resize: "both", - overflow: "hidden", - zIndex: 10005, - }} - hideBackdrop={true} - variant="persistent" - BackdropProps={{ - style: { - //backgroundColor: "transparent", - } - }} - PaperProps={{ - style: { - resize: "both", - overflow: "hidden", - minWidth: isMobile ? "100%" : 360, - maxWidth: isMobile ? "100%" : 360, - backgroundColor: theme.palette.platformColor, - color: "white", - fontSize: 18, - zIndex: 15001, - borderRight: theme.palette.defaultBorder, + const workflowRevisions = !showWorkflowRevisions ? null : +
    + { + //setShowWorkflowRevisions(false) + }} + style={{ + resize: "both", + overflow: "hidden", + zIndex: 10005, + }} + hideBackdrop={true} + variant="persistent" + BackdropProps={{ + style: { + //backgroundColor: "transparent", + } + }} + PaperProps={{ + style: { + resize: "both", + overflow: "hidden", + minWidth: isMobile ? "100%" : 360, + maxWidth: isMobile ? "100%" : 360, + backgroundColor: theme.palette.platformColor, + color: "white", + fontSize: 18, + zIndex: 15001, + borderRight: theme.palette.defaultBorder, - paddingLeft: leftSideBarOpenByClick ? 280 : 100, - transition: "padding-left 0.3s", + paddingLeft: leftSideBarOpenByClick ? 280 : 100, + transition: "padding-left 0.3s", - borderRadius: theme.palette.borderRadius, - backgroundColor: "black", - }, - }} - > - {drawerData} - -
    -
    - {/*selectedRevision.edited !== undefined && selectedRevision.edited !== null && selectedRevision.edited !== originalWorkflow.edited ? + borderRadius: theme.palette.borderRadius, + backgroundColor: "black", + }, + }} + > + {drawerData} + +
    +
    + {/*selectedRevision.edited !== undefined && selectedRevision.edited !== null && selectedRevision.edited !== originalWorkflow.edited ?
    -
    - - {selectedVersion?.name} - -
    -
    - -
    -
    -
    +
    +
    + + {selectedVersion?.name} + +
    +
    + +
    +
    +
    - const changeActionParameterCodeMirror = (event, count, data, actionlist, parametername) => { + const changeActionParameterCodeMirror = (event, count, data, actionlist, parametername) => { - // FIXME: This exists ONLY to make sure focus + blur actually changes the field value - // in fields from ParsedAction.jsx such as rightside_field_2 - const simulateTyping = (inputElement, text) => { - if (!inputElement) { - console.error("Target element not found!") - return; - } + // FIXME: This exists ONLY to make sure focus + blur actually changes the field value + // in fields from ParsedAction.jsx such as rightside_field_2 + const simulateTyping = (inputElement, text) => { + if (!inputElement) { + console.error("Target element not found!") + return; + } - //console.log("Simulating typing for: ", text, "in", inputElement) + //console.log("Simulating typing for: ", text, "in", inputElement) - inputElement.value = "" - setTimeout(() => { - text.split("").forEach((char, index) => { - setTimeout(() => { - inputElement.value = text.slice(0, index + 1) + inputElement.value = "" + setTimeout(() => { + text.split("").forEach((char, index) => { + setTimeout(() => { + inputElement.value = text.slice(0, index + 1) - // Simulate an event object like React's synthetic event - const event = { - target: { - value: inputElement.value, - }, - }; + // Simulate an event object like React's synthetic event + const event = { + target: { + value: inputElement.value, + }, + }; - // Find the onChange handler, assuming you're calling it from here - if (typeof inputElement.onchange === 'function') { - inputElement.onchange(event); // Call onChange with the synthetic event - } + // Find the onChange handler, assuming you're calling it from here + if (typeof inputElement.onchange === 'function') { + inputElement.onchange(event); // Call onChange with the synthetic event + } - // Dispatch the input event for React's internal event system - const inputEvent = new Event("input", { bubbles: true }); - inputElement.dispatchEvent(inputEvent); + // Dispatch the input event for React's internal event system + const inputEvent = new Event("input", { bubbles: true }); + inputElement.dispatchEvent(inputEvent); - }, 10) - }) - }, 50) + }, 10) + }) + }, 50) - setTimeout(() => { - inputElement.focus() - }, 500) - }; + setTimeout(() => { + inputElement.focus() + }, 500) + }; - // Check if event.target.value is an array. If it is, split with comma - if (parametername !== undefined && parametername.startsWith("${") && parametername.endsWith("}")) { - var paramcheckIndex = selectedAction.parameters.findIndex(param => param.name === parametername) - if (paramcheckIndex !== -1) { - // Replace the value in the field - const toReplace = data.replaceAll("\\\"", "\"").replaceAll("\"", "\\\""); - selectedAction.parameters[paramcheckIndex].value = toReplace - setSelectedAction(selectedAction) - setUpdate(Math.random()) + // Check if event.target.value is an array. If it is, split with comma + if (parametername !== undefined && parametername.startsWith("${") && parametername.endsWith("}")) { + var paramcheckIndex = selectedAction.parameters.findIndex(param => param.name === parametername) + if (paramcheckIndex !== -1) { + // Replace the value in the field + const toReplace = data.replaceAll("\\\"", "\"").replaceAll("\"", "\\\""); + selectedAction.parameters[paramcheckIndex].value = toReplace + setSelectedAction(selectedAction) + setUpdate(Math.random()) - // Find the fieldname - const clickedFieldId = "rightside_field_" + count; - const clickedField = document.getElementById(clickedFieldId) - if (clickedField !== undefined && clickedField !== null) { - simulateTyping(clickedField, toReplace) + // Find the fieldname + const clickedFieldId = "rightside_field_" + count; + const clickedField = document.getElementById(clickedFieldId) + if (clickedField !== undefined && clickedField !== null) { + simulateTyping(clickedField, toReplace) - /* - clickedField.value = toReplace - const newEvent = new Event("input", { bubbles: true }) - Object.defineProperty(event, "target", { - value: { ...clickedField, value: toReplace }, - writable: false, - }) + /* + clickedField.value = toReplace + const newEvent = new Event("input", { bubbles: true }) + Object.defineProperty(event, "target", { + value: { ...clickedField, value: toReplace }, + writable: false, + }) - //const newEvent = new Event("input", { bubbles: true }) - clickedField.dispatchEvent(newEvent) - console.log("Found field: ", clickedField) - */ - } + //const newEvent = new Event("input", { bubbles: true }) + clickedField.dispatchEvent(newEvent) + console.log("Found field: ", clickedField) + */ + } - //toast("Replaced field!") + //toast("Replaced field!") - return - } - } - - if (data.startsWith("${") && data.endsWith("}")) { - console.log("Changing field with variable: ", data) + return + } + } - // PARAM FIX - Gonna use the ID field, even though it's a hack - var paramcheck = selectedAction.parameters.find(param => param.name === "body") - if (paramcheck !== undefined) { - // Escapes all double quotes - const toReplace = event.target.value.replaceAll("\\\"", "\"").replaceAll("\"", "\\\""); - if (paramcheck["value_replace"] === undefined || paramcheck["value_replace"] === null) { - paramcheck["value_replace"] = [{ - "key": data.name, - "value": toReplace, - }] + if (data.startsWith("${") && data.endsWith("}")) { + console.log("Changing field with variable: ", data) - } else { - const subparamindex = paramcheck["value_replace"].findIndex(param => param.key === data.name) - if (subparamindex === -1) { - paramcheck["value_replace"].push({ - "key": data.name, - "value": toReplace, - }) - } else { - paramcheck["value_replace"][subparamindex]["value"] = toReplace - } - } + // PARAM FIX - Gonna use the ID field, even though it's a hack + var paramcheck = selectedAction.parameters.find(param => param.name === "body") + if (paramcheck !== undefined) { + // Escapes all double quotes + const toReplace = event.target.value.replaceAll("\\\"", "\"").replaceAll("\"", "\\\""); + if (paramcheck["value_replace"] === undefined || paramcheck["value_replace"] === null) { + paramcheck["value_replace"] = [{ + "key": data.name, + "value": toReplace, + }] - if (paramcheck["value_replace"] === undefined) { - selectedAction.parameters[count]["value_replace"] = paramcheck - } else { - //selectedActionParameters[count]["value_replace"] = paramcheck["value_replace"] - selectedAction.parameters[count]["value_replace"] = paramcheck["value_replace"] - } + } else { + const subparamindex = paramcheck["value_replace"].findIndex(param => param.key === data.name) + if (subparamindex === -1) { + paramcheck["value_replace"].push({ + "key": data.name, + "value": toReplace, + }) + } else { + paramcheck["value_replace"][subparamindex]["value"] = toReplace + } + } - setSelectedAction(selectedAction) - return - } - } + if (paramcheck["value_replace"] === undefined) { + selectedAction.parameters[count]["value_replace"] = paramcheck + } else { + //selectedActionParameters[count]["value_replace"] = paramcheck["value_replace"] + selectedAction.parameters[count]["value_replace"] = paramcheck["value_replace"] + } - if (event.target.value[event.target.value.length-1] === "." && actionlist.length > 0) { - var curstring = "" - var record = false - for (let [key,keyval] in Object.entries(selectedAction.parameters[count].value)) { - const item = selectedAction.parameters[count].value[key] - if (record) { - curstring += item - } + setSelectedAction(selectedAction) + return + } + } - if (item === "$") { - record = true - curstring = "" - } - } + if (event.target.value[event.target.value.length - 1] === "." && actionlist.length > 0) { + var curstring = "" + var record = false + for (let [key, keyval] in Object.entries(selectedAction.parameters[count].value)) { + const item = selectedAction.parameters[count].value[key] + if (record) { + curstring += item + } - if (curstring.length > 0 && actionlist !== null) { - // Search back in the action list - curstring = curstring.split(" ").join("_").toLowerCase() - var actionItem = actionlist.find(data => data.autocomplete.split(" ").join("_").toLowerCase() === curstring) - if (actionItem !== undefined) { - console.log("Found item: ", actionItem) + if (item === "$") { + record = true + curstring = "" + } + } - var jsonvalid = true - try { - const tmp = String(JSON.parse(actionItem.example)) - if (!actionItem.example.includes("{") && !actionItem.example.includes("[")) { - jsonvalid = false - } - } catch (e) { - jsonvalid = false - } - } - } - } + if (curstring.length > 0 && actionlist !== null) { + // Search back in the action list + curstring = curstring.split(" ").join("_").toLowerCase() + var actionItem = actionlist.find(data => data.autocomplete.split(" ").join("_").toLowerCase() === curstring) + if (actionItem !== undefined) { + console.log("Found item: ", actionItem) - if (selectedAction.app_name === "Shuffle Tools" && selectedAction.name === "filter_list" && count === 0) { - const parsedvalue = data - if (parsedvalue.includes("#")) { - const splitparsed = parsedvalue.split(".#.") - //console.log("Cant contain #: ", splitparsed) - if (splitparsed.length > 1) { - //data.value = splitparsed[0] + var jsonvalid = true + try { + const tmp = String(JSON.parse(actionItem.example)) + if (!actionItem.example.includes("{") && !actionItem.example.includes("[")) { + jsonvalid = false + } + } catch (e) { + jsonvalid = false + } + } + } + } - selectedAction.parameters[0].value = splitparsed[0] - selectedAction.parameters[1].value = splitparsed[1] + if (selectedAction.app_name === "Shuffle Tools" && selectedAction.name === "filter_list" && count === 0) { + const parsedvalue = data + if (parsedvalue.includes("#")) { + const splitparsed = parsedvalue.split(".#.") + //console.log("Cant contain #: ", splitparsed) + if (splitparsed.length > 1) { + //data.value = splitparsed[0] - selectedAction.parameters[0].autocompleted = true - selectedAction.parameters[1].autocompleted = true - setUpdate(Math.random()) - } - } - } else { - if (selectedAction.parameters !== undefined && selectedAction.parameters !== null && selectedAction.parameters.length > count) { - selectedAction.parameters[count].autocompleted = false - selectedAction.parameters[count].value = data - } - } + selectedAction.parameters[0].value = splitparsed[0] + selectedAction.parameters[1].value = splitparsed[1] - setSelectedAction(selectedAction) - //setUpdate(Math.random()) - } + selectedAction.parameters[0].autocompleted = true + selectedAction.parameters[1].autocompleted = true + setUpdate(Math.random()) + } + } + } else { + if (selectedAction.parameters !== undefined && selectedAction.parameters !== null && selectedAction.parameters.length > count) { + selectedAction.parameters[count].autocompleted = false + selectedAction.parameters[count].value = data + } + } + + setSelectedAction(selectedAction) + //setUpdate(Math.random()) + } /* var foundusecase = {} if (workflow.actions !== undefined && workflow.actions !== null && workflow.actions.length > 0 && userdata !== undefined && userdata !== null && userdata.priorities !== undefined && userdata.priorities !== null && userdata.priorities.length > 0) { - for (let priokey in userdata.priorities) { - const prio = userdata.priorities[priokey] - if (prio.type !== "usecase") { - continue - } + for (let priokey in userdata.priorities) { + const prio = userdata.priorities[priokey] + if (prio.type !== "usecase") { + continue + } - const descsplit = prio.description.split("&") - var srcapp = "" - var dstapp = "" - if (descsplit.length > 0) { - srcapp = descsplit[0].toLowerCase().replaceAll(" ", "_") + const descsplit = prio.description.split("&") + var srcapp = "" + var dstapp = "" + if (descsplit.length > 0) { + srcapp = descsplit[0].toLowerCase().replaceAll(" ", "_") - if (descsplit.length > 2) { - dstapp = descsplit[2].toLowerCase().replaceAll(" ", "_") - } - } + if (descsplit.length > 2) { + dstapp = descsplit[2].toLowerCase().replaceAll(" ", "_") + } + } - if (srcapp.length > 0 && dstapp.length > 0) { - for (let actionkey in workflow.actions) { - const curaction = workflow.actions[actionkey] - const appname = curaction.app_name.toLowerCase().replaceAll(" ", "_") - if (appname === srcapp || appname === dstapp) { - foundusecase = prio - break - } - } - } + if (srcapp.length > 0 && dstapp.length > 0) { + for (let actionkey in workflow.actions) { + const curaction = workflow.actions[actionkey] + const appname = curaction.app_name.toLowerCase().replaceAll(" ", "_") + if (appname === srcapp || appname === dstapp) { + foundusecase = prio + break + } + } + } - if (foundusecase.name !== undefined && foundusecase.name !== null && foundusecase.name !== "") { - break - } - } + if (foundusecase.name !== undefined && foundusecase.name !== null && foundusecase.name !== "") { + break + } + } } const templatePopup = foundusecase.name === undefined || foundusecase.name === null || foundusecase.name === "" ? null : - -
    - +
    + -
    - - */ + srcapp={foundusecase.description.split("&")[0]} + img1={foundusecase.description.split("&")[1]} + dstapp={foundusecase.description.split("&")[2]} + img2={foundusecase.description.split("&")[3]} + /> +
    +
    + */ const loadedCheck = isLoaded && workflowDone ? ( -
    +
    {newView} - {aiQueryModal} + {aiQueryModal} {conditionsModal} {codePopoutModal} - {workflowRevisions} + {workflowRevisions} {authenticationModal} {tenzirConfigModal} {/*editWorkflowModal*/} - {authgroupModal} - {executionArgumentModal} + {authgroupModal} + {executionArgumentModal} {configureWorkflowModal} - {/*usecaseSlidein*/} + {/*usecaseSlidein*/} - + - {codeEditorModalOpen ? - - : null} + setAiQueryModalOpen={setAiQueryModalOpen} + /> + : null} {editWorkflowModalOpen === true ? : null} - + {/*selectionOpen === true ?
    @@ -22942,12 +22989,12 @@ const releaseToConnectLabel = "Release to Connect"
    : null*/} -
    +
    {showVideo !== undefined && showVideo.length > 0 ?
    @@ -22988,12 +23035,12 @@ const releaseToConnectLabel = "Release to Connect" />
    ) : ( -
    - - - Loading Workflow & Apps... - -
    +
    + + + Loading Workflow & Apps... + +
    ); // Awful way of handling scroll diff --git a/frontend/src/views/AppCreator.jsx b/frontend/src/views/AppCreator.jsx index 8e474eed..3ce3cf09 100755 --- a/frontend/src/views/AppCreator.jsx +++ b/frontend/src/views/AppCreator.jsx @@ -5435,6 +5435,7 @@ const AppCreator = (defaultprops) => { minWidth: 174, minHeight: 174, objectFit: "contain", + borderRadius: theme.palette?.borderRadius, }} /> ); @@ -5450,6 +5451,7 @@ const AppCreator = (defaultprops) => { margin: "auto", marginTop: 30, marginLeft: 40, + borderRadius: theme.palette?.borderRadius, }} onClick={() => { upload.click(); @@ -5832,6 +5834,7 @@ const AppCreator = (defaultprops) => { //setOpenApiModal(true) toast.info("Action merging & fork management coming soon") }} + disabled={true} style={{marginLeft: 10, }} > { href="https://shuffler.io/docs/app_creation#app-creator-instructions" style={{ textDecoration: "none", color: "#f85a3e" }} > - Click here to learn more about app creation + Click to learn more about app creation
    { { style={{ width: 150, backgroundColor: theme.palette.surfaceColor, - backgroundColor: inputColor, color: "white", height: 35, marginleft: 10, diff --git a/frontend/src/views/Apps2.jsx b/frontend/src/views/Apps2.jsx index 10b9cd78..1b62cf14 100644 --- a/frontend/src/views/Apps2.jsx +++ b/frontend/src/views/Apps2.jsx @@ -1853,6 +1853,7 @@ const Apps2 = (props) => { app={selectedApp} userdata={userdata} globalUrl={globalUrl} + getApps={getApps} /> { maxHeight: 500, position: "absolute", left: 150, - top: 0, + top: 75, border: "1px solid rgba(255,255,255,0.3)", borderRadius: theme.palette?.borderRadius, diff --git a/frontend/src/views/Usecases2.jsx b/frontend/src/views/Usecases2.jsx index dd7fc19d..3e64f723 100644 --- a/frontend/src/views/Usecases2.jsx +++ b/frontend/src/views/Usecases2.jsx @@ -621,7 +621,7 @@ const UsecaseListComponent = (props) => { parsedUsecase.dstapp = newsubcase.dstapp - var workflowBuilt = false + var workflowBuilt = "" const newname = subcase.name.toLowerCase().replaceAll(" ", "_") for (var workflowkey in workflows) { const workflow = workflows[workflowkey] @@ -635,7 +635,7 @@ const UsecaseListComponent = (props) => { //console.log("WORKFLOW: ", newname, newusecases) if (newusecases.includes(newname)) { - workflowBuilt = true + workflowBuilt = workflow.id break } } @@ -676,6 +676,7 @@ const UsecaseListComponent = (props) => { showTryit={false} shownColor={""} workflowBuilt={workflowBuilt} + inputWorkflowId={workflowBuilt} usecaseDetails={usecaseDetails} /> diff --git a/frontend/src/views/Workflows2.jsx b/frontend/src/views/Workflows2.jsx index c58001da..b659ab7b 100644 --- a/frontend/src/views/Workflows2.jsx +++ b/frontend/src/views/Workflows2.jsx @@ -92,6 +92,7 @@ import { ArrowRight as ArrowRightIcon, Visibility as VisibilityIcon, EditNote as EditNoteIcon, + ErrorOutline as ErrorOutlineIcon, } from "@mui/icons-material"; // Additional Components @@ -108,6 +109,8 @@ import { InstantSearch, Configure, connectHits, connectSearchBox, connectRefinem import { debounce } from "lodash"; import { removeQuery } from "../components/ScrollToTop.jsx"; +import {green, yellow, red, grey } from "../views/AngularWorkflow.jsx" + const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240"); @@ -1114,12 +1117,9 @@ const Workflows2 = (props) => {
    ) : null} + {(data.sharing !== undefined && data.sharing !== null && data.sharing === "form") || (data?.form_control?.input_markdown !== undefined && data?.form_control?.input_markdown !== null && data?.form_control?.input_markdown !== "") && type !== "public" ?
    @@ -2821,10 +2822,33 @@ const Workflows2 = (props) => { > - {workflowMenuButtons}
    - : null} + : null} + + {(data?.validation?.validation_ran === true && data?.validation?.valid === false && data?.validation?.errors?.length > 0 ) ? + +
    + { + window.open(`/admin?admin_tab=notifications&workflow_id=${data.id}`, "_blank") + }} + style={{ + padding: "0px", + color: "#979797", + }} + > + + +
    +
    + + : null}
    From 68a4652da3e50f5792a4c397e84661341faa8a39 Mon Sep 17 00:00:00 2001 From: Frikky Date: Mon, 3 Feb 2025 11:32:37 +0100 Subject: [PATCH 22/67] Last sync for onprem --- frontend/src/components/ApiExplorer.jsx | 25 +- .../src/components/ShuffleCodeEditor1.jsx | 335 ++++++++++++++++-- frontend/src/views/ApiExplorerWrapper.jsx | 74 +++- frontend/src/views/AppCreator.jsx | 6 + 4 files changed, 378 insertions(+), 62 deletions(-) diff --git a/frontend/src/components/ApiExplorer.jsx b/frontend/src/components/ApiExplorer.jsx index 7e3092ea..8cdb3f0c 100644 --- a/frontend/src/components/ApiExplorer.jsx +++ b/frontend/src/components/ApiExplorer.jsx @@ -1183,6 +1183,7 @@ const ApiExplorer = memo(({ openapi, globalUrl, userdata, HandleApiExecution, se } } } + var prefixCheck = "/v1"; if (parentUrl.includes("/")) { const urlsplit = parentUrl.split("/"); @@ -1242,7 +1243,7 @@ const ApiExplorer = memo(({ openapi, globalUrl, userdata, HandleApiExecution, se } } - newActions = newActions2; + newActions = newActions2 // Rearrange them by which has action_label const firstActions = newActions.filter( @@ -1257,9 +1258,9 @@ const ApiExplorer = memo(({ openapi, globalUrl, userdata, HandleApiExecution, se data.action_label === null || data.action_label === "No Label" ); - newActions = firstActions.concat(secondActions); - setActions(newActions); - setExampleBody(newActions[0]?.body); + newActions = firstActions.concat(secondActions) + setActions(newActions) + setExampleBody(newActions[0]?.body) }, [openapi]); return ( @@ -1546,7 +1547,7 @@ const ActionsList = memo(({ overflow: "hidden", }} > - {action.name.replaceAll("_", " ")} + {action?.name?.replaceAll("_", " ")} )) @@ -1798,7 +1799,7 @@ const Action = memo(( response.result = JSON.parse(response.result); } catch (parseError) { console.error("Error parsing result:", parseError); - toast.error("Error parsing response result."); + //toast.error("Error parsing response result."); } } @@ -2228,11 +2229,12 @@ const Action = memo(( }} onChange={(e) => { - const newUrl = e.target.value; + const newUrl = e.target.value setActionUrl(newUrl); const params = extractParamsFromText(newUrl); setRequestParams(params); - + + if (newUrl.startsWith("http://") || newUrl.startsWith("https://")) { try { const url = new URL(newUrl); @@ -2241,9 +2243,12 @@ const Action = memo(( setPath(newPath); } catch (error) { - console.error("Invalid URL:", error); + console.error("Invalid URL:", newUrl, error); + toast("The URL is not a valid one. Please check and try again.") } - } + } else { + //toast("The URL needs to start with http:// or https://") + } }} /> diff --git a/frontend/src/components/ShuffleCodeEditor1.jsx b/frontend/src/components/ShuffleCodeEditor1.jsx index deca44a7..8acc23a8 100644 --- a/frontend/src/components/ShuffleCodeEditor1.jsx +++ b/frontend/src/components/ShuffleCodeEditor1.jsx @@ -15,6 +15,7 @@ import { MenuItem, Button, ButtonGroup, + Collapse, } from '@mui/material'; import theme from '../theme.jsx'; @@ -23,6 +24,7 @@ import { isMobile } from "react-device-detect" import { NestedMenuItem } from "mui-nested-menu" import { GetParsedPaths, FindJsonPath } from "../views/Apps.jsx"; import { SetJsonDotnotation } from "../views/AngularWorkflow.jsx"; +import Draggable from "react-draggable"; import { FullscreenExit as FullscreenExitIcon, @@ -152,6 +154,7 @@ const CodeEditor = (props) => { const [anchorEl3, setAnchorEl3] = React.useState(null); const [mainVariables, setMainVariables] = React.useState([]); const [availableVariables, setAvailableVariables] = React.useState([]); + const [sourceDataOpen, setSourceDataOpen] = React.useState(false); const [codeTheme, setcodeTheme] = React.useState("gruvbox-dark"); @@ -1150,20 +1153,23 @@ const CodeEditor = (props) => { // Define a custom completer for the Ace Editor const customCompleter = { getCompletions: function(editor, session, pos, prefix, callback) { - console.log("CUSTOM COMPLETER: ", prefix) - callback(null, availableVariables.map((variable) => { - console.log("CUSTOM VAR: ", variable) + //console.log("CUSTOM VAR: ", variable) return ({ caption: variable, value: variable, - meta: 'custom', + meta: 'var', }) })) } } + const editorLoad = (editor) => { + console.log("EDITOR: ", editor) + editor.completers = [customCompleter] + } + if (fullScreenMode) { return ( { highlightActiveLine={false} enableBasicAutocompletion={true} - completers={[customCompleter]} style={{ wordBreak: "break-word", @@ -1212,12 +1217,146 @@ const CodeEditor = (props) => { ) } + + const ValueBox = (props) => { + const { name, value } = props + + const [dragging, setDragging] = React.useState(false) + const [hovering, setHovering] = React.useState(false) + + if (name === undefined || name === null || name.length === 0) { + return null + } + + if (value === undefined || value === null || value.length === 0) { + return null + } + + return ( + { + e.preventDefault() + // Check if inside div.ace_content + if (e.srcElement.className === "ace_content") { + // Input on the correct line. Each line is: + // Show some tooltip at mouse cursor that shows "Insert Action" + + // Append the text to the DOM + } else { + //console.log("PAGEX: ", e.pageX, e.pageY) + //console.log("OffsetX: ", e.offsetX, e.offsetY) + //console.log("E: ", e) + } + + if (!dragging) { + setDragging(true) + } + }} + onStop={(e) => { + if (e.srcElement.className === "ace_content") { + console.log("DRAG STOP IN CONTENT!", e.srcElement.className) + + const usedposition = e.offsetY + if (usedposition === undefined || usedposition === null) { + toast.info("Error: LayerY is undefined or null. Please contact support@shuffler.io") + return + } + + if (usedposition === 0) { + usedposition = 1 + } + + const lineheight = 15 + const codedatasplit = localcodedata.split('\n') + if (codedatasplit === undefined || codedatasplit === null || codedatasplit.length === 0) { + return + } + + // Int + const lineposition = parseInt(usedposition/lineheight) + + // Find the correct line + if (lineposition > codedatasplit.length) { + codedatasplit[codedatasplit.length-1] += value + } else { + codedatasplit[lineposition] += value + } + + + //e.srcElement.layerY + setlocalcodedata(codedatasplit.join('\n')) + } + + setDragging(false) + }} + dragging={dragging} + position={{ + x: 0, + y: 0, + }} + onMouseHover={() => { + setHovering(true) + }} + onMouseLeave={() => { + setHovering(false) + }} + > +
    + {value} +
    +
    + ) + } + + const SourceDataOption = (option) => { + const { innerdata, parsedPaths, defaultExpanded } = option + + const [expanded, setExpanded] = React.useState(defaultExpanded === true ? true : false) + + + return ( +
    +
    { + setExpanded(!expanded) + }}> + + + {innerdata?.name} + +
    + + HELO + +
    + ) + } + return ( { @@ -1248,7 +1387,6 @@ const CodeEditor = (props) => { maxHeight: isMobile ? "100%" : 700, border: "3px solid rgba(255,255,255,0.3)", padding: isMobile ? "25px 10px 25px 10px" : 25, - // zoom: 0.8, backgroundColor: "black", }, }} @@ -1308,7 +1446,92 @@ const CodeEditor = (props) => {
    -
    + {sourceDataOpen ? +
    + + Source Data + + + Drag the data you want into the text editor + + + {actionlist?.map((innerdata) => { + const icon = + innerdata.type === "action" ? ( + + ) : innerdata.type === "workflow_variable" || + innerdata.type === "execution_variable" ? ( + + ) : ( + + ); + + const handleExecArgumentHover = (inside) => { + var exec_text_field = document.getElementById( + "execution_argument_input_field" + ); + if (exec_text_field !== null) { + if (inside) { + exec_text_field.style.border = "2px solid #f85a3e"; + } else { + exec_text_field.style.border = ""; + } + } + }; + + const handleActionHover = (inside, actionId) => { + }; + + const handleMouseover = () => { + if (innerdata.type === "Execution Argument") { + handleExecArgumentHover(true); + } else if (innerdata.type === "action") { + handleActionHover(true, innerdata.id); + } + }; + + const handleMouseOut = () => { + if (innerdata.type === "Execution Argument") { + handleExecArgumentHover(false); + } else if (innerdata.type === "action") { + handleActionHover(false, innerdata.id); + } + }; + + var parsedPaths = []; + if (innerdata.type === "workflow_variable") { + // Try to parse the value if it's a string that could be JSON + if (typeof innerdata.value === "string") { + try { + const parsedValue = JSON.parse(innerdata.value) + if (typeof parsedValue === "object") { + parsedPaths = GetParsedPaths(parsedValue, ""); + } + } catch (e) { + // Not valid JSON, use the value directly + parsedPaths = GetParsedPaths(innerdata.value, ""); + } + } else if (typeof innerdata.value === "object") { + parsedPaths = GetParsedPaths(innerdata.value, ""); + } + } else if (typeof innerdata.example === "object") { + parsedPaths = GetParsedPaths(innerdata.example, ""); + } + + const coverColor = "#82ccc3" + + if (innerdata?.name === "Execution Argument") { + innerdata.name = "Runtime Argument" + } + + return ( + + ) + })} +
    + : null} + +
    {isFileEditor ?
    { {isFileEditor ? null :
    + {/* + + */} + + { })} - {
    } -
    +
    { + console.log("DRAGGING OVER: ", e) + }} + onDrop={(e) => { + console.log("DROP: ", e) + }} + > {(availableVariables !== undefined && availableVariables !== null && availableVariables.length > 0) || isFileEditor ? ( { wordWrap: "break-word", backgroundColor: "rgba(40,40,40,1)", zIndex: activeDialog === "codeeditor" ? 1200 : 1100, + + }} onLoad={(editor) => { highlight_variables(localcodedata) + editorLoad(editor) }} onCursorChange={(cursorPosition, editor, value) => { setCurrentCharacter(cursorPosition.cursor.column) @@ -1847,8 +2099,15 @@ const CodeEditor = (props) => {
    - {isFileEditor ? null : -
    + {isFileEditor ? null : +
    {isMobile ? null : { >
    - {selectedAction === undefined ? "" : selectedAction.name === "execute_python" || selectedAction.name === "execute_bash" ? "Code to run" : `Expected Output'`} + {selectedAction === undefined ? "" : selectedAction.name === "execute_python" || selectedAction.name === "execute_bash" ? "Code to run" : `Output`}
    diff --git a/frontend/src/views/ApiExplorerWrapper.jsx b/frontend/src/views/ApiExplorerWrapper.jsx index 57070c8e..dec13a5b 100644 --- a/frontend/src/views/ApiExplorerWrapper.jsx +++ b/frontend/src/views/ApiExplorerWrapper.jsx @@ -95,6 +95,10 @@ const ApiExplorerWrapper = (props) => { }; useEffect(() => { + if (openapi?.id === "HTTP") { + selectedAppData.name = "HTTP" + } + if (selectedAppData !== undefined && selectedAppData !== null && Object.getOwnPropertyNames(selectedAppData).length > 0) { HandleAppAuthentication(selectedAppData?.name) } @@ -136,7 +140,10 @@ const ApiExplorerWrapper = (props) => { const runAlgoliaAppSearch = (appname) => { const index = searchClient.initIndex("appsearch"); - console.log("Running appsearch for: ", appname); + if (appname === "HTTP" || appname === "http") { + navigate("/apis") + return + } index .search(appname) @@ -151,6 +158,7 @@ const ApiExplorerWrapper = (props) => { if (newname?.includes(appsearchname)) { found = true + getAppData(hit.objectID) break } @@ -160,6 +168,7 @@ const ApiExplorerWrapper = (props) => { toast.error(`Failed to get API data for '${appname}' (1). Contact support@shuffler.io if this persists.`, { "autoClose": 10000, }) + setTimeout(()=>{ navigate("/search?tab=apps"); },3000) @@ -181,11 +190,29 @@ const ApiExplorerWrapper = (props) => { // Fetch data when appid is available const getAppData = useCallback((appid) => { if (appid === undefined || appid === null || appid.length === 0) { - toast.error("No API data to load (4). Please contact support@shuffler.io if this persists.") + toast.warning("No app ID loaded. Showing default API testing window. ") + setOpenapi({ + "id": "HTTP", + "servers": [ + {"url": "https://shuffler.io"}, + ], + "info": { + "title": "HTTP", + "x-logo": theme.palette?.defaultImage, + }, + "paths": { + "/api/v1/workflows/usecases": { + "get": { + "summary": "Custom Action", + } + } + } + }) + setAppLoaded(true) - setTimeout(() => { - navigate("/search?tab=apps") - }, 3000) + //setTimeout(() => { + // navigate("/search?tab=apps") + //}, 3000) return } @@ -413,6 +440,13 @@ const ApiExplorerWrapper = (props) => { selectedAppData.name = appname } + console.log("APPNAME: ", appname, openapi.id) + if (openapi?.id === "HTTP" || appname === "HTTP" || appname === "http") { + setAppAuthentication(data) + setSelectedAuthentication({}) + return + } + const filteredData = data.filter((appAuth) => appAuth?.app?.id === appid || appAuth?.app?.name?.replaceAll(" ", "_").toLowerCase() === selectedAppData?.name?.replaceAll(" ", "_").toLowerCase()); if (filteredData.length === 0) { setAppAuthentication([]) @@ -505,7 +539,7 @@ const ApiExplorerWrapper = (props) => { }else if (openapi?.id?.length > 0) { appid = openapi?.id; }else{ - toast.error("App id is missing. Please try again."); + toast.error("App id is missing and we can't run the API. Please contact support@shuffler.io if this persists."); return; } @@ -692,6 +726,7 @@ const ApiExplorerWrapper = (props) => { '& .MuiList-root': { backgroundColor: "#1f1f1f", }, + maxWidth: 500, }, } }} @@ -723,6 +758,7 @@ const ApiExplorerWrapper = (props) => { No Selection + {appAuthentication?.length > 0 ? appAuthentication.map((appAuth) => (
    { backgroundColor: '#1f1f1f', color: 'white', padding: '5px', + textAlign: "left", }} > { @@ -746,6 +782,11 @@ const ApiExplorerWrapper = (props) => { setAuthenticationName(appAuth.app?.name) }} > + {appAuth?.app?.large_image !== undefined && appAuth?.app?.large_image !== null && appAuth?.app?.large_image.length > 0 ? + + {appAuth?.app?.name} + + : null} {appAuth?.validation?.valid === true ? @@ -1645,7 +1686,9 @@ const ApiExplorerWrapper = (props) => { >
    - {isLoggedIn === true ? + {openapi?.id === "HTTP" ? + null + : isLoggedIn === true ? - {!leftSideBarOpenByClick && setExpandLeftNav(true)}} onMouseLeave={()=>{!leftSideBarOpenByClick && setExpandLeftNav(false);setOpenAutocomplete(false)}}> + {(!leftSideBarOpenByClick || window?.location?.pathname?.includes("/workflows/")) && setExpandLeftNav(true)}} onMouseLeave={()=>{(!leftSideBarOpenByClick || window?.location?.pathname?.includes("/workflows/")) && setExpandLeftNav(false);setOpenAutocomplete(false)}}> { sx={{ display: "flex", flexDirection: "row", - marginTop: 1 + marginTop: 0 }} > @@ -1229,7 +1229,7 @@ useEffect(() => { width: 18, height: 18, marginRight: expandLeftNav ? 10 : 0, - color: userdata?.support ? "inherit" : "#6F6F6F", + color: "inherit", }} /> { - + - - ), - - }} - onChange={(e)=>{handleQueryChange(e)}} - color="primary" - placeholder="Filter by Workflow Name, Status, Execution Argument, Results.." - id="shuffle_search_field" - /> -
    - -
    - {selectedWorkflowExecutions.length > 0 ? +
    +

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

    + {selectedWorkflowExecutions.length > 0 ? : null} +
    +
    +
    + + + + ), + endAdornment: ( + + {searchQuery.length > 0 && ( + setSearchQuery('')} + /> + )} + + + ), + + }} + onChange={(e)=>{handleQueryChange(e)}} + color="primary" + placeholder="Filter by Workflow Name, Status, Execution Argument, Results.." + id="shuffle_search_field" + /> +
    + {userdata?.active_org?.creator_org?.length === 0 ? ( +
    + { + setSuborgWorkflowRuns(!suborgWorkflowRuns); + //set selected workflow to all workflows when switching between suborg and all workflows + setWorkflowId("") + setWorkflow({"id": "", "name": "All Workflows"}) + setStatus("") + setStartTime("") + setEndTime("") + setSearchQuery("") + submitSearch("", "", "", "", rowCursor, rowsPerPage, !suborgWorkflowRuns)} + } + color="secondary" + /> + Show workflow runs from suborgs +
    + ) : null} +
    +
    { - submitSearch(workflowId, status, startTime, endTime, rowCursor, rowsPerPage) - }} style={{display: "flex", }}> + submitSearch(workflowId, status, startTime, endTime, rowCursor, rowsPerPage, suborgWorkflowRuns) + }} style={{display: "flex", justifyContent: "center", alignItems: "center", }}> Status { workflow.triggers[ selectedTriggerIndex ].parameters[0].value.includes("127.0.0.1")) ? ( + @@ -15044,7 +15313,6 @@ const AngularWorkflow = (defaultprops) => { onClick={() => { var copyText = document.getElementById("webhook_uri_field"); if (copyText !== undefined && copyText !== null) { - console.log("NAVIGATOR: ", navigator); const clipboard = navigator.clipboard; if (clipboard === undefined) { toast("Can only copy over HTTPS (port 3443)"); @@ -15370,13 +15638,20 @@ const AngularWorkflow = (defaultprops) => { custom_response: custom_response, version: version, version_timeout: 15, - }; + } - console.log("Trigger data: ", data) + var headers = { + "Content-Type": "application/json", + "Accept": "application/json", + } + + if (workflow.org_id !== undefined && workflow.org_id !== null && workflow.org_id.length > 0) { + headers["Org-Id"] = workflow.org_id + } fetch(globalUrl + "/api/v1/hooks/new", { method: "POST", - headers: { "content-type": "application/json" }, + headers: headers, body: JSON.stringify(data), credentials: "include", }) @@ -15390,6 +15665,8 @@ const AngularWorkflow = (defaultprops) => { workflow.triggers[selectedTriggerIndex].status = "running"; setWorkflow(workflow); saveWorkflow(workflow); + + loadTriggers(workflow.org_id) } else { toast("Failed starting webhook: " + responseJson.reason); } @@ -15405,12 +15682,25 @@ const AngularWorkflow = (defaultprops) => { return; } - fetch(globalUrl + "/api/v1/hooks/" + trigger.id + "/delete", { + // Unselect everything in cytoscape + if (cy !== undefined && cy !== null) { + cy.$(":selected").unselect() + } + + var headers = { + "Content-Type": "application/json", + "Accept": "application/json", + } + + console.log("ORGID: ", workflow.org_id) + if (workflow.org_id !== undefined && workflow.org_id !== null && workflow.org_id.length > 0) { + headers["Org-Id"] = workflow.org_id + } + + const url = `${globalUrl}/api/v1/hooks/${trigger.id}/delete` + fetch(url, { method: "DELETE", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, + headers: headers, credentials: "include", }) .then((response) => { @@ -15423,18 +15713,23 @@ const AngularWorkflow = (defaultprops) => { .then((responseJson) => { if (!responseJson.success) { if (responseJson.reason !== undefined) { - toast("Failed to stop webhook: " + responseJson.reason); - } + toast.error("Failed to stop webhook: " + responseJson.reason); + } else { + //toast.error("Failed to stop webhook. Please try again, or contact support@shuffler.io to get it sorted."); + } } else { toast("Successfully stopped webhook"); + + loadTriggers(workflow.org_id) } if (workflow.triggers[triggerindex] !== undefined) { workflow.triggers[triggerindex].status = "stopped"; } - trigger.status = "stopped"; - setSelectedTrigger(trigger); - setWorkflow(workflow); - saveWorkflow(workflow); + + trigger.status = "stopped" + setSelectedTrigger(trigger) + setWorkflow(workflow) + saveWorkflow(workflow) setSelectedTrigger({}) }) @@ -15449,11 +15744,18 @@ const AngularWorkflow = (defaultprops) => { // POST to /api/v1/workflows const createWorkflow = (workflow, trigger_index) => { + var headers = { + "Content-Type": "application/json", + "Accept": "application/json", + } + + if (workflow.org_id !== undefined && workflow.org_id !== null && workflow.org_id.length > 0) { + headers["Org-Id"] = workflow.org_id + } + fetch(globalUrl + "/api/v1/workflows", { method: "POST", - headers: { - "Content-Type": "application/json", - }, + headers: headers, body: JSON.stringify(workflow), credentials: "include", }) @@ -16314,7 +16616,9 @@ const AngularWorkflow = (defaultprops) => { selectedTrigger.parameters === undefined ? isCloud || selectedTrigger?.environment === "cloud" ? "*/25 * * * *" : "60" : selectedTrigger.parameters[0]?.value } color="primary" - placeholder="" + placeholder={ + selectedTrigger.parameters === undefined ? isCloud || selectedTrigger?.environment === "cloud" ? "*/25 * * * *" : "60" : selectedTrigger.parameters[0]?.value + } onBlur={(e) => { setTriggerCronWrapper(e.target.value); }} @@ -16575,331 +16879,432 @@ const AngularWorkflow = (defaultprops) => { : - Warning: This workflow is controlled by your parent org and may not be editable. + Warning: This workflow is controlled by your parent org and may not be editable. } - {originalWorkflow.suborg_distribution === undefined || originalWorkflow.suborg_distribution === null || originalWorkflow.suborg_distribution.length === 0 || originalWorkflow.suborg_distribution.includes("none") ? null : - + {originalWorkflow.suborg_distribution === undefined || originalWorkflow.suborg_distribution === null || originalWorkflow.suborg_distribution.length === 0 || originalWorkflow.suborg_distribution.includes("none") ? + originalWorkflow?.parentorg_workflow !== undefined && originalWorkflow?.parentorg_workflow !== null && originalWorkflow?.parentorg_workflow.length > 0 ? + + : userdata !== undefined && userdata !== null && userdata.orgs !== undefined && userdata.orgs !== null && userdata.orgs.length > 0 ? + + : null - - Select an Org - - { + if (lastSaved === false && originalWorkflow.id === workflow.id) { + saveWorkflow(workflow, undefined, undefined, e.target.value) + toast.warn("Saving workflow first due to detected changes. Please try to change workflow again when it is finished.", { + autoClose: 2000, + }) + return + } - // FIXME: There is a timing problem here. - // For events to have the data they need, they - // need to be registered with setupGraph() - // AFTER all the APIs are done + if (workflow.org_id === e.target.value) { + console.log("Same org selected. No change.") + return + } else { + //if (savingState === 0) { + // saveWorkflow(workflow, undefined, undefined, undefined) + // return + //} + } - // Should look through childorg workflow - setTimeout(() => { - if (e.target.value === originalWorkflow.org_id) { - console.log("Original org selected. No change.") + navigate(`?org_id=${e.target.value}`) - updateCurrentWorkflow(originalWorkflow) - return - } else { - // Load environments, auth, auth groups - //toast("Loading correct info for suborg") - } + // Unselect in cy + if (cy !== undefined && cy !== null) { + cy.nodes().unselect() + cy.edges().unselect() + } - if (originalWorkflow.childorg_workflow_ids === undefined || originalWorkflow.childorg_workflow_ids === null || originalWorkflow.childorg_workflow_ids.length === 0) { - //console.log("Childorg doesn't exist (?). Suborgworkflows: ", suborgWorkflows) + ReactDOM.unstable_batchedUpdates(() => { + getEnvironments(e.target.value) + getAppAuthentication(undefined, undefined, undefined, e.target.value) + getFiles(e.target.value) + listOrgCache(e.target.value) - if (suborgWorkflows !== undefined && suborgWorkflows !== null && suborgWorkflows.length > 0) { - var found = false - for (var suborgkey in suborgWorkflows) { - const suborgWorkflow = suborgWorkflows[suborgkey] - if (suborgWorkflow.org_id === e.target.value) { - found = true - updateCurrentWorkflow(suborgWorkflow) - break - } - } + // Reset the save button to ensure random saves don't occur during move + setLastSaved(true) - if (!found) { - toast("(3) Creating new workflow for this org. Please wait a second while we duplicate.") - //console.log("No workflow found out of suborg workflows.") + // FIXME: There is a timing problem here. + // For events to have the data they need, they + // need to be registered with setupGraph() + // AFTER all the APIs are done - //saveWorkflow(originalWorkflow, undefined, undefined, e.target.value) - } - } else { - console.log("Suborgworkflows: ", suborgWorkflows) - toast("(1) Loading NEW workflow for this org (?). Please wait a second.") - saveWorkflow(originalWorkflow, undefined, undefined, e.target.value) - } - } else { - console.log("In childorg EXIST!") + // Should look through childorg workflow + setTimeout(() => { + if (e.target.value === originalWorkflow.org_id) { + updateCurrentWorkflow(originalWorkflow) + return + } else { + // Load environments, auth, auth groups + //toast("Loading correct info for suborg") + } - var workflowFound = false - for (var childorgidkey in originalWorkflow.childorg_workflow_ids) { - const childworkflowid = originalWorkflow.childorg_workflow_ids[childorgidkey] - for (var suborgWorkflowKey in suborgWorkflows) { - const suborgWorkflow = suborgWorkflows[suborgWorkflowKey] - if (suborgWorkflow.org_id === e.target.value) { - workflowFound = true + if (originalWorkflow.childorg_workflow_ids === undefined || originalWorkflow.childorg_workflow_ids === null || originalWorkflow.childorg_workflow_ids.length === 0) { + //console.log("Childorg doesn't exist (?). Suborgworkflows: ", suborgWorkflows) - updateCurrentWorkflow(suborgWorkflow) - break - } - } + if (suborgWorkflows !== undefined && suborgWorkflows !== null && suborgWorkflows.length > 0) { + var found = false + for (var suborgkey in suborgWorkflows) { + const suborgWorkflow = suborgWorkflows[suborgkey] + if (suborgWorkflow.org_id === e.target.value) { + found = true + updateCurrentWorkflow(suborgWorkflow) + break + } + } - if (workflowFound) { - break - } - } + if (!found) { + toast("(3) Creating new workflow for this org. Please wait a second while we duplicate.") + //console.log("No workflow found out of suborg workflows.") - if (!workflowFound) { - console.log("No workflow found.") - toast("(2) Creating new workflow for this org. Please wait a few seconds while we prepare it for you.") - //saveWorkflow(originalWorkflow, undefined, undefined, e.target.value) - } - } - }, 500) - }) - }} - label="Suborg Distribution" - fullWidth - > - - Parent: {userdata.active_org.large_image}{" "} - - {userdata.active_org.name} - - + //saveWorkflow(originalWorkflow, undefined, undefined, e.target.value) + } + } else { + console.log("Suborgworkflows: ", suborgWorkflows) + toast("(1) Loading NEW workflow for this org (?). Please wait a second.") + saveWorkflow(originalWorkflow, undefined, undefined, e.target.value) + } + } else { + console.log("In childorg EXIST!") - + var workflowFound = false + for (var childorgidkey in originalWorkflow.childorg_workflow_ids) { + const childworkflowid = originalWorkflow.childorg_workflow_ids[childorgidkey] + for (var suborgWorkflowKey in suborgWorkflows) { + const suborgWorkflow = suborgWorkflows[suborgWorkflowKey] + if (suborgWorkflow.org_id === e.target.value) { + workflowFound = true - {originalWorkflow.suborg_distribution.map((org_id, index) => { - var data = {} - for (var key in userdata.orgs) { - if (userdata.orgs[key].id === org_id) { - data = userdata.orgs[key] - break - } - } + updateCurrentWorkflow(suborgWorkflow) + break + } + } - if (data.id === undefined || data.id === null) { - //toast("No org found for id: " + org_id) - return null - } + if (workflowFound) { + break + } + } - var skipOrg = false; + if (!workflowFound) { + console.log("No workflow found.") + toast("(2) Creating new workflow for this org. Please wait a few seconds while we prepare it for you.") + //saveWorkflow(originalWorkflow, undefined, undefined, e.target.value) + } + } + }, 500) + }) + }} + label="Suborg Distribution" + fullWidth + > + + + { + e.preventDefault() + e.stopPropagation() + }} + /> {userdata.active_org.large_image}{" "} + + {userdata.active_org.name} + + - const imagesize = 22 - const imageStyle = { - width: imagesize, - height: imagesize, - pointerEvents: "none", - marginRight: 10, - marginLeft: - data.creator_org !== undefined && - data.creator_org !== null && - data.creator_org.length > 0 - ? data.id === userdata.active_org.id - ? 0 - : 0 - : 0, - } + - const image = - data.image === "" ? ( - {data.name} - ) : ( - {data.name} - ) + {originalWorkflow.suborg_distribution.map((org_id, index) => { + var data = {} + for (var key in userdata.orgs) { + if (userdata.orgs[key].id === org_id) { + data = userdata.orgs[key] + break + } + } + + if (data.id === undefined || data.id === null) { + //toast("No org found for id: " + org_id) + return null + } + + var skipOrg = false; + + const imagesize = 22 + const imageStyle = { + width: imagesize, + height: imagesize, + pointerEvents: "none", + marginRight: 10, + marginLeft: + data.creator_org !== undefined && + data.creator_org !== null && + data.creator_org.length > 0 + ? data.id === userdata.active_org.id + ? 0 + : 0 + : 0, + } + + const image = + data.image === "" ? ( + {data.name} + ) : ( + {data.name} + ) - return ( - - {image}{" "} - - {data.name} - - - ) - })} - - + return ( + + {image}{" "} + + {data.name} + + + ) + })} + + + }
    {showEnvironment === true && environments.length > 0 && selectedActionEnvironment !== undefined && selectedActionEnvironment !== null && selectedActionEnvironment.Name !== undefined && selectedActionEnvironment.Name !== null ? - + 0 && Object.getOwnPropertyNames(selectedActionEnvironment).length !== 0 && selectedActionEnvironment?.Name !== "Cloud" && savingState === 0 ? ( + +
    + 0} + onChange={(event) => { + toast("Opening in new tab. Refresh this page after adding it.") - - Runtime Location - - { + getEnvironments(workflow.org_id) + }} + onChange={(e) => { + setLastSaved(false) + const env = environments.find((a) => a.Name === e.target.value); + setSelectedActionEnvironment(env) + selectedAction.environment = env.Name + setSelectedAction(selectedAction) - backgroundColor: theme.palette.inputColor, - height: 40, - }} - > - {environments.map((data, index) => { - if (data.archived === true) { - return null - } + for (let actionkey in workflow.actions) { + workflow.actions[actionkey].environment = env.Name + } - const isRunning = data.running_ip !== "" + setWorkflow(workflow) + //toast.success("Set execution location for ALL actions to " + env.Name) + }} + style={{ + pointerEvents: "auto", + color: "white", + maxWidth: 250, + minWidth: 250, + borderRadius: theme.palette?.borderRadius, + marginLeft: 35, - return ( - + backgroundColor: theme.palette.inputColor, + height: 40, + }} + > + {environments.map((data, index) => { + if (data.archived === true) { + return null + } - {data.Name === "cloud" || data.Name === "Cloud" ? null : !isRunning ? + const isRunning = data.running_ip !== "" - - - { - e.preventDefault() - e.stopPropagation() + return ( + - window.open(`/admin?tab=locations&env=${data.Name}`, "_blank", "noopener,noreferrer") - }} + {data.Name === "cloud" || data.Name === "Cloud" ? null : !isRunning ? + + + + { + e.preventDefault() + e.stopPropagation() + + window.open(`/admin?tab=locations&env=${data.Name}`, "_blank", "noopener,noreferrer") + }} - /> - - - : - { - //handleChipClick - }} - variant="outlined" - color="primary" - /> - } + /> + + + : + { + //handleChipClick + }} + variant="outlined" + color="primary" + /> + } - {data.default === true ? - - : null} + {data.default === true ? + + : null} - {data.Name} - - ); - })} - - + {data.Name} + + ); + })} + + + : null} @@ -17250,7 +17655,7 @@ const AngularWorkflow = (defaultprops) => { variant="body2" color="textSecondary" > - {workflow.errors.slice(0, 3).map((error) => { + {workflow.errors.slice(0, 3).map((error, index) => { // Loop through each word, and if it matches "Action " then replace it with a link to the action var colornext = false const newerror = error === undefined || error == null ? "" : error.split(" ").map((word) => { @@ -17258,6 +17663,7 @@ const AngularWorkflow = (defaultprops) => { colornext = false return ( { // Find it in cytoscape @@ -17446,7 +17852,7 @@ const AngularWorkflow = (defaultprops) => { onMouseLeave={() => setHovered(false)} onClick={() => { setExecutionModalOpen(true); - getWorkflowExecution(props.match.params.key, ""); + getWorkflowExecution(workflow.id, "", executionFilter, workflow.org_id) }} > @@ -17475,9 +17881,39 @@ const AngularWorkflow = (defaultprops) => { setSelectedApp({}) setWorkflow(inputworkflow) + if (workflow.id === originalWorkflow.id) { + if (selectedActionEnvironment !== undefined && selectedActionEnvironment !== null && selectedActionEnvironment.Name !== undefined && selectedActionEnvironment.Name !== null) { + setOriginalSelectedEnvironment(selectedActionEnvironment) + } + } + + if (inputworkflow.id === originalWorkflow.id && originalSelectedEnvironment !== undefined && originalSelectedEnvironment !== null && originalSelectedEnvironment.Name !== undefined && originalSelectedEnvironment.Name !== null) { + setSelectedActionEnvironment(originalSelectedEnvironment) + } else { + if (inputworkflow.actions !== undefined && inputworkflow.actions !== null && inputworkflow.actions.length > 0) { + + for (var actionkey in inputworkflow.actions) { + const action = inputworkflow.actions[actionkey] + if (action.environment === undefined || action.environment === null || action.environment === "") { + continue + } + + //const env = environments.find((a) => a.Name === action.environment) + const newenv = { + Name: action.environment, + Type: action.environment === "cloud" ? "cloud" : "onprem", + } + + setSelectedActionEnvironment(newenv) + break + } + } + } + if (inputworkflow !== undefined && inputworkflow !== null && inputworkflow.id !== undefined && inputworkflow.id !== null) { getRevisionHistory(inputworkflow.id, 50, 0, inputworkflow.org_id) - getWorkflowExecution(inputworkflow.id, "", "") + getWorkflowExecution(inputworkflow.id, "", executionFilter, inputworkflow.org_id) + loadTriggers(inputworkflow.org_id) } // Update props match key @@ -17501,9 +17937,9 @@ const AngularWorkflow = (defaultprops) => { cy.removeListener("free"); cy.removeListener("cxttap"); - setElements([]) // Remove all edges + setElements([]) cy.edges().remove() cy.nodes().remove() } @@ -17618,7 +18054,7 @@ const AngularWorkflow = (defaultprops) => { style={{ width: 55, }} onClick={() => { setExecutionModalOpen(true); - getWorkflowExecution(props.match.params.key, ""); + getWorkflowExecution(workflow.id, "", executionFilter, workflow.org_id) }} > @@ -18273,10 +18709,11 @@ const AngularWorkflow = (defaultprops) => { getAppAuthentication() getEnvironments(workflow.org_id) - getWorkflowExecution(props.match.params.key, "") getAvailableWorkflows(-1) getFiles() + getWorkflowExecution(workflow.id, "", executionFilter, workflow.org_id) + // For loading datastore setUpdate(Math.random()); @@ -19032,8 +19469,7 @@ const AngularWorkflow = (defaultprops) => { variant="outlined" fullWidth onClick={() => { - - getWorkflowExecution(props.match.params.key, "", executionFilter); + getWorkflowExecution(workflow.id, "", executionFilter, workflow.org_id) }} color="secondary" > @@ -19050,7 +19486,7 @@ const AngularWorkflow = (defaultprops) => { variant={executionFilter === "ALL" ? "contained" : "outlined"} onClick={() => { setExecutionFilter("ALL") - getWorkflowExecution(props.match.params.key, "", "ALL") + getWorkflowExecution(workflow.id, "", "ALL", workflow.org_id) }} > All @@ -19060,7 +19496,7 @@ const AngularWorkflow = (defaultprops) => { variant={executionFilter === "FINISHED" ? "contained" : "outlined"} onClick={() => { setExecutionFilter("FINISHED") - getWorkflowExecution(props.match.params.key, "", "FINISHED") + getWorkflowExecution(workflow.id, "", "FINISHED", workflow.org_id) }} > Finished @@ -19070,7 +19506,7 @@ const AngularWorkflow = (defaultprops) => { variant={executionFilter === "EXECUTING" ? "contained" : "outlined"} onClick={() => { setExecutionFilter("EXECUTING") - getWorkflowExecution(props.match.params.key, "", "EXECUTING") + getWorkflowExecution(workflow.id, "", "EXECUTING", workflow.org_id) }} > Executing @@ -19080,7 +19516,7 @@ const AngularWorkflow = (defaultprops) => { variant={executionFilter === "ABORTED" ? "contained" : "outlined"} onClick={() => { setExecutionFilter("ABORTED") - getWorkflowExecution(props.match.params.key, "", "ABORTED") + getWorkflowExecution(workflow.id, "", "ABORTED", workflow.org_id) }} > Aborted @@ -19141,7 +19577,9 @@ const AngularWorkflow = (defaultprops) => { const foundnotifications = data.notifications_created === undefined || data.notifications_created === null ? 0 : data.notifications_created return ( - + {/**/}
    { {workflowRevisions} {authenticationModal} {tenzirConfigModal} - {/*editWorkflowModal*/} {authgroupModal} {executionArgumentModal} {configureWorkflowModal} - {/*usecaseSlidein*/} @@ -22958,6 +23394,8 @@ const AngularWorkflow = (defaultprops) => { isEditing={true} userdata={userdata} usecases={usecases} + + scrollTo={"mssp_control"} /> : null} diff --git a/frontend/src/views/AppCreator.jsx b/frontend/src/views/AppCreator.jsx index e6a9888e..6e6dda22 100755 --- a/frontend/src/views/AppCreator.jsx +++ b/frontend/src/views/AppCreator.jsx @@ -276,7 +276,14 @@ export const appCategories = [ "color": "#FFC107", "icon": "network", "action_labels": ["Get Rules", "Allow IP", "Block IP",], - }, { + }, + { + "name": "AI", + "color": "#FFC107", + "icon": "AI", + "action_labels": ["Answer Question", "Run Action"], + }, + { "name": "Other", "color": "#FFC107", "icon": "other", @@ -418,6 +425,7 @@ const AppCreator = (defaultprops) => { const [appBuilding, setAppBuilding] = useState(false); const [fileDownloadEnabled, setFileDownloadEnabled] = useState(false); const [actionAmount, setActionAmount] = useState(increaseAmount); + const [newAppGroup, setNewAppGroup] = useState("") const [oauth2Scopes, setOauth2Scopes] = useState([]); const [oauth2Type, setOauth2Type] = useState("delegated"); @@ -4622,23 +4630,25 @@ const AppCreator = (defaultprops) => { }} /> */} -

    Choose a Category

    - { + setNewWorkflowCategories([e.target.value]); + setUpdate("added " + e.target.value); + }} + value={newWorkflowCategories.length === 0 ? "Select a category" : newWorkflowCategories[0]} + + style={{ backgroundColor: inputColor, color: "white", height: "50px" }} + > + {categories.map((data, index) => { if (data === undefined || data === null || data === "" || data === undefined || data === null || data === "") { return null } @@ -4651,9 +4661,38 @@ const AppCreator = (defaultprops) => { > {data.name} - ) + ) })} - + +
    + {/* +
    + +

    Group

    +
    + { + setNewAppGroup(e.target.value) + setUpdate("group added "+e.target.value) + }} + InputProps={{ + style: { + color: "white", + }, + }} + /> +
    + */} +

    Tags

    { { key: "cache_add", values: ["set_cache"] }, { key: "cache_get", values: ["get_cache"] }, { key: "filter", values: ["filter"] }, - { key: "merge", values: ["join", "merge", "route", "router"] }, + { key: "merge", values: ["join", "merge", "route", "router", "routing"] }, { key: "search", values: ["search", "find", "locate", "index", "analyze", "anal", "match", "check cache", "check", "verify", "validate"], @@ -200,7 +200,7 @@ export const GetIconInfo = (action) => { }, { key: "compare", - values: ["compare", "convert", "to", "filter", "translate", "parse"], + values: ["compare", "convert", "to", "filter", "translate", "parse", "generate", ], }, { key: "close", values: ["close", "stop", "cancel", "block"] }, { key: "communication", values: ["communication", "comms", "email", "mail",] }, From 840347844a65c5eb194571649d4d0d57f7c33d55 Mon Sep 17 00:00:00 2001 From: lalitdeore Date: Thu, 6 Feb 2025 15:39:19 +0530 Subject: [PATCH 24/67] sync onprem features/pages with cloud --- backend/go-app/main.go | 39 +- frontend/public/images/frameworks/attack.png | Bin 0 -> 6612 bytes frontend/public/images/frameworks/openapi.png | Bin 0 -> 35673 bytes frontend/public/images/frameworks/python.jpeg | Bin 0 -> 7647 bytes .../images/frameworks/resized/attack.png | Bin 0 -> 7496 bytes .../images/frameworks/resized/openapi.png | Bin 0 -> 12226 bytes .../images/frameworks/resized/sigma.png | Bin 0 -> 12542 bytes frontend/public/images/frameworks/sigma.png | Bin 0 -> 27681 bytes frontend/src/App.jsx | 115 +- frontend/src/components/AdminNavBar.jsx | 227 + frontend/src/components/AppAuthTab.jsx | 2655 ++++++++++ frontend/src/components/AppGrid.jsx | 20 +- frontend/src/components/AppModal.jsx | 6 +- frontend/src/components/AppStats.jsx | 368 ++ frontend/src/components/CloudSyncTab.jsx | 780 +++ frontend/src/components/EditOrgTab.jsx | 446 ++ frontend/src/components/EnvironmentTab.jsx | 1541 ++++++ frontend/src/components/LeftSideBar.jsx | 4 +- frontend/src/components/OrgHeaderNew.jsx | 520 ++ .../src/components/OrgHeaderexpandedNew.jsx | 1158 +++++ frontend/src/components/OrganizationTab.jsx | 222 + frontend/src/components/SchedulesTab.jsx | 1097 ++++ frontend/src/components/TenantsTab.jsx | 1592 ++++++ frontend/src/components/UserManagmentTab.jsx | 1670 ++++++ frontend/src/components/ssoTab.jsx | 649 +++ frontend/src/views/Admin2.jsx | 13 +- frontend/src/views/AngularWorkflow.jsx | 2 +- frontend/src/views/AppCreator.jsx | 2 +- frontend/src/views/AppExplorer.jsx | 4623 +++++++++++++++++ frontend/src/views/Usecases2.jsx | 3 +- 30 files changed, 17680 insertions(+), 72 deletions(-) create mode 100644 frontend/public/images/frameworks/attack.png create mode 100644 frontend/public/images/frameworks/openapi.png create mode 100644 frontend/public/images/frameworks/python.jpeg create mode 100644 frontend/public/images/frameworks/resized/attack.png create mode 100644 frontend/public/images/frameworks/resized/openapi.png create mode 100644 frontend/public/images/frameworks/resized/sigma.png create mode 100644 frontend/public/images/frameworks/sigma.png create mode 100644 frontend/src/components/AdminNavBar.jsx create mode 100644 frontend/src/components/AppAuthTab.jsx create mode 100644 frontend/src/components/AppStats.jsx create mode 100644 frontend/src/components/CloudSyncTab.jsx create mode 100644 frontend/src/components/EditOrgTab.jsx create mode 100644 frontend/src/components/EnvironmentTab.jsx create mode 100644 frontend/src/components/OrgHeaderNew.jsx create mode 100644 frontend/src/components/OrgHeaderexpandedNew.jsx create mode 100644 frontend/src/components/OrganizationTab.jsx create mode 100644 frontend/src/components/SchedulesTab.jsx create mode 100644 frontend/src/components/TenantsTab.jsx create mode 100644 frontend/src/components/UserManagmentTab.jsx create mode 100644 frontend/src/components/ssoTab.jsx create mode 100644 frontend/src/views/AppExplorer.jsx diff --git a/backend/go-app/main.go b/backend/go-app/main.go index 66046eac..042a30aa 100755 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -1048,6 +1048,20 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) { licensed := shuffle.IsLicensed(ctx, *org) + workflowapps, err := shuffle.GetAllWorkflowApps(ctx, 1000, 0) + if err != nil { + log.Printf("{WARNING] Failed getting apps (getworkflowapps): %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + orgApps := workflowapps + activatedAppIds := []string{} + for _, app := range orgApps { + activatedAppIds = append(activatedAppIds, app.ID) + } + returnValue := shuffle.HandleInfo{ Success: true, Username: userInfo.Username, @@ -1069,6 +1083,7 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) { Interests: orgInterests, Priorities: orgPriorities, Licensed: licensed, + ActiveApps: activatedAppIds, } returnData, err := json.Marshal(returnValue) @@ -3175,7 +3190,7 @@ func buildSwaggerApp(resp http.ResponseWriter, body []byte, user shuffle.User, s err = shuffle.DeployAppToDatastore(ctx, api) //func DeployAppToDatastore(ctx context.Context, workflowapp WorkflowApp, bucketName string) error { if err != nil { - log.Printf("Failed adding app to db: %s", err) + log.Printf("[ERROR] Failed adding app to db: %s", err) resp.WriteHeader(500) resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed adding app to db: %s"}`, err))) return @@ -3184,7 +3199,7 @@ func buildSwaggerApp(resp http.ResponseWriter, body []byte, user shuffle.User, s // 2. Get all the required code appbase, staticBaseline, err := shuffle.GetAppbase() if err != nil { - log.Printf("Failed getting appbase: %s", err) + log.Printf("[ERROR] Failed getting appbase: %s", err) resp.WriteHeader(500) resp.Write([]byte(`{"success": false, "reason": "Failed getting appbase code"}`)) return @@ -4842,7 +4857,7 @@ func makeWorkflowPublic(resp http.ResponseWriter, request *http.Request) { // FIXME - add org check too, and not just owner // Check workflow.Sharing == private / public / org too if user.Id != workflow.Owner || len(user.Id) == 0 { - if workflow.OrgId == user.ActiveOrg.Id { + if workflow.OrgId == user.ActiveOrg.Id { log.Printf("[AUDIT] User %s is accessing workflow %s as admin (public)", user.Username, workflow.ID) } else { log.Printf("[AUDIT] Wrong user (%s) for workflow %s (public)", user.Username, workflow.ID) @@ -5107,6 +5122,13 @@ func initHandlers() { r.HandleFunc("/api/v1/apps/authentication/{appauthId}/config", shuffle.SetAuthenticationConfig).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/apps/authentication/{appauthId}", shuffle.DeleteAppAuthentication).Methods("DELETE", "OPTIONS") + r.HandleFunc("/api/v1/authentication/group", shuffle.AddAppAuthenticationGroup).Methods("POST", "OPTIONS") + r.HandleFunc("/api/v1/authentication/group", shuffle.GetAppAuthenticationGroup).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/authentication/group/{key}", shuffle.DeleteAppAuthenticationGroup).Methods("DELETE", "OPTIONS") + + r.HandleFunc("/api/v1/authentication/groups", shuffle.AddAppAuthenticationGroup).Methods("POST", "OPTIONS") + r.HandleFunc("/api/v1/authentication/groups", shuffle.GetAppAuthenticationGroup).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/authentication/groups/{key}", shuffle.DeleteAppAuthenticationGroup).Methods("DELETE", "OPTIONS") // Related to use-cases that are not directly workflows. r.HandleFunc("/api/v1/workflows/usecases/{key}", shuffle.HandleGetUsecase).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/workflows/usecases", shuffle.LoadUsecases).Methods("GET", "OPTIONS") @@ -5173,9 +5195,11 @@ func initHandlers() { r.HandleFunc("/api/v1/triggers/gmail/register", shuffle.HandleNewGmailRegister).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/triggers/gmail/getFolders", shuffle.HandleGetGmailFolders).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/triggers/pipeline", shuffle.HandleNewPipelineRegister).Methods("POST", "OPTIONS") + r.HandleFunc("/api/v1/triggers/github/register", shuffle.HandleNewGithubRegister).Methods("PUT", "OPTIONS") //r.HandleFunc("/api/v1/triggers/pipeline/save", shuffle.HandleSavePipelineInfo).Methods("PUT", "OPTIONS") r.HandleFunc("/api/v1/pipelines/{key}", handlePipelineCallback).Methods("POST", "GET", "PATCH", "PUT", "DELETE", "OPTIONS") r.HandleFunc("/api/v1/triggers", shuffle.HandleGetTriggers).Methods("GET", "OPTIONS") + //r.HandleFunc("/api/v1/triggers/gmail/routing", handleGmailRouting).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/triggers/gmail/{key}", shuffle.HandleGetSpecificTrigger).Methods("GET", "OPTIONS") @@ -5204,8 +5228,13 @@ func initHandlers() { // This is a new API that validates if a key has been seen before. // Not sure what the best course of action is for it. + r.HandleFunc("/api/v1/getenvironments", shuffle.HandleGetEnvironments).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/setenvironments", shuffle.HandleSetEnvironments).Methods("PUT", "OPTIONS") r.HandleFunc("/api/v1/environments/{key}/stop", shuffle.HandleStopExecutions).Methods("GET", "POST", "OPTIONS") r.HandleFunc("/api/v1/environments/{key}/rerun", shuffle.HandleRerunExecutions).Methods("GET", "POST", "OPTIONS") + r.HandleFunc("/api/v1/environments/{key}/stats", shuffle.HandleGetenvStats).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/environments/{key}/config", shuffle.HandleSetenvConfig).Methods("POST", "OPTIONS") + r.HandleFunc("/api/v1/environments", shuffle.HandleGetEnvironments).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/orgs/{orgId}/validate_app_values", shuffle.HandleKeyValueCheck).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/orgs/{orgId}/list_cache", shuffle.HandleListCacheKeys).Methods("GET", "OPTIONS") @@ -5214,8 +5243,10 @@ func initHandlers() { r.HandleFunc("/api/v1/orgs/{orgId}/set_cache", shuffle.HandleSetCacheKey).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/orgs/{orgId}/delete_cache", shuffle.HandleDeleteCacheKeyPost).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/orgs/{orgId}/cache/{cache_key}", shuffle.HandleDeleteCacheKey).Methods("DELETE", "OPTIONS") + r.HandleFunc("/api/v1/orgs/{orgId}/cache/config", shuffle.HandleCacheConfig).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/orgs/{orgId}/stats", shuffle.HandleGetStatistics).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/orgs/{orgId}/stats", shuffle.HandleAppendStatistics).Methods("POST", "OPTIONS") + r.HandleFunc("/api/v1/orgs/{orgId}/stats/{key}", shuffle.GetSpecificStats).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/orgs/{orgId}/statistics", shuffle.HandleGetStatistics).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/orgs/{orgId}/cache", shuffle.HandleListCacheKeys).Methods("GET", "OPTIONS") @@ -5242,6 +5273,8 @@ func initHandlers() { r.HandleFunc("/api/v1/files/{fileId}", shuffle.HandleGetFileMeta).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/files/{fileId}", shuffle.HandleDeleteFile).Methods("DELETE", "OPTIONS") r.HandleFunc("/api/v1/files", shuffle.HandleGetFiles).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/files/{fileId}/config", shuffle.HandleSetFileConfig).Methods("POST", "OPTIONS") + r.HandleFunc("/api/v1/files/namespaces/{namespace}/share", shuffle.HandleShareNamespace).Methods("POST", "OPTIONS") // This structure is horrendous. Needs fixing after we got the prototype up r.HandleFunc("/api/v1/detections/{detectionType}/connect", shuffle.HandleDetectionAutoConnect).Methods("GET", "OPTIONS") diff --git a/frontend/public/images/frameworks/attack.png b/frontend/public/images/frameworks/attack.png new file mode 100644 index 0000000000000000000000000000000000000000..80f2dbb2bcc41f2fae4f4268568c6fbbb8cdb24c GIT binary patch literal 6612 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 F{{Y=ta^C;| literal 0 HcmV?d00001 diff --git a/frontend/public/images/frameworks/python.jpeg b/frontend/public/images/frameworks/python.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..b72240b6812e1f9f0cf7d68e093319e88c6c91cb GIT binary patch literal 7647 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 literal 0 HcmV?d00001 diff --git a/frontend/public/images/frameworks/sigma.png b/frontend/public/images/frameworks/sigma.png new file mode 100644 index 0000000000000000000000000000000000000000..0bd0db1432a8091d6825d417ea0d2141271d08fd GIT binary patch literal 27681 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 jKDj0Sc~u { } - {curpath.includes("/workflows") && curpath.includes("/run") ? -
    - : - isLoggedIn ? -
    - + { window?.location?.pathname === "/" || window?.location?.pathname === "/training" || !(isLoggedIn && isLoaded) ? ( +
    +
    - : -
    -
    -
    - } + ) : ( +
    + +
    + ) } {/*
    @@ -262,7 +254,7 @@ const App = (message, props) => { /> { /> } /> + + } + /> } /> { /> { /> { } /> { /> { /> } /> - } /> + {/* } /> + } /> */} { /> { /> { isMobile={isMobile} isLoaded={isLoaded} globalUrl={globalUrl} + isLoggedIn={isLoggedIn} {...props} /> } @@ -567,6 +579,7 @@ const App = (message, props) => { isMobile={isMobile} isLoaded={isLoaded} globalUrl={globalUrl} + isLoggedIn={isLoggedIn} {...props} /> } diff --git a/frontend/src/components/AdminNavBar.jsx b/frontend/src/components/AdminNavBar.jsx new file mode 100644 index 00000000..2c538d7b --- /dev/null +++ b/frontend/src/components/AdminNavBar.jsx @@ -0,0 +1,227 @@ +import React, { useState, useEffect, useContext, memo } from 'react'; +import { useNavigate, useLocation } from 'react-router-dom'; +import OrganizationTab from '../components/OrganizationTab.jsx'; +import UserManagmentTab from '../components/UserManagmentTab.jsx'; +import CacheView from "../components/CacheView.jsx"; +import Files from "../components/Files.jsx"; +import AppAuthTab from "../components/AppAuthTab.jsx"; +import SchedulesTab from "../components/SchedulesTab.jsx"; +import EnvironmentTab from "../components/EnvironmentTab.jsx"; +import TenantsTab from "../components/TenantsTab.jsx"; + +import { + Business as BusinessIcon, + PermIdentity as PermIdentityIcon, + HttpsOutlined as HttpsOutlinedIcon, + InsertDriveFileOutlined as InsertDriveFileOutlinedIcon, + StorageOutlined as StorageOutlinedIcon, + AccessTimeOutlined as AccessTimeOutlinedIcon, + FmdGoodOutlined as FmdGoodOutlinedIcon, + GroupOutlined as GroupOutlinedIcon +} from '@mui/icons-material'; +import theme from '../theme.jsx'; +import { Button, Tooltip } from '@mui/material'; +import { Index } from 'react-instantsearch-dom'; +import { Context } from '../context/ContextApi.jsx'; + +const AdminNavBar = (props) => { + const location = useLocation(); + const { globalUrl, userdata, isCloud, isLoaded,removeCookie, handleStatusChange, selectedStatus, setSelectedStatus, handleEditOrg, serverside, notifications, handleGetOrg, orgId, checkLogin, setNotifications, stripeKey, setSelectedOrganization, selectedOrganization } = props; + const [selectedItem, setSelectedItem] = useState("Organization"); + const [isSelectedFiles, setIsSelectedFiles] = useState(true); + const [isSelectedDataStore, setIsSelectedDataStore] = useState(true); + + const navigate = useNavigate(); + + useEffect(() => { + const queryParams = new URLSearchParams(location.search); + const tabName = queryParams.get('tab'); + + if (tabName === "environments") { + setSelectedItem("Locations"); + } else if (tabName === "suborgs") { + setSelectedItem("Tenants"); + }else if (tabName === "cache") { + setSelectedItem("Datastore"); + }else if (tabName) { + setSelectedItem(tabName.charAt(0).toUpperCase() + tabName.slice(1)); + } else { + setSelectedItem("Organization"); + } + }, [location.search]); + + + const items = [ + { iconSrc: , alt: "Organization Icon", text: "Organization", component: OrganizationTab, props: { globalUrl,removeCookie, selectedStatus, isLoaded, setSelectedStatus, handleStatusChange, handleEditOrg, handleGetOrg, userdata, isCloud, serverside, notifications, checkLogin, setNotifications, stripeKey, setSelectedOrganization, selectedOrganization } }, + { iconSrc: , alt: "Users Icon", text: "Users", component: UserManagmentTab, props: { globalUrl, userdata, serverside, isCloud, selectedOrganization, setSelectedOrganization, handleEditOrg } }, + { iconSrc: , alt: "App Auth Icon", text: "App_auth", component: AppAuthTab, props: { globalUrl, userdata, isCloud, selectedOrganization } }, + { iconSrc: , alt: "Datastore Icon", text: "Datastore", component: CacheView, props: { globalUrl, userdata, selectedOrganization, serverside, isSelectedDataStore, orgId , isCloud} }, + { iconSrc: , alt: "Files Icon", text: "Files", component: Files, props: { isCloud, globalUrl, userdata, serverside, selectedOrganization, isSelectedFiles } }, + { iconSrc: , alt: "Trigger Icon", text: "Triggers", component: SchedulesTab, props: { globalUrl, userdata, isCloud, serverside } }, + { iconSrc: , alt: "Environments Icon", text: "Locations", component: EnvironmentTab, props: { globalUrl, userdata, isCloud, selectedOrganization } }, + { iconSrc: , alt: "Tenants Icon", text: "Tenants", component: TenantsTab, props: {isCloud, globalUrl, userdata, serverside, selectedOrganization, setSelectedOrganization, checkLogin } } + ]; + + const setConfig = (newValue) => { + setSelectedItem(newValue); + if (newValue === "App Auth") { + const tabName = newValue.toLowerCase().replace(/\s+/g, '_'); + navigate(`?tab=${tabName}`, { replace: true }); + } else { + const tabName = newValue.toLowerCase().replace(/\s+/g, '_'); + navigate(`?tab=${tabName}`, { replace: true }); + } + }; + + const renderComponent = () => { + const selectedItemData = items.find(item => item.text === selectedItem); + if (!selectedItemData) { + setSelectedItem("Organization"); + // If no tab is specified, default to "Organization" tab + return ; + }; + + const ComponentToRender = selectedItemData.component; + const componentProps = selectedItemData.props; + + return ; + }; + + const defaultImage = "/images/logos/orange_logo.svg" + const imageData = + selectedOrganization?.image === undefined || selectedOrganization?.image.length === 0 + ? defaultImage + : selectedOrganization?.image; + + return ( + +
    + +
    + {renderComponent()} +
    + ); +}; + +export default AdminNavBar; + +const PaddingWrapper2 = memo(({ children }) => { + + return ( +
    + {children} +
    + ) +}); + +const Wrapper2 = memo(({children}) => { + + return ( + + {children} + + ); +}) + +const PaddingWrapper = memo(({ children }) => { + const { leftSideBarOpenByClick, windowWidth } = useContext(Context); + + return ( +
    + {children} +
    + ); +}); + +const Wrapper = memo(({ children }) => { + return ( + + {children} + + ); +}) diff --git a/frontend/src/components/AppAuthTab.jsx b/frontend/src/components/AppAuthTab.jsx new file mode 100644 index 00000000..dbd29e3d --- /dev/null +++ b/frontend/src/components/AppAuthTab.jsx @@ -0,0 +1,2655 @@ +import React, { memo, useContext, useEffect, useState } from 'react'; +import { + Edit as EditIcon, + SelectAll as SelectAllIcon, + Delete as DeleteIcon, + CheckCircle as CheckCircleIcon, + Cancel as CancelIcon, + Search as SearchIcon, + Clear as ClearIcon, + DragIndicator as DragIndicatorIcon, + Close as CloseIcon, + LockOpen as LockOpenIcon, +} from "@mui/icons-material"; +import { useNavigate } from "react-router-dom"; +import { toast } from "react-toastify"; +import theme from "../theme.jsx"; +import Markdown from "react-markdown"; +import AuthenticationOauth2 from "../components/Oauth2Auth.jsx"; +import { isMobile } from "react-device-detect" +import PaperComponent from "../components/PaperComponent.jsx"; +import { CodeHandler, Img, OuterLink, } from '../views/Docs.jsx' +import { v4 as uuidv4} from "uuid"; + +import { + Divider, + List, + ListItem, + ListItemText, + IconButton, + Tooltip, + Chip, + Checkbox, + Typography, + TextField, + Button, + Dialog, + DialogTitle, + DialogActions, + DialogContent, + Select, + FormControl, + InputLabel, + MenuItem, + FormControlLabel, + InputAdornment, + Grid, + Zoom, + Paper, + Skeleton, + Link, +} from "@mui/material"; + +import algoliasearch from "algoliasearch/lite"; +import { + InstantSearch, + Configure, + connectSearchBox, + connectHits, + connectHitInsights, + RefinementList, + ClearRefinements, + connectStateResults +} from "react-instantsearch-dom"; +import aa from "search-insights"; +import { Context } from '../context/ContextApi.jsx'; + +const searchClient = algoliasearch( + "JNSS5CFDZZ", + "db08e40265e2941b9a7d8f644b6e5240" +); + +const AppAuthTab = memo((props) => { + const { globalUrl, userdata, isCloud, selectedOrganization } = props; + const [selectedAuthentication, setSelectedAuthentication] = React.useState({}); + const [authentication, setAuthentication] = React.useState([]); + const [selectedUserModalOpen, setSelectedUserModalOpen] = React.useState(false); + const [authenticationFields, setAuthenticationFields] = React.useState([]); + const [selectedAuthenticationModalOpen, setSelectedAuthenticationModalOpen] = React.useState(false); + const [appAuthenticationGroupEnvironment, setAppAuthenticationGroupEnvironment] = React.useState(""); + const [environments, setEnvironments] = React.useState([]); + const [listItemExpanded, setListItemExpanded] = React.useState(-1); + const [appAuthenticationGroupModalOpen, setAppAuthenticationGroupModalOpen] = React.useState(false); + const [appAuthenticationGroupId, setAppAuthenticationGroupId] = React.useState(""); + const [appAuthenticationGroups, setAppAuthenticationGroups] = React.useState([]); + const [appAuthenticationGroupName, setAppAuthenticationGroupName] = React.useState(""); + const [appAuthenticationGroupDescription, setAppAuthenticationGroupDescription] = React.useState(""); + const [appsForAppAuthGroup, setAppsForAppAuthGroup] = React.useState([]); + const [searchQuery, setSearchQuery] = React.useState(""); + const [showAppModal, setShowAppModal] = useState(false) + const [showAuthenticationLoader, setShowAuthenticationLoader] = useState(true) + const [showAppAuthGroupLoader, setShowAppAuthGroupLoader] = useState(true) + const changeDistribution = (data) => { + //changeDistributed(data, !isDistributed) + editAuthenticationConfig(data.id, "suborg_distribute") + } + + useEffect(() => { + getAppAuthentication(); + getAppAuthenticationGroups(); + getEnvironments(); + }, []) + + const getAppAuthentication = () => { + fetch(globalUrl + "/api/v1/apps/authentication", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for apps :O!"); + return; + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === true) { + setAuthentication(responseJson.data); + setShowAuthenticationLoader(false) + } else { + toast("Failed getting authentications"); + } + }) + .catch((error) => { + toast(error.toString()); + }); + }; + + const updateAppAuthentication = (field) => { + setSelectedAuthenticationModalOpen(true); + setSelectedAuthentication(field); + //{selectedAuthentication.fields.map((data, index) => { + var newfields = []; + for (var key in field.fields) { + newfields.push({ + key: field.fields[key].key, + value: "", + }); + } + setAuthenticationFields(newfields); + }; + const saveAuthentication = (authentication) => { + const data = authentication; + const url = globalUrl + "/api/v1/apps/authentication"; + + fetch(url, { + mode: "cors", + method: "PUT", + body: JSON.stringify(data), + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then((response) => + response.json().then((responseJson) => { + if (responseJson["success"] === false) { + // Check if .reason exists + if (responseJson.reason !== undefined) { + toast("Failed changing authentication: " + responseJson.reason); + } else { + toast("Failed changing authentication"); + } + } else { + getAppAuthentication(); + + + setSelectedAuthentication({}); + setSelectedAuthenticationModalOpen(false); + } + }) + ) + .catch((error) => { + toast("Err: " + error.toString()); + }); + }; + + const deleteAuthentication = (data) => { + toast("Deleting auth " + data.label); + + // Just use this one? + const url = globalUrl + "/api/v1/apps/authentication/" + data.id; + console.log("URL: ", url); + fetch(url, { + method: "DELETE", + credentials: "include", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => + response.json().then((responseJson) => { + if (responseJson.success === false) { + toast("Failed deleting auth"); + } else { + // Need to wait because query in ES is too fast + setTimeout(() => { + getAppAuthentication(); + }, 1000); + //toast("Successfully deleted authentication!") + } + }) + ) + .catch((error) => { + console.log("Error in userdata: ", error); + }); + }; + + const editAuthenticationConfig = (id, parentAction) => { + const data = { + id: id, + action: parentAction !== undefined && parentAction !== null ? parentAction : "assign_everywhere", + } + + const url = globalUrl + "/api/v1/apps/authentication/" + id + "/config"; + + fetch(url, { + mode: "cors", + method: "POST", + body: JSON.stringify(data), + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then((response) => + response.json().then((responseJson) => { + if (responseJson["success"] === false) { + toast("Failed overwriting appauth"); + } else { + toast("Successfully updated auth!"); + setSelectedUserModalOpen(false); + setTimeout(() => { + getAppAuthentication(); + }, 1000); + } + }) + ) + .catch((error) => { + toast("Err: " + error.toString()); + }); + }; + + const editAuthenticationModal = selectedAuthenticationModalOpen ? ( + { + setSelectedAuthenticationModalOpen(false); + }} + PaperProps={{ + sx: { + borderRadius: theme?.palette?.DialogStyle?.borderRadius, + border: theme?.palette?.DialogStyle?.border, + minWidth: "800px", + minHeight: "320px", + fontFamily: theme?.typography?.fontFamily, + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + zIndex: 1000, + '& .MuiDialogContent-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogTitle-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogActions-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + }, + }} + > + + + Edit authentication for {selectedAuthentication.app.name.replaceAll("_", " ")} ( + {selectedAuthentication.label}) + + + You can not see the previous values for an authentication while editing. This is to keep your data secure. You can overwrite one- or multiple fields at a time. + + + + + Authentication Label + + { + selectedAuthentication.label = e.target.value + }} + /> + + + {selectedAuthentication.type === "oauth" || selectedAuthentication.type === "oauth2" || selectedAuthentication.type === "oauth2-app" ? +
    + + Only the name and url can be modified for Oauth2/OpenID connect. Please remake the authentication if you want to change the other fields like Client ID, Secret, Scopes etc. + +
    + : null} + + {selectedAuthentication.fields.map((data, index) => { + var fieldname = data.key.replaceAll("_", " ") + if (fieldname.endsWith(" basic")) { + fieldname = fieldname.substring(0, fieldname.length - 6) + } + + if (selectedAuthentication.type === "oauth" || selectedAuthentication.type === "oauth2" || selectedAuthentication.type === "oauth2-app") { + if (selectedAuthentication.fields[index].key !== "url") { + return null + } + } + + + //console.log("DATA: ", data, selectedAuthentication) + return ( +
    + + {fieldname} + + { + authenticationFields[index].value = e.target.value; + setAuthenticationFields(authenticationFields); + }} + /> +
    + ); + })} +
    + + + + +
    + ) : null; + + const getEnvironments = () => { + fetch(globalUrl + "/api/v1/getenvironments", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for apps :O!"); + return; + } + + return response.json(); + }) + .then((responseJson) => { + setEnvironments(responseJson); + + // Helper info for users in case they have a large queue and don't know about queue flushing + if (responseJson !== undefined && responseJson !== null && responseJson.length > 0) { + if (responseJson.length === 1 && responseJson[0].Type !== "cloud") { + setListItemExpanded(0) + } + + for (var i = 0; i < responseJson.length; i++) { + const env = responseJson[i]; + + // Check if queuesize is too large + if ( + env.queue !== undefined && + env.queue !== null && + env.queue > 100 + ) { + toast( + "Queue size for " + + env.name + + " is very large. We recommend you to reduce it by flushing the queue before continuing.", + ); + break; + } + } + } + }) + .catch((error) => { + toast(error.toString()); + }); + }; + + const getAppAuthenticationGroups = () => { + //console.log("DEBUG: Skipping app auth group loading") + //return + + fetch(globalUrl + "/api/v1/authentication/group", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for apps :O!"); + return; + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === true) { + setAppAuthenticationGroups(responseJson.data); + setShowAppAuthGroupLoader(false) + } + }) + .catch((error) => { + toast(error.toString()); + }); + }; + + const deleteAppAuthenticationGroup = (appAuthGroupId) => { + const url = `${globalUrl}/api/v1/authentication/group/${appAuthGroupId}` + fetch(url, { + method: "DELETE", + credentials: "include", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for deleting app auth group"); + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === false) { + toast("Failed to delete app authentication group"); + } else { + toast("App authentication group deleted") + getAppAuthenticationGroups() + } + }) + .catch((error) => { + toast(error.toString()) + }) + } + + const createAppAuthenticationGroup = (name, environment, description, appAuthIds) => { + // Makes list of ids into a full-on list of auth, but just with the ID + // The backend fills in the rest + console.log("INput auth: ", appAuthIds) + let app_auths = appAuthIds.map((appAuthId) => { + return { id: appAuthId }; + }) + + var parsedAppGroup = { + label: name, + environment: environment, + description: description, + app_auths: app_auths + } + + if (appAuthenticationGroupId !== undefined && appAuthenticationGroupId !== null && appAuthenticationGroupId !== "") { + parsedAppGroup.id = appAuthenticationGroupId + } + + fetch(globalUrl + "/api/v1/authentication/group", { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + body: JSON.stringify(parsedAppGroup), + }) + .then((response) => { + if (response.status !== 200) { + throw new Error("Failed to create app authentication group"); + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === false) { + toast("Failed to create. Please try again, or contact support@shuffler.io") + } else { + // Close the modal + setAppAuthenticationGroupModalOpen(false) + + toast("App authentication group created") + getAppAuthenticationGroups() + } + }) + .catch((error) => { + toast(error.toString()); + }); + }; + + const handleAppAuthGroupCheckbox = (data) => { + //let groupApp = data.app.id + var newappauth = appsForAppAuthGroup + if (appsForAppAuthGroup.includes(data.app.id)) { + newappauth = newappauth.filter((item) => item !== data.app.id) + } + + if (appsForAppAuthGroup.includes(data.id)) { + // Remove app from app auth group + newappauth = newappauth.filter((item) => item !== data.id) + setAppsForAppAuthGroup(newappauth) + return + } + + for (var i = 0; i < authentication.length; i++) { + if (authentication[i].id === data.id) { + continue + } + + if (!appsForAppAuthGroup.includes(authentication[i].id)) { + continue + } + + if (authentication[i].app.id === data.app.id) { + // Remove app from app auth group + newappauth = newappauth.filter((item) => item !== authentication[i].id) + toast(`App ${data.app.name} is already in this group`) + } + } + + setAppsForAppAuthGroup(newappauth.concat(data.id)) + } + + const authenticationView = appAuthenticationGroupModalOpen ? + ( +
    + {/* (appAuthenticationGroupModalOpen : { */} + {appAuthenticationGroupModalOpen && ( + { + setAppAuthenticationGroupModalOpen(false); + + setAppAuthenticationGroupId("") + setAppAuthenticationGroupName("") + setAppAuthenticationGroupEnvironment("") + setAppAuthenticationGroupDescription("") + setAppsForAppAuthGroup([]) + }} + PaperProps={{ + sx: { + borderRadius: theme?.palette?.DialogStyle?.borderRadius, + border: theme?.palette?.DialogStyle?.border, + minWidth: "1000px", + padding: "25px", + paddingLeft: "50px", + minHeight: "320px", + fontFamily: theme?.typography?.fontFamily, + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + zIndex: 1000, + '& .MuiDialogContent-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogTitle-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogActions-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + }, + }} + sx={{ + "& .MuiDialog-paper": { + backgroundColor: "rgb(26, 26, 26)", + }, + }} + > + + App Authentication Groups + + + +
    +
    + + Name + + { + setAppAuthenticationGroupName(event.target.value); + }} + /> +
    +
    + + Evironment + + {environments !== undefined && environments !== null && environments.length > 0 ? + + : + + Locations failed to load. Please try again + + } +
    +
    + +
    +
    + + + +
    + {/* Show a check box list of all app authentications to add to the auth group */} +
    + {authentication.map((data, index) => { + var checked = data.checked + if (data.label !== undefined && data.label !== null && data.label.toLowerCase() === "kms shuffle storage") { + return null + } + + if (checked === undefined || checked === null) { + checked = false + } + + if (appsForAppAuthGroup.includes(data.id)) { + checked = true + } + + return ( +
    + +
    + + { + handleAppAuthGroupCheckbox(data) + }} + name={data.label} + disabled={data.app.id in appsForAppAuthGroup} + /> +
    + + } + label={data.label} + /> +
    + ) + })} +
    +
    + + +
    +
    + )} + + +
    + ): null + + + + const appModal = showAppModal ? ( + { + setShowAppModal(false); + }} + PaperProps={{ + sx: { + borderRadius: theme?.palette?.DialogStyle?.borderRadius, + border: theme?.palette?.DialogStyle?.border, + width: '700px', + maxWidth: '700px', + overflowY: 'hidden', + height: "600px", + maxHeight: "600px", + fontFamily: theme?.typography?.fontFamily, + zIndex: 1000, + '& .MuiDialogContent-root': { + padding: '30px', + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogTitle-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogActions-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + }, + }} + > +
    + + + Add App Authentication + + + + + + +
    +
    + ) : null; + + return ( +
    + {appModal} +
    +
    +
    +

    App Authentication

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

    App Authentication Groups

    + + Disabled until further notice. Makes a workflow run replicate across all relevant authentications in an app auth group. Useful when the EXACT same workflow is supposed to run many times from one single input. {" "} + + Learn more about App Authentication Groups + + + +
    + +
    + + + {["Label", "Environment", "App Auth", "Created At", "Actions"].map((header, index) => ( + + ))} + + {showAppAuthGroupLoader ? + [...Array(6)].map((_, rowIndex) => ( + + {Array(5) + .fill() + .map((_, colIndex) => ( + + + + ))} + + )) + : appAuthenticationGroups.length === 0 ? ( +
    + + No authentication groups found. + +
    + ): appAuthenticationGroups.map((data, index) => { + var bgColor = "#212121"; + if (index % 2 === 0) { + bgColor = "#1A1A1A"; + } + + if (data.app_auths === undefined || data.app_auths === null) { + data.app_auths = [] + } + + return ( + + + + + {data.app_auths.map((appAuth, index) => { + if (appAuth.app.large_image === undefined || appAuth.app.large_image === null || appAuth.app.large_image === "") { + const foundImage = authentication.find((auth) => auth.app.id === appAuth.app.id) + if (foundImage !== undefined) { + appAuth.app.large_image = foundImage.app.large_image + + appAuth.app.name = foundImage.app.name + } + } + + const tooltip = `${appAuth.app.name.replaceAll("_", " ")} (authname: ${appAuth.label})` + + return ( + + {appAuth.app.name} + + ) + })} +
    + } + style={{ display:'table-cell', verticalAlign: 'middle' }} + /> + + + { + setAppAuthenticationGroupId(data.id) + + setAppAuthenticationGroupName(data.label) + setAppAuthenticationGroupDescription(data.description) + + setAppsForAppAuthGroup(data.app_auths.map((appAuth) => appAuth.id)) + setAppAuthenticationGroupEnvironment(data.environment) + setAppAuthenticationGroupModalOpen(true) + }} + > + edit icon + + { + deleteAppAuthenticationGroup(data.id) + }} + > + delete icon + +
    + } + style={{ display:'table-cell', verticalAlign: 'middle' }} + /> + + + ); + } + )} + +
    + +
    +
    +
    +
    + ); +}); + +export default AppAuthTab; + + +const SearchBox = ({ currentRefinement, refine, isSearchStalled, searchQuery, setSearchQuery }) => { + const handleSearch = (e) => { + refine(searchQuery.trim()); + }; + + return ( + + + + + ), + endAdornment: ( + + {searchQuery?.length > 0 && ( + { + setSearchQuery('') + // removeQuery("q"); + refine('') + }} + /> + )} + + + ), + + }} + autoComplete="off" + color="primary" + placeholder="Search more than 2500 Apps" + id="shuffle_search_field" + onChange={(event) => { + setSearchQuery(event.currentTarget.value); + // removeQuery("q"); + refine(event.currentTarget.value); + }} + onKeyDown={(event) => { + if(event.key === "Enter") { + event.preventDefault(); + } + }} + limit={5} + /> + {/*isSearchStalled ? 'My search is stalled' : ''*/} + + ); +}; + +const Hits = ({ + hits, + insights, + setIsAnyAppActivated, + searchQuery, + isCloud, + globalUrl, + userdata, + getAppAuthentication +}) => { + const [mouseHoverIndex, setMouseHoverIndex] = useState(-1); + const [selectedAppData, setSelectedAppData] = useState({}) + const [appid, setAppId] = useState("") + const [selectedAuthentication, setSelectedAuthentication] = useState({}) + const [authenticationModalOpen, setAuthenticationModalOpen] = useState(false) + const [authenticationType, setAuthenticationType] = React.useState(""); + const [appAuthentication, setAppAuthentication] = useState([]); + const [selectedMeta, setSelectedMeta] = useState(undefined); + const [selectedAction, setSelectedAction] = useState( + { + "app_name": selectedAppData.name, + "app_id": selectedAppData.id, + "app_version": selectedAppData.version, + "large_image": selectedAppData.large_image, + } + ) + const navigate = useNavigate(); + + const normalizedString = (name) => { + if (typeof name === 'string') { + return name.replace(/_/g, ' '); + } else { + return name; + } + }; + + + let workflowDelay = 0; + const isHeader = true; + const paperStyle = { + color: "rgba(241, 241, 241, 1)", + padding: isHeader ? null : 15, + cursor: "pointer", + width: '100%', + maxHeight: 96, + borderRadius: 4, + transition: 'background-color 0.3s ease', + }; + + const base64_decode = (str) => { + return decodeURIComponent( + atob(str) + .split("") + .map(function (c) { + return "%" + ("00" + c.charCodeAt(0).toString(16)).slice(-2); + }) + .join("") + ); + }; + + const handleAppAuthenticationType = (selectedAppData)=> { + + if (selectedAppData.authentication === undefined || selectedAppData.authentication === null) { + setAuthenticationType({ + type: "", + }) + + selectedAppData.authentication = { + type: "", + required: false, + } + } else { + setAuthenticationType( + selectedAppData.authentication.type === "oauth2-app" || (selectedAppData.authentication.type === "oauth2" && selectedAppData.authentication.redirect_uri !== undefined && selectedAppData.authentication.redirect_uri !== null) ? { + type: selectedAppData.authentication.type, + redirect_uri: selectedAppData.authentication.redirect_uri, + refresh_uri: selectedAppData.authentication.refresh_uri, + token_uri: selectedAppData.authentication.token_uri, + scope: selectedAppData.authentication.scope, + client_id: selectedAppData.authentication.client_id, + client_secret: selectedAppData.authentication.client_secret, + grant_type: selectedAppData.authentication.grant_type, + } : { + type: "", + } + ) + } + } + + function Heading(props) { + const element = React.createElement( + `h${props.level}`, + { style: { marginTop: 40 } }, + props.children + ); + return ( + + {props.level !== 1 ? ( + + ) : null} + {element} + + ); + } + + const getAppDocs = (appname, location, version) => { + fetch(`${globalUrl}/api/v1/docs/${appname}?location=${location}&version=${version}`, { + headers: { + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status === 200) { + //toast("Successfully GOT app "+appId) + } else { + //toast("Failed getting app"); + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === true) { + if (responseJson.meta !== undefined && responseJson.meta !== null && Object.getOwnPropertyNames(responseJson.meta).length > 0) { + setSelectedMeta(responseJson.meta) + } + + if (responseJson.reason !== undefined && responseJson.reason !== undefined && responseJson.reason.length > 0) { + if (!responseJson.reason.includes("404: Not Found") && responseJson.reason.length > 25) { + // Translate into markdown ![]() + const imgRegex = / ({ + ...prevState, + documentation: newdata, + })); + } + } + } + + }) + .catch((error) => { + toast(error.toString()); + }); + }; + + const handleDecodeOfOpenApiData = (data) => { + var appexists = false; + var parsedapp = {}; + + if (data.app !== undefined && data.app !== null) { + var parsedBaseapp = ""; + try { + parsedBaseapp = base64_decode(data.app); + } catch (e) { + parsedBaseapp = data; + } + + parsedapp = JSON.parse(parsedBaseapp); + parsedapp.name = parsedapp.name.replaceAll("_", " "); + + appexists = + parsedapp.name !== undefined && + parsedapp.name !== null && + parsedapp.name.length !== 0; + if(parsedapp?.id.length > 0){ + setSelectedAppData(parsedapp) + handleAppAuthenticationType(parsedapp) + const apptype = selectedAppData?.generated === false ? "python" : "openapi" + getAppDocs(parsedapp.name, apptype, parsedapp.version); + setAuthenticationModalOpen(true); + } + } + + if (data.openapi === undefined || data.openapi === null) { + return; + } + + var parsedDecoded = ""; + try { + parsedDecoded = base64_decode(data.openapi); + } catch (e) { + parsedDecoded = data; + } + + parsedapp = JSON.parse(parsedDecoded); + data = + parsedapp.body === undefined ? parsedapp : JSON.parse(parsedapp.body); + }; + + const getAppData = (appid) => { + if (appid === undefined || appid === null || appid.length === 0) { + return; + } + const url = `${globalUrl}/api/v1/apps/${appid}/config`; + + fetch(url, { + credentials: "include", + method: "GET", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => { + if (response.status !== 200) { + toast.error("Failed to get app data or App doesn't. Please contact support@shuffler.io"); + return; + } + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === true) { + handleDecodeOfOpenApiData(responseJson); + } else { + toast.error("Failed to get app data or App doesn't exist"); + } + }) + .catch((error) => { + console.error("error for app is :", error); + }); + }; + + const UpdateAppAuthentication = (data) => { + if (data === undefined || data === null) { + return; + } + const filteredData = data.filter((appAuth) => appAuth?.app?.id === appid); + if (filteredData.length === 0) { + setAppAuthentication([]); + setSelectedAuthentication({}); + } else { + setAppAuthentication(filteredData); + setSelectedAuthentication(filteredData[0]); + } + }; + + const HandleAppAuthentication = ()=>{ + + const url = `${globalUrl}/api/v1/apps/authentication`; + + fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + }).then((response) => { + if (response.status !== 200) { + return; + } + return response.json(); + }).then((responseJson) => { + if (responseJson.success === true) { + UpdateAppAuthentication(responseJson.data); + } else { + toast.error("Failed to get app authentication data"); + } + }).catch((error) => { + console.error("error for app is :", error); + }); + } + + const handleAppAuthenticationNew = (data) => () => { + + const appid = data.objectID; + if (appid > 0) { + setAppId(appid); + } + if (appid.length > 0) { + toast.info(`Getting authentication for ${data.name}. Please wait...`); + getAppData(appid); + HandleAppAuthentication(); + } + } + + const AuthenticationData = (props) => { + const selectedApp = props.app; + + const [authenticationOption, setAuthenticationOptions] = React.useState({ + app: JSON.parse(JSON.stringify(selectedApp)), + fields: {}, + label: "", + usage: [ + { + // workflow_id: workflow.id, + }, + ], + id: uuidv4(), + active: true, + }); + + if ( + selectedApp.authentication === undefined || + selectedApp.authentication.parameters === null || + selectedApp.authentication.parameters === undefined || + selectedApp.authentication.parameters.length === 0 + ) { + return ( + + + {selectedApp.name} does not require authentication + + + ); + } + + authenticationOption.app.actions = []; + + for (let paramkey in selectedApp.authentication.parameters) { + if ( + authenticationOption.fields[ + selectedApp.authentication.parameters[paramkey].name + ] === undefined + ) { + authenticationOption.fields[ + selectedApp.authentication.parameters[paramkey].name + ] = ""; + } + } + + const setNewAppAuth = (appAuthData, refresh) => { + setSelectedAuthentication(appAuthData); + var headers = { + "Content-Type": "application/json", + "Accept": "application/json", + } + + headers["Org-Id"] = userdata?.active_org?.id + + fetch(globalUrl + "/api/v1/apps/authentication", { + method: "PUT", + headers: headers, + body: JSON.stringify(appAuthData), + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for setting app auth :O!"); + + if (response.status === 400) { + toast.error("Failed setting new auth. Please try again", { + "autoClose": true, + }) + } + } + + return response.json(); + }) + .then((responseJson) => { + if (!responseJson.success) { + toast.error("Error: " + responseJson.reason, { + "autoClose": false, + }) + + } else { + HandleAppAuthentication() + setAuthenticationModalOpen(false) + getAppAuthentication() + } + }) + .catch((error) => { + console.log("New auth error: ", error.toString()); + }); + }; + + const handleSubmitCheck = () => { + if (authenticationOption.label.length === 0) { + authenticationOption.label = `Auth for ${selectedApp.name}`; + } + for (let paramkey in selectedApp.authentication.parameters) { + if ( + authenticationOption.fields[ + selectedApp.authentication.parameters[paramkey].name + ].length === 0 + ) { + if ( + selectedApp.authentication.parameters[paramkey].value !== undefined && + selectedApp.authentication.parameters[paramkey].value !== null && + selectedApp.authentication.parameters[paramkey].value.length > 0 + ) { + authenticationOption.fields[ + selectedApp.authentication.parameters[paramkey].name + ] = selectedApp.authentication.parameters[paramkey].value; + } else { + if ( + selectedApp.authentication.parameters[paramkey].schema.type === "bool" + ) { + authenticationOption.fields[ + selectedApp.authentication.parameters[paramkey].name + ] = "false"; + } else { + toast( + "Field " + + selectedApp.authentication.parameters[paramkey].name + + " can't be empty" + ); + return; + } + } + } + } + + var newAuthOption = JSON.parse(JSON.stringify(authenticationOption)); + var newFields = []; + for (let authkey in newAuthOption.fields) { + const value = newAuthOption.fields[authkey]; + newFields.push({ + "key": authkey, + "value": value, + }); + } + + newAuthOption.fields = newFields + setNewAppAuth(newAuthOption) + } + + if (authenticationOption.label === null || authenticationOption.label === undefined) { + authenticationOption.label = selectedApp.name + " authentication"; + } + + return ( +
    + +
    + Authentication for {selectedApp.name.replaceAll("_", " ", -1)} +
    +
    + + + What is app authentication? + +
    + These are required fields for authenticating with {selectedApp.name} +
    + Label for you to remember + { + authenticationOption.label = event.target.value; + }} + /> + +
    + {selectedApp.authentication.parameters.map((data, index) => { + if (data.value === "" || data.value === null || data.value === undefined || data.name === "url") { + } + + + return ( +
    + + {data.name} + + {data.schema !== undefined && + data.schema !== null && + data.schema.type === "bool" ? ( + + ) : ( + { + authenticationOption.fields[data.name] = + event.target.value; + }} + /> + )} +
    + ); + })} + + + + + +
    + ); + }; + + const authenticationModal = authenticationModalOpen ? ( + {setSelectedMeta(undefined)}} + PaperProps={{ + style: { + pointerEvents: "auto", + color: "white", + minWidth: 1100, + minHeight: 700, + maxHeight: 700, + padding: 15, + overflow: "hidden", + zIndex: 10012, + border: theme.palette.defaultBorder, + }, + }} + > +
    + { selectedAppData.reference_info === undefined || + selectedAppData.reference_info === null || + selectedAppData.reference_info.github_url === undefined || + selectedAppData.reference_info.github_url === null || + selectedAppData.reference_info.github_url.length === 0 ? ( + + + {`Documentation + + ) : ( + + {`Documentation + + )} +
    + + + + + + { + setAuthenticationModalOpen(false); + }} + > + + +
    +
    + {authenticationType.type === "oauth2" || authenticationType.type === "oauth2-app" ? + + : + + } +
    +
    + {selectedAppData.documentation === undefined || + selectedAppData.documentation === null || + selectedAppData.documentation.length === 0 ? ( + +
    + + {selectedAppData?.description} + +
    + + +
    + + There is no Shuffle-specific documentation for this app yet outside of the general description above. Documentation is written for each api, and is a community effort. We hope to see your contribution! + + +
    + + + Want to help the making of, or improve this app?{" "} +
    + + Join the community on Discord! + +
    + + + Want to help change this app directly? + + {selectedAppData.reference_info === undefined || + selectedAppData.reference_info === null || + selectedAppData.reference_info.github_url === undefined || + selectedAppData.reference_info.github_url === null || + selectedAppData.reference_info.github_url.length === 0 ? ( + + + + Check it out on Github! + + + + ) : ( + + + + Check it out on Github! + + + + )} +
    + ) : ( +
    + {selectedMeta !== undefined && selectedMeta !== null && Object.getOwnPropertyNames(selectedMeta).length > 0 && selectedMeta.name !== undefined && selectedMeta.name !== null ? +
    +
    + {isMobile ? null : ( + + + + + + )} + {isMobile ? null : ( +
    + )} + + {selectedMeta.read_time} minute + {selectedMeta.read_time === 1 ? "" : "s"} to read + +
    +
    + {isMobile || + selectedMeta.contributors === undefined || + selectedMeta.contributors === null ? ( + "" + ) : ( +
    + {selectedMeta.contributors.slice(0, 7).map((data, index) => { + return ( + + + {data.url} + + + ); + })} +
    + )} +
    +
    + : null} + + + {selectedAppData.documentation} + +
    + )} +
    +
    +
    +) : null; + + return ( +
    + {authenticationModal} + {hits.length === 0 && searchQuery.length >= 0 ? ( +
    + + No Apps Found + +
    + ) : ( + +
    + {hits.map((data, index) => { + const appUrl = isCloud + ? `/apps/${data.objectID}?queryID=${data.__queryID}` + : `https://shuffler.io/apps/${data.objectID}?queryID=${data.__queryID}`; + + return ( + + + + setMouseHoverIndex(index)} + onMouseLeave={() => setMouseHoverIndex(-1)} + > + +
    +
    + + + + + + ); + })} +
    + + )} +
    + ); + +}; + +const CustomSearchBox = connectSearchBox(SearchBox); +const CustomHits = connectHits(Hits); diff --git a/frontend/src/components/AppGrid.jsx b/frontend/src/components/AppGrid.jsx index b0577937..46afcb6b 100644 --- a/frontend/src/components/AppGrid.jsx +++ b/frontend/src/components/AppGrid.jsx @@ -416,9 +416,13 @@ const AppGrid = (props) => { > {hits.map((data, index) => { const appUrl = - isCloud - ? `/apps/${data.objectID}?queryID=${data.__queryID}` - : `https://shuffler.io/apps/${data.objectID}?queryID=${data.__queryID}`; + isCloud === true ? + `/apps/${data.id}` + : `https://shuffler.io/apps/${data.objectID}`; + + if (data.name === "" && data.id === "") { + return null + } return ( { }; const appUrl = - isCloud === true - ? `/apps/${data.id}` + isCloud === true ? + `/apps/${data.id}` : `https://shuffler.io/apps/${data.id}`; - if (data.name === "" && data.id === "") { - return null - } + if (data.name === "" && data.id === "") { + return null + } return ( { const isCloud = window.location.host === "localhost:3002" || - window.location.host === "shuffler.io" || window.location.host === "localhost:3000" + window.location.host === "shuffler.io" ? true : false; @@ -574,8 +574,10 @@ const AppModal = ({ open, onClose, app, globalUrl, getApps }) => { {newAppname} { + const [hovered, setHovered] = useState(""); + + //console.log("Date: ", new Date("2019-11-14T08:00:00.000Z")) + //var inputdata = keys.data + //const inputdata = keys.data === undefined ? [{"key": inputname, "data": keys}] : keys.data + const inputdata = keys.data === undefined ? keys : keys.data + + /* + series={ + + } + area={ + } + gradient={ + , + + ]} + /> + } + /> + } + gridlines={} />} + colorScheme={(colorInput) => { + var color = "#f86a3e" + //if (colorInput !== undefined && colorInput.length > 0) { + // color = colorInput[0].metadata !== undefined && colorInput[0].metadata.color !== undefined ? colorInput[0].metadata.color : color + //} + + return color + }} + /> + } + */ + + return ( +
    + + {inputname} + + } /> + } + /> + {/* + ( + + )} + /> + } + /> + } + /> + */} +
    + ) +} + + +const AppStats = (defaultprops) => { + const { globalUrl, appId , workflowId} = defaultprops; + const [keys, setKeys] = useState([]) + const [widgetData, setWidgetData] = useState({}); + const [searches, setSearches] = useState([]); + const [clickData, setClickData] = useState(undefined); + const [conversionData, setConversionData] = useState(undefined); + + const handleDataSetting = (inputdata, grouping) => { + var newlist = [] + + for (var key in inputdata.events) { + var newlist = [] + + for (var subkey in inputdata.events[key].data) { + const subdata = inputdata.events[key].data[subkey] + //console.log("Timestamp: ", subdata.key) + + if (grouping === "day") { + const daysplit = subdata.key.split("T")[0] + //console.log("Grouping by day: ", daysplit) + + const foundIndex = newlist.findIndex(data => data.key === daysplit) + if (foundIndex !== undefined && foundIndex !== null && foundIndex >= 0) { + newlist[foundIndex].data += 1 + newlist[foundIndex].y += 1 + } else { + newlist.push({ + "key": daysplit, + "x": daysplit, + "data": 1, + "y": 1, + }) + } + } else { + console.log("No grouping set?") + try { + inputdata.events[key].data[subkey].key = new Date(subdata.key) + } catch (e) { + console.log("Failed timestamp: ", e) + } + } + } + + // Fixing timestamps after sorting based on day + for (var subkey in newlist) { + const subdata = newlist[subkey] + newlist[subkey].key = new Date(subdata.key) + } + + console.log("Inputdata: ", inputdata.events[key]) + if (inputdata.events[key].key === "click") { + setClickData(newlist) + } else if (inputdata.events[key].key === "conversion") { + setConversionData(newlist) + } else { + console.log("No handler for ", inputdata.events[key].key) + } + } + + //new Date('11/22/2019') + setWidgetData(inputdata) + } + + const getAppStats = (appId) => { + fetch(`${globalUrl}/api/v1/apps/${appId}/stats`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for workflows :O!: ", response.status); + return; + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson["success"] === false) { + return + } + + handleDataSetting(responseJson, "day") + }) + .catch((error) => { + console.log("error: ", error) + }); + } + + const getWorkflowStats = (workflowId) => { + fetch(`${globalUrl}/api/v1/workflow/${workflowId}/stats`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for workflows :O!: ", response.status); + return; + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson["success"] === false) { + return + } + + handleDataSetting(responseJson, "day") + }) + .catch((error) => { + console.log("error: ", error) + }); + } + + + useEffect(() => { + //setWidgetData(inputdata) + getAppStats(appId) + getWorkflowStats(workflowId) + }, []) + + const paperStyle = { + textAlign: "center", + padding: 40, + margin: 5, + backgroundColor: theme.palette.inputColor, + } + + console.log("Widget: ", widgetData) + const data = ( +
    +
    + + + {widgetData.orgs} + + + Orgs + + + + + {widgetData.searches} + + + Searches + + + {/* + + + {widgetData.clicks} + + + Clicks + + + + + {widgetData.conversions} + + + Conversions + + + + + {widgetData.forks} + + + Forks + + + */} +
    + + {clickData === undefined ? + null + : + + } + +
    + {conversionData === undefined ? + null + : + + } +
    + ) + + const dataWrapper = ( +
    {data}
    + ); + + return dataWrapper; +} + +export default AppStats; \ No newline at end of file diff --git a/frontend/src/components/CloudSyncTab.jsx b/frontend/src/components/CloudSyncTab.jsx new file mode 100644 index 00000000..08926570 --- /dev/null +++ b/frontend/src/components/CloudSyncTab.jsx @@ -0,0 +1,780 @@ +import React, { useEffect, useState, useContext } from "react"; +import { + FormControl, + Card, + Tooltip, + Typography, + TextField, + Button, + Grid, + ListItem, + ListItemText, + ListItemAvatar, + IconButton, + Avatar, + Zoom, + InputAdornment, + Switch, + Skeleton +} from "@mui/material"; +import { ToastContainer, toast } from "react-toastify"; +import { + Edit as EditIcon, + Polyline as PolylineIcon, + CheckCircle as CheckCircleIcon, + Close as CloseIcon, + Visibility as VisibilityIcon, + VisibilityOff as VisibilityOffIcon, +} from "@mui/icons-material"; +import theme from "../theme.jsx"; +import { styled } from '@mui/styles'; +import { Context } from "../context/ContextApi.jsx"; + +const CloudSyncTab = (props) => { + const { + userdata, + globalUrl, + serverside + } = props; + const [cloudSyncApikey, setCloudSyncApikey] = useState(""); + const [loading, setLoading] = useState(false); + const [showApiKey, setShowApiKey] = useState(false); + const [orgSyncResponse, setOrgSyncResponse] = React.useState(""); + const [organizationFeatures, setOrganizationFeatures] = React.useState({}); + const [selectedOrganization, setSelectedOrganization] = React.useState({}); + const [selectedStatus, setSelectedStatus] = React.useState([]); + const [orgRequest, setOrgRequest] = React.useState(true); + const [userSettings, setUserSettings] = React.useState({}); + const [, forceUpdate] = React.useState(); + const itemColor = "white"; + const isCloud = window?.location?.host === "localhost:3002" || window?.location?.host === "shuffler.io"; + useEffect(() => { getSettings(); }, []); + const GridItem = (props) => { + const [expanded, setExpanded] = React.useState(false); + const [showEdit, setShowEdit] = React.useState(false); + const [newValue, setNewValue] = React.useState(-100); + + const primary = props.data.primary; + const secondary = props.data.secondary; + const primaryIcon = props.data.icon; + const secondaryIcon = props.data.active ? + + : + + + const submitFeatureEdit = (sync_features) => { + if (!userdata.support) { + console.log("User does not have support access and can't edit features"); + return + } + + sync_features.editing = true + const data = { + org_id: selectedOrganization.id, + sync_features: sync_features, + }; + console.log("sync_features: ", sync_features); + const url = globalUrl + `/api/v1/orgs/${selectedOrganization.id}`; + fetch(url, { + mode: "cors", + method: "POST", + body: JSON.stringify(data), + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then((response) => + response.json().then((responseJson) => { + if (responseJson["success"] === false) { + toast("Failed updating org: ", responseJson.reason); + } else { + toast("Successfully edited org!"); + } + }) + ) + .catch((error) => { + toast("Err: " + error.toString()); + }); + } + + const enableFeature = () => { + console.log("Enabling " + primary) + + console.log(selectedOrganization.sync_features) + // Check if primary is in sync_features + var tmpprimary = primary.replaceAll(" ", "_") + if (!(tmpprimary in selectedOrganization.sync_features)) { + console.log("Primary not in sync_features: " + tmpprimary) + return + } + + if (props.data.active) { + selectedOrganization.sync_features[tmpprimary].active = false + } else { + selectedOrganization.sync_features[tmpprimary].active = true + } + + setSelectedOrganization(selectedOrganization) + forceUpdate(Math.random()) + submitFeatureEdit(selectedOrganization.sync_features) + } + + const submitEdit = (e) => { + e.preventDefault(); + e.stopPropagation(); + + // Check if primary is in sync_features + var tmpprimary = primary.replaceAll(" ", "_") + if (!(tmpprimary in selectedOrganization.sync_features)) { + console.log("Primary not in sync_features: " + tmpprimary) + return + } + + // Make it into a number + var tmp = parseInt(newValue) + if (isNaN(tmp)) { + console.log("Not a number: " + newValue) + return + } + + selectedOrganization.sync_features[tmpprimary].limit = tmp + + setSelectedOrganization(selectedOrganization) + forceUpdate(Math.random()) + submitFeatureEdit(selectedOrganization.sync_features) + } + const handleToggleFeature = (e) => { + // Your logic for toggling the feature's active state + console.log(`Toggling ${primary}`); + if (!isCloud || userdata.support !== true) { + return + } + + e.preventDefault(); + e.stopPropagation(); + + enableFeature() + }; + + return ( + +
    + { + setExpanded(prev => !prev); + if(showEdit){ + setShowEdit(false) + } + }} + > + + {primaryIcon} + + + {isCloud && userdata.support === true ? + + { + e.preventDefault(); + e.stopPropagation(); + console.log('expanded', expanded) + if (expanded){ + setExpanded(false) + } + if (showEdit) { + setShowEdit(false) + return + } + + console.log("Edit") + + setShowEdit(true) + }} + /> + + : null} + {userdata.support === true ?( + + + + ):( + + { + if (!isCloud || userdata.support !== true) { + return + } + + e.preventDefault(); + e.stopPropagation(); + + enableFeature() + }} + > + {secondaryIcon} + + + )} + + + {expanded ? +
    + + Usage:  + {props.data.limit === 0 ? ( + "Unlimited" + ) : ( + + {props.data.usage} / {props.data.limit === "" ? "Unlimited" : props.data.limit} + + )} + + {/* + Data sharing: {props.data.data_collection} + */} + Description: {secondary} +
    + : null} + + + {showEdit ? + { + console.log("Submit") + submitEdit(e) + }}> + + + { + setNewValue(event.target.value) + }} + /> + + + + : null} +
    +
    + ); + }; + const handleGetOrg = (orgId) => { + + if (serverside !== true && window.location.search !== undefined && window.location.search !== null) { + const urlSearchParams = new URLSearchParams(window.location.search); + const params = Object.fromEntries(urlSearchParams.entries()); + const foundorgid = params["org_id"]; + if (foundorgid !== undefined && foundorgid !== null) { + orgId = foundorgid; + } + } + + if (orgId.length === 0) { + toast("Organization ID not defined. Please contact us on https://shuffler.io if this persists logout."); + return; + } + + // Just use this one? + + fetch(`${globalUrl}/api/v1/orgs/${orgId}`, { + method: "GET", + credentials: "include", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => { + if (response.status === 401) { + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson["success"] === false) { + toast("Failed getting your org. If this persists, please contact support."); + } else { + if ( + responseJson.sync_features === undefined || + responseJson.sync_features === null + ) { + responseJson.sync_features = {}; + } + + setSelectedOrganization(responseJson) + var lists = { + active: { + triggers: [], + features: [], + sync: [], + }, + inactive: { + triggers: [], + features: [], + sync: [], + }, + }; + setOrganizationFeatures(lists); + } + }) + .catch((error) => { + console.log("Error getting org: ", error); + toast("Error getting current organization"); + }); + }; + const handleStopOrgSync = (org_id) => { + if (org_id === undefined || org_id === null) { + toast("Couldn't get org " + org_id); + return; + } + + const data = {}; + + const url = globalUrl + "/api/v1/orgs/" + org_id + "/stop_sync"; + fetch(url, { + mode: "cors", + method: "POST", + body: JSON.stringify(data), + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then((response) => { + if (response.status === 200) { + console.log("Cloud sync success?"); + toast("Successfully stopped cloud sync"); + } else { + console.log("Cloud sync fail?"); + toast( + "Failed stopping sync. Try again, and contact support if this persists." + ); + } + + return response.json(); + }) + .then((responseJson) => { + setTimeout(() => { + handleGetOrg(org_id); + }, 1000); + }) + .catch((error) => { + toast("Err: " + error.toString()); + }); + }; + + const enableCloudSync = (apikey, organization, disableSync) => { + setOrgSyncResponse(""); + + const data = { + apikey: apikey, + organization: organization, + disable: disableSync, + }; + + const url = globalUrl + "/api/v1/cloud/setup"; + fetch(url, { + mode: "cors", + method: "POST", + body: JSON.stringify(data), + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then((response) => { + setLoading(false); + if (response.status === 200) { + console.log("Cloud sync success?"); + } else { + console.log("Cloud sync fail?"); + } + + return response.json(); + //setTimeout(() => { + //}, 1000) + }) + .then((responseJson) => { + console.log("RESP: ", responseJson); + if ( + responseJson.success === false && + responseJson.reason !== undefined + ) { + setOrgSyncResponse(responseJson.reason); + toast("Failed to handle sync: " + responseJson.reason); + } else if (!responseJson.success) { + toast("Failed to handle sync."); + } else { + //getOrgs(); API no longer in use, as it's in handleInfo request + if (disableSync) { + toast("Successfully disabled sync!"); + setOrgSyncResponse("Successfully disabled syncronization"); + } else { + toast("Cloud Syncronization successfully set up!"); + setOrgSyncResponse( + "Successfully started syncronization. Cloud features you now have access to can be seen below." + ); + } + + selectedOrganization.cloud_sync = !selectedOrganization.cloud_sync; + setSelectedOrganization(selectedOrganization); + setCloudSyncApikey(""); + + handleGetOrg(userdata.active_org.id); + } + }) + .catch((error) => { + setLoading(false); + toast("Err: " + error.toString()); + }); + }; + const getSettings = () => { + fetch(globalUrl + "/api/v1/getsettings", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 when getting settings :O!"); + } + + return response.json(); + }) + .then((responseJson) => { + setUserSettings(responseJson); + }) + .catch((error) => { + console.log(error); + }); + }; + if ( + selectedOrganization.id === undefined && + userdata !== undefined && + userdata.active_org !== undefined && + orgRequest === true + ) { + setOrgRequest(false); + handleGetOrg(userdata.active_org.id); + } + + return ( +
    +
    +

    + Cloud syncronization +

    + + What does cloud sync do? Cloud synchronization is a way of getting more out of Shuffle. Shuffle will ALWAYS make every option open source, but features relying on other users can't be done without a collaborative approach. + +
    + + {isCloud ? ( +
    +
    + + Currently syncronizing:{" "} + {selectedOrganization.cloud_sync_active === true + ? True + : False} + + {selectedOrganization.cloud_sync_active ? ( + + Syncronization interval:{" "} + {selectedOrganization.sync_config.interval === 0 + ? "60" + : selectedOrganization.sync_config.interval} + + ) : null} + + Your Api key + + {userSettings?.apikey === undefined || userSettings?.apikey === null || userSettings?.apikey?.length <=0 ? ( + + ): +
    + + { + setShowApiKey(!showApiKey) + }} + > + {showApiKey ? : } + + + ) + }} + required + fullWidth={true} + disabled={true} + autoComplete="cloud apikey" + id="apikey_field" + margin="normal" + placeholder="Cloud Apikey" + variant="outlined" + value={userSettings?.apikey} + defaultValue={userSettings?.apikey} + type={!isCloud || showApiKey ? "text" : "password"} + /> + {selectedOrganization.cloud_sync_active ? ( + + ) : null} +
    } +
    +
    + ) : ( +
    +
    + { + setCloudSyncApikey(event.target.value); + }} + /> + +
    + {orgSyncResponse.length > 0 ? ( + + Message from Shuffle Cloud: {orgSyncResponse} + + ) : null} +
    + )} + +

    + Features +

    + + Features and Limitations that are currently available to you in your Cloud or Hybrid Organization. App Executions (App Runs) reset monthly. If the organization is a customer or in a trial, these features limitations are not always enforced. + + + {selectedOrganization.sync_features === undefined || + selectedOrganization.sync_features === null + ? + {[...Array(18)].map((_, i) => ( + +
    + +
    +
    + ))} +
    + : Object.keys(selectedOrganization.sync_features).map(function ( + key, + index + ) { + + if (key === "schedule" || key === "apps" || key === "updates" || key === "editing") { + return null; + } + + const item = selectedOrganization.sync_features[key]; + if (item === null) { + return null + } + + const newkey = key.replaceAll("_", " "); + const griditem = { + primary: newkey, + secondary: + item.description === undefined || + item.description === null || + item.description.length === 0 + ? "Not defined yet" + : item.description, + limit: item.limit, + usage: item.usage === undefined || + item.usage === null ? 0 : item.usage, + data_collection: "None", + active: item.active, + icon: , + }; + + return ( + + + + ); + })} +
    +
    + ); +}; + +export default CloudSyncTab; diff --git a/frontend/src/components/EditOrgTab.jsx b/frontend/src/components/EditOrgTab.jsx new file mode 100644 index 00000000..6a5e1662 --- /dev/null +++ b/frontend/src/components/EditOrgTab.jsx @@ -0,0 +1,446 @@ +import React, { useEffect, useState, useContext } from 'react'; +import OrgHeaderexpanded from "./OrgHeaderexpandedNew.jsx"; +import OrgHeader from './OrgHeaderNew.jsx'; +import { toast } from "react-toastify"; +import CloudSyncTab from './CloudSyncTab.jsx'; +import { + FileCopy as FileCopyIcon, +} from "@mui/icons-material"; +import { + Button, + Tooltip, + IconButton, +} from "@mui/material"; + +const EditOrgTab = (props) => { + const { + userdata, + globalUrl, + serverside, + selectedOrganization, + setSelectedOrganization, + handleGetOrg, + selectedStatus, setSelectedStatus, + handleEditOrg, + } = props; + const [organizationFeatures, setOrganizationFeatures] = React.useState({}); + const [users, setUsers] = React.useState([]); + const [orgRequest, setOrgRequest] = React.useState(true); + const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true"); + useEffect(() => { + if(users.length === 0) { + getUsers(); + } + }, []); + + const handleStatusChange = (event) => { + const { value } = event.target; + setSelectedStatus(value); + + handleEditOrg( + selectedOrganization?.name, + selectedOrganization?.description, + selectedOrganization.id, + selectedOrganization.image, + { + app_download_repo: selectedOrganization?.defaults?.app_download_repo, + app_download_branch: selectedOrganization?.defaults?.app_download_branch, + workflow_download_repo: selectedOrganization?.defaults?.workflow_download_repo, + workflow_download_branch: selectedOrganization?.defaults?.workflow_download_branch, + notification_workflow: selectedOrganization?.defaults?.notification_workflow, + documentation_reference: selectedOrganization?.defaults?.documentation_reference, + workflow_upload_repo: selectedOrganization?.defaults?.workflow_upload_repo, + workflow_upload_branch: selectedOrganization?.defaults?.workflow_upload_branch, + workflow_upload_username: selectedOrganization?.defaults?.workflow_upload_username, + workflow_upload_token: selectedOrganization?.defaults?.workflow_upload_token, + newsletter: selectedOrganization?.defaults?.newsletter, + weekly_recommendations: selectedOrganization?.defaults?.weekly_recommendations, + }, + { + sso_entrypoint: selectedOrganization?.sso_config?.sso_entrypoint, + sso_certificate: selectedOrganization?.sso_config?.sso_certificate, + client_id: selectedOrganization?.sso_config?.client_id, + client_secret: selectedOrganization?.sso_config?.client_secret, + openid_authorization: selectedOrganization?.sso_config?.openid_authorization, + openid_token: selectedOrganization?.sso_config?.openid_token, + SSORequired: selectedOrganization?.sso_config?.SSORequired, + auto_provision: selectedOrganization?.sso_config?.auto_provision, + }, + value.length === 0 ? ["none"] : value, + ); + }; + + + const getUsers = () => { + fetch(globalUrl + "/api/v1/getusers", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + // Ahh, this happens because they're not admin + // window.location.pathname = "/workflows" + return; + } + + return response.json(); + }) + .then((responseJson) => { + setUsers(responseJson); + }) + .catch((error) => { + toast(error.toString()); + }); + }; + const mailsendingButton = (org) => { + if (org === undefined || org === null) { + return "" + } + + if (users.length === 0) { + return "" + } + + // 1 mail based on users that have only apps + // Another based on those doing workflows + // Another based on those trying usecases(?) or templates + // + // Start based on edr, siem & ticketing + // Talk about enrichment? + // Check suggested usecases + // Check suggested workflows + var your_apps = "- Connecting " + + var subject_add = 0 + var subject = "POC to automate " + + if (org.security_framework !== undefined && org.security_framework !== null) { + if (org.security_framework.cases.name !== undefined && org.security_framework.cases.name !== null && org.security_framework.cases.name !== "") { + your_apps += org.security_framework.cases.name.replace("_", " ", -1).replace(" API", "", -1) + ", " + + if (subject_add < 2) { + if (subject_add === 1) { + subject += " and " + } + + subject_add += 1 + subject += org.security_framework.cases.name.replace("_", " ", -1).replace(" API", "", -1) + } + } + + if (org.security_framework.siem.name !== undefined && org.security_framework.siem.name !== null && org.security_framework.siem.name !== "") { + your_apps += org.security_framework.siem.name.replace("_", " ", -1).replace(" API", "", -1) + ", " + if (subject_add < 2) { + if (subject_add === 1) { + subject += " and " + } + + subject_add += 1 + subject += org.security_framework.siem.name.replace("_", " ", -1).replace(" API", "", -1) + } + } + + if (org.security_framework.communication.name !== undefined && org.security_framework.communication.name !== null && org.security_framework.communication.name !== "") { + your_apps += org.security_framework.communication.name.replace("_", " ", -1).replace(" API", "", -1) + ", " + + if (subject_add < 2) { + if (subject_add === 1) { + subject += " and " + } + + subject_add += 1 + subject += org.security_framework.communication.name.replace("_", " ", -1).replace(" API", "", -1) + } + } + + if (org.security_framework.edr.name !== undefined && org.security_framework.edr.name !== null && org.security_framework.edr.name !== "") { + your_apps += org.security_framework.edr.name.replace("_", " ", -1).replace(" API", "", -1) + ", " + + if (subject_add < 2) { + if (subject_add === 1) { + subject += " and " + } + + subject_add += 1 + subject += org.security_framework.edr.name.replace("_", " ", -1).replace(" API", "", -1) + } + } + + if (org.security_framework.intel.name !== undefined && org.security_framework.intel.name !== null && org.security_framework.intel.name !== "") { + your_apps += org.security_framework.intel.name.replace("_", " ", -1).replace(" API", "", -1) + ", " + + if (subject_add < 2) { + if (subject_add === 1) { + subject += " and " + } + + subject_add += 1 + subject += org.security_framework.intel.name.replace("_", " ", -1).replace(" API", "", -1) + } + } + + + // Remove comma + //subject += "?" + your_apps = your_apps.substring(0, your_apps.length - 2) + } + + + // Add usecases they may not have tried (from recommendations): org.priorities where item type is usecase + var usecases = "- Building usecases like " + const active_usecase = org.priorities.filter((item) => item.type === "usecase" && item.active === true) + if (active_usecase.length > 0) { + for (var i = 0; i < active_usecase.length; i++) { + if (active_usecase[i].name.includes("Suggested Usecase: ")) { + usecases += active_usecase[i].name.replace("Suggested Usecase: ", "", -1) + ", " + } else { + usecases += active_usecase[i].name + ", " + } + } + + usecases = usecases.substring(0, usecases.length - 2) + } + + if (your_apps.length <= 15) { + your_apps = "" + } + + if (usecases.length <= 30) { + usecases = "" + } + + var workflow_amount = "a few" + var admins = "" + + // Loop users + var lastLogin = 0 + for (var i = 0; i < users.length; i++) { + if (users[i].username.includes("shuffler")) { + continue + } + + if (users[i].role === "admin") { + admins += users[i].username + "," + } + + const data = users[i] + for (var i = 0; i < data.login_info.length; i++) { + if (data.login_info[i].timestamp > lastLogin) { + lastLogin = data.login_info[i].timestamp + } + } + } + + + // Remove last comma + admins = admins.substring(0, admins.length - 1) + + if (your_apps.length > 5) { + your_apps += "%0D%0A" + } + + if (usecases.length > 5) { + usecases += "%0D%0A" + } + + // Get drift username from userdata.username before @ in email + const username = userdata.username.substring(0, userdata.username.indexOf("@")) + + // Check if timestamp is more than 2 weeks ago and add "a while back" to the message + const timeComparison = 1209600 + const extra_timestamp_text = lastLogin === 0 ? 0 : (Date.now() / 1000 - lastLogin) > timeComparison ? " a while back" : "" + console.log("LAST LOGIN: " + lastLogin, extra_timestamp_text) + + // Check if cloud sync is active, and if so, add a message about it + const cloudSyncInfo = selectedOrganization.cloud_sync === true ? "- Scale your onprem installation" : "" + + var body = `Hey,%0D%0A%0D%0AI noticed you tried to use Shuffle${extra_timestamp_text}, and thought you may be interested in a POC. It looks like you have ${workflow_amount} workflows made, but it still doesn't look like you are getting what you wanted out of Shuffle. If you're interested, I'd love to set up a quick call to see if we can help you get more out of Shuffle. %0D%0A%0D%0A + +Some of the things we can help with:%0D%0A +${your_apps} +- Configuring and authenticating your apps%0D%0A +${usecases} +- Multi-Tenancy and creating special usecases%0D%0A +${cloudSyncInfo}%0D%0A + +If you're interested, please let me know a time that works for you, or set up a call here: https://drift.me/${username}` + + return `mailto:${admins}?bcc=frikky@shuffler.io,binu@shuffler.io&subject=${subject}&body=${body}` + } + + if ( + selectedOrganization.id === undefined && + userdata !== undefined && + userdata.active_org !== undefined && + orgRequest + ) { + setOrgRequest(false); + } + + + return ( +
    +
    +
    +
    +
    +

    Organization overview

    + + On this page organization admins can configure organisations, and sub-orgs (MSSP).{" "} + + Learn more + + +
    +
    + + { + const org_id = selectedOrganization.id; + + // Check if organization ID exists + if (!org_id) { + toast("No organization ID found"); + return; + } + + // Use clipboard API + navigator.clipboard.writeText(org_id) + .then(() => { + toast.success(`${org_id} copied to clipboard`); + }) + .catch((error) => { + // Fallback for browsers that don't support clipboard API + try { + // Create temporary input element + const tempInput = document.createElement('input'); + tempInput.value = org_id; + document.body.appendChild(tempInput); + tempInput.select(); + document.execCommand('copy'); + document.body.removeChild(tempInput); + toast(`${org_id} copied to clipboard`); + } catch (err) { + toast("Failed to copy. Please try again."); + console.error("Copy failed:", err); + } + }); + }} + > + + + + {userdata.support === true ? + + {/**/} + + + + : null} +
    +
    + + {/* {isCloud ? + + { + if (userdata.support === false) { + toast("Region change is not directly implemented yet, and requires support help.") + + if (window.drift !== undefined) { + window.drift.api.startInteraction({ + interactionId: 386411, + }) + } + } else { + // Show region change modal + console.log("Should open region change modal") + setRegionChangeModalOpen(true) + } + }} + > + {regiontag} + + + : null} */} +
    + + +
    +
    + ) +} + +export default EditOrgTab; \ No newline at end of file diff --git a/frontend/src/components/EnvironmentTab.jsx b/frontend/src/components/EnvironmentTab.jsx new file mode 100644 index 00000000..a7fb6bae --- /dev/null +++ b/frontend/src/components/EnvironmentTab.jsx @@ -0,0 +1,1541 @@ +import React, { memo, useContext, useEffect, useState } from 'react'; +import theme from "../theme.jsx"; +import { + Tooltip, + Typography, + Switch, + TextField, + Button, + ButtonGroup, + List, + ListItem, + ListItemText, + IconButton, + Dialog, + DialogTitle, + DialogActions, + DialogContent, + Checkbox, + Divider, + Tab, + Tabs, + Collapse, + Skeleton, + Grid, + Chip, + MenuItem, +} from "@mui/material"; +import { CopyToClipboard } from "../views/Docs.jsx" +import { + FileCopy as FileCopyIcon, + CheckCircle as CheckCircleIcon, + Cached as CachedIcon, + Cloud as CloudIcon, + Cancel as CancelIcon, + Help as HelpIcon, + ExpandLess as ExpandLessIcon, + ExpandMore as ExpandMoreIcon, +} from "@mui/icons-material"; +import { toast } from 'react-toastify'; +import { Context } from '../context/ContextApi.jsx'; +import { green, red } from '../views/AngularWorkflow.jsx' + +const EnvironmentTab = memo((props) => { + const { globalUrl, isCloud, userdata, selectedOrganization } = props; + const [environments, setEnvironments] = React.useState([]); + const [showArchived, setShowArchived] = React.useState(false); + const [modalUser, setModalUser] = React.useState({}); + const [loginInfo, setLoginInfo] = React.useState(""); + const [modalOpen, setModalOpen] = React.useState(false); + const [showLoader, setShowLoader] = useState(true) + const [commandController, setCommandController] = React.useState({ + pipelines: false, + proxies: false, + }) + const [installationTab, setInstallationTab] = React.useState(0); + const [isExpanded, setIsExpanded] = React.useState(false); + const [listItemExpanded, setListItemExpanded] = React.useState(-1); + const [, setUpdate] = React.useState(0); + const [showDistributionPopup, setShowDistributionPopup] = React.useState(false); + const [selectedEnvironment, setSelectedEnvironment] = React.useState(null); + const [selectedSubOrg, setSelectedSubOrg] = React.useState([]); + useEffect(() => { + getEnvironments(); + setModalUser({}); + }, []); + + const changeModalData = (field, value) => { + modalUser[field] = value; + }; + + // Horrible frontend fix for environments + const setDefaultEnvironment = (environment) => { + // FIXME - add more checks to this + toast("Setting default location to " + environment.Name); + var newEnv = []; + for (var key in environments) { + if (environments[key].id == environment.id) { + if (environments[key].archived) { + toast("Can't set archived to default"); + return; + } + + environments[key].default = true; + } else if ( + environments[key].default == true && + environments[key].id !== environment.id + ) { + environments[key].default = false; + } + + newEnv.push(environments[key]); + } + + // Just use this one? + const url = globalUrl + "/api/v1/setenvironments"; + fetch(url, { + method: "PUT", + credentials: "include", + body: JSON.stringify(newEnv), + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => + response.json().then((responseJson) => { + if (responseJson["success"] === false) { + toast(responseJson.reason); + setTimeout(() => { + getEnvironments(); + }, 1500); + } else { + setLoginInfo(""); + setModalOpen(false); + setTimeout(() => { + getEnvironments(); + }, 1500); + } + }), + ) + .catch((error) => { + console.log("Error in backend data: ", error); + }); + }; + + const rerunCloudWorkflows = (environment) => { + toast("Starting execution reruns. This can run in the background."); + fetch(`${globalUrl}/api/v1/environments/${environment.id}/rerun`, { + method: "GET", + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for apps :O!"); + return; + } else { + toast(response.reason); + //toast("Aborted all dangling workflows"); + } + + return response.json(); + }) + .then((responseJson) => { + console.log("Got response for execution: ", responseJson); + //console.log("RESPONSE: ", responseJson) + //setFiles(responseJson) + }) + .catch((error) => { + //toast(error.toString()) + }); + } + + const getEnvironments = () => { + fetch(globalUrl + "/api/v1/getenvironments", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for apps :O!"); + return; + } + + return response.json(); + }) + .then((responseJson) => { + setEnvironments(responseJson); + setShowLoader(false) + // Helper info for users in case they have a large queue and don't know about queue flushing + if (responseJson !== undefined && responseJson !== null && responseJson.length > 0) { + if (responseJson.length === 1 && responseJson[0].Type !== "cloud") { + setListItemExpanded(0) + } + for (var i = 0; i < responseJson.length; i++) { + const env = responseJson[i]; + + // Check if queuesize is too large + if (env.queue !== undefined && env.queue !== null && env.queue > 100) { + toast("Queue size for " + env.name + " is very large. We recommend you to reduce it by flushing the queue before continuing."); + break + } + } + } + }) + .catch((error) => { + toast(error.toString()); + }); + }; + + const flushQueue = (name) => { + // Just use this one? + const url = globalUrl + "/api/v1/flush_queue"; + fetch(url, { + method: "DELETE", + credentials: "include", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => + response.json().then((responseJson) => { + if (responseJson["success"] === false) { + toast(responseJson.reason); + getEnvironments(); + } else { + setLoginInfo(""); + setModalOpen(false); + getEnvironments(); + } + }), + ) + .catch((error) => { + console.log("Error when deleting: ", error); + }); + }; + + + const deleteEnvironment = (environment) => { + // FIXME - add some check here ROFL + //const name = environment.name + + //toast("Modifying environment " + name) + //var newEnv = [] + //for (var key in environments) { + // if (environments[key].Name == name) { + // if (environments[key].default) { + // toast("Can't modify the default environment") + // return + // } + + // if (environments[key].type === "cloud" && !environments[key].archived) { + // toast("Can't modify cloud environments") + // return + // } + + // environments[key].archived = !environments[key].archived + // } + + // newEnv.push(environments[key]) + //} + const id = environment.id; + + //toast("Modifying environment " + environment.Name) + var newEnv = []; + for (var key in environments) { + if (environments[key].id == id) { + if (environments[key].default) { + toast("Can't modify the default environment. Change the default environment first."); + return; + } + + if (environments[key].type === "cloud" && !environments[key].archived) { + toast("Can't modify cloud environments"); + return; + } + + environments[key].archived = !environments[key].archived; + } + + newEnv.push(environments[key]); + } + + // Just use this one? + const url = globalUrl + "/api/v1/setenvironments"; + fetch(url, { + method: "PUT", + credentials: "include", + body: JSON.stringify(newEnv), + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => + response.json().then((responseJson) => { + if (responseJson["success"] === false) { + toast(responseJson.reason); + getEnvironments(); + } else { + setLoginInfo(""); + setModalOpen(false); + getEnvironments(); + } + }), + ) + .catch((error) => { + console.log("Error when deleting: ", error); + }); + }; + + const abortEnvironmentWorkflows = (environment) => { + //console.log("Aborting all workflows started >10 minutes ago, not finished"); + toast( + "Clearing the queue - this may take some time. A new will show up when finished.", + ); + + fetch( + `${globalUrl}/api/v1/environments/${environment.id}/stop?deleteall=true`, + { + method: "GET", + credentials: "include", + }, + ) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for apps :O!"); + toast("Failed aborting dangling workflows"); + return; + } else { + toast("Successfully cleared the queue"); + + getEnvironments(); + } + + return response.json(); + }) + .then((responseJson) => { + console.log("Got response for execution: ", responseJson); + //console.log("RESPONSE: ", responseJson) + //setFiles(responseJson) + }) + .catch((error) => { + //toast(error.toString()) + }); + }; + + const changeRecommendation = (recommendation, action) => { + const data = { + action: action, + name: recommendation.name, + }; + + fetch(`${globalUrl}/api/v1/recommendations/modify`, { + mode: "cors", + method: "POST", + body: JSON.stringify(data), + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then((response) => { + if (response.status === 200) { + } else { + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === true) { + getEnvironments(); + } else { + if ( + responseJson.success === false && + responseJson.reason !== undefined + ) { + toast("Failed change recommendation: ", responseJson.reason); + } else { + toast("Failed change recommendation"); + } + } + }) + .catch((error) => { + toast( + "Failed dismissing alert. Please contact support@shuffler.io if this persists.", + ); + }); + }; + + const getOrborusCommand = (environment) => { + if (environment.Type === "cloud") { + //toast("No Orborus necessary for environment cloud. Create and use a different environment to run executions on-premises.",) + return + } + + if ( + props.userdata.active_org === undefined || + props.userdata.active_org === null + ) { + return; + } + + const elementName = "copy_element_shuffle"; + var auth = + environment.auth === "" + ? "cb5st3d3Z!3X3zaJ*Pc" + : environment.auth + + // Escape exclamation marks for copying + auth = auth.replace("\\!", "!").replace(/!/g, "\\!") + + const newUrl = + globalUrl === "https://shuffler.io" + ? "https://shuffle-backend-stbuwivzoq-nw.a.run.app" + : globalUrl; + + var skipPipeline = false + if (commandController.pipelines === true) { + skipPipeline = true + } + + var addProxy = false + if (commandController.proxies === true) { + addProxy = true + } + + if (installationTab === 1) { + return (`docker run -d \\ + --restart=always \\ + --name="shuffle-orborus" \\ + --pull=always \\ + --volume "/var/run/docker.sock:/var/run/docker.sock" \\ + -e AUTH="${auth}" \\ + -e ENVIRONMENT_NAME="${environment.Name}" \\ + -e ORG="${environment.org_id}" \\ + -e SHUFFLE_WORKER_IMAGE="ghcr.io/shuffle/shuffle-worker:nightly" \\ + -e SHUFFLE_SWARM_CONFIG=run \\ + -e SHUFFLE_LOGS_DISABLED=true \\ + -e BASE_URL="${newUrl}" \\${addProxy ? "\n -e HTTPS_PROXY=IP:PORT \\" : ""}${skipPipeline ? "\n -e SHUFFLE_SKIP_PIPELINES=true \\" : ""} + ghcr.io/shuffle/shuffle-orborus:latest + `) + } else if (installationTab === 2) { + return `https://shuffler.io/docs/configuration#kubernetes` + } + + const commandData = `docker rm shuffle-orborus --force; \\\ndocker run -d \\ + --restart=always \\ + --name="shuffle-orborus" \\ + --pull=always \\ + --volume "/var/run/docker.sock:/var/run/docker.sock" \\ + -e AUTH="${auth}" \\ + -e ENVIRONMENT_NAME="${environment.Name}" \\ + -e ORG="${props.userdata.active_org.id}" \\ + -e BASE_URL="${newUrl}" \\${addProxy ? "\n -e HTTPS_PROXY=IP:PORT \\" : ""}${skipPipeline ? "\n -e SHUFFLE_SKIP_PIPELINES=true \\" : ""} + ghcr.io/shuffle/shuffle-orborus:latest` + + return commandData + }; + + const submitEnvironment = (data) => { + // FIXME - add some check here ROFL + environments.push({ + name: data.environment, + type: "onprem", + }); + + // Just use this one? + var baseurl = globalUrl; + const url = baseurl + "/api/v1/setenvironments"; + fetch(url, { + method: "PUT", + credentials: "include", + body: JSON.stringify(environments), + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => + response.json().then((responseJson) => { + if (responseJson["success"] === false) { + setLoginInfo("Error in input: " + responseJson.reason); + getEnvironments(); + } else { + setLoginInfo(""); + setModalOpen(false); + getEnvironments(); + } + }), + ) + .catch((error) => { + console.log("Error in userdata: ", error); + }); + }; + + const modalView = ( + { + setModalOpen(false); + }} + PaperProps={{ + sx: { + borderRadius: theme?.palette?.DialogStyle?.borderRadius, + border: theme?.palette?.DialogStyle?.border, + minWidth: "800px", + minHeight: "320px", + fontFamily: theme?.typography?.fontFamily, + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + zIndex: 1000, + '& .MuiDialogContent-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogTitle-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogActions-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + }, + }} + > + + Add Location + + +
    + Location Name + + changeModalData("environment", event.target.value) + } + /> +
    + {loginInfo} {/* Assuming loginInfo is part of the relevant content */} +
    + + + + +
    + ); + + + const textColor = "#9E9E9E !important"; + + const handleSelectSubOrg = (id, action) => { + if (action === "all") { + const childOrgs = userdata.orgs.filter( + (data) => data.creator_org === userdata.active_org.id + ); + setSelectedSubOrg((prev) => { + if (prev.length === childOrgs.length) { + // If all child orgs are already selected, clear the selection + return []; + } else { + // Otherwise, select all child org IDs + return childOrgs.map((data) => data.id); + } + }); + } else if (action === "none") { + setSelectedSubOrg([]); + } else { + setSelectedSubOrg((prev) => { + if (prev.includes(id)) { + return prev.filter((data) => data !== id); + } else { + return [...prev, id]; + } + }); + } + }; + + const queueSizeText = (queue) => { + if (queue === undefined || queue === null) return 0; + if (queue < 0) return 0; + if (queue > 1000) return ">1000"; + return queue; + }; + + const editEnvironmentConfig = (id, selectedSubOrg, cacheKey) => { + const data = { + action: "suborg_distribute", + selected_suborgs: selectedSubOrg, + } + + const url = `${globalUrl}/api/v1/environments/${id}/config`; + + fetch(url, { + mode: "cors", + method: "POST", + body: JSON.stringify(data), + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then((response) => + response.json().then((responseJson) => { + if (responseJson["success"] === false) { + toast("Failed overwriting environments"); + } else { + toast("Successfully updated environments!"); + setTimeout(() => { + getEnvironments(); + setShowDistributionPopup(false); + }, 1000); + } + }) + ) + .catch((error) => { + toast("Err: " + error.toString()); + }); + }; + + const changeDistribution = (id, selectedSubOrg) => { + + editEnvironmentConfig(id, [...new Set(selectedSubOrg)]) + } + + const EnvironmentDistributionModal = showDistributionPopup ? ( + setShowDistributionPopup(false)} + PaperProps={{ + sx: { + borderRadius: theme?.palette?.DialogStyle?.borderRadius, + border: theme?.palette?.DialogStyle?.border, + fontFamily: theme?.typography?.fontFamily, + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + zIndex: 1000, + minWidth: "600px", + minHeight: "320px", + overflow: "auto", + '& .MuiDialogContent-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogTitle-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogActions-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + }, + }} + > + +
    + Select sub-org to distribute Environments +
    +
    + + {handleSelectSubOrg(null, "none")}}>None + {handleSelectSubOrg(null, "all")}}>All + {userdata.orgs.map((data, index) => { + if (data.creator_org !== userdata.active_org.id) { + return null; + } + + const imagesize = 22; + const imageStyle = { + width: imagesize, + height: imagesize, + pointerEvents: "none", + marginRight: 10, + marginLeft: data.id === userdata.active_org.id ? 0 : 20, + }; + + const image = data.image === "" ? ( + {data.name} + ) : ( + {data.name} + ); + + return ( + handleSelectSubOrg(data.id)} + style={{ display: "flex", alignItems: "center" }} + > + + {image} + {data.name} + + ); + })} + +
    + + +
    +
    +
    + ) : null; + + return ( +
    + {modalView} + {EnvironmentDistributionModal} +
    +
    + + + + setShowArchived(!showArchived)} + />{" "} + Show disabled + {/* */} +
    + + + {["Type", "Status", "Scale", "Pipeline", "Name", "Type", "Queue", "Actions", "Distribution"].map((header, index) => ( + + ))} + + {showLoader + ? [...Array(6)].map((_, rowIndex) => ( + + {Array(9).fill(null).map((_, colIndex) => ( + + + + ))} + + )) + : environments?.length === 0 ? ( + + No Locations Found + + ):( + environments?.map((environment, index) => { + if (!showArchived && environment.archived) { + return null; + } + + if (environment.archived === undefined) { + return null; + } + + var bgColor = "#212121"; + if (index % 2 === 0) { + bgColor = "#1A1A1A"; + } + + // Check if there's a notification for it in userdata.priorities + var showCPUAlert = false; + var foundIndex = -1; + if ( + userdata !== undefined && + userdata !== null && + userdata.priorities !== undefined && + userdata.priorities !== null && + userdata.priorities.length > 0 + ) { + foundIndex = userdata.priorities.findIndex( + (prio) => prio.name.includes("CPU") && prio.active === true, + ); + + if ( + foundIndex >= 0 && + userdata.priorities[foundIndex].name.endsWith( + environment.Name, + ) + ) { + showCPUAlert = true; + } + } + + const queueSize = + environment.queue !== undefined && environment.queue !== null + ? environment.queue < 0 + ? 0 + : environment.queue > 1000 + ? ">1000" + : environment.queue + : 0; + + + const orborusCommandWrapper = () => { + // Check the current text + const orborusCommand = document.getElementById("orborus_command") + if (orborusCommand === undefined || orborusCommand === null) { + return getOrborusCommand(environment) + } + + return orborusCommand.textContent + } + + const isDistributed = environment?.suborg_distribution?.length > 0 ? true : false; + + return ( + <> + { + if (environment.Type === "cloud") { + toast("Cloud environments are not configurable. To see what is possible, create a new environment.") + return + } + + setListItemExpanded(listItemExpanded === index ? -1 : index) + }} + > + + + + ) : environment.run_type === "docker" ? ( + + + + ) : environment.run_type === "k8s" ? ( + + + + ) : ( + + + + ) + } + style={{ + minWidth: 80, + padding: "8px 8px 8px 0", + overflow: "hidden", + whiteSpace: "normal", + wordWrap: "break-word", + textAlign: "center", + display: "table-cell", + }} + /> + + IP / label: {environment?.running_ip?.split(":")[0]}. May stay running up to a minute after stopping Orborus. + : + "Cloud is automatically configured. Reachout to support@shuffler.io if you have any questions." + } placement="top"> + + {environment.Type !== "cloud" && + (environment.running_ip === undefined || + environment.running_ip === null || + environment.running_ip.length === 0) + ? + { + //handleChipClick + }} + variant="outlined" + color="primary" + /> + : + { + //handleChipClick + }} + variant="outlined" + color="primary" + /> + } + + + } + /> + + + + + ) : ( + + + + + + ) + } + style={{ + minWidth: 60, + marginLeft: 20, + overflow: "hidden", + whiteSpace: "normal", + wordWrap: "break-word", + padding: 8, + display: "table-cell", + }} + /> + + + + + : + environment?.data_lake?.enabled && environment?.archived !== true ? ( + + + + + + + ) : ( + { + e.preventDefault() + e.stopPropagation() + + window.open("/detections/Sigma", "_blank") + }} + > + + + + + ) + } + style={{ + minWidth: 60, + marginLeft: 40, + overflow: "hidden", + whiteSpace: "normal", + wordWrap: "break-word", + display: "table-cell", + }} + /> + + + {environment.Name} + + )} + primaryTypographyProps={{ + style:{ + maxWidth: 150, + whiteSpace: 'nowrap', + overflow: "hidden", + textOverflow: 'ellipsis', + wordWrap: "break-word", + transition: "all 0.3s ease", + }}} + style={{ + minWidth: 120, + maxWidth: 150, + display: "table-cell", + }} + /> + + + + +
    + + + + + + {setIsExpanded(prev => !prev)}}> + {listItemExpanded === index ? : } + +
    +
    + {selectedOrganization.id !== undefined && environment?.org_id !== selectedOrganization.id ? + + + + } + style={{ textAlign: 'center', verticalAlign: 'middle', }} + /> + : + + { + e.stopPropagation() + setShowDistributionPopup(true) + if(environment?.suborg_distribution?.length > 0){ + setSelectedSubOrg(environment.suborg_distribution) + }else{ + setSelectedSubOrg([]) + } + setSelectedEnvironment(environment.id) + }}> + + + + } +
    + + + +
    +
    + + Your Self-Hosted Orborus instance + + + Orborus is the Shuffle queue handler that runs your hybrid workflows and manages pipelines. It can be run in Docker/k8s container on your server or in your cluster. Follow the steps below, and configure as need be. + + + { + setInstallationTab(inputValue) + }} + aria-label="disabled tabs example" + variant="scrollable" + scrollButtons="auto" + style={{textAlign: "center", marginTop: 25, }} + > + + Verbose (default) + + /> + + Scale + + /> + + k8s + + /> + + + {installationTab === 2 ? + + Check our Kubernetes documentation for more information on how to run Shuffle on Kubernetes. The status of the node will change when connected. + + : + + 1. Ensure Docker is installed and the target server can reach '{globalUrl}' + + } + + + + {installationTab === 2 ? null : + "2. Run this command on the server you want to run workflows or store Pipeline data on"} + + + {installationTab === 2 ? null : +
    +
    + + {getOrborusCommand(environment)} + + +
    + + + Configure HTTP Proxies: { + if (commandController.proxies === undefined) { + commandController.proxies = true + } else { + commandController.proxies = !commandController.proxies + } + + setCommandController(commandController) + setUpdate(Math.random()) + }} + /> +
    + Disable Pipelines & Data Lake: { + if (commandController.pipelines === undefined) { + commandController.pipelines = true + } else { + commandController.pipelines = !commandController.pipelines + } + setCommandController(commandController) + setUpdate(Math.random()) + }} + /> +
    + } + + + {installationTab === 2 ? null : + + 3. Verify if the node is running. Try to refresh the page a little while after running the command. + + } + +
    +
    + + + + + {showCPUAlert === false ? null : ( + +
    +
    + + 90% CPU the server(s) hosting the Shuffle App + Runner (Orborus) was found. + + + Need help with High Availability and Scale?{" "} + + Read documentation + {" "} + and{" "} + + Get in touch + + . + +
    +
    + +
    +
    +
    + )} + + ); + }) + ) } + +
    + {/* */} +
    +
    +
    + + ) +}); + +export default EnvironmentTab; diff --git a/frontend/src/components/LeftSideBar.jsx b/frontend/src/components/LeftSideBar.jsx index dd7fac86..f8acc3f1 100644 --- a/frontend/src/components/LeftSideBar.jsx +++ b/frontend/src/components/LeftSideBar.jsx @@ -889,7 +889,7 @@ useEffect(() => { - {!leftSideBarOpenByClick && setExpandLeftNav(true)}} onMouseLeave={()=>{!leftSideBarOpenByClick && setExpandLeftNav(false);setOpenAutocomplete(false)}}> + {(!leftSideBarOpenByClick || window?.location?.pathname?.includes("/workflows/")) && setExpandLeftNav(true)}} onMouseLeave={()=>{(!leftSideBarOpenByClick || window?.location?.pathname?.includes("/workflows/")) && setExpandLeftNav(false);setOpenAutocomplete(false)}}> { ); }; -export default LeftSideBar; +export default LeftSideBar; \ No newline at end of file diff --git a/frontend/src/components/OrgHeaderNew.jsx b/frontend/src/components/OrgHeaderNew.jsx new file mode 100644 index 00000000..b319f3f3 --- /dev/null +++ b/frontend/src/components/OrgHeaderNew.jsx @@ -0,0 +1,520 @@ +import React, { useEffect, useState } from "react"; +import theme from "../theme.jsx"; +import { makeStyles } from "@mui/styles"; +import { toast } from 'react-toastify'; + +import { + Tooltip, + TextField, + FormControl, + InputLabel, + OutlinedInput, + Checkbox, + Select, + MenuItem, + Button, + ListItemText, + Dialog, + DialogActions, + DialogContent, + Divider, + DialogTitle, +} from "@mui/material"; + +import AvatarEditor from "react-avatar-editor"; + + +import { + AddAPhotoOutlined as AddAPhotoOutlinedIcon, + ZoomInOutlined as ZoomInOutlinedIcon, + ZoomOutOutlined as ZoomOutOutlinedIcon, + Loop as LoopIcon, + AddPhotoAlternate as AddPhotoAlternateIcon, +} from "@mui/icons-material"; + +import { + ExpandLess as ExpandLessIcon, + ExpandMore as ExpandMoreIcon, + Save as SaveIcon, +} from "@mui/icons-material"; + +const useStyles = makeStyles({ + notchedOutline: { + borderColor: "#f85a3e !important", + }, +}); + +const defaultImage = + "data:image/png;base64,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"; + +const OrgHeader = (props) => { + const { + userdata, + selectedOrganization, + setSelectedOrganization, + globalUrl, + isCloud, + adminTab, + handleEditOrg, + isEditOrgTab, + serverside, + } = props; + + const classes = useStyles(); + + var upload = ""; + const defaultBranch = "master"; + const [orgName, setOrgName] = React.useState(selectedOrganization.name); + const [orgDescription, setOrgDescription] = React.useState( + selectedOrganization.description + ); + + + const [file, setFile] = React.useState(""); + const [fileBase64, setFileBase64] = React.useState( + selectedOrganization.image + ); + useEffect(() => { + if (selectedOrganization.image !== undefined && selectedOrganization.image !== null && selectedOrganization.image.length > 0) { + setFileBase64(selectedOrganization.image); + setFile(selectedOrganization.image); + } + }, [selectedOrganization]); + + const removeImage = () => { + setFile(""); + setFileBase64(""); + setCroppedData(defaultImage); + handleEditOrg( + orgName, + orgDescription, + selectedOrganization.id, + defaultImage, + { + app_download_repo: selectedOrganization?.defaults?.app_download_repo, + app_download_branch: selectedOrganization?.defaults?.app_download_branch, + workflow_download_repo: selectedOrganization?.defaults?.workflow_download_repo, + workflow_download_branch: selectedOrganization?.defaults?.workflow_download_branch, + notification_workflow: selectedOrganization?.defaults?.notification_workflow, + documentation_reference: selectedOrganization?.defaults?.documentation_reference, + workflow_upload_repo: selectedOrganization?.defaults?.workflow_upload_repo, + workflow_upload_branch: selectedOrganization?.defaults?.workflow_upload_branch, + workflow_upload_username: selectedOrganization?.defaults?.workflow_upload_username, + workflow_upload_token: selectedOrganization?.defaults?.workflow_upload_token, + newsletter: selectedOrganization?.defaults?.newsletter, + weekly_recommendations: selectedOrganization?.defaults?.weekly_recommendations, + }, + { + sso_entrypoint: selectedOrganization?.sso_config?.sso_entrypoint, + sso_certificate: selectedOrganization?.sso_config?.sso_certificate, + client_id: selectedOrganization?.sso_config?.client_id, + client_secret: selectedOrganization?.sso_config?.client_secret, + openid_authorization: selectedOrganization?.sso_config?.openid_authorization, + openid_token: selectedOrganization?.sso_config?.openid_token, + SSORequired: selectedOrganization?.sso_config?.SSORequired, + auto_provision: selectedOrganization?.sso_config?.auto_provision, + }, + [], + ) + }; + + const surfaceColor = "#27292D"; + const inputColor = "#383B40"; + + const bodyDivStyle = { + margin: "auto", + width: "900px", + }; + + const appIconStyle = { + marginLeft: "5px", + }; + + const dividerStyle = { + marginBottom: "10px", + marginTop: "10px", + height: "1px", + width: "100%", + backgroundColor: "grey", + }; + + if (file !== "") { + const img = document.getElementById("logo"); + var canvas = document.createElement("canvas"); + canvas.width = 174; + canvas.height = 174; + var ctx = canvas.getContext("2d"); + + img.onload = function () { + // img, x, y, width, height + //ctx.drawImage(img, 174, 174) + //console.log("IMG natural: ", img.naturalWidth, img.naturalHeight) + //ctx.drawImage(img, 0, 0, 174, 174) + ctx.drawImage( + img, + 0, + 0, + img.width, + img.height, + 0, + 0, + canvas.width, + canvas.height + ); + + const canvasUrl = canvas.toDataURL(); + if (canvasUrl !== fileBase64) { + setFileBase64(canvasUrl); + selectedOrganization.image = canvasUrl; + setSelectedOrganization(selectedOrganization); + } + }; + } + + + var image = ""; + const editHeaderImage = (event) => { + const file = event.target.value; + const actualFile = event.target.files[0]; + const fileObject = URL.createObjectURL(actualFile); + setFile(fileObject); + }; + + //console.log("USER: ", userdata) + const orgSaveButton = ( + + + + ); + + const [imageUploadError, setImageUploadError] = React.useState(""); + const [openImageModal, setOpenImageModal] = React.useState(false); + const [scale, setScale] = React.useState(1); + const [rotate, setRotation] = React.useState(0); + const [disableImageUpload, setDisableImageUpload] = React.useState(true); + const [croppedData, setCroppedData] = React.useState(defaultImage); + const [imageData, setImageData] = useState(selectedOrganization?.image?.lenth > 0 ? selectedOrganization?.image : defaultImage) + + React.useEffect(() => { + if (file.length > 0) { + setCroppedData(file); + } else if (fileBase64 !== undefined && fileBase64 !== null && fileBase64.length > 0) { + setCroppedData(fileBase64); + } else { + setCroppedData(defaultImage); + } + + if((imageData !== selectedOrganization?.image) && selectedOrganization?.image?.length > 0){ + setImageData(selectedOrganization?.image) + } + }, [selectedOrganization, file]); + + + const alternateImg = ( + { + upload.click(); + }} + /> + ); + + const zoomIn = () => { + setScale(scale + 0.1); + }; + + const zoomOut = () => { + setScale(scale - 0.1); + }; + + const rotation = () => { + setRotation(rotate + 10); + }; + + const onPositionChange = () => { + setDisableImageUpload(false); + }; + + const onCancelSaveAppIcon = () => { + setOpenImageModal(false); + setImageUploadError(""); + }; + + let editor; + const setEditorRef = (imgEditor) => { + editor = imgEditor; + }; + + + const onSaveAppIcon = () => { + const canvas = editor.getImageScaledToCanvas(); + const newImageData = canvas.toDataURL(); + setCroppedData(newImageData); // Update croppedData with the new image data + setOpenImageModal(false); + setDisableImageUpload(true); + handleEditOrg( + orgName, + orgDescription, + selectedOrganization.id, + newImageData, + { + app_download_repo: selectedOrganization?.defaults?.app_download_repo, + app_download_branch: selectedOrganization?.defaults?.app_download_branch, + workflow_download_repo: selectedOrganization?.defaults?.workflow_download_repo, + workflow_download_branch: selectedOrganization?.defaults?.workflow_download_branch, + notification_workflow: selectedOrganization?.defaults?.notification_workflow, + documentation_reference: selectedOrganization?.defaults?.documentation_reference, + workflow_upload_repo: selectedOrganization?.defaults?.workflow_upload_repo, + workflow_upload_branch: selectedOrganization?.defaults?.workflow_upload_branch, + workflow_upload_username: selectedOrganization?.defaults?.workflow_upload_username, + workflow_upload_token: selectedOrganization?.defaults?.workflow_upload_token, + newsletter: selectedOrganization?.defaults?.newsletter, + weekly_recommendations: selectedOrganization?.defaults?.weekly_recommendations, + }, + { + sso_entrypoint: selectedOrganization?.sso_config?.sso_entrypoint, + sso_certificate: selectedOrganization?.sso_config?.sso_certificate, + client_id: selectedOrganization?.sso_config?.client_id, + client_secret: selectedOrganization?.sso_config?.client_secret, + openid_authorization: selectedOrganization?.sso_config?.openid_authorization, + openid_token: selectedOrganization?.sso_config?.openid_token, + SSORequired: selectedOrganization?.sso_config?.SSORequired, + auto_provision: selectedOrganization?.sso_config?.auto_provision, + }, + [], + ) + }; + + const imageInfo = ( + + ); + + const errorText = imageUploadError.length > 0 ? ( +
    Error: {imageUploadError}
    + ) : null; + + + const imageUploadModalView = openImageModal ? ( + + + +
    Upload Organization Image
    +
    + {errorText} + + setRotation(0)} + /> + +
    + + + + + + + + + + + + +
    + +
    + + + + +
    +
    + ) : null; + + + return ( +
    +
    + +
    { + setOpenImageModal(true); + }} + > + (upload = ref)} + onChange={(e) => { + const reader = new FileReader(); + reader.onload = (event) => { + setCroppedData(event.target.result); + }; + reader.readAsDataURL(e.target.files[0]); + }} + /> + {imageInfo} +
    + {imageUploadModalView} +
    +
    +
    + +
    +
    + +
    +
    +
    +
    + ); +}; + +export default OrgHeader; diff --git a/frontend/src/components/OrgHeaderexpandedNew.jsx b/frontend/src/components/OrgHeaderexpandedNew.jsx new file mode 100644 index 00000000..e7ba725f --- /dev/null +++ b/frontend/src/components/OrgHeaderexpandedNew.jsx @@ -0,0 +1,1158 @@ +import React, { memo, useEffect, useState } from "react"; + +import { makeStyles } from "@mui/styles"; +import { toast } from "react-toastify" +import theme from '../theme.jsx'; +//import { useAlert + +import { + FormControl, + InputLabel, + Paper, + OutlinedInput, + Checkbox, + Card, + Tooltip, + FormControlLabel, + Chip, + Link, + Typography, + Switch, + Select, + MenuItem, + Divider, + ListItemText, + TextField, + Button, + Tabs, + Tab, + Grid, + Autocomplete, +} from "@mui/material"; + +import { + Icon as IconButton, + ExpandLess as ExpandLessIcon, + ExpandMore as ExpandMoreIcon, + Save as SaveIcon, + CookieSharp, +} from "@mui/icons-material"; +import CloudSyncTab from "./CloudSyncTab.jsx"; + +const useStyles = makeStyles({ + notchedOutline: { + borderColor: "#f85a3e !important", + }, +}); + +const OrgHeaderexpandedNew = (props) => { + const { + userdata, + selectedOrganization, + setSelectedOrganization, + globalUrl, + isCloud, + adminTab, + selectedStatus, + setSelectedStatus, + isEditOrgTab + } = props; + + const classes = useStyles(); + const defaultBranch = "main"; + const ITEM_HEIGHT = 48; + const ITEM_PADDING_TOP = 8; + const MenuProps = { + PaperProps: { + style: { + maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, + width: 300, + borderRadius: 20, + overflowY: "scroll", + }, + }, + getContentAnchorEl: () => null, + }; + +const [orgName, setOrgName] = useState(selectedOrganization?.name); + const [orgDescription, setOrgDescription] = React.useState( + selectedOrganization.description + ); + + const [appDownloadUrl, setAppDownloadUrl] = React.useState( + selectedOrganization.defaults === undefined + ? "https://github.com/frikky/shuffle-apps" + : selectedOrganization.defaults.app_download_repo === undefined || + selectedOrganization.defaults.app_download_repo.length === 0 + ? "https://github.com/frikky/shuffle-apps" + : selectedOrganization.defaults.app_download_repo + ); + + const [openNotification, setOpenNotification] = React.useState(false); + + const handleStatusChange = (event) => { + const { value } = event.target; + handleEditOrg( + orgName, + orgDescription, + selectedOrganization.id, + selectedOrganization?.image, + { + app_download_repo: selectedOrganization?.defaults?.app_download_repo, + app_download_branch: selectedOrganization?.defaults?.app_download_branch, + workflow_download_repo: selectedOrganization?.defaults?.workflow_download_repo, + workflow_download_branch: selectedOrganization?.defaults?.workflow_download_branch, + notification_workflow: selectedOrganization?.defaults?.notification_workflow, + documentation_reference: selectedOrganization?.defaults?.documentation_reference, + workflow_upload_repo: selectedOrganization?.defaults?.workflow_upload_repo, + workflow_upload_branch: selectedOrganization?.defaults?.workflow_upload_branch, + workflow_upload_username: selectedOrganization?.defaults?.workflow_upload_username, + workflow_upload_token: selectedOrganization?.defaults?.workflow_upload_token, + newsletter: selectedOrganization?.defaults?.newsletter, + weekly_recommendations: selectedOrganization?.defaults?.weekly_recommendations, + }, + { + sso_entrypoint: selectedOrganization?.sso_config?.sso_entrypoint, + sso_certificate: selectedOrganization?.sso_config?.sso_certificate, + client_id: selectedOrganization?.sso_config?.client_id, + client_secret: selectedOrganization?.sso_config?.client_secret, + openid_authorization: selectedOrganization?.sso_config?.openid_authorization, + openid_token: selectedOrganization?.sso_config?.openid_token, + SSORequired: selectedOrganization?.sso_config?.SSORequired, + auto_provision: selectedOrganization?.sso_config?.auto_provision, + }, + value.length === 0 ? ["none"] : value, + ) + } + const [appDownloadBranch, setAppDownloadBranch] = React.useState( + selectedOrganization.defaults === undefined + ? defaultBranch + : selectedOrganization.defaults.app_download_branch === undefined || + selectedOrganization.defaults.app_download_branch.length === 0 + ? defaultBranch + : selectedOrganization.defaults.app_download_branch + ); + const [workflowDownloadUrl, setWorkflowDownloadUrl] = React.useState( + selectedOrganization.defaults === undefined + ? "https://github.com/frikky/shuffle-apps" + : selectedOrganization.defaults.workflow_download_repo === undefined || + selectedOrganization.defaults.workflow_download_repo.length === 0 + ? "https://github.com/frikky/shuffle-workflows" + : selectedOrganization.defaults.workflow_download_repo + ); + const [workflowDownloadBranch, setWorkflowDownloadBranch] = React.useState( + selectedOrganization.defaults === undefined + ? defaultBranch + : selectedOrganization.defaults.workflow_download_branch === undefined || + selectedOrganization.defaults.workflow_download_branch.length === 0 + ? defaultBranch + : selectedOrganization.defaults.workflow_download_branch + ); + + const [documentationReference, setDocumentationReference] = React.useState( + selectedOrganization.defaults === undefined + ? "" + : selectedOrganization.defaults.documentation_reference === undefined || + selectedOrganization.defaults.documentation_reference.length === 0 + ? "" + : selectedOrganization.defaults.documentation_reference + ); + const [newsletter, setNewsletter] = React.useState( + selectedOrganization.defaults === undefined + ? true + : selectedOrganization.defaults.newsletter === undefined || + selectedOrganization.defaults.newsletter.length === 0 + ? true + : !selectedOrganization.defaults.newsletter + ) + + const [weeklyRecommendations, setWeeklyRecommendations] = React.useState( + selectedOrganization.defaults === undefined + ? true + : selectedOrganization.defaults.weekly_recommendations === undefined || + selectedOrganization.defaults.weekly_recommendations.length === 0 + ? true + : !selectedOrganization.defaults.weekly_recommendations + ) + + const [uploadRepo, setUploadRepo] = React.useState(selectedOrganization.defaults === undefined ? "" : selectedOrganization.defaults.workflow_upload_repo === undefined || selectedOrganization.defaults.workflow_upload_repo.length === 0 ? "" : selectedOrganization.defaults.workflow_upload_repo) + const [uploadBranch, setUploadBranch] = React.useState(selectedOrganization.defaults === undefined ? defaultBranch : selectedOrganization.defaults.workflow_upload_branch === undefined || selectedOrganization.defaults.workflow_upload_branch.length === 0 ? defaultBranch : selectedOrganization.defaults.workflow_upload_branch) + const [uploadUsername, setUploadUsername] = React.useState(selectedOrganization.defaults === undefined ? "" : selectedOrganization.defaults.workflow_upload_username === undefined || selectedOrganization.defaults.workflow_upload_username.length === 0 ? "" : selectedOrganization.defaults.workflow_upload_username) + const [uploadToken, setUploadToken] = React.useState(selectedOrganization.defaults === undefined ? "" : selectedOrganization.defaults.workflow_upload_token === undefined || selectedOrganization.defaults.workflow_upload_token.length === 0 ? "" : selectedOrganization.defaults.workflow_upload_token) + const [regionStatus, setRegionStatus] = useState(); + + useEffect(() => { + + if (documentationReference !== selectedOrganization?.defaults?.documentation_reference) { + setDocumentationReference(selectedOrganization?.defaults?.documentation_reference) + } + + if (uploadRepo !== selectedOrganization?.defaults?.workflow_upload_repo){ + setUploadRepo(selectedOrganization?.defaults?.workflow_upload_repo) + } + + if (uploadBranch !== selectedOrganization?.defaults?.workflow_upload_branch){ + setUploadBranch(selectedOrganization?.defaults?.workflow_upload_branch) + } + + if (uploadUsername !== selectedOrganization?.defaults?.workflow_upload_username){ + setUploadUsername(selectedOrganization?.defaults?.workflow_upload_username) + } + + if (uploadToken !== selectedOrganization?.defaults?.workflow_upload_token){ + setUploadToken(selectedOrganization?.defaults?.workflow_upload_token) + } + }, [selectedOrganization]) + + useEffect(() => { + if (selectedOrganization !== undefined && selectedOrganization !== null) { + if((orgName === undefined || orgName === null || orgName.length === 0) && selectedOrganization?.name !== orgName) { + setOrgName(selectedOrganization?.name) + } + if((orgDescription === undefined || orgDescription === null || orgDescription.length === 0) && selectedOrganization?.description !== orgDescription) { + setOrgDescription(selectedOrganization?.description) + } + } + }, [selectedOrganization]) + + const handleEditOrg = ( + name, + description, + orgId, + image, + defaults, + sso_config, + lead_info, + ) => { + const data = { + name: name, + description: description, + org_id: orgId, + image: image, + defaults: defaults, + sso_config: sso_config, + lead_info: lead_info, + mfa_required: selectedOrganization?.mfa_required, + Billing: selectedOrganization?.Billing, + }; + + const url = globalUrl + `/api/v1/orgs/${selectedOrganization.id}`; + fetch(url, { + mode: "cors", + method: "POST", + body: JSON.stringify(data), + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then((response) => + response.json().then((responseJson) => { + if (responseJson["success"] === false) { + toast("Failed updating org: ", responseJson.reason); + } else { + toast("Successfully edited org!"); + } + }) + ) + .catch((error) => { + toast("Err: " + error.toString()); + }); + } + + const handleSendChangeRegionMail = (region)=> { + if(selectedOrganization === undefined || selectedOrganization === null){ + toast.error("Failed to send request for changing region. Please contact support@shuffler.io.") + return + } + let destinationRegion = region; + if (region === "US") { + destinationRegion = "us-west2"; + } else if (region === "EU") { + destinationRegion = "europe-west3"; + } else if (region === "CA") { + destinationRegion = "northamerica-northeast1"; + } else if (region === "UK") { + destinationRegion = "europe-west2"; + } + + var data = { + dst_region: destinationRegion + } + + toast.info("Sending request for changing region to " + region) + + fetch(`${globalUrl}/api/v1/orgs/${selectedOrganization?.id}/change/region/request`,{ + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + body: JSON.stringify(data), + }).then((response)=>{ + if(response.status !== 200){ + toast.error("Failed to send request for changing region. Please contact support@shuffler.io.") + }else{ + toast.success("successfully send request for changing region. We will contact you shortly.") + } + }).catch((err)=>{ + console.log(err) + toast.error("Failed to send request for changing region. Please contact support@shuffler.io.") + }) + } + + const setSelectedRegion = (region) => { + + // send a POST request to /api/v1/orgs/{org_id}/region with the region as the body + if(region === "US") { + region = "us-west2" + } else if(region === "EU") { + region = "europe-west3" + } else if(region === "CA") { + region = "northamerica-northeast1" + } else if(region === "UK") { + region = "europe-west2" + } + + var data = { + dst_region: region + } + + toast.info("Changing region to " + region + "...This may take a few minutes.") + + fetch(`${globalUrl}/api/v1/orgs/${selectedOrganization.id}/change/region`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + body: JSON.stringify(data), + timeOut: 1000 + }).then((response) => { + if (response.status !== 200) { + toast("Failed to change region!") + } + else { + toast("Region changed successfully! Reloading in 5 seconds..") + // Reload the page in 2 seconds + setTimeout(() => { + window.location.reload() + }, 5000) + + } + + return response.json(); + }) + } + + + const orgSaveButton = ( + + + + ); + + return ( +
    + + {/* + + + + Email settings + + + Enable or disable email notifications for your organization. + + +
    + + { + setNewsletter(e.target.checked) + }} + disabled={!isCloud} + /> + + + { + setWeeklyRecommendations(e.target.checked) + }} + disabled={!isCloud} + /> +
    +
    +
    + */} + + + + +
    +
    +
    +
    +
    + Name + { + if((orgName !== selectedOrganization?.name) && (orgName !== "")) { + handleEditOrg( + orgName, + orgDescription, + selectedOrganization.id, + selectedOrganization.image, + { + app_download_repo: selectedOrganization?.defaults?.app_download_repo, + app_download_branch: selectedOrganization?.defaults?.app_download_branch, + workflow_download_repo: selectedOrganization?.defaults?.workflow_download_repo, + workflow_download_branch: selectedOrganization?.defaults?.workflow_download_branch, + notification_workflow: selectedOrganization?.defaults?.notification_workflow, + documentation_reference: selectedOrganization?.defaults?.documentation_reference, + workflow_upload_repo: selectedOrganization?.defaults?.workflow_upload_repo, + workflow_upload_branch: selectedOrganization?.defaults?.workflow_upload_branch, + workflow_upload_username: selectedOrganization?.defaults?.workflow_upload_username, + workflow_upload_token: selectedOrganization?.defaults?.workflow_upload_token, + newsletter: !newsletter, + weekly_recommendations: !weeklyRecommendations, + }, + { + sso_entrypoint: selectedOrganization?.sso_config?.sso_entrypoint, + sso_certificate: selectedOrganization?.sso_config?.sso_certificate, + client_id: selectedOrganization?.sso_config?.client_id, + client_secret: selectedOrganization?.sso_config?.client_secret, + openid_authorization: selectedOrganization?.sso_config?.openid_authorization, + openid_token: selectedOrganization?.sso_config?.openid_token, + SSORequired: selectedOrganization?.sso_config?.SSORequired, + auto_provision: selectedOrganization?.sso_config?.auto_provision, + } + ) + }}} + onChange={(e) => { + if (e.target.value.length > 100) { + toast("Choose a shorter name."); + return; + } + + setOrgName(e.target.value); + }} + color="primary" + InputProps={{ + style: { + color: "white", + height: "35px", + fontSize: "1em", + borderRadius: 4, + }, + classes: { + notchedOutline: isEditOrgTab ? null : classes.notchedOutline, + }, + }} + /> +
    + {userdata?.support ? ( +
    +
    Status
    + + + +
    + ): null} + + + {isCloud ? ( +
    + Region + +
    + ): null} +
    +
    + About +
    + {if((orgDescription !== selectedOrganization?.description) && (orgDescription !== "")) { + handleEditOrg( + orgName, + orgDescription, + selectedOrganization.id, + selectedOrganization.image, + { + app_download_repo: selectedOrganization?.defaults?.app_download_repo, + app_download_branch: selectedOrganization?.defaults?.app_download_branch, + workflow_download_repo: selectedOrganization?.defaults?.workflow_download_repo, + workflow_download_branch: selectedOrganization?.defaults?.workflow_download_branch, + notification_workflow: selectedOrganization?.defaults?.notification_workflow, + documentation_reference: selectedOrganization?.defaults?.documentation_reference, + workflow_upload_repo: selectedOrganization?.defaults?.workflow_upload_repo, + workflow_upload_branch: selectedOrganization?.defaults?.workflow_upload_branch, + workflow_upload_username: selectedOrganization?.defaults?.workflow_upload_username, + workflow_upload_token: selectedOrganization?.defaults?.workflow_upload_token, + newsletter: !newsletter, + weekly_recommendations: !weeklyRecommendations, + }, + { + sso_entrypoint: selectedOrganization?.sso_config?.sso_entrypoint, + sso_certificate: selectedOrganization?.sso_config?.sso_certificate, + client_id: selectedOrganization?.sso_config?.client_id, + client_secret: selectedOrganization?.sso_config?.client_secret, + openid_authorization: selectedOrganization?.sso_config?.openid_authorization, + openid_token: selectedOrganization?.sso_config?.openid_token, + SSORequired: selectedOrganization?.sso_config?.SSORequired, + auto_provision: selectedOrganization?.sso_config?.auto_provision, + } + ) + }}} + onChange={(e) => { + setOrgDescription(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: isEditOrgTab ? null : classes.notchedOutline, + }, + style: { + color: "white", + height: 89, + borderRadius: 4, + }, + }} + /> +
    +
    + +
    +
    + + Preferences + + + {/*isCloud ? + + : null*/} +
    + {/* + + Add a Workflow that receives notifications from Shuffle when an error occurs in one of your workflows + + */} + + + + + Org Documentation reference + + + Add a URL that is added as a link, pointing to any external documentation page you want. + + + { + if(documentationReference !== selectedOrganization?.defaults?.documentation_reference) { + handleEditOrg( + orgName, + orgDescription, + selectedOrganization.id, + selectedOrganization.image, + { + app_download_repo: selectedOrganization?.defaults?.app_download_repo, + app_download_branch: selectedOrganization?.defaults?.app_download_branch, + workflow_download_repo: selectedOrganization?.defaults?.workflow_download_repo, + workflow_download_branch: selectedOrganization?.defaults?.workflow_download_branch, + notification_workflow: selectedOrganization?.defaults?.notification_workflow, + documentation_reference: documentationReference, + workflow_upload_repo: selectedOrganization?.defaults?.workflow_upload_repo, + workflow_upload_branch: selectedOrganization?.defaults?.workflow_upload_branch, + workflow_upload_username: selectedOrganization?.defaults?.workflow_upload_username, + workflow_upload_token: selectedOrganization?.defaults?.workflow_upload_token, + newsletter: !newsletter, + weekly_recommendations: !weeklyRecommendations, + }, + { + sso_entrypoint: selectedOrganization?.sso_config?.sso_entrypoint, + sso_certificate: selectedOrganization?.sso_config?.sso_certificate, + client_id: selectedOrganization?.sso_config?.client_id, + client_secret: selectedOrganization?.sso_config?.client_secret, + openid_authorization: selectedOrganization?.sso_config?.openid_authorization, + openid_token: selectedOrganization?.sso_config?.openid_token, + SSORequired: selectedOrganization?.sso_config?.SSORequired, + auto_provision: selectedOrganization?.sso_config?.auto_provision, + } + ) + } + }} + onChange={(e) => { + setDocumentationReference(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: isEditOrgTab ? null : classes.notchedOutline, + }, + style: { + color: "white", + + fontWeight: 400, + fontSize: 16, + borderRadius: 4, + height: 35 + }, + }} + /> + + + + + Workflow Backup Repository + + Decide where workflows are backed up in a Git repository. Will create logs and notifications if upload fails. The repository and branch must already have been initialized. Files will show up in the repo root in the /orgId/workflow-status/workflowId.json format. MSSP: If suborg exists, this will automatically be applied for them as well (not retroactive). Credentials are encrypted. + + + + + Repository for workflow backup + { + setUploadRepo(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: isEditOrgTab ? null : classes.notchedOutline, + }, + style: { + color: "white", + + fontWeight: 400, + fontSize: 16, + borderRadius: 4, + height: 35, + }, + }} + /> + + + + + Branch + { + setUploadBranch(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: isEditOrgTab ? null : classes.notchedOutline, + }, + style: { + color: "white", + + fontWeight: 400, + fontSize: 16, + borderRadius: 4, + height: 35, + }, + }} + /> + + + + + + + Username for backup of workflows + { + setUploadUsername(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: isEditOrgTab ? null : classes.notchedOutline, + }, + style: { + color: "white", + + fontWeight: 400, + fontSize: 16, + borderRadius: 4, + height: 35, + }, + }} + /> + + + + + Git token/password + { + setUploadToken(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: isEditOrgTab ? null : classes.notchedOutline, + }, + style: { + color: "white", + + fontWeight: 400, + fontSize: 16, + borderRadius: 4, + height: 35, + }, + }} + type="password" + /> + + + + + {/*isCloud ? null : */} + + {isCloud ? null : ( + + + App Download URL + { + setAppDownloadUrl(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: isEditOrgTab ? null : classes.notchedOutline, + }, + style: { + color: "white", + + fontWeight: 400, + fontSize: 16, + borderRadius: 4, + height: 35, + }, + }} + /> + + + )} + {isCloud ? null : ( + + + App Download Branch + { + setAppDownloadBranch(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: isEditOrgTab ? null : classes.notchedOutline, + }, + style: { + color: "white", + + fontWeight: 400, + fontSize: 16, + borderRadius: 4, + height: 35, + }, + }} + /> + + + )} + {isCloud ? null : ( + + + Workflow Download URL + { + setWorkflowDownloadUrl(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: isEditOrgTab ? null : classes.notchedOutline, + }, + style: { + color: "white", + + fontWeight: 400, + fontSize: 16, + borderRadius: 4, + height: 35, + }, + }} + /> + + + )} + {isCloud ? null : ( + + + Workflow Download Branch + { + setWorkflowDownloadBranch(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: isEditOrgTab ? null : classes.notchedOutline, + }, + style: { + color: "white", + + fontWeight: 400, + fontSize: 16, + borderRadius: 4, + height: 35, + }, + }} + /> + + + )} + {/* + + {expanded ? + + : + + } + + */} + +
    + {orgSaveButton} +
    +
    + ) +} + +export default OrgHeaderexpandedNew; + +const RegionChangeModal = memo(({selectedOrganization, setSelectedRegion, userdata, handleSendChangeRegionMail}) => { + // Show from options: "us-west2", "europe-west2", "europe-west3", "northamerica-northeast1" + // var regions = ["us-west2", "europe-west2", "europe-west3", "northamerica-northeast1"] + const regionMapping = { + "US": "us", + "EU-2": "eu", + "CA": "ca", + "EU": "eu" + }; + + //let regiontag = "UK"; + let regiontag = "EU"; + let regionCode = "gb"; + + const regionsplit = selectedOrganization?.region_url?.split("."); + if (regionsplit?.length > 2 && !regionsplit[0]?.includes("shuffler")) { + const namesplit = regionsplit[0]?.split("/"); + regiontag = namesplit[namesplit.length - 1]; + + if (regiontag === "california") { + regiontag = "US"; + regionCode = "us"; + } else if (regiontag === "frankfurt") { + regiontag = "EU"; + regionCode = "eu"; + } else if (regiontag === "ca") { + regiontag = "CA"; + regionCode = "ca"; + } + } +return ( + + {/* Region */} + + +); +}) diff --git a/frontend/src/components/OrganizationTab.jsx b/frontend/src/components/OrganizationTab.jsx new file mode 100644 index 00000000..01d39f09 --- /dev/null +++ b/frontend/src/components/OrganizationTab.jsx @@ -0,0 +1,222 @@ +import React, { useEffect, useState, useCallback } from 'react'; +import { Link, useNavigate, useLocation } from "react-router-dom"; +import Billing from "../components/Billing.jsx"; +import Priorities from "../components/Priorities.jsx"; +import Branding from "../components/Branding.jsx"; +// import AnalyticsTab from '../components/AnalyticsTab.jsx'; +import EditOrgTab from '../components/EditOrgTab.jsx'; +import CloudSyncTab from '../components/CloudSyncTab.jsx'; +import SSOTab from "../components/ssoTab.jsx" +import { ToastContainer, toast } from "react-toastify"; +import { Button, Tooltip } from '@mui/material'; + +const OrganizationTab = (props) => { + const location = useLocation(); + const navigate = useNavigate(); + const { + userdata, + globalUrl, + serverside, + isCloud, + checkLogin, + notifications, + setNotifications, + stripeKey, setSelectedOrganization, + selectedStatus, setSelectedStatus, + selectedOrganization, handleGetOrg, + handleStatusChange, handleEditOrg, + isLoaded, + removeCookie + } = props; + + const [selectedTab, setSelectedTab] = useState('org_config'); + const [organizationFeatures, setOrganizationFeatures] = useState({}); + const [billingInfo, setBillingInfo] = useState({}); + const [orgRequest, setOrgRequest] = React.useState(true); + const [curIndex, setCurIndex] = React.useState(0); + const [unreadNotifications, setUnreadNotifications] = React.useState( + notifications?.filter((notification) => notification.read === false)?.length + ); + + useEffect(() => { + const queryParams = new URLSearchParams(location.search); + const tabName = queryParams.get('admin_tab'); + if (tabName) { + const decodedTabName = decodeURIComponent(tabName); + setSelectedTab(decodedTabName); + if (decodedTabName === 'org_config') { + setCurIndex(0); + } else if(decodedTabName === 'sso'){ + setCurIndex(1) + }else if (decodedTabName === 'notifications' || decodedTabName === 'priorities') { + setCurIndex(2); + } else if (decodedTabName === 'billingstats' || decodedTabName === 'billing') { + setCurIndex(3); + } else if (decodedTabName === 'branding(beta)') { + setCurIndex(4); + } + // else if (decodedTabName === 'analytics') { + // setCurIndex(5); + // } + } + }, [location.search]); + + const handleTabClick = (tabName) => { + const formattedTabName = tabName.toLowerCase().replace(/[\s&]+/g, ''); + const encodedTabName = encodeURIComponent(formattedTabName); + setSelectedTab(formattedTabName); + document.title = `Shuffle - admin - ${formattedTabName}`; + navigate(`?admin_tab=${encodedTabName}`); + }; + + const handleNotifications = useCallback(() => { + const unreadCount = notifications?.filter((notification) => notification.read === false).length; + setUnreadNotifications(unreadCount); + },[unreadNotifications,notifications]); + + useEffect(() => { + if ((unreadNotifications !== notifications?.filter((notification) => notification.read === false).length) !== unreadNotifications) { + handleNotifications(); + } + }, [notifications]); + + const renderContent = () => { + switch (selectedTab) { + case 'org_config': + return ; + case 'sso': + return + case `notifications`: + case `priorities`: + return ( + + ); + case 'billingstats' : + case 'billing' : + return ( + + ); + case 'branding(beta)': + return ; + // case 'analytics': + // return ; + default: + return ; + } + }; + + return ( +
    +
    + {['Org Configuration', "sso", "Notifications", 'Billing & Stats', 'Branding (Beta)'].map((tabName, index) => ( + +
    + +
    +
    + ))} +
    +
    + {renderContent()} +
    +
    + ); +}; + +export default OrganizationTab; diff --git a/frontend/src/components/SchedulesTab.jsx b/frontend/src/components/SchedulesTab.jsx new file mode 100644 index 00000000..7ca85f0d --- /dev/null +++ b/frontend/src/components/SchedulesTab.jsx @@ -0,0 +1,1097 @@ +import React, { forwardRef, memo, useContext, useEffect } from 'react'; +import theme from "../theme.jsx"; +import { toast } from "react-toastify" ; +import { + Divider, + List, + ListItem, + ListItemText, + Button, + Tooltip, + IconButton, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + TextField, +} from '@mui/material'; + +import { + FileCopy as FileCopyIcon, + OpenInNew as OpenInNewIcon, + Padding, +} from "@mui/icons-material" +import { Box, Skeleton, Typography } from '@mui/material'; +import { Context } from '../context/ContextApi.jsx'; + +const SchedulesTab = memo((props) => { + const {globalUrl, users, } = props; + const [webHooks, setWebHooks] = React.useState([]); + const [allSchedules, setAllSchedules] = React.useState([]); + const [pipelines, setPipelines] = React.useState([]); + const [showLoader, setShowLoader] = React.useState(true); + const [pipelineModalOpen, setPipelineModalOpen] = React.useState(false); + const [newPipelineValue, setNewPipelineValue] = React.useState("export | sigma /tmp/sigma_rules | to SHUFFLE_WEBHOOK"); + + useEffect(() => { + if (allSchedules.length === 0 && webHooks.length === 0 && pipelines.length === 0) { + handleGetAllTriggers() + } + }, []) + + const textColor = "#9E9E9E !important"; + + const changePipelineState = (pipeline, state) => { + if (state.trim() === "") { + toast("state is not defined"); + return; + } + + const data = { + name: pipeline.name, + id: pipeline.id, + type: state, + command: pipeline.definition, + environment: pipeline.environment, + }; + + if (state === "start") toast("starting the pipeline"); + else toast.info("Stopping the pipeline. This may take a few minutes to propagate.") + + const url = `${globalUrl}/api/v1/triggers/pipeline`; + fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(data), + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for stream results :O!"); + toast("Failed to update the pipeline state"); + } + + return response.json(); + }) + .then((responseJson) => { + if (!responseJson.success) { + toast.error("Failed to update the pipeline: " + responseJson.reason); + } else { + setTimeout(() => { + handleGetAllTriggers() + }, 5000) + + setTimeout(() => { + handleGetAllTriggers() + }, 10000) + + setTimeout(() => { + handleGetAllTriggers() + }, 20000) + + setTimeout(() => { + handleGetAllTriggers() + }, 120000) + /* + if (state === "start") { + toast("Successfully created pipeline"); + } else { + toast("Sucessfully stopped the pipeline"); + } + */ + } + }) + .catch((error) => { + //toast(error.toString()); + console.log("Get schedule error: ", error.toString()); + }) + } + + const submitPipelineWrapper = (pipelineValue) => { + submitPipeline(pipelineValue) + } + + const NewPipelineView = ( + { + setPipelineModalOpen(false) + }} + PaperProps={{ + sx: { + borderRadius: theme?.palette?.DialogStyle?.borderRadius, + border: theme?.palette?.DialogStyle?.border, + minWidth: "800px", + minHeight: "320px", + fontFamily: theme?.typography?.fontFamily, + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + zIndex: 1000, + '& .MuiDialogContent-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogTitle-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogActions-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + }, + }} + > + + + Run a Tenzir pipeline + + + Alpha feature. Deploys to the first available Orborus location. Explore Tenzir Pipelines. The example below exports everything in the Tenzir database, runs Sigma rules on it, and forwards the results to a Shuffle webhook. + + + +
    + + setNewPipelineValue(event.target.value) + } + /> +
    +
    + + + + +
    + ) + + 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("Failed to set pipeline: " + responseJson.reason); + } else { + if (pipelineConfig.type === "create") { + toast("Pipeline will be created: " + responseJson.reason) + setPipelineModalOpen(false) + + } else if (pipelineConfig.type === "stop") { + toast("Pipeline will be stopped: " + responseJson.reason) + setPipelineModalOpen(false) + + } else { + toast("Unknown pipeline type: " + pipelineConfig.type) + } + } + + setTimeout(() => { + handleGetAllTriggers() + }, 5000) + + setTimeout(() => { + handleGetAllTriggers() + }, 10000) + + setTimeout(() => { + handleGetAllTriggers() + }, 15000) + + setTimeout(() => { + handleGetAllTriggers() + }, 20000) + }) + .catch((error) => { + console.log("Get pipeline error: ", error.toString()); + }); + } + + const deleteSchedule = (data) => { + // FIXME - add some check here ROFL + console.log("INPUT: ", data); + + + // Just use this one? + const url = + globalUrl + + "/api/v1/workflows/" + + data["workflow_id"] + + "/schedule/" + + data.id; + fetch(url, { + method: "DELETE", + credentials: "include", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => + response.json().then((responseJson) => { + if (responseJson["success"] === false) { + toast("Failed stopping schedule"); + } else { + setTimeout(() => { + handleGetAllTriggers(); + }, 1500); + //toast("Successfully stopped schedule!") + } + }) + ) + .catch((error) => { + console.log("Error in userdata: ", error); + }); + }; + + const deleteWebhook = (trigger) => { + if (trigger === undefined) { + return; + } + + fetch(globalUrl + "/api/v1/hooks/" + trigger.id + "/delete", { + method: "DELETE", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + 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) { + toast("Successfully stopped webhook"); + } else { + if (responseJson.reason !== undefined) { + toast("Failed stopping webhook: " + responseJson.reason); + } + } + setTimeout(handleGetAllTriggers, 1000); + }) + .catch((error) => { + toast( + "Delete webhook error. Contact support or check logs if this persists.", + ); + }); + }; + + + + 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 startSchedule = (trigger) => { + if (trigger.name.length <= 0) { + toast("Error: name can't be empty"); + return; + } + + toast("Creating schedule"); + const data = { + name: trigger.name, + frequency: trigger.frequency, + execution_argument: trigger.argument, + environment: trigger.environment, + id: trigger.id, + start: trigger.start_node, + }; + + fetch(`${globalUrl}/api/v1/workflows/${trigger.workflow_id}/schedule`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(data), + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for stream results :O!"); + } + + return response.json(); + }) + .then((responseJson) => { + if (!responseJson.success) { + toast("Failed to set schedule: " + responseJson.reason); + } else { + toast("Successfully created schedule"); + } + setTimeout(handleGetAllTriggers, 1000); + }) + .catch((error) => { + //toast(error.toString()); + console.log("Get schedule error: ", error.toString()); + }); + }; + + const startWebHook = (trigger) => { + const hookname = trigger.info.name; + if (hookname.length === 0) { + toast("Missing name"); + return; + } + + if (trigger.id.length !== 36) { + toast("Missing id"); + return; + } + + toast("Starting webhook"); + + const data = { + name: hookname, + type: "webhook", + id: trigger.id, + workflow: trigger.workflows[0], + start: trigger.start, + environment: trigger.environment, + auth: trigger.auth, + custom_response: trigger.custom_response, + version: trigger.version, + version_timeout: 15, + }; + + + fetch(globalUrl + "/api/v1/hooks/new", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(data), + credentials: "include", + }) + .then((response) => response.json()) + .then((responseJson) => { + if (responseJson.success) { + // Set the status + toast("Successfully started webhook"); + } else { + toast("Failed starting webhook: " + responseJson.reason); + } + setTimeout(handleGetAllTriggers, 1000); + }) + .catch((error) => { + //console.log(error.toString()); + console.log("New webhook error: ", error.toString()); + }); + }; + + return ( +
    + + {NewPipelineView} + +
    +

    + Triggers +

    + + Triggers are Automatic Workflow starters. Status: Schedules ({allSchedules.length}), Webhooks ({webHooks.length}), Pipelines ({pipelines.length}) + + +
    +

    + Schedules +

    + + Schedules used in Workflows. Makes locating and control easier.{" "} + + Learn more + + +
    +
    +
    + + + {["Name", "Interval", "Environment", "Workflow", "Argument", "Action"].map((header, index) => ( + + ))} + + {showLoader ? ( + [...Array(6)].map((_, rowIndex) => ( + + {Array(6) + .fill() + .map((_, colIndex) => ( + + + + ))} + + )) + ):( + allSchedules?.length === 0 ? ( +
    + No schedules found +
    + ):( + allSchedules.map((schedule, index) => { + var bgColor = "#212121" + if (index % 2 === 0) { + bgColor = "#1a1a1a"; + } + + return ( + + + 0 ? ( + schedule.frequency + ) : ( + {schedule.seconds} seconds + ) + } + /> + + + + + + + + + + + } + /> + + + + + )} + /> + + + ); + }) + ) + )} +
    +
    + +
    +

    Webhooks

    + Webhooks used in Shuffle workflows.  + + Learn more + + +
    +
    + + + {["Name", "Environment", "Workflow", "URL", "Action"].map((header, index) => ( + + ))} + + {showLoader ? ( + [...Array(6)].map((_, rowIndex) => ( + + {Array(5) + .fill() + .map((_, colIndex) => ( + + + + ))} + + )) + ):( + webHooks?.length === 0 ? ( +
    + No webhooks found +
    + ):( + webHooks.map((webhook, index) => { + var bgColor = "#212121" + if (index % 2 === 0) { + bgColor = "#1a1a1a"; + } + + return ( + + + + + + + + + + + + + } + /> + + + { + const copyText = navigator?.clipboard?.writeText(webhook.info.url); + if(copyText){ + toast.success("URL copied to clipboard"); + }else{ + toast.error("Failed to copy URL"); + } + }} + > + copy + + + ) + } + /> + + + + + )} + /> + + + ); + }) + ) + )} +
    +
    +
    +

    Pipelines

    + + + Controls Pipelines on your Orborus Runners, e.g. for Log Ingestion, MQ subscriptions or Sigma Detections.{" "} + + Learn more + + + +
    + + + +
    +
    + + + {["Command", "Environment", "Total Runs", "Actions"].map((header, index) => ( + + ))} + + {showLoader ? ( + [...Array(6)].map((_, rowIndex) => { + return ( + + {Array(5) + .fill() + .map((_, colIndex) => { + return ( + + + + ) + })} + + ) + } + ) + + ): ( + pipelines?.length === 0 ? ( +
    + No pipeline trigger found +
    + + ):( + pipelines.map((pipeline, index) => { + var bgColor = "#212121" + if (index % 2 === 0) { + bgColor = "#1a1a1a"; + } + + return ( + + + + + + + + )} + /> + + ); + }) + ) + )} +
    +
    +
    +
    +
    + ); +}); + +export default SchedulesTab; + diff --git a/frontend/src/components/TenantsTab.jsx b/frontend/src/components/TenantsTab.jsx new file mode 100644 index 00000000..8ee2f6f2 --- /dev/null +++ b/frontend/src/components/TenantsTab.jsx @@ -0,0 +1,1592 @@ +import React, { memo, useContext, useEffect, useState } from 'react'; +import theme from "../theme.jsx"; +import { + FormControl, + Card, + Tooltip, + Typography, + Switch, + Divider, + TextField, + Button, + Grid, + List, + ListItem, + ListItemText, + ListItemAvatar, + Avatar, + Dialog, + DialogTitle, + DialogActions, + DialogContent, + Skeleton, + } from "@mui/material"; + + import { + Edit as EditIcon, + Polyline as PolylineIcon, + CheckCircle as CheckCircleIcon, + Close as CloseIcon, + Apps as AppsIcon, + Business as BusinessIcon, + Flag, + } from "@mui/icons-material"; + +import { toast } from 'react-toastify'; + +const TenantsTab = memo((props) => { + const { + globalUrl, + isCloud, + userdata, + selectedOrganization, + setSelectedOrganization, + checkLogin + } = props; + const imageStyle = { width: 50, height: 50 }; + // const [selectedOrganization, setSelectedOrganization] = React.useState({}); + const [subOrgs, setSubOrgs] = useState([]); + const [cloudSyncApikey, setCloudSyncApikey] = React.useState(""); + const [orgName, setOrgName] = React.useState(""); + const [orgSyncResponse, setOrgSyncResponse] = React.useState(""); + const [loading, setLoading] = React.useState(false); + // const [loginInfo, setLoginInfo] = React.useState(""); + const [cloudSyncModalOpen, setCloudSyncModalOpen] = React.useState(false); + const [modalOpen, setModalOpen] = React.useState(false); + const [parentOrg, setParentOrg] = React.useState(null); + const [parentOrgFlag, setParentOrgFlag] = React.useState("eu"); + const [loadOrgs, setLoadOrgs] = React.useState(true); + const [, forceUpdate] = React.useState(); + const itemColor = "black"; + + useEffect(() => { + if(parentOrgFlag === null) { + let regiontag = "EU"; + let regionCode = "eu"; + + if (parentOrg?.region_url?.length > 0) { + const regionsplit = parentOrg?.region_url.split("."); + if (regionsplit.length > 2 && !regionsplit[0].includes("shuffler")) { + const namesplit = regionsplit[0].split("/"); + regiontag = namesplit[namesplit.length - 1]; + + if (regiontag === "california") { + regiontag = "US"; + regionCode = "us"; + } else if (regiontag === "frankfurt") { + regiontag = "EU-2"; + regionCode = "eu"; + } else if (regiontag === "ca") { + regiontag = "CA"; + regionCode = "ca"; + } + } + console.log("did i reach here, region code is: ", regionCode); + setParentOrgFlag(regionCode); + } + } + }, [parentOrg]); + + var syncList = [ + { + primary: "Workflows", + secondary: "", + active: true, + icon: , + }, + { + primary: "Apps", + secondary: "", + active: true, + icon: , + }, + { + primary: "Organization", + secondary: "", + active: true, + icon: , + }, + ]; + + useEffect(() => { + if (userdata.orgs !== undefined && userdata.orgs !== null && userdata.orgs.length > 0) { + handleGetSubOrgs(userdata.active_org.id); + } + else console.log("error in user data") + }, [userdata]); + + const handleGetSubOrgs = (orgId) => { + + if (orgId.length === 0) { + toast("Organization ID not defined. Please contact us on https://shuffler.io if this persists logout."); + return; + } + + fetch(`${globalUrl}/api/v1/orgs/${orgId}/suborgs`, { + method: "GET", + credentials: "include", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => { + if (!response.ok) { + throw new Error('Failed to fetch sub organizations'); + } + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === false) { + setLoadOrgs(false) + //toast("Failed getting your org. If this persists, please contact support."); + } else { + const { subOrgs, parentOrg } = responseJson; + setLoadOrgs(false) + setSubOrgs(subOrgs); + setParentOrg(parentOrg); + + let regiontag = "EU"; + let regionCode = "eu"; + + if (parentOrg?.region_url?.length > 0) { + const regionsplit = parentOrg?.region_url.split("."); + if (regionsplit.length > 2 && !regionsplit[0].includes("shuffler")) { + const namesplit = regionsplit[0].split("/"); + regiontag = namesplit[namesplit.length - 1]; + + if (regiontag === "california") { + regiontag = "US"; + regionCode = "us"; + } else if (regiontag === "frankfurt") { + regiontag = "EU-2"; + regionCode = "eu"; + } else if (regiontag === "ca") { + regiontag = "CA"; + regionCode = "ca"; + } + } + setParentOrgFlag(regionCode); + } + } + }) + .catch((error) => { + console.log("Error getting sub orgs: ", error); + //toast("Error getting sub organizations"); + setLoadOrgs(false) + }); + }; + + const GridItem = (props) => { + const [expanded, setExpanded] = React.useState(false); + const [showEdit, setShowEdit] = React.useState(false); + const [newValue, setNewValue] = React.useState(-100); + + const primary = props.data.primary; + const secondary = props.data.secondary; + const primaryIcon = props.data.icon; + const secondaryIcon = props.data.active ? ( + + ) : ( + + ); + + const submitFeatureEdit = (sync_features) => { + if (!userdata.support) { + console.log( + "User does not have support access and can't edit features", + ); + return; + } + + sync_features.editing = true; + const data = { + org_id: selectedOrganization.id, + sync_features: sync_features, + }; + + const url = globalUrl + `/api/v1/orgs/${selectedOrganization.id}`; + fetch(url, { + mode: "cors", + method: "POST", + body: JSON.stringify(data), + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then((response) => + response.json().then((responseJson) => { + if (responseJson["success"] === false) { + toast("Failed updating org: ", responseJson.reason); + } else { + toast("Successfully edited org!"); + } + }), + ) + .catch((error) => { + toast("Err: " + error.toString()); + }); + }; + + const enableFeature = () => { + console.log("Enabling " + primary); + + console.log(selectedOrganization.sync_features); + // Check if primary is in sync_features + var tmpprimary = primary.replaceAll(" ", "_"); + if (!(tmpprimary in selectedOrganization.sync_features)) { + console.log("Primary not in sync_features: " + tmpprimary); + return; + } + + if (props.data.active) { + selectedOrganization.sync_features[tmpprimary].active = false; + } else { + selectedOrganization.sync_features[tmpprimary].active = true; + } + + // setSelectedOrganization(selectedOrganization); + forceUpdate(Math.random()); + submitFeatureEdit(selectedOrganization.sync_features); + }; + + const submitEdit = (e) => { + e.preventDefault(); + e.stopPropagation(); + + // Check if primary is in sync_features + var tmpprimary = primary.replaceAll(" ", "_"); + if (!(tmpprimary in selectedOrganization.sync_features)) { + console.log("Primary not in sync_features: " + tmpprimary); + return; + } + + // Make it into a number + var tmp = parseInt(newValue); + if (isNaN(tmp)) { + console.log("Not a number: " + newValue); + return; + } + + selectedOrganization.sync_features[tmpprimary].limit = tmp; + + // setSelectedOrganization(selectedOrganization); + forceUpdate(Math.random()); + submitFeatureEdit(selectedOrganization.sync_features); + }; + + return ( + + + { + setExpanded(!expanded); + }} + > + + {primaryIcon} + + + {isCloud && userdata.support === true ? ( + + { + e.preventDefault(); + e.stopPropagation(); + + if (showEdit) { + setShowEdit(false); + return; + } + + console.log("Edit"); + + setShowEdit(true); + }} + /> + + ) : null} + + { + if (!isCloud || userdata.support !== true) { + return; + } + + e.preventDefault(); + e.stopPropagation(); + + enableFeature(); + }} + > + {secondaryIcon} + + + + {expanded ? ( +
    + + Usage:  + {props.data.limit === 0 ? ( + "Unlimited" + ) : ( + + {props.data.usage} /{" "} + {props.data.limit === "" ? "Unlimited" : props.data.limit} + + )} + + {/* + Data sharing: {props.data.data_collection} + */} + + Description: {secondary} + +
    + ) : null} + + {showEdit ? ( + { + console.log("Submit"); + submitEdit(e); + }} + > + + { + setNewValue(event.target.value); + }} + /> + + + + ) : null} +
    +
    + ); + }; + + const enableCloudSync = (apikey, organization, disableSync) => { + setOrgSyncResponse(""); + + const data = { + apikey: apikey, + organization: organization, + disable: disableSync, + }; + + const url = globalUrl + "/api/v1/cloud/setup"; + fetch(url, { + mode: "cors", + method: "POST", + body: JSON.stringify(data), + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then((response) => { + setLoading(false); + if (response.status === 200) { + console.log("Cloud sync success?"); + } else { + console.log("Cloud sync fail?"); + } + + return response.json(); + //setTimeout(() => { + //}, 1000) + }) + .then((responseJson) => { + console.log("RESP: ", responseJson); + if ( + responseJson.success === false && + responseJson.reason !== undefined + ) { + setOrgSyncResponse(responseJson.reason); + toast("Failed to handle sync: " + responseJson.reason); + } else if (!responseJson.success) { + toast("Failed to handle sync."); + } else { + // getOrgs(); + if (disableSync) { + toast("Successfully disabled sync!"); + setOrgSyncResponse("Successfully disabled syncronization"); + } else { + toast("Cloud Syncronization successfully set up!"); + setOrgSyncResponse( + "Successfully started syncronization. Cloud features you now have access to can be seen below.", + ); + } + + selectedOrganization.cloud_sync = !selectedOrganization.cloud_sync; + // setSelectedOrganization(selectedOrganization); + setCloudSyncApikey(""); + + // handleGetOrg(userdata.active_org.id); + } + }) + .catch((error) => { + setLoading(false); + toast("Err: " + error.toString()); + }); + }; + + const createSubOrg = (currentOrgId, name) => { + const data = { name: name, org_id: currentOrgId }; + console.log(data); + const url = globalUrl + `/api/v1/orgs/${currentOrgId}/create_sub_org`; + + fetch(url, { + mode: "cors", + method: "POST", + body: JSON.stringify(data), + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then((response) => + response.json().then((responseJson) => { + if (responseJson["success"] === false) { + if (responseJson.reason !== undefined) { + toast(responseJson.reason); + } else { + toast("Failed creating suborg. Please try again"); + } + } else { + toast("Successfully created suborg. Reloading in 3 seconds!"); + // setSelectedUserModalOpen(false); + + setTimeout(() => { + window.location.reload(); + }, 2500); + } + + setOrgName(""); + setModalOpen(false); + }), + ) + .catch((error) => { + toast("Err: " + error.toString()); + }); + }; + + const handleClickChangeOrg = (orgId) => { + // Don't really care about the logout + //name: org.name, + //orgId = "asd" + const data = { + org_id: orgId, + }; + + localStorage.setItem("globalUrl", ""); + localStorage.setItem("getting_started_sidebar", "open"); + + fetch(`${globalUrl}/api/v1/orgs/${orgId}/change`, { + mode: "cors", + credentials: "include", + crossDomain: true, + method: "POST", + body: JSON.stringify(data), + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then(function (response) { + if (response.status !== 200) { + console.log("Error in response"); + } else { + localStorage.setItem("apps", []) + } + + return response.json(); + }) + .then(function (responseJson) { + if (responseJson.success === true) { + if ( + responseJson.region_url !== undefined && + responseJson.region_url !== null && + responseJson.region_url.length > 0 + ) { + localStorage.setItem("globalUrl", responseJson.region_url); + //globalUrl = responseJson.region_url + } + + checkLogin() + setTimeout(() => { + window.location.reload(); + }, 3000); + toast("Successfully changed active organization - refreshing!"); + } else { + toast("Failed changing org: " + responseJson.reason); + } + }) + .catch((error) => { + console.log("error changing: ", error); + //removeCookie("session_token", {path: "/"}) + }); + }; + + const modalView = ( + { + setModalOpen(false); + }} + PaperProps={{ + sx: { + borderRadius: theme?.palette?.DialogStyle?.borderRadius, + border: theme?.palette?.DialogStyle?.border, + minWidth: "800px", + minHeight: "320px", + fontFamily: theme?.typography?.fontFamily, + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + zIndex: 1000, + '& .MuiDialogContent-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogTitle-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogActions-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + }, + }} + > + + Add Sub-Organization + + +
    + Name + { + setOrgName(event.target.value); + }} + /> +
    + {/* {loginInfo} */} +
    + + + + +
    + ); + + const cloudSyncModal = ( + { + setCloudSyncModalOpen(false); + }} + PaperProps={{ + sx: { + borderRadius: theme?.palette?.DialogStyle?.borderRadius, + border: theme?.palette?.DialogStyle?.border, + minWidth: "800px", + minHeight: "320px", + fontFamily: theme?.typography?.fontFamily, + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + zIndex: 1000, + '& .MuiDialogContent-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogTitle-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogActions-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + }, + }} + > + + Enable cloud features + + + What does{" "} + + cloud sync + {" "} + do? +
    + { + setCloudSyncApikey(event.target.value); + }} + /> + +
    + {orgSyncResponse.length > 0 ? ( + + Error: {orgSyncResponse} + + ) : null} + + {syncList.map((data, index) => { + return ; + })} + + * New triggers (userinput, hotmail realtime) +
    + * Execute in the cloud rather than onprem +
    + * Apps can be built in the cloud +
    + * Easily share apps and workflows +
    * Access to powerful cloud search + +
    + ); + + + const textColor = "#9E9E9E !important"; + + return ( +
    + {modalView} + {cloudSyncModal} +
    +
    +
    +

    Organizations

    + + Control sub organizations (tenants)! {" "} + {isCloud + ? "You can only make a sub organization if you are a customer of shuffle or running a POC of the platform. Please contact support@shuffler.io to try it out." + : ''} + + Learn more + + +
    + + + + + +
    +

    + Your Parent Organization +

    +
    +
    + + + {/* */} + + +
    + + + + + {isCloud && ( + + )} + + + + + {loadOrgs ? ( + [...Array(3)].map((_, rowIndex) => ( + + {[ + { width: 100, minWidth: 100, maxWidth: 100 }, + { width: 250, minWidth: 50, maxWidth: 250 }, + { width: 400, minWidth: 400, maxWidth: 400 }, + { width: "28%", minWidth: "28%" }, + { width: 400, minWidth: 400, maxWidth: 400 }, + ].map((style, colIndex) => ( + + + + ))} + + )) + ) : parentOrg?.id?.length > 0 ? ( + + + } + style={{ + width: 100, + minWidth: 100, + maxWidth: 100, + display: "table-cell", + padding: "8px 8px 8px 20px", + textAlign: "center", + }} + /> + + {isCloud && ( + + {parentOrgFlag} + +
    + } + style={{ display: "table-cell", padding: 8, verticalAlign: "middle" }} + /> + )} + + + + + + + } + style={{ display: "table-cell", verticalAlign: "middle" }} + /> + + ): ( + + {Array(5).fill().map((_, index) => ( + + ))} + + )} + +
    +
    + + {subOrgs.length > 0 && ( +
    + + +
    +

    + Sub Organizations of the Current Organization ({subOrgs.length}) +

    +
    + + {/* */} + +
    + + + + + {isCloud && ( + + )} + + + + {subOrgs.map((data, index) => ( + + } style={{ width: 100, + minWidth: 100, + maxWidth: 100, + display: "table-cell", + padding: "8px 8px 8px 20px", + textAlign: "center", }} /> + + {isCloud && ( + + {parentOrgFlag} + +
    + } + style={{ display: "table-cell", padding: 8, verticalAlign: "middle" }} + /> + )} + + + + + + } + style={{ display: "table-cell", verticalAlign: "middle" }} + /> + + ))} + +
    +
    + )} + + + +
    +

    + All Tenants +

    +
    + + {/* */} +
    + + + + + {isCloud && ( + + )} + + + + + {userdata?.orgs?.length <= 0 ? ( + [...Array(6)].map((_, rowIndex) => ( + + {Array(7) + .fill() + .map((_, colIndex) => ( + + + + ))} + + )) + ) : ( + userdata?.orgs?.length > 0 && + userdata.orgs.map((data, index) => { + let regiontag = "EU"; + let regionCode = "eu"; + + if (data.region_url?.length > 0) { + const regionsplit = data.region_url.split("."); + if (regionsplit.length > 2 && !regionsplit[0].includes("shuffler")) { + const namesplit = regionsplit[0].split("/"); + regiontag = namesplit[namesplit.length - 1]; + + if (regiontag === "california") { + regiontag = "US"; + regionCode = "us"; + } else if (regiontag === "frankfurt") { + regiontag = "EU-2"; + regionCode = "eu"; + } else if (regiontag === "ca") { + regiontag = "CA"; + regionCode = "ca"; + } + } + } + + return ( + + + } + style={{ + width: 100, + minWidth: 100, + maxWidth: 100, + display: "table-cell", + padding: "8px 8px 8px 20px", + textAlign: "center", + }} + /> + + {isCloud ? ( + + {regiontag} + + +
    + } + style={{ + display: "table-cell", + padding: 8, + verticalAlign: "middle", + }} + > + ) : null} + + { + handleClickChangeOrg(data?.id); + }} + > + Change Active Org + + } + style={{ + display: "table-cell", + padding: 8, + verticalAlign: "middle", + }} + > + + ); + }) + )} + +
    +
    +
    + +
    + ); +}); + +export default TenantsTab; diff --git a/frontend/src/components/UserManagmentTab.jsx b/frontend/src/components/UserManagmentTab.jsx new file mode 100644 index 00000000..0dfda66a --- /dev/null +++ b/frontend/src/components/UserManagmentTab.jsx @@ -0,0 +1,1670 @@ +import React, { useState, useEffect, useContext, memo } from "react"; +import { toast } from 'react-toastify'; + +import { + FormControl, + InputLabel, + OutlinedInput, + Checkbox, + Tooltip, + Typography, + Select, + MenuItem, + Divider, + TextField, + Button, + List, + ListItem, + ListItemText, + IconButton, + Dialog, + DialogTitle, + DialogActions, + DialogContent, + CircularProgress, + Skeleton, + Switch, + Box, +} from "@mui/material"; + +import { + Cached as CachedIcon, + Edit as EditIcon, + Style, +} from "@mui/icons-material"; +import ModeEditOutlineOutlinedIcon from '@mui/icons-material/ModeEditOutlineOutlined'; +import ContentCopyOutlinedIcon from '@mui/icons-material/ContentCopyOutlined'; + +import theme from "../theme.jsx"; +const ITEM_HEIGHT = 48; +const ITEM_PADDING_TOP = 8; +const MenuProps = { + PaperProps: { + style: { + maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, + width: 500, + }, + }, + getContentAnchorEl: () => null, +}; + +const logsViewModal = false; +const userdata = ""; + +const UserManagmentTab = memo((props) => { + const { userdata, isCloud, globalUrl, selectedOrganization, handleEditOrg} = props; + const [modalOpen, setModalOpen] = React.useState(false); + const [loginInfo, setLoginInfo] = React.useState(""); + const [modalUser, setModalUser] = React.useState({}); + const [selectedUser, setSelectedUser] = React.useState({}); + const [matchingOrganizations, setMatchingOrganizations] = React.useState([]); + const [selectedUserModalOpen, setSelectedUserModalOpen] = React.useState(false); + const [image2FA, setImage2FA] = React.useState(""); + const [secret2FA, setSecret2FA] = React.useState(""); + const [value2FA, setValue2FA] = React.useState(""); + const [newUsername, setNewUsername] = React.useState(""); + const [newPassword, setNewPassword] = React.useState(""); + const [show2faSetup, setShow2faSetup] = useState(false); + const [showDeleteAccountTextbox, setShowDeleteAccountTextbox] = React.useState(false); + const [MFARequired, setMFARequired] = React.useState(selectedOrganization.mfa_required === undefined ? false : selectedOrganization.mfa_required); + const [deleteAccountText, setDeleteAccountText] = React.useState(""); + const [users, setUsers] = React.useState([]); + const [showLoader, setShowLoader] = useState(true); + const [logsLoading, setLogsLoading] = React.useState(true); + const [logs, setLogs] = React.useState([]); + const [logsViewModal, setLogsViewModal] = React.useState(false); + const [ipSelected, setIpSelected] = React.useState(""); + const [userLogViewing, setUserLogViewing] = React.useState({}); + + useEffect(() => { + if (selectedOrganization?.mfa_required !== MFARequired) { + setMFARequired(selectedOrganization?.mfa_required); + } + }, [selectedOrganization]); + useEffect(() => { if(users?.length === 0){ + getUsers(); + } }, []); + + const changeModalData = (field, value) => { + modalUser[field] = value; + }; + + const submitUser = (data) => { + console.log("INPUT: ", data); + setLoginInfo(""); + + // Just use this one? + var data = { username: data.Username, password: data.Password }; + var baseurl = globalUrl; + const url = baseurl + "/api/v1/users/register"; + + fetch(url, { + method: "POST", + credentials: "include", + body: JSON.stringify(data), + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => + response.json().then((responseJson) => { + if (responseJson["success"] === false) { + setLoginInfo("Error: " + responseJson.reason); + } else { + setLoginInfo(""); + toast.success("User added successfully. They will show up in the list when they have accepted the invite."); + setModalOpen(false); + setTimeout(() => { + getUsers(); + }, 1000); + } + }) + ) + .catch((error) => { + console.log("Error in userdata: ", error); + }); + }; + + const setUser = (userId, field, value) => { + const data = { user_id: userId }; + data[field] = value; + + fetch(globalUrl + "/api/v1/users/updateuser", { + method: "PUT", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(data), + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for WORKFLOW EXECUTION :O!"); + } else { + getUsers(); + } + + return response.json(); + }) + .then((responseJson) => { + if (!responseJson.success && responseJson.reason !== undefined) { + toast("Failed setting user: " + responseJson.reason); + } else if (responseJson.success === false) { + toast("Failed to update user"); + } else { + //toast("Set the user field " + field + " to " + value); + toast("Successfully updated user field " + field); + + if (field !== "suborgs") { + setSelectedUserModalOpen(false); + } + } + }) + .catch((error) => { + console.log(error); + }); + }; + + const inviteUser = (data) => { + //console.log("INPUT: ", data); + setLoginInfo(""); + + // Just use this one? + var data = { + username: data.Username, + type: "invite", + org_id: selectedOrganization.id, + }; + var baseurl = globalUrl; + const url = baseurl + "/api/v1/users/register_org"; + + fetch(url, { + method: "POST", + credentials: "include", + body: JSON.stringify(data), + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => + response.json().then((responseJson) => { + if (responseJson["success"] === false) { + setLoginInfo("Error: " + responseJson.reason); + toast("Failed to send email (2). Please try again and contact support if this persists.") + } else { + setLoginInfo(""); + setModalOpen(false); + setTimeout(() => { + getUsers(); + }, 1000); + + toast("Invite sent! They will show up in the list when they have accepted the invite.") + } + }) + ) + .catch((error) => { + console.log("Error in userdata: ", error); + toast("Failed to send email. Please try again and contact support if this persists.") + }); + }; + const onPasswordChange = () => { + const data = { username: selectedUser.username, newpassword: newPassword }; + const url = globalUrl + "/api/v1/users/passwordchange"; + + fetch(url, { + mode: "cors", + method: "POST", + body: JSON.stringify(data), + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then((response) => + response.json().then((responseJson) => { + if (responseJson["success"] === false) { + if (responseJson.reason !== undefined) { + toast(responseJson.reason); + } else { + toast("Failed setting new password"); + } + } else { + toast("Successfully updated password!"); + setSelectedUserModalOpen(false); + } + }), + ) + .catch((error) => { + toast("Err: " + error.toString()); + }); + }; + + const handleOrgEditChange = (event) => { + if (userdata.id === selectedUser.id) { + toast("Can't remove orgs from yourself"); + return; + } + + console.log("event: ", event.target.value); + setMatchingOrganizations(event.target.value); + // Workaround for empty orgs + if (event.target.value.length === 0) { + event.target.value.push("REMOVE"); + } + + setUser(selectedUser.id, "suborgs", event.target.value); + //setUser(selectedUser.id, "suborgs", matchingOrganizations) + }; + + const userOrgEdit = + selectedUser.id !== undefined && + selectedUser?.orgs !== undefined && + selectedUser?.orgs !== null && + selectedOrganization?.child_orgs !== undefined && + selectedOrganization?.child_orgs !== null && + selectedOrganization?.child_orgs?.length > 0 ? ( + + + Accessible Sub-Organizations ( + {selectedUser?.orgs ? selectedUser?.orgs?.length - 1 : 0}) + + + + ) : null; + + const getUsers = () => { + fetch(globalUrl + "/api/v1/getusers", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + // Ahh, this happens because they're not admin + // window.location.pathname = "/workflows" + return; + } + + return response.json(); + }) + .then((responseJson) => { + setUsers(responseJson); + setShowLoader(false) + }) + .catch((error) => { + toast(error.toString()); + }); + }; + + const deleteUser = (data) => { + // Just use this one? + const userId = data.id; + + const url = globalUrl + "/api/v1/users/" + userId; + fetch(url, { + method: "DELETE", + credentials: "include", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => { + if (response.status === 200) { + getUsers(); + } + + return response.json(); + }) + .then((responseJson) => { + if (!responseJson.success && responseJson.reason !== undefined) { + toast("Failed to deactivate user: " + responseJson.reason); + } else if (responseJson.success === false) { + toast( + "Failed to deactivate user. Please contact support@shuffler.io if this persists.", + ); + } else { + toast("Changed activation for user " + data.id); + } + }) + + .catch((error) => { + console.log("Error in userdata: ", error); + }); + }; + + const handleDeleteAccount = (userID) => { + if (userID === undefined || userID === null || userID === "") { + return; + } + + const url = `${globalUrl}/api/v1/users/${userID}/remove`; + fetch(url, { + mode: "cors", + method: "DELETE", + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => response.json()) + .then((data) => { + if (data.success) { + toast.success( + "Deleted their account. Would reload users in a few seconds.", + ); + + setTimeout(() => { + getUsers(); + }); + } else { + toast.error(`${data.reason}`); + } + }) + .catch((error) => { + console.error( + "There was a problem with deleting the account. Please try again:", + error, + ); + toast.error( + "There was a problem with the delete request. Please try again", + ); + }); + }; + + const handleVerify2FA = (userId, code) => { + const data = { + code: code, + user_id: userId, + }; + + fetch(`${globalUrl}/api/v1/users/${userId}/set2fa`, { + mode: "cors", + method: "POST", + body: JSON.stringify(data), + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then((response) => { + if (response.status === 200) { + } else { + //toast("Wrong code sent.") + //toast("Wrong code sent. Please try again.") + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === true) { + toast("Successfully enabled 2fa"); + + setTimeout(() => { + getUsers(); + + setImage2FA(""); + setValue2FA(""); + setSecret2FA(""); + setShow2faSetup(false); + setSelectedUserModalOpen(false); + }, 1000); + } else { + toast("Wrong code sent. Please try again."); + //toast("Failed setting 2fa: ", responseJson.reason) + } + }) + .catch((error) => { + toast("Wrong code sent. Please try again."); + //toast("Err: " + error.toString()) + }); + }; + + const get2faCode = (userId) => { + fetch(`${globalUrl}/api/v1/users/${userId}/get2fa`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for apps :O!"); + } + + return response.json(); + }) + .then((responseJson) => { + //console.log("RESPONSE: ", responseJson) + if (responseJson.success === true) { + //toast(responseJson.reason) + setImage2FA(responseJson.reason); + setSecret2FA(responseJson.extra); + } + }) + .catch((error) => { + toast(error.toString()); + }); + }; + + const generateApikey = (user) => { + const userId = user.id; + const data = { user_id: userId }; + + toast("Generating new API key"); + + var fetchdata = { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }; + + if (userId === userdata.id) { + fetchdata.method = "GET"; + } else { + fetchdata.body = JSON.stringify(data); + } + + fetch(globalUrl + "/api/v1/generateapikey", fetchdata) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for WORKFLOW EXECUTION :O!"); + } else { + getUsers(); + } + + return response.json(); + }) + .then((responseJson) => { + console.log("RESP: ", responseJson); + if (!responseJson.success && responseJson.reason !== undefined) { + toast("Failed getting new: " + responseJson.reason); + } else { + toast("Got new API key"); + } + }) + .catch((error) => { + console.log(error); + }); + }; + + const UpdateMFAInUserOrg = (org_id) => { + + handleEditOrg( + selectedOrganization?.name, + selectedOrganization?.description, + selectedOrganization?.id, + selectedOrganization?.image, + { + app_download_repo: selectedOrganization?.defaults?.app_download_repo, + app_download_branch: selectedOrganization?.defaults?.app_download_branch, + workflow_download_repo: selectedOrganization?.defaults?.workflow_download_repo, + workflow_download_branch: selectedOrganization?.defaults?.workflow_download_branch, + notification_workflow: selectedOrganization?.defaults?.notification_workflow, + documentation_reference: selectedOrganization?.defaults?.documentation_reference, + workflow_upload_repo: selectedOrganization?.defaults?.workflow_upload_repo, + workflow_upload_branch: selectedOrganization?.defaults?.workflow_upload_branch, + workflow_upload_username: selectedOrganization?.defaults?.workflow_upload_username, + workflow_upload_token: selectedOrganization?.defaults?.workflow_upload_token, + newsletter: selectedOrganization?.defaults?.newsletter, + weekly_recommendations: selectedOrganization?.defaults?.weekly_recommendations, + }, + { + sso_entrypoint: selectedOrganization?.sso_config?.sso_entrypoint, + sso_certificate: selectedOrganization?.sso_config?.sso_certificate, + client_id: selectedOrganization?.sso_config?.client_id, + client_secret: selectedOrganization?.sso_config?.client_secret, + openid_authorization: selectedOrganization?.sso_config?.openid_authorization, + openid_token: selectedOrganization?.sso_config?.openid_token, + SSORequired: selectedOrganization?.sso_config?.SSORequired, + auto_provision: selectedOrganization?.sso_config?.auto_provision, + }, + [], + { + mfa_required: !MFARequired + } + ); + setMFARequired((prev)=> !prev) + } + + const modalView = ( + { + setModalOpen(false); + }} + PaperProps={{ + sx: { + borderRadius: theme?.palette?.DialogStyle?.borderRadius, + border: theme?.palette?.DialogStyle?.border, + minWidth: '440px', + fontFamily: theme?.typography?.fontFamily, + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + zIndex: 1000, + '& .MuiDialogContent-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogTitle-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + } + }} + > + + + Add user + + + + + We will send an email to invite them to your organization. + +
    + Username + { + if(e.key === "Enter"){ + if (isCloud) { + inviteUser(modalUser); + } else { + submitUser(modalUser); + } + } + }} + onChange={(event) => + changeModalData("Username", event.target.value) + } + /> + {isCloud ? null : ( + + Password + { + if(e.key === "enter"){ + if (isCloud) { + inviteUser(modalUser); + } else { + submitUser(modalUser); + } + } + }} + onChange={(event) => + changeModalData("Password", event.target.value) + } + /> + + )} +
    + {loginInfo} +
    + + + + +
    + ); + + const run2FASetup = (data) => { + if (!show2faSetup) { + get2faCode(data.id); + } else { + // Should remove? + setImage2FA(""); + setSecret2FA(""); + } + + setShow2faSetup(!show2faSetup); + //setShow2faSetup(true); + }; + + const editUserModal = ( + { + setSelectedUserModalOpen(false); + setImage2FA(""); + setValue2FA(""); + setSecret2FA(""); + setShow2faSetup(false); + }} + PaperProps={{ + sx: { + borderRadius: theme?.palette?.DialogStyle?.borderRadius, + border: theme?.palette?.DialogStyle?.border, + fontFamily: theme?.typography?.fontFamily, + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + zIndex: 1000, + minWidth: "800px", + minHeight: "320px", + overflow: "hidden", + '& .MuiDialogContent-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogTitle-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + } + }} + > + + + Editing {selectedUser.username} + + + + {isCloud ? null : ( +
    + { + setNewUsername(e.target.value); + }} + /> + +
    + )} + + {isCloud ? null : ( +
    + setNewPassword(e.target.value)} + /> + +
    + )} + + {userOrgEdit} + +
    + + + + + {isCloud && userdata.support && selectedUser.id !== userdata.id ? ( + + ) : null} + + {showDeleteAccountTextbox ? ( + { + setDeleteAccountText(e.target.value); + }} + /> + ) : null} +
    + {show2faSetup ? ( +
    + {/**/} + + {secret2FA !== undefined && + secret2FA !== null && + secret2FA?.length > 0 ? ( + + + Scan the image below with the two-factor authentication app on + your phone. If you can’t use a QR code, use the code{" "} + {secret2FA} instead. + + + ) : null} + {image2FA !== undefined && + image2FA !== null && + image2FA?.length > 0 ? ( + {"2 + ) : ( + + )} + + + After scanning the QR code image, the app will display a code that + you can enter below. + +
    + { + if (event.target.value.length > 6) { + return; + } + + setValue2FA(event.target.value); + }} + /> + +
    +
    + ) : null} +
    +
    + ); + + const getLogs = (ip, userId) => { + setLogsLoading(true); + console.log("logs loading: ", logsLoading); + fetch(`${globalUrl}/api/v1/users/${userId}/audit?user_ip=${ip}`, { + mode: "cors", + method: "GET", + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then((response) => { + return response.json(); + }) + .then((responseJson) => { + console.log("ResponseJSON: ", responseJson); + if (responseJson.success === true) { + setLogs(responseJson.logs); + } else { + if ( + responseJson.success === false || + responseJson.reason !== undefined + ) { + console.log("Reason given: ", responseJson.reason); + toast("Failed getting logs: " + responseJson.reason); + setLogs([]); + } else { + toast("Failed getting logs"); + } + } + console.log("logs loading now: ", logsLoading); + setLogsLoading(false); + }) + .catch((error) => { + console.log("Error: ", error); + toast("Failed getting logs. Please contact: ", error); + console.log("logs loading now: ", logsLoading); + setLogsLoading(false); + }); + }; + + const logview = logsViewModal ? ( + { + setLogsViewModal(false); + }} + PaperProps={{ + sx: { + borderRadius: theme?.palette?.DialogStyle?.borderRadius, + border: theme?.palette?.DialogStyle?.border, + minWidth: "1200px", + minHeight: "320px", + fontFamily: theme?.typography?.fontFamily, + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + zIndex: 1000, + '& .MuiDialogContent-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogTitle-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogActions-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + }, + }} + > + + User Logs + + + {/* ask user for which IP they want to see logs for by iterating of user.login_info */} + + + User IP + + + + + + {logsLoading && ipSelected.length !== 0 ? ( +
    + + Loading logs +
    + ) : null} + + + + + + + + {logs.map((data, index) => { + //console.log("LOG: ", data) + + return ( + // redirect user to logs + // using request id or trace id + + + + + + )})} + +
    +
    + ) : null + + return ( +
    + {modalView} + {editUserModal} + {logview} +
    +
    +
    +
    +
    +

    User Management

    + + Add, edit, distribute or remove users from your organization.{" "} + + Configure SSO + +   + or +   + + learn more about users + + +
    +
    + + +
    +
    + MFA Required + { + UpdateMFAInUserOrg(selectedOrganization.id); + }} + /> +
    +
    +
    + + + {["Username", "API Key", "Role", "Active", "Type", "MFA", ...(selectedOrganization?.child_orgs?.length > 0 ? ["Suborgs"]: []), "Actions", "Last Login"].map((header, index) => ( + + ))} + + {showLoader ? ( + [...Array(6)].map((_, rowIndex) => ( + + {Array(9) + .fill() + .map((_, colIndex) => ( + + + + ))} + + )) + ): users === 0 ? null + : users?.map((data, index) => { + var bgColor = "#212121"; + if (index % 2 === 0) { + bgColor = "#1A1A1A"; + } + + const timeNow = new Date().getTime(); + + // Get the highest timestamp in data.login_info + var lastLogin = "N/A"; + if (data.login_info !== undefined && data.login_info !== null) { + var loginInfo = 0; + for (var i = 0; i < data?.login_info?.length; i++) { + if (data.login_info[i].timestamp > loginInfo) { + loginInfo = data.login_info[i].timestamp; + } + } + + if (loginInfo > 0) { + lastLogin = + new Date(loginInfo * 1000).toISOString().slice(0, 10) + + " (" + + data?.login_info?.length + + ")"; + } + } + + var userData = data.username; + if (userdata.support === true) { + userData = ( + { + setLogsViewModal(true); + setUserLogViewing(data); + + if (userLogViewing.login_info !== undefined && userLogViewing.login_info !== null && userLogViewing.login_info.length > 0) { + getLogs(userLogViewing.login_info[0].ip, userLogViewing.id) + setIpSelected(userLogViewing.login_info[0].ip); + } + }} + > + {data.username} + + ); + } + + return ( + + + {userData || 'No username'} + + )} + primaryTypographyProps={{ + style: { + maxWidth: 150, + minWidth: 100, + width: 'auto', + color: "#FF8444", + textOverflow: "ellipsis", + whiteSpace: "nowrap", + overflow: "hidden", + padding: "8px 8px 8px 15px", + }, + }} + style={{display:'table-cell', verticalAlign: 'middle' }} + /> + + { + navigator.clipboard.writeText(data.apikey); + toast.success("Apikey copied to clipboard"); + }} + > + + + + ) + } + /> + { + console.log("VALUE: ", e.target.value); + setUser(data.id, "role", e.target.value); + }} + sx={{ + backgroundColor: "#1A1A1A", + color: "white", + height: "50px", + borderRadius: "4px", + marginTop: "8px", + marginBottom: "8px", + padding: "8px", + }} + MenuProps={{ + PaperProps: { + sx: { + "& .MuiList-root": { + padding: 0, + }, + }, + }, + }} + > + + Org Admin + + + Org User + + + Org Reader + + + } + style={{ display:'table-cell', verticalAlign: 'middle' }} + /> + + + + {selectedOrganization?.child_orgs !== undefined && + selectedOrganization?.child_orgs !== null && + selectedOrganization?.child_orgs?.length > 0 ? ( + + ) : null} + + { + setSelectedUserModalOpen(true); + setSelectedUser(data); + + // Find matching orgs between current org and current user's access to those orgs + if ( + userdata?.orgs !== undefined && + userdata?.orgs !== null && + userdata?.orgs?.length > 0 && + selectedOrganization?.child_orgs !== undefined && + selectedOrganization?.child_orgs !== null && + selectedOrganization?.child_orgs?.length > 0 + ) { + var active = []; + for (var key in userdata.orgs) { + const found = + selectedOrganization.child_orgs.find( + (item) => item.id === userdata.orgs[key].id + ); + if (found !== null && found !== undefined) { + if ( + data.orgs === undefined || + data.orgs === null + ) { + continue; + } + + const subfound = data.orgs.find( + (item) => item === found.id + ); + if ( + subfound !== null && + subfound !== undefined + ) { + active.push(subfound); + } + } + } + + setMatchingOrganizations(active); + } + }} + > + edit icon + + {/* */} + + + + + ); + })} + +
    +
    +
    +
    + ); +}) + +export default UserManagmentTab; diff --git a/frontend/src/components/ssoTab.jsx b/frontend/src/components/ssoTab.jsx new file mode 100644 index 00000000..8f39a296 --- /dev/null +++ b/frontend/src/components/ssoTab.jsx @@ -0,0 +1,649 @@ +import { useEffect } from "react"; +import React from "react"; +import { + Typography, + Switch, + Button, + Tooltip, + TextField, + Grid, + Skeleton, + Dialog, + DialogTitle, + DialogContent, + Box, +} from "@mui/material"; +import { makeStyles } from "@mui/styles"; +import { Link } from "react-router-dom"; +import theme from "../theme.jsx"; +import { toast } from "react-toastify"; + +const useStyles = makeStyles({ + notchedOutline: { + borderColor: "#f85a3e !important", + }, +}); + + +const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handleEditOrg})=>{ + + const classes = useStyles(); + const [show2faSetup, setShow2faSetup] = React.useState(false); + const [autoPrivision, setAutoProvision] = React.useState(selectedOrganization?.sso_config?.auto_provision) + const [ssoEntrypoint, setSsoEntrypoint] = React.useState( + selectedOrganization.sso_config === undefined + ? "" + : selectedOrganization.sso_config.sso_entrypoint === undefined || + selectedOrganization.sso_config.sso_entrypoint.length === 0 + ? "" + : selectedOrganization.sso_config.sso_entrypoint + ); + const [SSORequired, setSSORequired] = React.useState(selectedOrganization.sso_config === undefined + ? false + : selectedOrganization.sso_config.SSORequired === undefined + ? false + : selectedOrganization.sso_config.SSORequired); + + const [ssoCertificate, setSsoCertificate] = React.useState( + selectedOrganization.sso_config === undefined + ? "" + : selectedOrganization.sso_config.sso_certificate === undefined || + selectedOrganization.sso_config.sso_certificate.length === 0 + ? "" + : selectedOrganization.sso_config.sso_certificate + ); + + const [openidClientId, setOpenidClientId] = React.useState( + selectedOrganization.sso_config === undefined + ? "" + : selectedOrganization.sso_config.client_id === undefined || + selectedOrganization.sso_config.client_id.length === 0 + ? "" + : selectedOrganization.sso_config.client_id + ); + const [openidClientSecret, setOpenidClientSecret] = React.useState( + selectedOrganization.sso_config === undefined + ? "" + : selectedOrganization.sso_config.client_secret === undefined || + selectedOrganization.sso_config.client_secret.length === 0 + ? "" + : selectedOrganization.sso_config.client_secret + ); + const [openidAuthorization, setOpenidAuthorization] = React.useState( + selectedOrganization.sso_config === undefined + ? "" + : selectedOrganization.sso_config.openid_authorization === undefined || + selectedOrganization.sso_config.openid_authorization.length === 0 + ? "" + : selectedOrganization.sso_config.openid_authorization + ); + const [openidToken, setOpenidToken] = React.useState( + selectedOrganization.sso_config === undefined + ? "" + : selectedOrganization.sso_config.openid_token === undefined || + selectedOrganization.sso_config.openid_token.length === 0 + ? "" + : selectedOrganization.sso_config.openid_token + ) + + useEffect(()=>{ + + if (openidClientSecret !== selectedOrganization?.sso_config?.client_secret) { + setOpenidClientSecret(selectedOrganization?.sso_config?.client_secret) + } + + if (openidClientId !== selectedOrganization?.sso_config?.client_id) { + setOpenidClientId(selectedOrganization?.sso_config?.client_id) + } + + if (openidAuthorization !== selectedOrganization?.sso_config?.openid_authorization) { + setOpenidAuthorization(selectedOrganization?.sso_config?.openid_authorization) + } + + if (openidToken !== selectedOrganization?.sso_config?.openid_token) { + setOpenidToken(selectedOrganization?.sso_config?.openid_token) + } + + if (ssoCertificate !== selectedOrganization?.sso_config?.sso_certificate) { + setSsoCertificate(selectedOrganization?.sso_config?.sso_certificate) + } + + if (ssoEntrypoint !== selectedOrganization?.sso_config?.sso_entrypoint) { + setSsoEntrypoint(selectedOrganization?.sso_config?.sso_entrypoint) + } + + if (SSORequired !== selectedOrganization?.sso_config?.SSORequired) { + setSSORequired(selectedOrganization?.sso_config?.SSORequired) + } + if (autoPrivision !== selectedOrganization?.sso_config?.auto_provision) { + setAutoProvision(selectedOrganization?.sso_config?.auto_provision) + } + },[selectedOrganization]) + + const orgSaveButton = ( + + + + ); + + const toggleBetweenRequiredOrOptional = (event) => { + if ( + ssoEntrypoint === "" && + openidAuthorization === "" && + openidToken === "" + ) { + if (!SSORequired) { + toast.error( + "Please fill in fields for either OpenID connect or SSO before continuing. " + ); + return; + } + } else { + toast.info("Toggled SSO. Remember to save."); + } + + setSSORequired(event.target.checked); + }; + + const handleChangeAutoProvision = (event) => { + + if ( + ssoEntrypoint === "" && + openidAuthorization === "" && + openidToken === "" + ) { + if (!autoPrivision) { + toast.error( + "Please fill in fields for either OpenID connect or SSO before continuing. " + ); + return; + } + } else { + setAutoProvision((prev)=> !prev); + toast.info("Toggled Auto Provisioning. Remember to save."); + } + }; + + const HandleTestSSO = () => { + const url = `${globalUrl}/api/v1/orgs/${selectedOrganization?.id}/change`; + const data = { + org_id: selectedOrganization?.id, + sso_test: true, + }; + + fetch(url, { + mode: "cors", + credentials: "include", + crossDomain: true, + method: "POST", + body: JSON.stringify(data), + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then((response) => { + if (response.status !== 200) { + toast.error( + "Failed to test SSO. Please try again later or contact support@shuffler.io if issue persists.", + { duration: 3000 } + ); + return null; + } + return response.json(); + }) + .then((responjson) => { + if (!responjson) return; + + if (responjson["reason"] === "SSO_REDIRECT") { + toast.info( + "Redirecting to SSO login page as SSO is required for this organization.", + { + duration: 3000, + onClose: () => { + window.location.href = responjson["url"]; + } + } + ); + } else { + toast.error( + "No SSO found for this org. Please set up SSO for this org.", + { duration: 3000 } + ); + } + }) + .catch((error) => { + console.error("Error for SSO test:", error); + toast.error( + "An error occurred while testing SSO. Please try again.", + { duration: 3000 } + ); + }); + }; + + return ( +
    +
    +
    + + SSO Configuration + +
    + + Make SAML SSO or OpenID Authentication Required or Optional for Your Organization. + +
    + + {SSORequired ? "Required" : "Optional"} +
    + +
    + {/* auto privisiong in sso */} +
    + + Auto-provisioning of users in SSO. By default, users are auto-provisioned in SSO when they login. If you enable this, no new user will be added in your organization when they login via SSO. + +
    + +
    + +
    + +
    + + You can test your SSO configuration by clicking the button below. + Before testing, ensure you have set Open ID Connect or SAML SSO + credentials. + + 0 || + ssoCertificate?.length > 0 || + openidAuthorization?.length > 0 || + openidClientId?.length > 0 + ) + ? "Please ensure all SSO credentials are set before testing." + : "" + } + > + + + + +
    + + + OpenID connect + + Configure and Authorize SAML / SSO or OpenID connect. {" "} + + Learn more + + + + + IdP URL for Shuffle OpenID: {`${globalUrl}/api/v1/login_openid`} + + + + + Client ID + { + setOpenidClientId(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: isEditOrgTab ? null : classes.notchedOutline, + }, + style: { + color: "white", + fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)", + fontWeight: 400, + fontSize: 16, + borderRadius: 4, + }, + }} + /> + + + + + Client Secret + { + setOpenidClientSecret(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: isEditOrgTab ? null : classes.notchedOutline, + }, + style: { + color: "white", + fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)", + fontWeight: 400, + fontSize: 16, + borderRadius: 4, + }, + }} + /> + + + + + + + Authorization URL + { + setOpenidAuthorization(e.target.value) + }} + InputProps={{ + classes: { + notchedOutline: isEditOrgTab ? null : classes.notchedOutline, + }, + style: { + color: "white", + fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)", + fontWeight: 400, + fontSize: 16, + borderRadius: 4, + }, + }} + /> + + + + + Token URL + { + setOpenidToken(e.target.value) + }} + InputProps={{ + classes: { + notchedOutline: isEditOrgTab ? null : classes.notchedOutline, + }, + style: { + color: "white", + fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)", + fontWeight: 400, + fontSize: 16, + borderRadius: 4, + }, + }} + /> + + + + + {/**/} + {/*isCloud ? null : */} + + SAML SSO (v1.1) + + IdP URL for Shuffle SAML/SSO: {`${globalUrl}/api/v1/login_sso`} + + + + + SSO Entrypoint (IdP) + { + setSsoEntrypoint(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: isEditOrgTab ? null : classes.notchedOutline, + }, + style: { + color: "white", + fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)", + fontWeight: 400, + fontSize: 16, + borderRadius: 4, + }, + }} + /> + + + + + SSO Certificate (X509) + { + setSsoCertificate(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: isEditOrgTab ? null : classes.notchedOutline, + }, + style: { + color: "white", + fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)", + fontWeight: 400, + fontSize: 16, + borderRadius: 4, + }, + }} + /> + + + + +
    + {orgSaveButton} +
    +
    +
    +
    + ) +} + +export default SSOTab \ No newline at end of file diff --git a/frontend/src/views/Admin2.jsx b/frontend/src/views/Admin2.jsx index 2d630fbb..45d933b9 100644 --- a/frontend/src/views/Admin2.jsx +++ b/frontend/src/views/Admin2.jsx @@ -12,6 +12,14 @@ const Admin2 = (props) => { const [orgRequest, setOrgRequest] = React.useState(true); const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; + if (document !== undefined) { + if (selectedOrganization?.name !== undefined) { + document.title = selectedOrganization?.name + " - Admin - Shuffle" + } else { + document.title = "Admin - Shuffle" + } + } + const handleGetOrg = (orgId) => { fetch(`${globalUrl}/api/v1/orgs/${orgId}`, { @@ -103,7 +111,8 @@ const Admin2 = (props) => { setSelectedStatus(leads); } - setSelectedOrganization(responseJson); + + setSelectedOrganization(responseJson) var lists = { active: { triggers: [], @@ -309,7 +318,7 @@ const Admin2 = (props) => { return (
    - +
    ); }; diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 47a2f92e..6bee4e86 100755 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -18484,7 +18484,7 @@ const AngularWorkflow = (defaultprops) => { const defaultImage = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJYAAACOCAMAAADkWgEmAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAWlBMVEX4Wj69TDgmKCvkVTwlJyskJiokJikkJSkjJSn4Ykf+6+f5h3L////8xLr5alH/9fT7nYz4Wz/919H5cVn/+vr8qpv4XUL94d35e2X//v38t6v4YUbkVDy8SzcVIzHLAAAAAWJLR0QMgbNRYwAAAAlwSFlzAAARsAAAEbAByCf1VAAAAAd0SU1FB+QGGgsvBZ/GkmwAAAFKSURBVHja7dlrTgMxDEXhFgpTiukL2vLc/zbZQH5N7MmReu4KPmlGN4m9WgGzfhgtaOZxM1rQztNoQDvPowHtTKMB7WxHA2TJkiVLlixIZMmSRYgsWbIIkSVLFiGyZMkiRNZirBcma/eKZEW87ZGsOBxPRFbE+R3Jio/LlciKuH0iWfH1/UNkRSR3RRYruSvyWKldkcjK7IpUVl5X5LLSuiKbldQV6aycrihgZXRFCau/K2pY3V1RxersijJWX1cUsnq6opLV0RW1rNldUc2a2RXlrHldsQBrTlfcLwv5EZm/PLIgkHXKPHyQRzXzYoO8BjIvzcgnBvJBxny+Ih/7zNEIcpDEHLshh5TIkS5zAI5cFzCXK8hVFHNxh1xzQpfC0BV6XWTJkkWILFmyCJElSxYhsmTJIkSWLFmEyJIlixBZsmQB8stk/U3/Yb49pVcDMg4AAAAldEVYdGRhdGU6Y3JlYXRlADIwMjAtMDYtMjZUMTE6NDc6MDUrMDI6MDD8QCPmAAAAJXRFWHRkYXRlOm1vZGlmeQAyMDIwLTA2LTI2VDExOjQ3OjA1KzAyOjAwjR2bWgAAAABJRU5ErkJggg==" - const size = 40; + const size = isCloud ? 40 : 35; const borderRadius = 5 if (execution.execution_source === undefined || execution.execution_source === null || execution.execution_source.length === 0) { return ( diff --git a/frontend/src/views/AppCreator.jsx b/frontend/src/views/AppCreator.jsx index e6a9888e..22d17b01 100755 --- a/frontend/src/views/AppCreator.jsx +++ b/frontend/src/views/AppCreator.jsx @@ -6204,7 +6204,7 @@ const AppCreator = (defaultprops) => { {testView} */} -
    +
    {appDownloadData.length > 0 ? diff --git a/frontend/src/views/AppExplorer.jsx b/frontend/src/views/AppExplorer.jsx new file mode 100644 index 00000000..e746f75e --- /dev/null +++ b/frontend/src/views/AppExplorer.jsx @@ -0,0 +1,4623 @@ +import React, { useState, useEffect, useContext } from "react"; + +import theme from "../theme.jsx"; +import ReactGA from "react-ga4"; +import Markdown from "react-markdown"; +import algoliasearch from "algoliasearch/lite"; +import ReactJson from "react-json-view-ssr"; +//import { useAlert +import { ToastContainer, toast } from "react-toastify" +import { makeStyles, createStyles } from "@mui/styles"; +import { useParams, useNavigate, Link } from "react-router-dom"; + +import { + Autocomplete, + Box, + Zoom, + Card, + CardActionArea, + Fade, + Tabs, + Tab, + CircularProgress, + DialogActions, + Dialog, + DialogTitle, + DialogContent, + Tooltip, + IconButton, + Menu, + Paper, + Button, + Typography, + Divider, + MenuItem, + Avatar, + TextField, + Breadcrumbs, + Checkbox, + Chip, + Select, +} from "@mui/material"; + +import { + Business as BusinessIcon, + Edit as EditIcon, + CloudDownload as CloudDownloadIcon, + Warning as WarningIcon, + VerifiedUser as VerifiedUserIcon, + Close as CloseIcon, + LockOpen as LockOpenIcon, + PlayArrow as PlayArrowIcon, + GetApp as GetAppIcon, + Apps as AppsIcon, + Description as DescriptionIcon, + ShowChart as ShowChartIcon, + Person as PersonIcon, + Polyline as PolylineIcon, + OpenInNew as OpenInNewIcon, +} from "@mui/icons-material"; + +import ForkRightIcon from '@mui/icons-material/ForkRight'; + +import Alert from "@mui/material/Alert"; +import { Context } from "../context/ContextApi.jsx"; + +import { + SearchBox, + StaticRefinementList, + RefinementList, + InstantSearch, + connectSearchBox, + connectHits, + Index, +} from "react-instantsearch-dom"; +import AppStats from "../components/AppStats.jsx"; +import ParsedAction from "../components/ParsedAction.jsx"; +import { validateJson, GetIconInfo } from "../views/Workflows.jsx"; +import { base64_decode, appCategories } from "../views/AppCreator.jsx"; +import { triggers as workflowTriggers } from "../views/AngularWorkflow.jsx"; +import AuthenticationOauth2 from "../components/Oauth2Auth.jsx"; +import AuthenticationWindow from "../components/AuthenticationWindow.jsx"; +import { CodeHandler, Img, OuterLink, CopyToClipboard, } from "../views/Docs.jsx"; +import { useStyles, } from "../components/ParsedAction.jsx"; +import { sortByKey } from "../views/AngularWorkflow.jsx"; + +import { v4 as uuidv4 } from "uuid"; +import aa from "search-insights"; + +const surfaceColor = "#27292D"; +const inputColor = "#383B40"; + +const chipStyle = { + marginTop: 5, + backgroundColor: "#3d3f43", + height: 30, + marginRight: 5, + paddingLeft: 5, + paddingRight: 5, + height: 28, + cursor: "pointer", + borderColor: "#3d3f43", + color: "white", +}; + +const actionListStyle = { + paddingLeft: 10, + paddingRight: 10, + paddingTop: 10, + marginTop: 5, + backgroundColor: inputColor, + display: "flex", + color: "white", + maxWidth: 350, + minWidth: 350, + maxHeight: 54, + overflow: "hidden", +}; + +const boxStyle = { + color: "white", + flex: "3", + margin: 10, + paddingLeft: 30, + paddingRight: 30, + paddingBottom: 30, + paddingTop: 30, + display: "flex", + flexDirection: "column", + position: "relative", + maxHeight: 180, + overflow: "hidden", +}; + +const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)"; + + +// AppTypes: +// 0 = OpenAPI (VALID) +// 1 = Normal app (Python) +// 2 = OpenAPI (Invalid) +const searchClient = algoliasearch( + "JNSS5CFDZZ", + "db08e40265e2941b9a7d8f644b6e5240" +) + +const AppExplorer = (props) => { + const { + globalUrl, + userdata, + setUserData, + checkLogin, + isLoaded, + selectedApp, + serverside, + isMobile, + isLoggedIn, + selectedDoc, + secondApp, + } = props; + + //const alert = useAlert(); + const classes = useStyles() + let navigate = useNavigate() + + const { leftSideBarOpenByClick, } = useContext(Context); + + const params = useParams(); + //var props = JSON.parse(JSON.stringify(defaultprops)) + //props.match = {} + //params = params + + const bodyDivStyle = { + margin: "auto", + maxWidth: isMobile ? "100%" : 1350, + scrollX: "hidden", + overflowX: "hidden", + }; + + var upload = ""; + const actionNonBodyRequest = ["GET", "HEAD", "DELETE", "CONNECT"]; + const authenticationOptions = [ + "No authentication", + "API key", + "Bearer auth", + "Basic auth", + ]; + const apikeySelection = ["Header", "Query"]; + + const [app, setApp] = useState({}); + + const [openapi, setOpenapi] = useState({}); + const [name, setName] = useState(""); + const [appId, setAppId] = useState(""); + const [contact, setContact] = useState(""); + const [file] = useState(""); + const [fileBase64, setFileBase64] = useState(""); + const [isAppLoaded, setIsAppLoaded] = useState(false); + const [, setDescription] = useState(""); + const [, setBaseUrl] = useState(""); + const [, setAuthenticationRequired] = useState(false); + const [, setAuthenticationOption] = useState(authenticationOptions[0]); + const [newWorkflowTags, setNewWorkflowTags] = React.useState([]); + const [, setParameterName] = useState(""); + const [, setParameterLocation] = useState( + apikeySelection.length > 0 ? apikeySelection[0] : "" + ); + const [, setUrlPath] = useState(""); + const [urlPathQueries, setUrlPathQueries] = useState([]); + const [, setBasedata] = React.useState({}); + const [actions, setActions] = useState([]); + const [errorCode] = useState(""); + const [reloadUrl, setReloadUrl] = React.useState( + serverside === true ? "" : window.location.href + ); + const [relatedWorkflows, setRelatedWorkflows] = useState(0); + const [relatedApps, setRelatedApps] = useState(0); + const [appAuthentication, setAppAuthentication] = React.useState([]); + const [authLoaded, setAuthLoaded] = useState(false); + const baseResult = "The execution result will show up here"; + const [anchorElAvatar, setAnchorElAvatar] = React.useState(null); + const [executionResult, setExecutionResult] = useState({ + valid: false, + result: baseResult, + }); + const [executing, setExecuting] = useState(false); + const [anchorEl, setAnchorEl] = React.useState(null); + const [creatorProfile, setCreatorProfile] = React.useState({}); + const [selectedTab, setSelectedTab] = React.useState(0); + const defaultDocs = "\n\n## No Shuffle-specific app documentation is available yet.\n\n## Need more information about the app? [Contact us](/contact) and [Join the Community](https://discord.gg/B2CBzUm) and find others using this app." + const [sharingConfiguration, setSharingConfiguration] = React.useState("you"); + const [appdata, setAppData] = React.useState({}); + const [appDocumentation, setAppDocumentation] = useState(defaultDocs) + const [secondaryApp, setSecondaryApp] = useState({}); + const [firstRequest, setFirstRequest] = useState(true); + const [publishModalOpen, setPublishModalOpen] = React.useState(false); + + const [categories, setCategories] = useState(appCategories) + const [newWorkflowCategories, setNewWorkflowCategories] = React.useState([]); + const [update, setUpdate] = useState(""); + const [triggers, setTriggers] = useState([]) + const [selectedOrganization, setSelectedOrganization] = React.useState(undefined) + const [selectedValidationAction, setSelectedValidationAction] = React.useState({}) + + const [selectedMeta, setSelectedMeta] = React.useState({ + link: "https://github.com/Shuffle/openapi-apps/new/master/docs", + read_time: 1, + }) + + const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true"); + + + // FIXME: This is used, as useEffect() creates an issue with apps not loading at all + var to_be_copied = ""; + + // 0 = VALID OpenAPI, 1 = Python, 2 = INVALID OpenAPI + const [appType, setAppType] = React.useState(0); + + function handleClick(event) { + setAnchorEl(event.currentTarget); + } + + function handleClose() { + setAnchorEl(null); + } + + + const loadOrganization = (orgId) => { + fetch(`${globalUrl}/api/v1/orgs/${orgId}`, { + method: "GET", + credentials: "include", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => { + if (response.status === 401) { + } else { + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === false) { + + } else { + setSelectedOrganization(responseJson) + } + }) + .catch((error) => { + console.log("Error in fetching organization: ", error) + }) + } + + + useEffect(() => { + if (selectedApp !== undefined && selectedApp !== null && Object.getOwnPropertyNames(selectedApp).length > 0) { + //console.log("Firstrequest!!!") + } else { + if (serverside) { + console.log("Not getting app because serverside."); + } else { + if (params.appid.length === 32 || params.appid.length === 36) { + handleEditApp(params.appid); + runAlgoliaAppSearch(params.appid, false, true); + } else { + runAlgoliaAppSearch(params.appid); + + //handleEditApp() + } + } + //parseIncomingOpenapiData(YAML.parse(data)) + } + + if (serverside !== true) { + const urlSearchParams = new URLSearchParams(window.location.search); + const queries = Object.fromEntries(urlSearchParams.entries()); + const foundTab = queries["tab"]; + console.log("PROPS: ", queries); + + if (params.integrationid !== undefined) { + console.log( + "Should search for connection integration with ", + params.integrationid + ); + setSelectedTab(3); + + runAlgoliaAppSearch(params.integrationid, false); + } else if (foundTab !== null && foundTab !== undefined) { + if (foundTab === "stats") { + setSelectedTab(2); + } else if (foundTab === "run") { + setSelectedTab(1); + } else if (foundTab === "docs" || foundTab === "documentation") { + setSelectedTab(0); + } + } else { + //setSelectedTab(1); + } + + } + }, []); + + if (serverside === false && firstRequest && isLoggedIn === true && selectedOrganization === undefined && userdata !== undefined && userdata.active_org !== undefined && userdata.active_org !== null && userdata.active_org.id !== undefined && userdata.active_org.id !== null) { + loadOrganization(userdata.active_org.id) + } + + var activateButton = ( + + + + ); + + const Heading = (props) => { + const element = React.createElement(`h${props.level}`,{ style: { marginTop: props.level === 1 ? 20 : 50 } },props.children); + + const [hover, setHover] = useState(false); + + var extraInfo = ""; + if (props.level === 1) { + extraInfo = ( +
    +
    + {isMobile === true ? null : ( + + { + ReactGA.event({ + category: "Appexplorer", + action: "github_docs_edit_click", + label: params.appid, + }); + }} + > + + + + )} + {isMobile === true ? null : ( +
    + )} + + {selectedMeta.read_time} minute + {selectedMeta.read_time === 1 ? "" : "s"} to read + +
    + +
    + {isMobile === true || + selectedMeta.contributors === undefined || + selectedMeta.contributors === null ? ( + "" + ) : ( +
    + {selectedMeta.contributors.slice(0, 7).map((data, index) => { + return ( + + + {data.url} + + + ); + })} +
    + )} +
    +
    + ); + } + + // Still inside Heading + + // Find appCategory in appCategories + const appCategory = newWorkflowCategories.length === 0 ? "" : (newWorkflowCategories[0]).toLowerCase(); + const foundCategory = appCategories.find((category) => { + if (category.name.toLowerCase() === appCategory) { + return category + } + }) + const bgColor = foundCategory === undefined ? "" : foundCategory.color + + const getAllLabels = () => { + const actionLabels = actions.filter((input_action) => { + return input_action.action_label !== undefined && input_action.action_label !== null && input_action.action_label !== "No Label" + }) + + var labels = [] + actionLabels.forEach((action) => { + labels.push(action.action_label) + }) + + return labels + } + + const allLabels = getAllLabels() + + const findRelevantAction = (action_label) => { + const foundAction = actions.find((input_action) => { + if (input_action.action_label.toLowerCase() === action_label.toLowerCase()) { + return input_action + } + }) + + if (foundAction !== undefined) { + setCurrentAction(foundAction) + setCurrentActionMethod(foundAction.method); + setSelectedTab(1); + } else { + console.log("Could not find action with label: ", action_label) + } + } + + // Parses out extra category info and such for the app + const extraAppInfo = props.level === 1 ? +
    + {/* +
    + + Category: + + +
    + */} + {triggers.length === 0 ? null : +
    + {triggers.map((trigger, index) => { + + return ( + { + console.log("Clicked: ", trigger.name) + }} + style={{ + cursor: "pointer", + color: "white", + borderRadius: 40, + minWidth: 80, + marginRight: 10, + marginTop: 2, + fontSize: 14, + }} + avatar={} + label={trigger.name} + /> + ) + })} +
    + } + + {serverside === false && foundCategory !== undefined && foundCategory !== null && foundCategory.action_labels.length > 0 ? +
    + {foundCategory.action_labels.slice(0,5).map((action_label, index) => { + const included = allLabels.includes(action_label) + const iconInfo = GetIconInfo({ name: action_label }); + const useIcon = iconInfo.originalIcon; + + return ( + { + findRelevantAction(action_label) + }} + disabled={included === false} + style={{ + cursor: included ? "pointer" : "default", + color: "white", + borderRadius: 40, + minWidth: 80, + marginRight: 10, + marginTop: 2, + fontSize: 14, + textDecoration: included ? "none" : "line-through", + }} + avatar={useIcon} + label={action_label} + /> + ) + })} +
    + : null} +
    + : null + + return ( + { + setHover(true); + }} + > + {props.level !== 1 ? ( + + ) : null} + {element} + {extraAppInfo} + {extraInfo} + + ); + }; + + const [, setCurrentActionMethod] = useState(actionNonBodyRequest[0]); + + // Selectedaction = Shuffle style action + // Currentaction = OpenAPI style + const [selectedAction, setSelectedAction] = useState({}); + const [currentAction, setCurrentAction] = useState({ + name: "", + description: "", + url: "", + headers: "", + paths: [], + queries: [], + body: "", + errors: [], + method: actionNonBodyRequest[0], + }); + + if (params.appid === "new") { + return null; + } + + const WorkflowHits = ({ hits }) => { + //console.log("WORKFLOWS: ", hits) + + setRelatedWorkflows(hits.length); + return hits.length; + }; + + const AppHits = ({ hits }) => { + if (hits.length >= 1) { + setRelatedApps(hits.length - 1); + + return hits.length - 1; + } else { + setRelatedApps(0); + return 0; + } + }; + + const getUserProfile = (username) => { + if (serverside === true) { + return; + } + + fetch(`${globalUrl}/api/v1/users/creators/${username}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for WORKFLOW EXECUTION :O!"); + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success !== false) { + setCreatorProfile(responseJson); + } + }) + .catch((error) => { + console.log(error); + }); + }; + + const SearchBox = ({ currentRefinement, refine, isSearchStalled }) => { + return null; + }; + + const CustomWorkflowHits = connectHits(WorkflowHits); + const CustomAppHits = connectHits(AppHits); + const CustomSearchBox = connectSearchBox(SearchBox); + + const HandleJsonCopy = (base, copy, base_node_name) => { + console.log("COPY: ", copy); + var newitem = JSON.parse(base); + to_be_copied = "$" + base_node_name; + for (var key in copy.namespace) { + if (copy.namespace[key].includes("Results for")) { + continue; + } + + if (newitem !== undefined && newitem !== null) { + newitem = newitem[copy.namespace[key]]; + if (!isNaN(copy.namespace[key])) { + to_be_copied += ".#"; + } else { + to_be_copied += "." + copy.namespace[key]; + } + } + } + }; + + const handleReactJsonClipboard = (copy) => { + console.log("COPY: ", copy); + + const elementName = "copy_element_shuffle"; + var copyText = document.getElementById(elementName); + if (copyText !== null && copyText !== undefined) { + navigator.clipboard.writeText(JSON.stringify(copy)); + copyText.select(); + copyText.setSelectionRange(0, 99999); /* For mobile devices */ + + /* Copy the text inside the text field */ + document.execCommand("copy"); + toast("Copied data"); + } + }; + + const activateApp = () => { + if (serverside === true) { + return; + } + + const appExists = userdata.active_apps !== undefined && userdata.active_apps !== null && userdata.active_apps.includes(appId) + const url = appExists ? `${globalUrl}/api/v1/apps/${appId}/deactivate` : `${globalUrl}/api/v1/apps/${appId}/activate` + fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Failed to activate"); + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === false) { + if (responseJson.reason !== undefined) { + toast("Failed to activate the app: "+responseJson.reason); + } else { + toast("Failed to activate the app"); + } + } else { + if (checkLogin !== undefined && checkLogin !== null) { + checkLogin() + } + + if (appExists) { + toast("App deactivated for your organization! Existing workflows with the app will continue to work.") + } else { + toast("App activated for your organization!") + } + } + }) + .catch((error) => { + toast(error.toString()); + }); + }; + + const handleEditApp = (appid) => { + if (serverside === true) { + return; + } + + setAppId(appid) + + + fetch(`${globalUrl}/api/v1/apps/${appid}/config`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + //console.log("App doesn't exist or isn't available to you.") + //toast("Something went wrong - this app is not available to you. Redirecting you back to search.") + ReactGA.event({ + category: "appexplorer", + action: `app_not_found`, + label: appid, + }); + } else { + ReactGA.event({ + category: "appexplorer", + action: `app_found`, + label: appid, + }); + } + + return response.json(); + }) + .then((responseJson) => { + if ( + responseJson.success === false || + responseJson.success === undefined + ) { + toast("Failed to get the app") + setIsAppLoaded(true) + // setTimeout(() => { + // navigate("/search") + // }, 1000); + return; + } else { + parseIncomingOpenapiData(responseJson); + } + }) + .catch((error) => { + toast("Error in app fetch: " + error.toString()); + }); + }; + + + const parseIncomingAppdata = (data, openapiExists) => { + document.title = data.name + " App - OpenAPI and API"; + + + + setExecutionResult({ + valid: false, + result: baseResult, + }); + + setName((data.name.charAt(0).toUpperCase() + data.name.substring(1)).replaceAll("_"," ")); + + setDescription(data.description); + setFileBase64(data.large_image); + setContact(data.contact_info); + + if (data.categories !== undefined && data.categories !== null) { + setNewWorkflowCategories(data.categories); + } + setAppType(1); + + if (data.owner !== undefined && data.owner !== null) { + getUserProfile(data.owner); + //console.log("DATA: ", data) + } + + setAppData(data) + if (data.reference_info.triggers !== undefined && data.reference_info.triggers !== null && data.reference_info.triggers.length > 0) { + var parsedtriggers = [] + for (var key in data.reference_info.triggers) { + const curtrigger = data.reference_info.triggers[key] + + const foundTrigger = workflowTriggers.find((trigger) => trigger.name.toLowerCase() === curtrigger.toLowerCase()) + if (foundTrigger !== undefined && foundTrigger !== null) { + parsedtriggers.push(foundTrigger) + } + } + + setTriggers(parsedtriggers) + } + + var newactions = []; + if (!openapiExists) { + console.log("Skipping openapi"); + for (var key in data.actions) { + const action = data.actions[key]; + newactions.push({ + name: action.name, + description: action.description, + url: "", + headers: "", + paths: [], + queries: [], + body: "", + errors: [], + method: "CUSTOM", + }); + } + } + + + if (newactions.length > 0) { + setCurrentAction(newactions[0]); + + if (data.actions !== undefined) { + //var methodName = `${data.method}_${data.name}`.toLowerCase() + //if (data.name.toLowerCase().startsWith(data.method.toLowerCase())) { + // methodName = data.name.toLowerCase() + //} + //var newselectedaction = data.actions.find(item => item.name.toLowerCase() === methodName) + //if (newselectedaction === undefined || newselectedaction === null) { + // toast(`Name ${methodName} not found. Please contact us.`) + // return + //} + + //var newselectedaction = data.actions.find(item => item.name.toLowerCase() === ) + const newselectedaction = data.actions[0]; + newselectedaction.app_id = data.id; + newselectedaction.app_name = data.name; + newselectedaction.app_version = data.app_version; + + newselectedaction.authentication = selectedAction.authentication; + + newselectedaction.authentication_id = selectedAction.authentication_id; + newselectedaction.selectedAuthentication = selectedAction.selectedAuthentication; + + if ( + data.authentication.required && + newselectedaction.authentication_id !== undefined && + newselectedaction.authentication_id !== null && + newselectedaction.authentication_id.length === 0 + ) { + const tmpParams = selectedAction.parameters; + selectedAction.parameters = []; + + for (let paramkey in data.authentication.parameters) { + var item = data.authentication.parameters[paramkey]; + console.log("PARAM1: ", item) + item.configuration = true; + + const found = selectedAction.parameters.find((param) => param.name === item.name); + + if (found === null || found === undefined) { + selectedAction.parameters.push(item); + } + } + + for (let paramkey in tmpParams) { + var item = tmpParams[paramkey]; + console.log("PARAM2: ", item) + //item.configuration = true + const found = selectedAction.parameters.find((param) => param.name === item.name); + + if (found === null || found === undefined) { + selectedAction.parameters.push(item); + } + } + } + + setSelectedAction(newselectedaction); + } + + + const firstActions = newactions.filter(data => data.action_label !== undefined && data.action_label !== null && data.action_label !== "No Label") + //console.log("First actions: ", firstActions) + const secondActions = newactions.filter(data => data.action_label === undefined || data.action_label === null || data.action_label === "No Label") + const newActions = firstActions.concat(secondActions) + setActions(newActions); + } + + setIsAppLoaded(true); + }; + + // Sets the data up as it should be at later points + // This is the data FROM the database, not what's being saved + const parseIncomingOpenapiData = (data) => { + var appexists = false; + var nameExists = false; + var parsedapp = {}; + if (data.app !== undefined && data.app !== null) { + // Should basically always be true if openapi exists too + var parsedBaseapp = "" + try { + parsedBaseapp = base64_decode(data.app) + } catch (e) { + console.log("Failed JSON parsing: ", e) + parsedBaseapp = data + } + + parsedapp = JSON.parse(parsedBaseapp) + parsedapp.name = parsedapp.name.replaceAll("_", " "); + + setAppDocumentation("# "+parsedapp.name+defaultDocs); + setApp(parsedapp); + setSharingConfiguration(parsedapp.sharing === true ? "public" : "you") + + appexists = + parsedapp.name !== undefined && + parsedapp.name !== null && + parsedapp.name.length !== 0; + + if (appexists) { + getAppDocs(parsedapp.name, "python", parsedapp.app_version); + } + + if (data.openapi === undefined || data.openapi === null) { + console.log("Parsed app: ", parsedapp) + parseIncomingAppdata(parsedapp, false); + } else { + parseIncomingAppdata(parsedapp, true); + } + } + + if (data.openapi === undefined || data.openapi === null) { + return; + } + + var parsedDecoded = "" + try { + parsedDecoded = base64_decode(data.openapi) + } catch (e) { + console.log("Failed JSON parsing: ", e) + parsedDecoded = data + } + + setAppType(0); + parsedapp = JSON.parse(parsedDecoded); + data = parsedapp.body === undefined ? parsedapp : JSON.parse(parsedapp.body); + setOpenapi(data); + + getAppDocs(data.info.title, "openapi", data.app_version); + + setBasedata(data); + if (!appexists) { + setName( + ( + data.info.title.charAt(0).toUpperCase() + data.info.title.substring(1) + ).replaceAll("_", " ") + ); + setDescription(data.info.description); + + console.log("Found name: ", data.info.title); + } + + if (serverside !== true) { + var doctitle = "Shuffle App for " + data.info.title + if (!data.info.title.toLowerCase().includes("api")) { + doctitle += " API" + } + + document.title = doctitle + } + + if (data.info !== null && data.info !== undefined) { + if (data.info["x-logo"] !== undefined) { + setFileBase64(data.info["x-logo"]); + } + + if (data.info.contact !== undefined) { + setContact(data.info.contact); + } + + if (data.info["x-categories"] !== undefined && data.info["x-categories"].length > 0) { + setNewWorkflowCategories(data.info["x-categories"]); + } + } + + if (data.tags !== undefined && data.tags.length > 0) { + for (var key in data.tags) { + newWorkflowTags.push(data.tags[key].name); + } + + setNewWorkflowTags(newWorkflowTags); + } + + // This is annoying (: + var securitySchemes = data.components.securityDefinitions; + if (securitySchemes === undefined) { + securitySchemes = data.securitySchemes; + } + + if (securitySchemes === undefined) { + securitySchemes = data.components.securitySchemes; + } + + const allowedfunctions = [ + "GET", + "CONNECT", + "HEAD", + "DELETE", + "POST", + "PATCH", + "PUT", + ]; + + // FIXME - headers? + var newActions = []; + var wordlist = {}; + if (data.paths !== null && data.paths !== undefined) { + for (let [path, pathvalue] of Object.entries(data.paths)) { + if (path === "tmp0") { + setAppType(2); + } + + for (let [method, methodvalue] of Object.entries(pathvalue)) { + if (methodvalue === null) { + toast("Skipped method " + method); + continue; + } + + if (!allowedfunctions.includes(method.toUpperCase())) { + continue; + } + + var tmpname = methodvalue.summary; + if ( + methodvalue.operationId !== undefined && + methodvalue.operationId !== null && + methodvalue.operationId.length > 0 + ) { + tmpname = methodvalue.operationId; + } + + var newaction = { + name: tmpname, + description: methodvalue.description, + url: path, + method: method.toUpperCase(), + headers: "", + queries: [], + paths: [], + body: "", + errors: [], + example_response: "", + action_label: "No Label", + required_bodyfields: [], + } + + // Related to Label Management + if (methodvalue["x-label"] !== undefined && methodvalue["x-label"] !== null) { + // Check if there are commas in it then loop and find the correct one + // Should ignore 'No Label' and 'No label' + var correctlabel = "" + const labels = methodvalue["x-label"].split(",") + for (let labelkey in labels) { + var label = labels[labelkey].trim() + if (label.toLowerCase() === "no label") { + continue + } + + // Remove quotes and escapes + label = label.replace(/['"]+/g, '') + label = label.replace(/\\/g, '') + + //label = label.replace("_", " ", -1) + //label = label.charAt(0).toUpperCase() + label.slice(1) + + correctlabel = label + break + } + + // FIX: Map labels only if they're actually in the category list + newaction.action_label = correctlabel + } + + if (methodvalue["x-required-fields"] !== undefined && methodvalue["x-required-fields"] !== null) { + newaction.required_bodyfields = methodvalue["x-required-fields"] + } + + for (key in methodvalue.parameters) { + const parameter = methodvalue.parameters[key]; + if (parameter.in === "query") { + var tmpaction = { + description: parameter.description, + name: parameter.name, + required: parameter.required, + in: "query", + }; + + if (parameter.required === undefined) { + tmpaction.required = false; + } + + newaction.queries.push(tmpaction); + } else if (parameter.in === "path") { + // FIXME - parse this to the URL too + newaction.paths.push(parameter.name); + + // FIXME: This doesn't follow OpenAPI3 exactly. + // https://swagger.io/docs/specification/describing-request-body/ + // https://swagger.io/docs/specification/describing-parameters/ + // Need to split the data. + } else if (parameter.in === "body") { + // FIXME: Add tracking for components + // E.G: https://raw.githubusercontent.com/owentl/Shuffle/master/gosecure.yaml + if (parameter.example !== undefined) { + newaction.body = parameter.example; + } + } else if (parameter.in === "header") { + newaction.headers += `${parameter.name}=${parameter.example}\n`; + } + } + + if (newaction.name === "" || newaction.name === undefined) { + // Find a unique part of the string + // FIXME: Looks for length between /, find the one where they differ + // Should find others with the same START to their path + // Make a list of reserved names? Aka things that show up only once + if (Object.getOwnPropertyNames(wordlist).length === 0) { + for (let [newpath] of Object.entries(data.paths)) { + const newpathsplit = newpath.split("/"); + for (key in newpathsplit) { + const pathitem = newpathsplit[key].toLowerCase(); + if (wordlist[pathitem] === undefined) { + wordlist[pathitem] = 1; + } else { + wordlist[pathitem] += 1; + } + } + } + } + + //console.log("WORDLIST: ", wordlist) + + // Remove underscores and make it normal with upper case etc + const urlsplit = path.split("/"); + if (urlsplit.length > 0) { + var curname = ""; + for (key in urlsplit) { + var subpath = urlsplit[key]; + if (wordlist[subpath] > 2 || subpath.length < 1) { + continue; + } + + curname = subpath; + break; + } + + // FIXME: If name exists, + // FIXME: Check if first part of parsedname is verb, otherwise use method + const parsedname = curname + .split("_") + .join(" ") + .split("-") + .join(" ") + .split("{") + .join(" ") + .split("}") + .join(" ") + .trim(); + if (parsedname.length === 0) { + newaction.errors.push("Missing name"); + } else { + const newname = + method.charAt(0).toUpperCase() + + method.slice(1) + + " " + + parsedname; + const searchactions = newActions.find( + (data) => data.name === newname + ); + //console.log("SEARCH: ", searchactions); + if (searchactions !== undefined) { + newaction.errors.push("Missing name"); + } else { + newaction.name = newname; + } + } + } else { + newaction.errors.push("Missing name"); + } + } + newActions.push(newaction); + } + + if (data.servers !== undefined && data.servers.length > 0) { + var firstUrl = data.servers[0].url; + if ( + firstUrl.includes("{") && + firstUrl.includes("}") && + data.servers[0].variables !== undefined + ) { + const regex = /{\w+}/g; + const found = firstUrl.match(regex); + if (found !== null) { + for (key in found) { + const item = found[key].slice(1, found[key].length - 1); + const foundVar = data.servers[0].variables[item]; + if (foundVar["default"] !== undefined) { + firstUrl = firstUrl.replaceAll( + found[key], + foundVar["default"] + ); + } + } + } + } + + if (firstUrl.endsWith("/")) { + setBaseUrl(firstUrl.slice(0, firstUrl.length - 1)); + } else { + setBaseUrl(firstUrl); + } + } + } + } + + // FIXME: Have multiple authentication options? + if (securitySchemes !== undefined) { + for (const [, value] of Object.entries(securitySchemes)) { + if (value.scheme === "bearer") { + setAuthenticationOption("Bearer auth"); + setAuthenticationRequired(true); + break; + } else if (value.type === "apiKey") { + setAuthenticationOption("API key"); + + value.in = value.in.charAt(0).toUpperCase() + value.in.slice(1); + setParameterLocation(value.in); + if (!apikeySelection.includes(value.in)) { + //console.log("APIKEY SELECT: ", apikeySelection) + toast("Might be error in setting up API key authentication"); + } + + //console.log("PARAM NAME: ", value.name) + setParameterName(value.name); + setAuthenticationRequired(true); + break; + } else if (value.scheme === "basic") { + setAuthenticationOption("Basic auth"); + setAuthenticationRequired(true); + break; + } + } + } + + const firstActions = newActions.filter(data => data.action_label !== undefined && data.action_label !== null && data.action_label !== "No Label") + //console.log("First actions: ", firstActions) + const secondActions = newActions.filter(data => data.action_label === undefined || data.action_label === null || data.action_label === "No Label") + newActions = firstActions.concat(secondActions) + setActions(newActions); + setIsAppLoaded(true); + + if (newActions.length > 0) { + setCurrentAction(newActions[0]); + setCurrentActionMethod(newActions[0].method); + setUrlPathQueries(newActions[0].queries); + setUrlPath(newActions[0].url); + //setActionsModalOpen(true) + } + }; + + const getAppDocs = (appname, location, version) => { + if (serverside === true) { + return; + } + + fetch( + `${globalUrl}/api/v1/docs/${appname}?location=${location}&version=${version}`, + { + headers: { + Accept: "application/json", + }, + credentials: "include", + } + ) + .then((response) => { + if (response.status === 200) { + //toast("Successfully GOT app "+appId) + } else { + //toast("Failed getting app"); + } + + return response.json(); + }) + .then((responseJson) => { + var setMeta = false + if (responseJson.success === true) { + if (responseJson.reason !== undefined && responseJson.reason !== undefined && responseJson.reason.length > 0) { + + if (!responseJson.reason.includes("404: Not Found") && responseJson.reason.length > 25) { + const imgRegex = / { + toast("Error in doc loading: " + error.toString()); + }); + }; + + const runAlgoliaAppSearch = (appname, isOriginal, triggerOnly) => { + const index = searchClient.initIndex("appsearch"); + + console.log("Running appsearch for: ", appname); + + + index + .search(appname) + .then(({ hits }) => { + const appsearchname = appname.replaceAll("_", " ").toLowerCase(); + var found = false + + if (hits !== undefined && hits !== null && hits.length === 1) { + found = true + if (isOriginal !== false) { + handleEditApp(hits[0].objectID) + } else { + setSecondaryApp(hits[0]) + } + } + + for (var key in hits) { + const hit = hits[key]; + + if (hit["name"] === null || hit["name"] === undefined) { + continue; + } + + if (hit["name"].replaceAll("_", " ").toLowerCase().includes(appsearchname) || hit["objectID"] === appname) { + /* + if (hit.triggers !== undefined && hit.triggers !== null && hit.triggers.length > 0) { + var parsedtriggers = [] + for (var key in hit.triggers) { + const curtrigger = hit.triggers[key].toLowerCase() + + const foundTrigger = workflowTriggers.find((trigger) => trigger.name.toLowerCase() === curtrigger) + if (foundTrigger !== undefined && foundTrigger !== null) { + parsedtriggers.push(foundTrigger) + } + } + + setTriggers(parsedtriggers) + } + */ + + if (triggerOnly === true) { + + } else { + if (isOriginal !== false) { + found = true + handleEditApp(hit.objectID); + } else { + console.log("Found second app: ", hit); + hit.name = hit.name.charAt(0).toUpperCase() + hit.name.slice(1); + setSecondaryApp(hit); + } + } + + break; + } + } + + if (!found) { + if (hits.length > 0) { + if (isOriginal !== false) { + handleEditApp(hits[0].objectID) + } else { + setSecondaryApp(hits[0]); + } + + return + } + + //navigate("/search?message=App not found&q=" + appname + "&tab=apps") + //toast("App not found. Please contact support@shuffler.io if you believe this is an error.") + return + } + }) + .catch((err) => { + console.log(err); + }); + }; + + + + + if (serverside === true && firstRequest) { + setFirstRequest(false); + if ( + selectedApp !== undefined && + selectedApp !== null && + Object.getOwnPropertyNames(selectedApp).length > 0 + ) { + parseIncomingOpenapiData(selectedApp); + } + + if ( + selectedDoc !== undefined && + selectedDoc !== null && + Object.getOwnPropertyNames(selectedDoc).length > 0 + ) { + setAppDocumentation(selectedDoc.reason); + setSelectedTab(0); + } + + if ( + secondApp !== undefined && + secondApp !== null && + Object.getOwnPropertyNames(secondApp).length > 0 + ) { + setSelectedTab(3); + setSecondaryApp(secondApp); + } + } + + //, []) + + if (serverside !== true && window.location.href !== reloadUrl) { + setReloadUrl(window.location.href); + setAppDocumentation(defaultDocs); + setTriggers([]) + //handleEditApp(params.appid) + + if (params.appid.length === 32 || params.appid.length === 36) { + handleEditApp(params.appid); + runAlgoliaAppSearch(params.appid, false, true); + } else { + runAlgoliaAppSearch(params.appid); + } + } + + const loopQueries = + urlPathQueries === undefined || + urlPathQueries === null || + urlPathQueries.length === 0 ? null : ( +
    + + Queries + {urlPathQueries.map((data, index) => { + return ( + +
    + { + //urlPathQueries[index].name = e.target.value + //setUrlPathQueries(urlPathQueries) + }} + InputProps={{ + style: { + color: "white", + }, + }} + /> +
    +
    + ); + })} + +
    + ); + + const executeSingleAction = (appid, thisaction) => { + if (serverside === true) { + return; + } + + if (isCloud) { + thisaction.environment = "Cloud" + } else { + thisaction.environment = "Shuffle" + } + + setExecutionResult({ + valid: false, + result: baseResult, + }); + + setExecuting(true); + + fetch(globalUrl + "/api/v1/apps/" + appid + "/run", { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(thisaction), + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for stream results :O!"); + } + + return response.json(); + }) + .then((responseJson) => { + //console.log("RESPONSE: ", responseJson) + if ( + responseJson.success === true && + responseJson.result !== null && + responseJson.result !== undefined && + responseJson.result.length > 0 + ) { + const result = responseJson.result.slice(0, 50) + "..."; + //toast("SUCCESS: "+result) + + const validate = validateJson(responseJson.result); + setExecutionResult(validate); + } else if ( + responseJson.success === false && + responseJson.reason !== undefined && + responseJson.reason !== null + ) { + toast(responseJson.reason); + setExecutionResult({ valid: false, result: responseJson.reason }); + } else if (responseJson.success === true) { + setExecutionResult({ + valid: false, + result: + "Couldn't finish execution. Please fill all the required fields, and retry the execution.", + }); + } else { + setExecutionResult({ + valid: false, + result: + "Couldn't finish execution (2). Please fill all the required fields, and validate the execution.", + }); + } + + setExecuting(false); + }) + .catch((error) => { + toast("Execution error: " + error.toString()); + setExecuting(false); + }); + }; + + const MethodWrapper = (props) => { + const { data } = props; + + var bgColor = "#61afee"; + if (data.method === "POST") { + bgColor = "#49cc90"; + } else if (data.method === "PUT") { + bgColor = "#fca130"; + } else if (data.method === "PATCH") { + bgColor = "#50e3c2"; + } else if (data.method === "DELETE") { + bgColor = "#f93e3e"; + } else if (data.method === "HEAD") { + bgColor = "#9012fe"; + } + + return ( + + ); + }; + + const parseName = (name, length) => { + if (name === undefined || name === null) { + return "" + } + + var parsedName = name.charAt(0).toUpperCase() + name.slice(1); + parsedName = parsedName.replaceAll("_", " "); + if ( + length !== undefined && + length !== null && + length > 3 && + length < parsedName.length + ) { + parsedName = parsedName.slice(0, length) + ".."; + } + + return parsedName; + }; + + const SubAction = (props) => { + const { data, selected, hovered, index } = props; + + const [hoveredItem, setHoveredItem] = useState(true); + + var urlPath = data.url !== undefined && data.url !== null && data.url.length > 0 ? data.url : "" + var wrappedStyle = JSON.parse(JSON.stringify(actionListStyle)); + wrappedStyle.backgroundColor = selected || hovered ? theme.palette.platformColor : theme.palette.inputColor; + wrappedStyle.paddingBottom = urlPath.length > 0 ? 0 : 10 + wrappedStyle.border = selected || hovered ? "1px solid rgba(255,255,255,0.3)" : "" + + var methodName = `${data.method}_${data.name}`; + if ((data.name !== undefined && data.name !== null && data.method !== undefined && data.method !== null ) && (data.method.toLowerCase() === "custom" || data.name.toLowerCase().startsWith(data.method.toLowerCase()))) { + methodName = data.name; + } + + const invalid_keys = [".", "(", ")", "'", ",", "[", "]"]; + methodName = methodName.toLowerCase().replaceAll(" ", "_").replaceAll("-", "_"); + + for (var key in invalid_keys) { + methodName = methodName.replaceAll(invalid_keys[key], ""); + } + + const parsedName = parseName(data.name, 35); + if (parsedName.length === 0) { + return null + } + + const actionLabels = foundCategory !== undefined && foundCategory !== null && foundCategory.name !== "Other" && foundCategory.action_labels.length > 0 ? ["No Label"].concat(foundCategory.action_labels) : [] + + var bgColor = "#61afee"; + if (data.method === "POST") { + bgColor = "#49cc90"; + } else if (data.method === "PUT") { + bgColor = "#fca130"; + } else if (data.method === "PATCH") { + bgColor = "#50e3c2"; + } else if (data.method === "DELETE") { + bgColor = "#f93e3e"; + } else if (data.method === "HEAD") { + bgColor = "#9012fe"; + } + + //console.log("Category: ", foundCategory, actionLabels) + + return ( + { + setHoveredItem(true); + }} + onMouseOut={() => { + setHoveredItem(false); + }} + > + {parsedName}

    {data.method} {urlPath}

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

    + + Apps +

    + + +

    {name}

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