Merge branch '2.0.0'

This commit is contained in:
Aditya
2024-12-05 05:20:50 +05:30
136 changed files with 45316 additions and 19511 deletions
+10 -4
View File
@@ -40,6 +40,7 @@ BACKEND_HOSTNAME=shuffle-backend
BACKEND_PORT=5001
FRONTEND_PORT=3001
FRONTEND_PORT_HTTPS=3443
AUTH_FOR_ORBORUS =
# CHANGE THIS IF YOU WANT GOOD LOCAL EXECUTIONS:
OUTER_HOSTNAME=shuffle-backend
@@ -51,8 +52,8 @@ HTTP_PROXY=
HTTPS_PROXY=
SHUFFLE_PASS_WORKER_PROXY=TRUE
SHUFFLE_PASS_APP_PROXY=TRUE
SHUFFLE_INTERNAL_HTTP_PROXY=NOPROXY
SHUFFLE_INTERNAL_HTTPS_PROXY=NOPROXY
SHUFFLE_INTERNAL_HTTP_PROXY=noproxy
SHUFFLE_INTERNAL_HTTPS_PROXY=noproxy
# Timezone-handler in Orborus, Worker and Apps
TZ=Europe/Amsterdam
# Used to FIND the containername. cgroup v2: issue 501
@@ -68,6 +69,10 @@ IS_KUBERNETES=false
SHUFFLE_BASE_IMAGE_REPOSITORY=frikky
#SHUFFLE_BASE_IMAGE_TAG_SUFFIX="-1.4.0"
# For environments using their own docker registry
# where they don't want to update http, subflow and shuffle tools again
SHUFFLE_USE_GCHR_OVERRIDE_FOR_AUTODEPLOY=true
# The eth0 interface inside a container corresponds
# to the virtual Ethernet interface that connects
# the container to the docker0
@@ -97,14 +102,15 @@ SHUFFLE_MAX_EXECUTION_DEPTH=
DATASTORE_EMULATOR_HOST=shuffle-database:8000
#SHUFFLE_OPENSEARCH_URL=http://shuffle-opensearch:9200
SHUFFLE_OPENSEARCH_URL=https://shuffle-opensearch:9200
SHUFFLE_OPENSEARCH_USERNAME="admin"
SHUFFLE_OPENSEARCH_PASSWORD="StrongShufflePassword321!"
SHUFFLE_OPENSEARCH_CERTIFICATE_FILE=
SHUFFLE_OPENSEARCH_APIKEY=
SHUFFLE_OPENSEARCH_CLOUDID=
SHUFFLE_OPENSEARCH_PROXY=
SHUFFLE_OPENSEARCH_INDEX_PREFIX=
SHUFFLE_OPENSEARCH_SKIPSSL_VERIFY=true
SHUFFLE_OPENSEARCH_USERNAME="admin"
SHUFFLE_OPENSEARCH_PASSWORD="StrongShufflePassword321!" # In use for the first time setup of OpenSearch + backend of Shuffle
OPENSEARCH_INITIAL_ADMIN_PASSWORD="StrongShufflePassword321!" # In use for the first time setup of OpenSearch
#Tenzir related
SHUFFLE_TENZIR_URL=
+4 -8
View File
@@ -19,23 +19,19 @@ jobs:
include:
- app: frontend
path: frontend
version: 1.4.2
version: 2.0.0
experimental: true
- app: backend
path: backend
version: 1.4.2
experimental: true
- app: app_sdk
path: backend/app_sdk
version: 1.4.2
version: nightly
experimental: true
- app: orborus
path: functions/onprem/orborus
version: 1.4.2
version: 2.0.0
experimental: true
- app: worker
path: functions/onprem/worker
version: 1.4.2
version: 2.0.0
experimental: true
steps:
- name: Checkout
+85
View File
@@ -0,0 +1,85 @@
name: Nightly Release
on:
release:
types: [published]
branches:
- 2.0.0
jobs:
main:
runs-on: ubuntu-latest
continue-on-error: ${{ matrix.experimental }}
strategy:
fail-fast: false
matrix:
include:
- app: frontend
path: frontend
experimental: true
- app: backend
path: backend
experimental: true
- app: app_sdk
path: backend/app_sdk
experimental: true
- app: orborus
path: functions/onprem/orborus
experimental: true
- app: worker
path: functions/onprem/worker
experimental: true
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set version
id: set_version
run: |
if [[ ${{ github.event_name }} == 'release' ]]; then
echo "VERSION=${{ github.event.release.tag_name }}" >> $GITHUB_OUTPUT
else
echo "VERSION=nightly-untagged-latest" >> $GITHUB_OUTPUT
fi
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
with:
platforms: "amd64,arm64,arm"
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to Ghcr
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Ghcr Build and push
id: docker_build
uses: docker/build-push-action@v4
env:
BUILDX_NO_DEFAULT_LOAD: true
with:
logout: false
context: ${{ matrix.path }}/
file: ${{ matrix.path }}/Dockerfile
platforms: linux/amd64,linux/arm64
push: true
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache
tags: |
ghcr.io/shuffle/shuffle-${{ matrix.app }}:${{ steps.set_version.outputs.VERSION }}
${{ secrets.DOCKERHUB_USERNAME }}/shuffle-${{ matrix.app }}:${{ steps.set_version.outputs.VERSION }}
frikky/shuffle-${{ matrix.app }}:${{ steps.set_version.outputs.VERSION }}
frikky/shuffle:${{ matrix.app }}
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}
-13
View File
@@ -1,13 +0,0 @@
on:
push:
branches:
- launch
name: release-please
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: google-github-actions/release-please-action@v3
with:
release-type: node
package-name: release-please-action
-51
View File
@@ -1,51 +0,0 @@
# This is a basic workflow to help you get started with Actions
name: App SDK upload
# Controls when the workflow will run
on:
# Triggers the workflow on push or pull request events but only for the main branch
push:
branches: [ master, launch ]
# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
build:
# The type of runner that the job will run on
runs-on: ubuntu-latest
# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v3
- id: 'auth'
name: 'Authenticate to Google Cloud'
uses: 'google-github-actions/auth@v0'
with:
credentials_json: '${{ secrets.SANDBOX_CREDENTIALS }}'
- id: 'upload_sdk'
name: Cloud Storage Uploader
uses: google-github-actions/upload-cloud-storage@v0.9.0
with:
path: 'backend/app_sdk/app_base.py'
destination: 'shuffle-sandbox-337810.appspot.com/generated_apps/baseline'
- id: 'upload_requirement'
name: Cloud Storage Uploader
uses: google-github-actions/upload-cloud-storage@v0.9.0
with:
path: 'backend/app_sdk/requirements.txt'
destination: 'shuffle-sandbox-337810.appspot.com/generated_apps/baseline'
- id: 'upload_Dockerfile'
name: Cloud Storage Uploader
uses: google-github-actions/upload-cloud-storage@v0.9.0
with:
path: 'backend/app_sdk/Dockerfile'
destination: 'shuffle-sandbox-337810.appspot.com/generated_apps/baseline'
+8 -1
View File
@@ -4,15 +4,22 @@
Shuffle Automation
[![CodeQL](https://github.com/Shuffle/Shuffle/actions/workflows/codeql-analysis.yml/badge.svg?branch=launch)](https://github.com/Shuffle/Shuffle/actions/workflows/codeql-analysis.yml)
[![Autobuild](https://github.com/Shuffle/Shuffle/actions/workflows/dockerbuild.yaml/badge.svg?branch=launch)](https://github.com/Shuffle/Shuffle/actions/workflows/dockerbuild.yaml)
[![Deploy to AWS](https://d1.awsstatic.com/cloudformation-deploy-to-aws-button.png)](https://console.aws.amazon.com/cloudformation/home?#/stacks/new?stackName=Shuffle-Instance&templateURL=https://shuffle-public-amis.s3.eu-north-1.amazonaws.com/template.yaml)
</h1><h4 align="center">
[Shuffle](https://shuffler.io) is an open source automation platform, built for and by the security professionals. Security operations is complex, but it doesn't have to be. Built to work well with MSSP's and other service providers in mind.
[ Get training ](https://shuffler.io/training)
[_Key Features_](https://shuffler.io/docs/features) —
[_Community & Support_](https://discord.gg/B2CBzUm) —
[ Get training ](https://shuffler.io/training) -
[_Documentation_](https://shuffler.io/docs) —
[_Getting Started_](https://shuffler.io/docs/getting_started)
[_Getting Started_](https://shuffler.io/docs/getting_started)
[_Development_](https://github.com/shuffle/Shuffle/blob/master/.github/CONTRIBUTING.md)
[ Set up a demo call ](https://shuffler.io/contact)
Follow us on Twitter at [@shuffleio](https://twitter.com/shuffleio).
+1 -1
View File
@@ -11,7 +11,7 @@ ADD ./go-app/docker.go /app
ADD ./go-app/go.mod /app
# Required files for code generation
ADD ./app_sdk/app_base.py /app_sdk
RUN wget -O /app_sdk/app_base.py https://raw.githubusercontent.com/Shuffle/app_sdk/refs/heads/main/shuffle_sdk/shuffle_sdk.py
ADD ./app_gen /app_gen
RUN go get -v
-21
View File
@@ -1,21 +0,0 @@
#FROM python:3.9.1-alpine as base
FROM python:3.10.0-alpine as base
#FROM python:3.11.3-alpine as base
FROM base as builder
RUN apk --no-cache add --update alpine-sdk libffi libffi-dev musl-dev openssl-dev tzdata coreutils
RUN mkdir /install
WORKDIR /install
FROM base
#--no-cache
RUN apk update && apk add --update tzdata libmagic alpine-sdk libffi libffi-dev musl-dev openssl-dev coreutils
COPY --from=builder /install /usr/local
COPY requirements.txt /requirements.txt
RUN pip3 install -r /requirements.txt
COPY __init__.py /app/walkoff_app_sdk/__init__.py
COPY app_base.py /app/walkoff_app_sdk/app_base.py
-42
View File
@@ -1,42 +0,0 @@
FROM python:3.10.0-alpine as base
FROM base as builder
RUN apk --no-cache add --update \
alpine-sdk \
build-base \
g++ \
gcc \
libffi \
libffi-dev \
libstdc++ \
linux-headers \
musl-dev \
openssl-dev \
tzdata \
coreutils
RUN pip install --upgrade pip && \
pip install --prefix="/install" --no-cache-dir grpcio grpcio-tools && \
apk del --purge \
g++ \
gcc \
musl-dev \
libffi-dev \
libstdc++ \
build-base \
linux-headers
RUN mkdir -p /install
WORKDIR /install
FROM base
#--no-cache
RUN apk update && apk add --update tzdata libmagic alpine-sdk libffi libffi-dev musl-dev openssl-dev coreutils
COPY --from=builder /install /usr/local
COPY requirements.txt /requirements.txt
RUN pip3 install -r /requirements.txt
COPY __init__.py /app/walkoff_app_sdk/__init__.py
COPY app_base.py /app/walkoff_app_sdk/app_base.py
-19
View File
@@ -1,19 +0,0 @@
FROM blackarchlinux/blackarch as base
FROM base as builder
RUN /bin/pacman -Syu --noconfirm
RUN /bin/pacman -Sy --noconfirm base-devel libffi musl openssl python python-pip -y
RUN mkdir /install
WORKDIR /install
COPY requirements.txt /requirements.txt
RUN pip install --prefix="/install" -r /requirements.txt
FROM base
COPY --from=builder /install /usr/local
COPY __init__.py /app/walkoff_app_sdk/__init__.py
COPY app_base.py /app/walkoff_app_sdk/app_base.py
-19
View File
@@ -1,19 +0,0 @@
FROM kalilinux/kali-rolling as base
FROM base as builder
RUN apt-get update
RUN apt-get dist-upgrade -y
RUN apt install build-essential libffi-dev musl-dev openssl python3 python3-pip -y
RUN mkdir /install
WORKDIR /install
COPY requirements.txt /requirements.txt
RUN pip install --prefix="/install" -r /requirements.txt
FROM base
COPY --from=builder /install /usr/local
COPY __init__.py /app/walkoff_app_sdk/__init__.py
COPY app_base.py /app/walkoff_app_sdk/app_base.py
-22
View File
@@ -1,22 +0,0 @@
FROM ubuntu as base
FROM base as builder
RUN apt-get update
RUN apt-get dist-upgrade -y
RUN apt install build-essential libffi-dev musl-dev openssl python3 python3-pip -y
RUN mkdir /install
WORKDIR /install
COPY requirements.txt /requirements.txt
RUN pip install --prefix="/install" -r /requirements.txt
FROM base
RUN apt-get update
RUN apt-get dist-upgrade -y
RUN apt install build-essential libffi-dev musl-dev openssl python3 python3-pip -y
COPY --from=builder /install /usr/local
COPY __init__.py /app/walkoff_app_sdk/__init__.py
COPY app_base.py /app/walkoff_app_sdk/app_base.py
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2020 Frikkylikeme
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Executable → Regular
+2 -22
View File
@@ -1,22 +1,2 @@
# app_sdk.py
This is the SDK used for apps to behave like they should.
## If you want to update apps.. PS: downloads from docker hub do overrides.. :)
1. Write your code & check if runtime works
2. Build app_base image
3. docker rm $(docker ps -aq) # Remove all stopped containers
4. Delete the specific app's Docker image (docker rmi frikky/shuffle:...)
5. Rebuild the Docker image (click load in GUI?)
## Cloud updates
1. Go to shuffle cloud on GCP
2. Go to Cloud Storage
3. Find shuffler.appspot.com
4. Navigate to generated_apps/baseline
5. Update SDK there. This will make all new apps run with the new SDK
## Cloud app force-updates
1. Run the "stitcher.go" program in the public shuffle-shared repository.
# LICENSE
Everything in here is MIT, not AGPLv3 as indicated by the license.
## CHANGES
In November 2024, we moved this to its own repistory: https://github.com/shuffle/app_sdk
View File
File diff suppressed because it is too large Load Diff
-187
View File
@@ -1,187 +0,0 @@
import re
import json
input_data = """{
"test4": $test,
"test5": ,
"test6": "what"
}
"""
input_data = """{
"test0": {{ '' | default: [] }},
"test": {{ | default: [] }},
"test2": {{ $test.asd | default: [] }},
"test3": {{ {"key": "val} | default: [] }},
"test4": $test,
"test5": ,
"test6": "what"
}
"""
liquiddata = "{{ $test.asd | some other stuff {{ $test.xyz | more stuff"
pattern = r'\{\{\s*\$[^|}]+\s*\|'
replaced_data = re.sub(pattern, "{{ '' |", liquiddata)
print(replaced_data)
def patternfix_string(liquiddata, patterns, regex_patterns, inputtype="liquid"):
if not inputtype or inputtype == "liquid":
if "{{" not in liquiddata or "}}" not in liquiddata:
return liquiddata
elif inputtype == "json":
liquiddata = liquiddata.strip()
# Validating if it looks like json or not
if liquiddata[0] == "{" and liquiddata[len(liquiddata)-1] == "}":
pass
else:
if liquiddata[0] == "[" and liquiddata[len(liquiddata)-1] == "]":
pass
else:
return liquiddata
# If it's already json, don't touch it
try:
json.loads(liquiddata)
return liquiddata
except Exception as e:
pass
else:
print("No replace handler for %s" % inputtype)
return liquiddata
skipkeys = [" "]
newoutput = liquiddata[:]
for pattern in patterns:
keylocations = []
parsedvalue = ""
record = False
index = -1
for key in liquiddata:
# Return instant if possible
if inputtype == "json":
try:
json.loads(newoutput)
return newoutput
except:
pass
index += 1
if not key:
if record:
keylocations.append(index)
parsedvalue += key
continue
if key in skipkeys:
if record:
keylocations.append(index)
parsedvalue += key
continue
if key == pattern[0] and not record:
record = True
if key not in pattern:
keylocations = []
parsedvalue = ""
record = False
if record:
keylocations.append(index)
parsedvalue += key
if len(parsedvalue) == 0:
continue
evaluated_value = parsedvalue[:]
for skipkey in skipkeys:
evaluated_value = "".join(evaluated_value.split(skipkey))
if evaluated_value == pattern:
#print("Found matching: %s (%s)" % (parsedvalue, keylocations))
#print("Should replace with: %s" % patterns[pattern])
newoutput = newoutput.replace(parsedvalue, patterns[pattern], -1)
# Return instant if possible
if inputtype == "json":
try:
json.loads(newoutput)
return newoutput
except:
pass
for pattern in regex_patterns:
newlines = []
for line in newoutput.split("\n"):
replaced_line = re.sub(pattern, regex_patterns[pattern], line)
newlines.append(replaced_line)
newoutput = "\n".join(newlines)
# Return instant if possible
if inputtype == "json":
try:
json.loads(newoutput)
return newoutput
except:
pass
# Dont return json properly unless actually json
if inputtype == "json":
try:
json.loads(newoutput)
return newoutput
except:
# Returns original if json fixing didn't work
return liquiddata
return newoutput
print("Start:\n%s" % input_data)
try:
newinput = patternfix_string(input_data,
{
"{{|": '{{ "" |',
},
{
#r'\{\{\s*|': "{{ '' |",
r'\{\{\s*\$[^|}]+\s*\|': '{{ "" |',
}
,
inputtype="liquid"
)
except Exception as e:
print("[ERROR} Failed liquid parsing fix: %s" % e)
newinput = input_data
try:
newinput = patternfix_string(newinput,
{
},
{
r'\"\s*\:\s*,': '\": "",',
r'\"\s*\:\s*\$[^,]+\w*\,': '\": "",',
}
,
inputtype="json"
)
try:
json.loads(newinput)
print("It's json! Override.")
except Exception as e:
print("Bad json. DONT use the value at all: %s" % e)
except Exception as e:
print("[ERROR} Failed json parsing fix: %s" % e)
print("\nEnd:\n%s" % newinput)
-51
View File
@@ -1,51 +0,0 @@
#!/bin/bash
### DEFAULT
NAME=shuffle-app_sdk
VERSION=1.2.0
docker rmi docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION --force
docker build . -f Dockerfile -t frikky/shuffle:app_sdk -t frikky/$NAME:$VERSION -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION -t ghcr.io/frikky/$NAME:nightly -t shuffle/shuffle:app_sdk -t shuffle/$NAME:$VERSION -t docker.pkg.github.com/shuffle/shuffle/$NAME:$VERSION -t ghcr.io/shuffle/$NAME:$VERSION -t ghcr.io/shuffle/$NAME:nightly
docker push frikky/shuffle:app_sdk
docker push ghcr.io/frikky/$NAME:$VERSION
docker push ghcr.io/frikky/$NAME:nightly
docker push ghcr.io/frikky/$NAME:latest
docker push shuffle/shuffle:app_sdk
docker push ghcr.io/shuffle/$NAME:$VERSION
docker push ghcr.io/shuffle/$NAME:nightly
docker push ghcr.io/shuffle/$NAME:latest
#### UBUNTU
NAME=shuffle-app_sdk_ubuntu
docker build . -f Dockerfile_ubuntu -t frikky/shuffle:app_sdk_ubuntu -t frikky/$NAME:$VERSION -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION
docker push frikky/shuffle:app_sdk_ubuntu
docker push ghcr.io/frikky/$NAME:$VERSION
#### Alpine GRPC
NAME=shuffle-app_sdk_grpc
docker build . -f Dockerfile_alpine_grpc -t frikky/shuffle:app_sdk_grpc -t frikky/$NAME:$VERSION -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION
docker push frikky/shuffle:app_sdk_grpc
docker push ghcr.io/frikky/$NAME:$VERSION
#### KALI ###
#NAME=shuffle-app_sdk_kali
#docker build . -f Dockerfile_kali -t frikky/shuffle:app_sdk_kali -t frikky/$NAME:$VERSION -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION
#
#docker push frikky/shuffle:app_sdk_kali
#docker push ghcr.io/frikky/$NAME:$VERSION
#docker push ghcr.io/frikky/$NAME:nightly
### BLACKARCH ###
#NAME=shuffle-app_sdk_blackarch
#docker build . -f Dockerfile_blackarch -t frikky/shuffle:app_sdk_blackarch -t frikky/$NAME:$VERSION -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION
#
#docker push frikky/shuffle:app_sdk_blackarch
#docker push ghcr.io/frikky/$NAME:$VERSION
#docker push ghcr.io/frikky/$NAME:nightly
-326
View File
@@ -1,326 +0,0 @@
## A test script of the recurse_json function
## to validate that it can handle the different types of data
## and follow the dot formation format
import re
import json
def recurse_json(basejson, parsersplit):
match = "#([0-9a-z]+):?-?([0-9a-z]+)?#?"
try:
outercnt = 0
# Loops over split values
splitcnt = -1
for value in parsersplit:
splitcnt += 1
#if " " in value:
# value = value.replace(" ", "_", -1)
actualitem = re.findall(match, value, re.MULTILINE)
# Goes here if loop
if value == "#":
newvalue = []
if basejson == None:
return "", False
for innervalue in basejson:
# 1. Check the next item (message)
# 2. Call this function again
try:
ret, is_loop = recurse_json(innervalue, parsersplit[outercnt+1:])
except IndexError:
# Only in here if it's the last loop without anything in it?
ret, is_loop = recurse_json(innervalue, parsersplit[outercnt:])
newvalue.append(ret)
# Magical way of returning which makes app sdk identify
# it as multi execution
return newvalue, True
# Checks specific regex like #1-2 for index 1-2 in a loop
elif len(actualitem) > 0:
is_loop = True
newvalue = []
firstitem = actualitem[0][0]
seconditem = actualitem[0][1]
if isinstance(firstitem, int):
firstitem = str(firstitem)
if isinstance(seconditem, int):
seconditem = str(seconditem)
#print("[DEBUG] ACTUAL PARSED: %s" % actualitem)
# Means it's a single item -> continue
if seconditem == "":
#print("[INFO] In first - handling %s. Len: %d" % (firstitem, len(basejson)))
if str(firstitem).lower() == "max" or str(firstitem).lower() == "last" or str(firstitem).lower() == "end":
firstitem = len(basejson)-1
elif str(firstitem).lower() == "min" or str(firstitem).lower() == "first":
firstitem = 0
else:
firstitem = int(firstitem)
#print(f"[DEBUG] Post lower checks with item {firstitem}")
tmpitem = basejson[int(firstitem)]
try:
newvalue, is_loop = recurse_json(tmpitem, parsersplit[outercnt+1:])
except IndexError:
newvalue, is_loop = (tmpitem, parsersplit[outercnt+1:])
else:
#print("[INFO] In ELSE - handling %s and %s" % (firstitem, seconditem))
if isinstance(firstitem, str):
if firstitem.lower() == "max" or firstitem.lower() == "last" or firstitem.lower() == "end":
firstitem = len(basejson)-1
elif firstitem.lower() == "min" or firstitem.lower() == "first":
firstitem = 0
else:
firstitem = int(firstitem)
else:
firstitem = int(firstitem)
if isinstance(seconditem, str):
if str(seconditem).lower() == "max" or str(seconditem).lower() == "last" or str(firstitem).lower() == "end":
seconditem = len(basejson)-1
elif str(seconditem).lower() == "min" or str(seconditem).lower() == "first":
seconditem = 0
else:
seconditem = int(seconditem)
else:
seconditem = int(seconditem)
#print(f"[DEBUG] Post lower checks 2: {firstitem} AND {seconditem}")
newvalue = []
if int(seconditem) > len(basejson):
seconditem = len(basejson)
for i in range(int(firstitem), int(seconditem)+1):
# 1. Check the next item (message)
# 2. Call this function again
try:
ret, tmp_loop = recurse_json(basejson[i], parsersplit[outercnt+1:])
except IndexError:
#print("[DEBUG] INDEXERROR (1): ", parsersplit[outercnt])
#ret = innervalue
ret, tmp_loop = recurse_json(basejson[i], parsersplit[outercnt:])
newvalue.append(ret)
return newvalue, is_loop
else:
if len(value) == 0:
return basejson, False
try:
if isinstance(basejson, list):
#print("[WARNING] VALUE IN ISINSTANCE IS NOT TO BE USED (list): %s" % value)
return basejson, False
elif isinstance(basejson, bool):
#print("[WARNING] VALUE IN ISINSTANCE IS NOT TO BE USED (bool): %s" % value)
return basejson, False
elif isinstance(basejson, int):
#print("[WARNING] VALUE IN ISINSTANCE IS NOT TO BE USED (int): %s" % value)
return basejson, False
elif isinstance(basejson[value], str):
try:
if (basejson[value].endswith("}") and basejson[value].endswith("}")) or (basejson[value].startswith("[") and basejson[value].endswith("]")):
basejson = json.loads(basejson[value])
else:
# Should we sanitize here?
#print("[DEBUG] VALUE TO SANITIZE FOR KEY '%s'?: %s" % (value, basejson[value]))
# Check if we are on the last item?
if outercnt == len(parsersplit)-1:
#print("[DEBUG] LAST KEY")
return str(basejson[value]), False
else:
#print("[DEBUG] NOT LAST KEY")
pass
except json.decoder.JSONDecodeError as e:
return str(basejson[value]), False
else:
basejson = basejson[value]
except KeyError as e:
print("[WARNING] Running secondary value check with replacement of underscore in %s: %s" % (value, e))
if "_" in value:
value = value.replace("_", " ", -1)
elif " " in value:
value = value.replace(" ", "_", -1)
try:
if isinstance(basejson, list):
#print("[WARNING] VALUE IN ISINSTANCE IS NOT TO BE USED (list): %s" % value)
return basejson, False
elif isinstance(basejson, bool):
#print("[WARNING] VALUE IN ISINSTANCE IS NOT TO BE USED (bool): %s" % value)
return basejson, False
elif isinstance(basejson, int):
#print("[WARNING] VALUE IN ISINSTANCE IS NOT TO BE USED (int): %s" % value)
return basejson, False
elif isinstance(basejson[value], str):
#print(f"[INFO] LOADING STRING '%s' AS JSON" % basejson[value])
try:
#print("[DEBUG] BASEJSON: %s" % basejson)
if (basejson[value].endswith("}") and basejson[value].endswith("}")) or (basejson[value].startswith("[") and basejson[value].endswith("]")):
basejson = json.loads(basejson[value])
else:
if outercnt == len(parsersplit)-1:
#print("LAST KEY (2)")
return str(basejson[value]), False
else:
#print("NOT LAST KEY (2)")
pass
except json.decoder.JSONDecodeError as e:
#print("[DEBUG] RETURNING BECAUSE '%s' IS A NORMAL STRING (1)" % basejson[value])
return str(basejson[value]), False
else:
basejson = basejson[value]
except KeyError as e:
# Check if previous key was handled or not
previouskey = parsersplit[outercnt-1]
#print("[DEBUG] PREVIOUS KEY: ", previouskey)
tmpval = previouskey + "." + value
#print("\n\n[WARNING] Running third dot notation fix '%s' on data %s: %s" % (value, basejson, e))
if tmpval in basejson:
return basejson[tmpval], False
try:
currentsplitcnt = splitcnt
recursed_value = value
handled = False
#tmpbase = basejson
previouskey = value
while True:
#print("\n\n[DEBUG] CURRENTSPLITCNT: ", currentsplitcnt)
newvalue = parsersplit[currentsplitcnt+1]
if newvalue == "#" or newvalue == "":
break
recursed_value += "." + newvalue
#print("\n\nRECURSED: ", recursed_value)
found = False
for key, value in basejson.items():
if recursed_value.lower() in key.lower():
found = True
if found == False:
#print("[INFO] DIDN'T FIND similar VALUE: ", recursed_value)
# Check if we are on the last key or not
return "", False
#if outercnt == len(parsersplit)-1:
# print("[DEBUG] LAST KEY (3)")
# break
#else:
# print("[DEBUG] NOT LAST KEY (3)")
# return "", False
if recursed_value in basejson:
#print("[INFO] FOUND RECURSED VALUE: ", recursed_value)
basejson = basejson[recursed_value]
# Whether to dig deeper or not
if isinstance(basejson, bool) or isinstance(basejson, int) or isinstance(basejson, str):
handled = False
else:
handled = True
break
currentsplitcnt += 1
if handled:
continue
break
except IndexError as e:
print("[DEBUG] INDEXERROR (2):", parsersplit[outercnt])
return "", False
outercnt += 1
except KeyError as e:
print("[INFO] Lower keyerror: %s" % e)
return "", False
except Exception as e:
print("[WARNING] Exception: %s" % e)
return "", False
return basejson, False
print("[INFO] Starting")
#input_data = "test"
#input_data = "test2.data"
# Matchwith
basejson = {
"test": "hello",
"test2": {
"test3": "hello2",
"test3.data": "hello3",
"test4.data.testing": {
"value": "hello4"
},
"test5.data.hello": "wut",
},
"test3": ["hello", "hello2", "hello3"],
"test4": [{
"id": "1",
}]
}
# Inputexamples (ALL should be True)
inputs = {
#"": "",
"badkey": "",
"test": "hello",
"test2.badkey": "",
"test2.test3": "hello2",
"test2.test3.data": "hello3",
"test2.test4.data.testing": "{'value': 'hello4'}", # FIXME: Doesn't work due to break vs return "", False in last exception
"test2.test4.data.testing.value": "hello4", # FIXME: Doesn't work due to break vs return "", False in last exception. Not fixed as we didn't find one of these yet.
"test2.test5.data.hello": "wut",
"test2.test5.data.badkey": "",
"test3.#1": "hello2",
"test4.#0.id": "1",
"test4.#1.id": "",
}
outputs = []
for key, value in inputs.items():
parsersplit = key.split(".")
ret, is_loop = recurse_json(basejson, parsersplit)
print("\n\nOUTPUT RET (%s): %s" % (key, ret))
outputs.append("[%s]: %s = '%s' vs '%s'" % (str(ret) == str(value), key, ret, value))
print("\n\n%s" % "\n".join(outputs))
#input_data = ""
#input_data = "badkey"
#input_data = "test"
#input_data = "test2.data"
#input_data = "test2.test3.data"
#input_data = "test2.test4.data.testing.value.as"
#input_data = "test2.test5.data.hello"
+1
View File
@@ -6,3 +6,4 @@ flask[async]==2.0.2
waitress==2.1.0
#flask==1.1.2
python-dateutil==2.8.1
+16 -9
View File
@@ -211,6 +211,7 @@ func fixTags(tags []string) []string {
func buildImageMemory(fs billy.Filesystem, tags []string, dockerfileFolder string, downloadIfFail bool) error {
ctx := context.Background()
client, err := client.NewEnvClient()
defer client.Close()
if err != nil {
log.Printf("Unable to create docker client: %s", err)
return err
@@ -349,7 +350,7 @@ func deleteJob(client *kubernetes.Clientset, jobName, namespace string) error {
})
}
func buildImage(tags []string, dockerfileFolder string) error {
func buildImage(tags []string, dockerfileLocation string) error {
isKubernetes := false
if os.Getenv("IS_KUBERNETES") == "true" {
@@ -369,10 +370,8 @@ func buildImage(tags []string, dockerfileFolder string) error {
log.Printf("[INFO] registry name: %s", registryName)
contextDir := strings.Replace(dockerfileFolder, "Dockerfile", "", -1)
contextDir = "/app/" + contextDir
contextDir := filepath.Join("/app/", filepath.Dir(dockerfileLocation))
log.Print("contextDir: ", contextDir)
dockerFile := "./Dockerfile"
client, err := getK8sClient()
if err != nil {
@@ -407,7 +406,7 @@ func buildImage(tags []string, dockerfileFolder string) error {
Image: "gcr.io/kaniko-project/executor:latest",
Args: []string{
"--verbosity=debug",
"--dockerfile=" + dockerFile,
"--dockerfile=Dockerfile",
"--context=dir://" + contextDir,
"--skip-tls-verify",
"--destination=" + registryName + "/" + tags[1],
@@ -420,9 +419,7 @@ func buildImage(tags []string, dockerfileFolder string) error {
},
},
},
NodeSelector: map[string]string{
"node": backendNodeName,
},
NodeName: backendNodeName,
RestartPolicy: corev1.RestartPolicyNever,
Volumes: []corev1.Volume{
{
@@ -480,13 +477,14 @@ func buildImage(tags []string, dockerfileFolder string) error {
ctx := context.Background()
client, err := client.NewEnvClient()
defer client.Close()
if err != nil {
log.Printf("Unable to create docker client: %s", err)
return err
}
log.Printf("[INFO] Docker Tags: %s", tags)
dockerfileSplit := strings.Split(dockerfileFolder, "/")
dockerfileSplit := strings.Split(dockerfileLocation, "/")
// Create a buffer
buf := new(bytes.Buffer)
@@ -836,14 +834,23 @@ func handleRemoteDownloadApp(resp http.ResponseWriter, ctx context.Context, user
type tmpapp struct {
Success bool `json:"success"`
OpenAPI string `json:"openapi"`
App string `json:"app"`
}
app := tmpapp{}
err := json.Unmarshal(respBody, &app)
if err != nil || app.Success == false || len(app.OpenAPI) == 0 {
log.Printf("[ERROR] Failed app unmarshal during auto-download. Success: %#v. Applength: %d: %s", app.Success, len(app.OpenAPI), err)
resp.WriteHeader(401)
if len(app.App) > 0 {
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Not an OpenAPI app, but a Python app. Please download the app using the Remote Download system: https://shuffler.io/docs/apps#importing-remote-apps"}`)))
} else {
resp.Write([]byte(`{"success": false, "reason": "App doesn't exist"}`))
}
resp.Write([]byte(`{"success": false, "reason": "App doesn't exist"}`))
return
}
+2 -2
View File
@@ -2,7 +2,7 @@ module shuffle
go 1.22.0
// replace github.com/shuffle/shuffle-shared => ../../../shuffle-shared
//replace github.com/shuffle/shuffle-shared => ../../../shuffle-shared
toolchain go1.22.2
@@ -20,7 +20,7 @@ require (
github.com/gorilla/mux v1.8.1
github.com/h2non/filetype v1.1.3
github.com/satori/go.uuid v1.2.0
github.com/shuffle/shuffle-shared v0.6.50
github.com/shuffle/shuffle-shared v0.6.90
golang.org/x/crypto v0.22.0
google.golang.org/api v0.176.1
google.golang.org/grpc v1.63.2
+4 -2
View File
@@ -334,8 +334,10 @@ github.com/sendgrid/sendgrid-go v3.14.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdR
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/shuffle/shuffle-shared v0.6.50 h1:MBeGAiBNkw9Eg+3YTJIlBOuskWntGvT0uefFUYOBhbY=
github.com/shuffle/shuffle-shared v0.6.50/go.mod h1:RAJiSFjmuKmijKTbbEf9A6Ojb+3/te7g71lED7JjPus=
github.com/shuffle/shuffle-shared v0.6.77 h1:KKtM50xW2DLuRHINxhp3uXrNH0AhiwkeiiU93a8fB3A=
github.com/shuffle/shuffle-shared v0.6.77/go.mod h1:RAJiSFjmuKmijKTbbEf9A6Ojb+3/te7g71lED7JjPus=
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=
+202 -71
View File
@@ -35,6 +35,7 @@ import (
"github.com/go-git/go-billy/v5/memfs"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
gitProxy "github.com/go-git/go-git/v5/plumbing/transport"
"github.com/go-git/go-git/v5/storage/memory"
// Random
@@ -256,7 +257,6 @@ type Hook struct {
Environment string `json:"environment" datastore:"environment"`
}
func GetUsersHandler(w http.ResponseWriter, r *http.Request) {
data := map[string]interface{}{
"id": "12345",
@@ -396,6 +396,52 @@ func checkUsername(Username string) error {
return nil
}
func isGitNoProxy(rawURL string) bool {
noProxy := os.Getenv("NO_PROXY")
if noProxy == "" {
return false
}
if noProxy == "*" {
return true
}
noProxyList := strings.Split(noProxy, ",")
parsedURL, err := url.Parse(rawURL)
if err != nil {
return false
}
host := parsedURL.Hostname()
for _, value := range noProxyList {
value = strings.TrimSpace(value)
if host == value {
return true
}
if strings.HasPrefix(value, "*.") && strings.HasSuffix(host, value[2:]) {
return true
}
}
return false
}
func checkGitProxy(cloneOptions *git.CloneOptions) *git.CloneOptions {
if os.Getenv("HTTP_PROXY") != "" && !isGitNoProxy(cloneOptions.URL) {
cloneOptions.ProxyOptions = gitProxy.ProxyOptions{
URL: os.Getenv("HTTP_PROXY"),
}
}
if os.Getenv("HTTPS_PROXY") != "" && !isGitNoProxy(cloneOptions.URL) {
cloneOptions.ProxyOptions = gitProxy.ProxyOptions{
URL: os.Getenv("HTTPS_PROXY"),
}
}
return cloneOptions
}
func createNewUser(username, password, role, apikey string, org shuffle.OrgMini) error {
// Returns false if there is an issue
// Use this for register
@@ -450,6 +496,7 @@ func createNewUser(username, password, role, apikey string, org shuffle.OrgMini)
newUser.ActiveOrg = shuffle.OrgMini{
Id: org.Id,
Name: org.Name,
Role: newUser.Role,
}
if len(apikey) > 0 {
@@ -511,7 +558,6 @@ func createNewUser(username, password, role, apikey string, org shuffle.OrgMini)
}
}
return nil
}
@@ -615,7 +661,7 @@ func handleRegister(resp http.ResponseWriter, request *http.Request) {
Name: newOrg.Name,
}
user.ActiveOrg = currentOrg
user.ActiveOrg = currentOrg
}
}
}
@@ -884,18 +930,58 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) {
log.Printf("[DEBUG] Failed to get org during getinfo: %s", err)
}
//if err == nil {
if len(org.Id) > 0 {
if userInfo.Role == "" {
//err = shuffle.SetUser(ctx, &userInfo, false)
for _, user := range org.Users {
if user.Id != userInfo.Id {
continue
}
userInfo.ActiveOrg.Role = user.Role
}
}
userInfo.ActiveOrg = shuffle.OrgMini{
Id: org.Id,
Name: org.Name,
CreatorOrg: org.CreatorOrg,
ChildOrgs: org.ChildOrgs,
Role: userInfo.ActiveOrg.Role,
Image: org.Image,
}
if parsedAdmin == "false" {
// Validating admin user again just to make sure
// This is to avoid issues for the first org ever
for _, user := range org.Users {
if user.Id != userInfo.Id {
continue
}
if user.Role == "admin" {
break
}
}
}
}
//}
orgPriorities := org.Priorities
if len(org.Priorities) < 10 {
//log.Printf("[WARNING] Should find and add priorities as length is less than 10 for org %s", userInfo.ActiveOrg.Id)
newPriorities, err := shuffle.GetPriorities(ctx, userInfo, org)
if err != nil {
log.Printf("[WARNING] Failed getting new priorities for org %s: %s", org.Id, err)
//orgPriorities = []shuffle.Priority{}
} else {
orgPriorities = newPriorities
// A way to manage them over time
}
}
orgInterests := org.Interests
userInfo.ActiveOrg.Users = []shuffle.UserMini{}
userOrgs := []shuffle.OrgMini{}
@@ -942,19 +1028,6 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) {
}
userOrgs = shuffle.SortOrgList(userOrgs)
orgPriorities := org.Priorities
if len(org.Priorities) < 10 {
//log.Printf("[WARNING] Should find and add priorities as length is less than 10 for org %s", userInfo.ActiveOrg.Id)
newPriorities, err := shuffle.GetPriorities(ctx, userInfo, org)
if err != nil {
log.Printf("[WARNING] Failed getting new priorities for org %s: %s", org.Id, err)
//orgPriorities = []shuffle.Priority{}
} else {
orgPriorities = newPriorities
// A way to manage them over time
}
}
tutorialsFinished := []shuffle.Tutorial{}
for _, tutorial := range userInfo.PersonalInfo.Tutorials {
@@ -993,8 +1066,9 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) {
ChatDisabled: chatDisabled,
Tutorials: tutorialsFinished,
Interests: orgInterests,
Priorities: orgPriorities,
Licensed: licensed,
Licensed: licensed,
}
returnData, err := json.Marshal(returnValue)
@@ -1015,7 +1089,6 @@ type passwordReset struct {
Reference string `json:"reference"`
}
func checkAdminLogin(resp http.ResponseWriter, request *http.Request) {
cors := shuffle.HandleCors(resp, request)
if cors {
@@ -1054,9 +1127,9 @@ func checkAdminLogin(resp http.ResponseWriter, request *http.Request) {
}
// No childorg setup, only parent org
if len(org.ManagerOrgs) > 0 || len(org.CreatorOrg) > 0 {
continue
}
// if len(org.ManagerOrgs) > 0 || len(org.CreatorOrg) > 0 {
// continue
// }
// Should run calculations
if len(org.SSOConfig.OpenIdAuthorization) > 0 {
@@ -1978,7 +2051,6 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) {
}
func handlePipelineCallback(resp http.ResponseWriter, request *http.Request) {
if request.Method != "POST" {
request.Method = "POST"
}
@@ -1999,7 +2071,7 @@ func handlePipelineCallback(resp http.ResponseWriter, request *http.Request) {
location := strings.Split(request.URL.String(), "/")
var pipelineId string
if location[1] == "api" {
if len(location) <= 4 {
log.Printf("[INFO] Couldn't handle location. Too short in pipeline: %d", len(location))
@@ -2013,7 +2085,7 @@ func handlePipelineCallback(resp http.ResponseWriter, request *http.Request) {
userAgent := request.Header.Get("User-Agent")
if strings.Contains(strings.ToLower(userAgent), "microsoftpreview") || strings.Contains(strings.ToLower(userAgent), "googlebot") {
log.Printf("[AUDIT] Blocking googlebot and microsoftbot for pielines. UA: '%s'", userAgent)
log.Printf("[AUDIT] Blocking googlebot and microsoftbot for pipelines. UA: '%s'", userAgent)
resp.WriteHeader(400)
resp.Write([]byte(`{"success": false, "reason": "Google/Microsoft preview bots not allowed. Please change the useragent."}`))
return
@@ -2058,11 +2130,27 @@ func handlePipelineCallback(resp http.ResponseWriter, request *http.Request) {
return
}
parsedBody := shuffle.GetExecutionbody(body)
// Parse concatenated JSON logs
jsonList, err := parseConcatenatedJSONLogs(string(body))
if err != nil {
log.Printf("[DEBUG] JSON parsing error: %s", err)
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false}`))
return
}
parsedBody, err := json.Marshal(jsonList)
if err != nil {
log.Printf("[ERROR] Failed to marshal jsonList: %s", err)
resp.WriteHeader(500)
resp.Write([]byte(`{"success": false}`))
return
}
newBody := shuffle.ExecutionStruct{
Start: pipeline.StartNode,
ExecutionSource: "pipeline",
ExecutionArgument: parsedBody,
ExecutionArgument: string(parsedBody),
}
workflow, err := shuffle.GetWorkflow(ctx, pipeline.WorkflowId)
@@ -2093,8 +2181,7 @@ func handlePipelineCallback(resp http.ResponseWriter, request *http.Request) {
}
if len(pipeline.StartNode) == 0 {
log.Printf("[WARNING] No start node for pipeline %s - running with workflow default.", pipeline.TriggerId)
log.Printf("[WARNING] No start node for pipeline %s - running with workflow default.")
}
newRequest := &http.Request{
@@ -2108,6 +2195,9 @@ func handlePipelineCallback(resp http.ResponseWriter, request *http.Request) {
if err == nil {
resp.WriteHeader(200)
resp.Write([]byte(fmt.Sprintf(`{"success": true, "execution_id": "%s"}`, workflowExecution.ExecutionId)))
// Track Sigma rules
trackSigmaRules(ctx, pipeline.OrgId, jsonList)
return
}
@@ -2115,6 +2205,42 @@ func handlePipelineCallback(resp http.ResponseWriter, request *http.Request) {
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, executionResp)))
}
func parseConcatenatedJSONLogs(logs string) ([]map[string]interface{}, error) {
var jsonList []map[string]interface{}
decoder := json.NewDecoder(strings.NewReader(logs))
for decoder.More() {
var jsonObject map[string]interface{}
if err := decoder.Decode(&jsonObject); err != nil {
log.Printf("[WARNING] JSON decoding error: %s. Skipping this object.", err)
continue
}
jsonList = append(jsonList, jsonObject)
}
if err := decoder.Decode(&struct{}{}); err != io.EOF {
return nil, fmt.Errorf("error after decoding all JSON objects: %v", err)
}
return jsonList, nil
}
func trackSigmaRules(ctx context.Context, orgId string, jsonList []map[string]interface{}) {
ruleCount := make(map[string]int)
for _, logEntry := range jsonList {
if rule, ok := logEntry["rule"].(map[string]interface{}); ok {
if ruleName, ok := rule["title"].(string); ok {
ruleCount[ruleName]++
}
}
}
for ruleName, count := range ruleCount {
shuffle.IncrementCache(ctx, orgId, ruleName, count)
log.Printf("[INFO] Rule %s incremented by %d", ruleName, count)
}
}
func executeCloudAction(action shuffle.CloudSyncJob, apikey string) error {
data, err := json.Marshal(action)
if err != nil {
@@ -3208,7 +3334,6 @@ func buildSwaggerApp(resp http.ResponseWriter, body []byte, user shuffle.User, s
}
}
log.Printf("[DEBUG] Successfully built app %s (%s)", api.Name, api.ID)
if len(user.Id) > 0 {
resp.WriteHeader(200)
@@ -3249,8 +3374,6 @@ func verifySwagger(resp http.ResponseWriter, request *http.Request) {
buildSwaggerApp(resp, body, user, false)
}
// Hotloads new apps from a folder
func handleAppHotload(ctx context.Context, location string, forceUpdate bool) error {
@@ -3597,11 +3720,10 @@ func remoteOrgJobController(org shuffle.Org, body []byte) error {
return nil
}
func remoteOrgJobHandler(org shuffle.Org, interval int) error {
// Check if it's 1 in 10 (10% chance random)
backupJob := shuffle.BackupJob{}
backupJob := shuffle.BackupJob{}
// Check if workflow backup is active
// Check if app backup is active
@@ -3647,7 +3769,6 @@ func remoteOrgJobHandler(org shuffle.Org, interval int) error {
backupJobData = []byte{}
}
syncUrl := fmt.Sprintf("%s/api/v1/cloud/sync", syncUrl)
client := shuffle.GetExternalClient(syncUrl)
req, err := http.NewRequest(
@@ -3809,7 +3930,7 @@ func runInitEs(ctx context.Context) {
}
if strings.Contains(os.Getenv("SHUFFLE_OPENSEARCH_URL"), "https") {
log.Printf("[INFO] Waiting during init to make sure the opensearch instance is up and running with security features properly")
log.Printf("[INFO] Waiting 30 seconds during init to make sure the opensearch instance is up and running with security features enabled")
time.Sleep(30 * time.Second)
}
@@ -3853,7 +3974,7 @@ func runInitEs(ctx context.Context) {
}
// FIXME: Add a randomized timer to avoid all schedules running at the same time
// Many are at 5 minutes / 1 hour. The point is to spread these out
// Many are at 5 minutes / 1 hour. The point is to spread these out
// a bit instead of all of them starting at the exact same time
//log.Printf("Schedule: %#v", schedule)
@@ -3888,22 +4009,32 @@ func runInitEs(ctx context.Context) {
log.Printf("[DEBUG] Creating org for default user %s", username)
orgId := uuid.NewV4().String()
orgSetupName := "default"
tmpOrg := shuffle.OrgMini{
Name: orgSetupName,
Id: orgId,
}
err = createNewUser(username, password, "admin", apikey, tmpOrg)
if err != nil {
log.Printf("[ERROR] Failed to create default user %s: %s", username, err)
} else {
log.Printf("[INFO] Successfully created user %s", username)
}
user, err := shuffle.GetUser(ctx, username)
newOrg := shuffle.Org{
Name: orgSetupName,
Id: orgId,
Org: orgSetupName,
Users: []shuffle.User{},
Users: []shuffle.User{*user},
Roles: []string{"admin", "user"},
CloudSync: false,
}
err = shuffle.SetOrg(ctx, newOrg, newOrg.Id)
setUsers := false
if err != nil {
log.Printf("[WARNING] Failed setting organization when creating original user: %s", err)
log.Printf("[ERROR] Failed setting organization when creating original user: %s", err)
} else {
log.Printf("[DEBUG] Successfully created the default org with id %s!", orgId)
setUsers = true
item := shuffle.Environment{
Name: defaultEnv,
@@ -3918,20 +4049,6 @@ func runInitEs(ctx context.Context) {
log.Printf("[WARNING] Failed setting up new environment")
}
}
if setUsers {
tmpOrg := shuffle.OrgMini{
Name: orgSetupName,
Id: orgId,
}
err = createNewUser(username, password, "admin", apikey, tmpOrg)
if err != nil {
log.Printf("[INFO] Failed to create default user %s: %s", username, err)
} else {
log.Printf("[INFO] Successfully created user %s", username)
}
}
}
} else {
for _, user := range users {
@@ -4145,6 +4262,8 @@ func runInitEs(ctx context.Context) {
}
}
cloneOptions = checkGitProxy(cloneOptions)
branch := os.Getenv("SHUFFLE_DOWNLOAD_AUTH_BRANCH")
if len(branch) > 0 && branch != "master" && branch != "main" {
cloneOptions.ReferenceName = plumbing.ReferenceName(branch)
@@ -4189,6 +4308,9 @@ func runInitEs(ctx context.Context) {
cloneOptions := &git.CloneOptions{
URL: apis,
}
cloneOptions = checkGitProxy(cloneOptions)
_, err = git.Clone(storer, fs, cloneOptions)
if err != nil {
log.Printf("[ERROR] Failed loading repo %s into memory: %s", apis, err)
@@ -4205,17 +4327,16 @@ func runInitEs(ctx context.Context) {
log.Printf("[INFO] Skipping download of extra API samples as %d were found", len(workflowapps))
}
if os.Getenv("SHUFFLE_HEALTHCHECK_DISABLED") != "true" {
healthcheckInterval := 30
healthcheckInterval := 30
log.Printf("[INFO] Starting healthcheck job every %d minute. Stats available on /api/v1/health/stats. Disable with SHUFFLE_HEALTHCHECK_DISABLED=true", healthcheckInterval)
job := func() {
// Prepare a fake http.responsewriter
// Prepare a fake http.responsewriter
resp := httptest.NewRecorder()
request := http.Request{}
// Add the "force=true" query to the fake request
request.URL, err = url.Parse("/api/v1/health/stats?force=true")
request.URL, err = url.Parse("/api/v1/health/stats?force=true")
if err != nil {
log.Printf("[ERROR] Failed to parse test url for healthstats: %s", err)
}
@@ -4234,7 +4355,6 @@ func runInitEs(ctx context.Context) {
log.Printf("[INFO] Finished INIT (ES)")
}
func handleVerifyCloudsync(orgId string) (shuffle.SyncFeatures, error) {
ctx := context.Background()
org, err := shuffle.GetOrg(ctx, orgId)
@@ -4813,8 +4933,6 @@ func makeWorkflowPublic(resp http.ResponseWriter, request *http.Request) {
resp.Write([]byte(fmt.Sprintf(`{"success": true}`)))
}
func handleAppZipUpload(resp http.ResponseWriter, request *http.Request) {
cors := shuffle.HandleCors(resp, request)
if cors {
@@ -4873,8 +4991,6 @@ func handleAppZipUpload(resp http.ResponseWriter, request *http.Request) {
resp.Write([]byte("OK"))
}
func initHandlers() {
var err error
ctx := context.Background()
@@ -4906,7 +5022,7 @@ func initHandlers() {
go runInitEs(ctx)
} else {
//go shuffle.runInit(ctx)
log.Printf("[ERROR] Opensearch is the only viable option. Please set SHUFFLE_ELASTIC=true")
log.Printf("[ERROR] Opensearch is the only viable option. Please set SHUFFLE_ELASTIC=true")
os.Exit(1)
}
@@ -4921,7 +5037,7 @@ func initHandlers() {
r.HandleFunc("/api/v1/users/register", handleRegister).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/users/checkusers", checkAdminLogin).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/users/getinfo", handleInfo).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/users/{userId}/apps", shuffle.HandleGetUserApps).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/users/apps", shuffle.HandleGetUserApps).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/users/generateapikey", shuffle.HandleApiGeneration).Methods("GET", "POST", "OPTIONS")
r.HandleFunc("/api/v1/users/logout", shuffle.HandleLogout).Methods("POST", "OPTIONS")
@@ -4942,6 +5058,7 @@ func initHandlers() {
r.HandleFunc("/api/v1/register", handleRegister).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/checkusers", checkAdminLogin).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/getinfo", handleInfo).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/me", handleInfo).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/getsettings", shuffle.HandleSettings).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/generateapikey", shuffle.HandleApiGeneration).Methods("GET", "POST", "OPTIONS")
r.HandleFunc("/api/v1/passwordchange", shuffle.HandlePasswordChange).Methods("POST", "OPTIONS")
@@ -4976,7 +5093,8 @@ func initHandlers() {
r.HandleFunc("/api/v1/apps/{appId}", shuffle.UpdateWorkflowAppConfig).Methods("PATCH", "OPTIONS")
r.HandleFunc("/api/v1/apps/{appId}", shuffle.DeleteWorkflowApp).Methods("DELETE", "OPTIONS")
r.HandleFunc("/api/v1/apps/{appId}/config", shuffle.GetWorkflowAppConfig).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/apps/run_hotload", handleAppHotloadRequest).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/apps/run_hotload", handleAppHotloadRequest).Methods("GET", "POST", "OPTIONS")
r.HandleFunc("/api/v1/apps/{appName}/run_hotload", handleSingleAppHotloadRequest).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/apps/get_existing", LoadSpecificApps).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/apps/download_remote", LoadSpecificApps).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/apps/validate", validateAppInput).Methods("POST", "OPTIONS")
@@ -5055,7 +5173,7 @@ 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/pipeline/save", shuffle.HandleSavePipelineInfo).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")
@@ -5077,12 +5195,13 @@ func initHandlers() {
//r.HandleFunc("/api/v1/orgs/", shuffle.HandleGetOrgs).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/orgs/{orgId}", shuffle.HandleGetOrg).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/orgs/{orgId}", shuffle.HandleEditOrg).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/orgs/{orgid}/forms", shuffle.HandleGetOrgForms).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/orgs/{orgId}/create_sub_org", shuffle.HandleCreateSubOrg).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/orgs/{orgId}/change", shuffle.HandleChangeUserOrg).Methods("POST", "OPTIONS") // Swaps to the org
r.HandleFunc("/api/v1/orgs/{orgId}", shuffle.HandleDeleteOrg).Methods("DELETE", "OPTIONS")
r.HandleFunc("/api/v1/orgs/{orgId}/suborgs", shuffle.HandleGetSubOrgs).Methods("GET", "OPTIONS")
// 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/environments/{key}/stop", shuffle.HandleStopExecutions).Methods("GET", "POST", "OPTIONS")
@@ -5090,11 +5209,13 @@ func initHandlers() {
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")
r.HandleFunc("/api/v1/orgs/{orgId}/cache/{cache_key}", shuffle.HandleGetCacheKey).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/orgs/{orgId}/get_cache", shuffle.HandleGetCacheKey).Methods("POST", "OPTIONS")
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}/stats", shuffle.HandleGetStatistics).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/orgs/{orgId}/stats", shuffle.HandleAppendStatistics).Methods("POST", "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")
@@ -5104,7 +5225,6 @@ func initHandlers() {
r.HandleFunc("/api/v1/orgs/{orgId}/datastore", shuffle.HandleSetCacheKey).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/orgs/{orgId}/datastore/{cache_key}", shuffle.HandleDeleteCacheKey).Methods("DELETE", "OPTIONS")
// Docker orborus specific - downloads an image
r.HandleFunc("/api/v1/get_docker_image", getDockerImage).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/login_sso", shuffle.HandleSSO).Methods("GET", "POST", "OPTIONS")
@@ -5123,6 +5243,17 @@ func initHandlers() {
r.HandleFunc("/api/v1/files/{fileId}", shuffle.HandleDeleteFile).Methods("DELETE", "OPTIONS")
r.HandleFunc("/api/v1/files", shuffle.HandleGetFiles).Methods("GET", "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")
r.HandleFunc("/api/v1/detections/{detection_type}", shuffle.HandleGetDetectionRules).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/detections/{triggerId}/selected_rules", shuffle.HandleGetSelectedRules).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/detections/{triggerId}/selected_rules/save", shuffle.HandleSaveSelectedRules).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/detections/{action}", shuffle.HandleFolderToggle).Methods("PUT", "OPTIONS")
// This is weird.
r.HandleFunc("/api/v1/detections/{fileId}/{action}", shuffle.HandleToggleRule).Methods("PUT", "OPTIONS")
//r.HandleFunc("/api/v1/detections/siem/node_health", shuffle.HandleTenzirHealthUpdate).Methods("POST","OPTIONS")
// Introduced in 0.9.21 to handle notifications for e.g. failed Workflow
r.HandleFunc("/api/v1/notifications", shuffle.HandleCreateNotification).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/notifications", shuffle.HandleGetNotifications).Methods("GET", "OPTIONS")
+178 -53
View File
@@ -106,7 +106,6 @@ func createSchedule(ctx context.Context, scheduleId, workflowId, name, startNode
}
log.Printf("[INFO] Starting frequency for execution: %d", newfrequency)
//jobret, err := newscheduler.Every(newfrequency).Seconds().NotImmediately().Run(job)
jobret, err := newscheduler.Every(newfrequency).Seconds().Run(job)
@@ -292,29 +291,44 @@ func handleGetWorkflowqueue(resp http.ResponseWriter, request *http.Request) {
ctx := shuffle.GetContext(request)
env, err := shuffle.GetEnvironment(ctx, orgId, "")
timeNow := time.Now().Unix()
if err == nil && len(env.Id) > 0 && len(env.Name) > 0 {
if err == nil && len(env.Id) > 0 && len(env.Name) > 0 && request.Method == "POST" {
// Updates every 60 seconds~
if time.Now().Unix() > env.Edited+60 {
env.RunningIp = shuffle.GetRequestIp(request)
// Orborus label = custom label for Orborus
if len(orborusLabel) > 0 {
env.RunningIp = orborusLabel
}
if request.Method == "POST" {
body, err := ioutil.ReadAll(request.Body)
if err == nil {
var envData shuffle.OrborusStats
err = json.Unmarshal(body, &envData)
if err == nil {
if envData.Swarm {
env.Licensed = true
env.RunType = "docker"
}
// Set the checkin cache
if envData.Kubernetes {
env.RunType = "k8s"
}
body, err := ioutil.ReadAll(request.Body)
if err == nil {
var envData shuffle.OrborusStats
err = json.Unmarshal(body, &envData)
if err == nil {
envData.RunningIp = env.RunningIp
marshalled, err := json.Marshal(envData)
if err == nil {
cacheKey := fmt.Sprintf("queueconfig-%s-%s", env.Name, env.OrgId)
go shuffle.SetCache(context.Background(), cacheKey, marshalled, 2)
}
if envData.Swarm {
env.Licensed = true
env.RunType = "docker"
}
if envData.Kubernetes {
env.RunType = "k8s"
}
envData.DataLake = env.DataLake
}
}
@@ -572,14 +586,20 @@ func handleGetStreamResults(resp http.ResponseWriter, request *http.Request) {
//return
}
if len(actionResult.ExecutionId) == 0 {
resp.WriteHeader(400)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Provide execution_id and authorization"}`)))
return
}
ctx := context.Background()
workflowExecution, err := shuffle.GetWorkflowExecution(ctx, actionResult.ExecutionId)
if err != nil {
if err != nil || workflowExecution.ExecutionId != actionResult.ExecutionId {
if len(actionResult.ExecutionId) > 0 {
log.Printf("[WARNING][%s] Failed getting execution (streamresult): %s", actionResult.ExecutionId, err)
}
resp.WriteHeader(401)
resp.WriteHeader(400)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Bad authorization key or execution_id might not exist."}`)))
return
}
@@ -638,9 +658,27 @@ func handleGetStreamResults(resp http.ResponseWriter, request *http.Request) {
}
}
if workflowExecution.Workflow.Sharing == "form" {
newWorkflow := shuffle.Workflow{
Name: workflowExecution.Workflow.Name,
ID: workflowExecution.Workflow.ID,
Owner: workflowExecution.Workflow.Owner,
OrgId: workflowExecution.Workflow.OrgId,
Sharing: workflowExecution.Workflow.Sharing,
Description: workflowExecution.Workflow.Description,
InputQuestions: workflowExecution.Workflow.InputQuestions,
FormControl: workflowExecution.Workflow.FormControl,
}
workflowExecution.Results = []shuffle.ActionResult{}
workflowExecution.Workflow = newWorkflow
}
newjson, err := json.Marshal(workflowExecution)
if err != nil {
resp.WriteHeader(401)
resp.WriteHeader(500)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed unpacking workflow execution"}`)))
return
}
@@ -670,7 +708,7 @@ func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) {
}
//log.Printf("Actionresult unmarshal: %s", string(body))
log.Printf("[DEBUG] Got workflow result from %s of length %d", request.RemoteAddr, len(body))
//log.Printf("[DEBUG] Got workflow result from %s of length %d", request.RemoteAddr, len(body))
ctx := context.Background()
err = shuffle.ValidateNewWorkerExecution(ctx, body)
if err == nil {
@@ -681,7 +719,7 @@ func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) {
log.Printf("[DEBUG] Handling other execution variant (subflow?): %s", err)
}
log.Printf("[DEBUG] Got workflow result from %s of length %d.", request.RemoteAddr, len(body))
//log.Printf("[DEBUG] Got workflow result from %s of length %d.", request.RemoteAddr, len(body))
var actionResult shuffle.ActionResult
err = json.Unmarshal(body, &actionResult)
@@ -739,8 +777,7 @@ func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) {
// Will make sure transactions are always ran for an execution. This is recursive if it fails. Allowed to fail up to 5 times
func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workflowExecutionId string, actionResult shuffle.ActionResult, resp http.ResponseWriter) {
log.Printf("[DEBUG][%s] Running workflow execution update", workflowExecutionId)
log.Printf("[DEBUG][%s] Running workflow execution update with result from %s (%s) of status %s", workflowExecutionId, actionResult.Action.Label, actionResult.Action.ID, actionResult.Status)
// Should start a tx for the execution here
workflowExecution, err := shuffle.GetWorkflowExecution(ctx, workflowExecutionId)
@@ -771,7 +808,6 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl
setExecution := true
if setExecution || workflowExecution.Status == "FINISHED" || workflowExecution.Status == "ABORTED" || workflowExecution.Status == "FAILURE" {
err = shuffle.SetWorkflowExecution(ctx, *workflowExecution, true)
//err = shuffle.SetWorkflowExecution(ctx, *workflowExecution, dbSave)
if err != nil {
resp.WriteHeader(401)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed setting workflowexecution actionresult: %s"}`, err)))
@@ -925,7 +961,7 @@ func deleteWorkflow(resp http.ResponseWriter, request *http.Request) {
if len(workflow.ParentWorkflowId) > 0 {
resp.WriteHeader(403)
resp.Write([]byte(`{"success": false, "reason": "Can't delete a workflow distributed from your parent org"}`))
return
return
}
if user.Id != workflow.Owner || len(user.Id) == 0 {
@@ -939,6 +975,27 @@ func deleteWorkflow(resp http.ResponseWriter, request *http.Request) {
}
}
// Look for Child workflows and delete them
if workflow.ParentWorkflowId == "" {
log.Printf("[DEBUG] Looking for child workflows for workflow %s to delete. User %s (%s) in org %s (%s)", workflow.ID, user.Username, user.Id, user.ActiveOrg.Name, user.ActiveOrg.Id)
childWorkflows, err := shuffle.ListChildWorkflows(ctx, workflow.ID)
if err != nil {
log.Printf("[ERROR] Failed to list child workflows: %s", err)
} else {
log.Printf("\n\n[DEBUG] Found %d child workflows for workflow %s\n\n", len(childWorkflows), workflow.ID)
// Find cookies and append them to request.Header to replicate current request as closely as possible
for _, childWorkflow := range childWorkflows {
if childWorkflow.ID == workflow.ID {
continue
}
go shuffle.SendDeleteWorkflowRequest(childWorkflow, request)
}
}
}
// Clean up triggers and executions
for _, item := range workflow.Triggers {
if item.TriggerType == "SCHEDULE" && item.Status != "uninitialized" {
@@ -984,8 +1041,6 @@ func deleteWorkflow(resp http.ResponseWriter, request *http.Request) {
resp.Write([]byte(`{"success": true}`))
}
func handleExecution(id string, workflow shuffle.Workflow, request *http.Request, orgId string) (shuffle.WorkflowExecution, string, error) {
//go func() {
// log.Printf("\n\nPRE TIME: %s\n\n", time.Now().Format("2006-01-02 15:04:05"))
@@ -1004,17 +1059,6 @@ func handleExecution(id string, workflow shuffle.Workflow, request *http.Request
workflow = *tmpworkflow
}
/*
if len(workflow.ExecutingOrg.Id) == 0 {
if len(orgId) > 0 {
workflow.ExecutingOrg.Id = orgId
} else {
log.Printf("[INFO] Stopped execution because there is no executing org for workflow %s", workflow.ID)
return shuffle.WorkflowExecution{}, fmt.Sprintf("Workflow has no executing org defined"), errors.New("Workflow has no executing org defined")
}
}
*/
if len(workflow.Actions) == 0 {
workflow.Actions = []shuffle.Action{}
} else {
@@ -1065,28 +1109,31 @@ func handleExecution(id string, workflow shuffle.Workflow, request *http.Request
workflowExecution, execInfo, _, workflowExecErr := shuffle.PrepareWorkflowExecution(ctx, workflow, request, int64(maxExecutionDepth))
if workflowExecErr != nil {
err := shuffle.SetWorkflowExecution(ctx, workflowExecution, true)
if err != nil {
log.Printf("[ERROR] Failed setting workflow execution during init (2): %s", err)
if len(workflowExecution.Workflow.Actions) > 0 && len(workflowExecution.Results) > 0 && len(workflowExecution.ExecutionId) > 0 {
err := shuffle.SetWorkflowExecution(ctx, workflowExecution, true)
if err != nil {
log.Printf("[ERROR] Failed setting workflow execution during init (2): %s", err)
}
}
if strings.Contains(fmt.Sprintf("%s", workflowExecErr), "User Input") {
// Special for user input callbacks
log.Printf("[INFO] User input callback: %s", workflowExecErr)
// return workflowExecution, fmt.Sprintf("%s", err), nil
//log.Printf("[INFO] User input callback: %s", workflowExecErr)
return shuffle.WorkflowExecution{}, "", nil
} else {
log.Printf("[ERROR] Failed in prepareExecution: '%s'", err)
return shuffle.WorkflowExecution{}, fmt.Sprintf("Failed running: %s", err), err
log.Printf("[ERROR] Failed in prepareExecution: '%s'", workflowExecErr)
return shuffle.WorkflowExecution{}, fmt.Sprintf("Failed running: %s", workflowExecErr), workflowExecErr
}
}
err := imageCheckBuilder(execInfo.ImageNames)
if err != nil {
log.Printf("[ERROR] Failed building the required images from %#v: %s", execInfo.ImageNames, err)
return shuffle.WorkflowExecution{}, "Failed unmarshal during execution", err
}
/*
makeNew := true
start, startok := request.URL.Query()["start"]
if request.Method == "POST" {
@@ -1206,7 +1253,7 @@ func handleExecution(id string, workflow shuffle.Workflow, request *http.Request
answer, answerok := request.URL.Query()["answer"]
referenceId, referenceok := request.URL.Query()["reference_execution"]
if answerok && referenceok {
if answerok && referenceok && len(answer) > 0 && len(referenceId) > 0 {
// If answer is false, reference execution with result
log.Printf("[INFO] Answer is OK AND reference is OK!")
if answer[0] == "false" {
@@ -1230,7 +1277,7 @@ func handleExecution(id string, workflow shuffle.Workflow, request *http.Request
log.Printf("%s - %s", result.Action.ID, start[0])
if result.Action.ID == start[0] {
note, noteok := request.URL.Query()["note"]
if noteok {
if noteok && len(note) > 0 {
result.Result = fmt.Sprintf("User note: %s", note[0])
} else {
result.Result = fmt.Sprintf("User clicked %s", answer[0])
@@ -1354,7 +1401,7 @@ func handleExecution(id string, workflow shuffle.Workflow, request *http.Request
}
}
childNodes := shuffle.FindChildNodes(workflowExecution, workflowExecution.Start, []string{}, []string{})
childNodes := shuffle.FindChildNodes(workflowExecution.Workflow, workflowExecution.Start, []string{}, []string{})
startFound := false
newActions := []shuffle.Action{}
@@ -1560,7 +1607,6 @@ func handleExecution(id string, workflow shuffle.Workflow, request *http.Request
// newTriggers = append(newTriggers, trigger)
//}
//workflowExecution.Workflow.Triggers = newTriggers
_ = removeTriggers
if !startFound {
if len(workflowExecution.Start) == 0 && len(workflowExecution.Workflow.Start) > 0 {
@@ -1585,12 +1631,17 @@ func handleExecution(id string, workflow shuffle.Workflow, request *http.Request
if len(workflowExecution.ExecutionOrg) == 0 && len(workflow.ExecutingOrg.Id) > 0 {
workflowExecution.ExecutionOrg = workflow.ExecutingOrg.Id
}
*/
//workflowExecution, execInfo, _, workflowExecErr := shuffle.PrepareWorkflowExecution(ctx, workflow, request, int64(maxExecutionDepth))
err = shuffle.SetWorkflowExecution(ctx, workflowExecution, true)
if err != nil {
log.Printf("[ERROR] Failed setting workflow execution during init (2): %s", err)
}
onpremExecution := execInfo.OnpremExecution
_ = onpremExecution
environments := execInfo.Environments
var allEnvs []shuffle.Environment
if len(workflowExecution.ExecutionOrg) > 0 {
//log.Printf("[INFO] Executing ORG: %s", workflowExecution.ExecutionOrg)
@@ -1854,7 +1905,6 @@ func executeWorkflow(resp http.ResponseWriter, request *http.Request) {
}
log.Printf("[INFO] Inside execute workflow for ID %s", fileId)
ctx := context.Background()
workflow, err := shuffle.GetWorkflow(ctx, fileId)
if err != nil && workflow.ID == "" {
@@ -2408,7 +2458,7 @@ func scheduleWorkflow(resp http.ResponseWriter, request *http.Request) {
return
}
workflow.Schedules = append(workflow.Schedules, schedule)
//workflow.Schedules = append(workflow.Schedules, schedule)
err = shuffle.SetWorkflow(ctx, *workflow, workflow.ID)
if err != nil {
log.Printf("Failed setting workflow for schedule: %s", err)
@@ -2665,6 +2715,8 @@ func loadGithubWorkflows(url, username, password, userId, branch, orgId string)
cloneOptions.ReferenceName = plumbing.ReferenceName(branch)
}
cloneOptions = checkGitProxy(cloneOptions)
storer := memory.NewStorage()
r, err := git.Clone(storer, fs, cloneOptions)
if err != nil {
@@ -2766,6 +2818,70 @@ func loadSpecificWorkflows(resp http.ResponseWriter, request *http.Request) {
resp.Write([]byte(fmt.Sprintf(`{"success": true}`)))
}
func handleSingleAppHotloadRequest(resp http.ResponseWriter, request *http.Request) {
cors := shuffle.HandleCors(resp, request)
if cors {
return
}
ctx := context.Background()
cacheKey := fmt.Sprintf("workflowapps-sorted-1000")
shuffle.DeleteCache(ctx, cacheKey)
cacheKey = fmt.Sprintf("workflowapps-sorted-500")
shuffle.DeleteCache(ctx, cacheKey)
cacheKey = fmt.Sprintf("workflowapps-sorted-0")
shuffle.DeleteCache(ctx, cacheKey)
// Just need to be logged in
// FIXME - should have some permissions?
user, err := shuffle.HandleApiAuthentication(resp, request)
if err != nil {
log.Printf("Api authentication failed in app hotload: %s", err)
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false}`))
return
}
if user.Role != "admin" {
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false, "reason": "Must be admin to hotload apps"}`))
return
}
location := os.Getenv("SHUFFLE_APP_HOTLOAD_FOLDER")
if len(location) == 0 {
resp.WriteHeader(500)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "SHUFFLE_APP_HOTLOAD_FOLDER not specified in .env"}`)))
return
}
requestUrlFields := strings.Split(request.URL.String(), "/")
var appName string
if requestUrlFields[1] == "api" {
if len(requestUrlFields) <= 4 {
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false}`))
return
}
appName = requestUrlFields[4]
if strings.Contains(appName, "?") {
appName = strings.Split(appName, "?")[0]
}
}
location = location + "/" + appName
log.Printf("[INFO] Starting hotloading from %s", location)
err = handleAppHotload(ctx, location, true)
if err != nil {
log.Printf("[WARNING] Failed app hotload: %s", err)
resp.WriteHeader(500)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err)))
return
}
cacheKey = fmt.Sprintf("workflowapps-sorted-100")
shuffle.DeleteCache(ctx, cacheKey)
cacheKey = fmt.Sprintf("workflowapps-sorted-500")
shuffle.DeleteCache(ctx, cacheKey)
cacheKey = fmt.Sprintf("workflowapps-sorted-1000")
shuffle.DeleteCache(ctx, cacheKey)
resp.WriteHeader(200)
resp.Write([]byte(fmt.Sprintf(`{"success": true}`)))
}
func handleAppHotloadRequest(resp http.ResponseWriter, request *http.Request) {
cors := shuffle.HandleCors(resp, request)
if cors {
@@ -3374,7 +3490,15 @@ func executeSingleAction(resp http.ResponseWriter, request *http.Request) {
}
ctx := context.Background()
workflowExecution, err := shuffle.PrepareSingleAction(ctx, user, fileId, body)
runValidationAction := false
query := request.URL.Query()
validation, ok := query["validation"]
if ok && validation[0] == "true" {
runValidationAction = true
}
workflowExecution, err := shuffle.PrepareSingleAction(ctx, user, fileId, body, runValidationAction)
if err != nil {
log.Printf("[INFO] Failed workflowrequest POST read: %s", err)
resp.WriteHeader(401)
@@ -3410,7 +3534,7 @@ func executeSingleAction(resp http.ResponseWriter, request *http.Request) {
// FIXME: Should use environment that is in the source workflow if it exists
for i, _ := range workflowExecution.Workflow.Actions {
workflowExecution.Workflow.Actions[i].Environment = environment
workflowExecution.Workflow.Actions[i].Label = "TMP"
workflowExecution.Workflow.Actions[i].Label = "TMP"
}
shuffle.SetWorkflowExecution(ctx, workflowExecution, false)
@@ -3940,6 +4064,8 @@ func LoadSpecificApps(resp http.ResponseWriter, request *http.Request) {
}
}
cloneOptions = checkGitProxy(cloneOptions)
storer := memory.NewStorage()
r, err := git.Clone(storer, fs, cloneOptions)
if err != nil {
@@ -4193,7 +4319,6 @@ func checkUnfinishedExecution(resp http.ResponseWriter, request *http.Request) {
log.Printf("[ERROR] Failed adding execution to db: %s", err)
}
resp.WriteHeader(200)
resp.Write([]byte(fmt.Sprintf(`{"success": true, "reason": "Reran workflow in %s"}`, parsedEnv)))
+18 -16
View File
@@ -1,7 +1,6 @@
version: '3'
services:
frontend:
image: ghcr.io/shuffle/shuffle-frontend:latest
image: ghcr.io/shuffle/shuffle-frontend:nightly
container_name: shuffle-frontend
hostname: shuffle-frontend
ports:
@@ -15,7 +14,7 @@ services:
depends_on:
- backend
backend:
image: ghcr.io/shuffle/shuffle-backend:latest
image: ghcr.io/shuffle/shuffle-backend:nightly
container_name: shuffle-backend
hostname: ${BACKEND_HOSTNAME}
# Here for debugging:
@@ -34,7 +33,7 @@ services:
- SHUFFLE_FILE_LOCATION=/shuffle-files
restart: unless-stopped
orborus:
image: ghcr.io/shuffle/shuffle-orborus:latest
image: ghcr.io/shuffle/shuffle-orborus:nightly
container_name: shuffle-orborus
hostname: shuffle-orborus
networks:
@@ -45,12 +44,10 @@ services:
- SHUFFLE_APP_SDK_TIMEOUT=300
- SHUFFLE_ORBORUS_EXECUTION_CONCURRENCY=7 # The amount of concurrent executions Orborus can handle.
#- DOCKER_HOST=tcp://docker-socket-proxy:2375
- ENVIRONMENT_NAME=${ENVIRONMENT_NAME}
- ENVIRONMENT_NAME=Shuffle
- ORG_ID=Shuffle
- BASE_URL=http://${OUTER_HOSTNAME}:5001
- DOCKER_API_VERSION=1.40
- SHUFFLE_BASE_IMAGE_NAME=${SHUFFLE_BASE_IMAGE_NAME}
- SHUFFLE_BASE_IMAGE_REGISTRY=${SHUFFLE_BASE_IMAGE_REGISTRY}
- SHUFFLE_BASE_IMAGE_TAG_SUFFIX=${SHUFFLE_BASE_IMAGE_TAG_SUFFIX}
- HTTP_PROXY=${HTTP_PROXY}
- HTTPS_PROXY=${HTTPS_PROXY}
- SHUFFLE_PASS_WORKER_PROXY=${SHUFFLE_PASS_WORKER_PROXY}
@@ -58,7 +55,8 @@ services:
- SHUFFLE_STATS_DISABLED=true
- SHUFFLE_SWARM_CONFIG=run
- SHUFFLE_LOGS_DISABLED=true
- SHUFFLE_WORKER_IMAGE=ghcr.io/shuffle/shuffle-worker:latest
- SHUFFLE_WORKER_IMAGE=ghcr.io/shuffle/shuffle-worker:nightly
env_file: .env
restart: unless-stopped
security_opt:
- seccomp:unconfined
@@ -66,7 +64,6 @@ services:
image: opensearchproject/opensearch:2.14.0
hostname: shuffle-opensearch
container_name: shuffle-opensearch
env_file: .env
environment:
- "OPENSEARCH_JAVA_OPTS=-Xms2048m -Xmx2048m" # minimum and maximum Java heap size, recommend setting both to 50% of system RAM
- bootstrap.memory_lock=true
@@ -86,7 +83,7 @@ services:
soft: 65536
hard: 65536
volumes:
- ${DB_LOCATION}:/usr/share/opensearch/data:z
- shuffle-database:/usr/share/opensearch/data:z
ports:
- 9200:9200
networks:
@@ -132,13 +129,18 @@ services:
# networks:
# - shuffle
#
volumes:
shuffle-database:
driver: local
driver_opts:
type: none
device: ${DB_LOCATION}
o: bind
networks:
shuffle:
driver: bridge
# uncomment to set MTU for swarm mode.
# MTU should be whatever is your host's preferred MTU is.
# Refer to this doc to figure out what your host's MTU is:
# https://shuffler.io/docs/troubleshooting#TLS_timeout_error/Timeout_Errors/EOF_Errors
# driver_opts:
# com.docker.network.driver.mtu: 1460
# uncomment to set MTU for swarm mode. MTU should be whatever is your host's preferred MTU is: https://shuffler.io/docs/troubleshooting#TLS_timeout_error/Timeout_Errors/EOF_Errors
+15 -8
View File
@@ -1,6 +1,8 @@
# Build environment
FROM node:21 as builder
ENV NODE_OPTIONS="--max-old-space-size=4096"
RUN mkdir /usr/src/app
WORKDIR /usr/src/app
ENV PATH /usr/src/app/node_modules/.bin:$PATH
@@ -11,7 +13,7 @@ COPY package.json /usr/src/app/package.json
#RUN yarn config set "strict-ssl" false -g
#RUN yarn install --network-timeout 1000000
RUN npm install --legacy-peer-deps
RUN npm install --timeout=60000 --legacy-peer-deps
# copy only required files to not trigger rebuilding every time
COPY ./certs /usr/src/app/certs/
@@ -25,28 +27,33 @@ COPY ./*.json /usr/src/app/
RUN npm run build --loglevel verbose 2>&1
# Production environment
FROM nginx:1.21.5
FROM nginx:1.26.0
RUN mkdir -p /usr/share/nginx/html/build
RUN mkdir -p /usr/share/nginx/html/css
RUN mkdir -p /usr/share/nginx/html/js
RUN mkdir -p /usr/share/nginx/html/img
COPY --from=builder /usr/src/app/build /usr/share/nginx/html
#Localhost certificate challenge: Y#XwrJ#DoZGz2w6x
# Localhost certificate challenge: Y#XwrJ#DoZGz2w6x
# Cert challenge doesn't matter to be here or not, as ALL production setups should be using their own certificates + reverse proxy: https://shuffler.io/docs/configuration#using-the-nginx-reverse-proxy-for-tls/ssl
COPY --from=builder /usr/src/app/build /usr/share/nginx/html
COPY --from=builder /usr/src/app/certs/fullchain.pem /etc/nginx/fullchain.cert.pem
COPY --from=builder /usr/src/app/certs/privkey.pem /etc/nginx/privkey.pem
# install CONFD
ENV CONFD_VERSION 0.16.0
RUN apt-get update && apt-get install -y curl && apt-get clean
RUN curl -sSL https://github.com/kelseyhightower/confd/releases/download/v${CONFD_VERSION}/confd-${CONFD_VERSION}-linux-amd64 -o /usr/local/bin/confd && \
chmod +x /usr/local/bin/confd
COPY ./confd /etc/confd
COPY ./confd/templates/nginx.conf /etc/nginx/nginx.conf.tmpl
## OLD CONFD THINGS (not compatible with arm)
#ENV CONFD_VERSION 0.16.0
#RUN curl -sSL https://github.com/kelseyhightower/confd/releases/download/v${CONFD_VERSION}/confd-${CONFD_VERSION}-linux-amd64 -o /usr/local/bin/confd && \
# chmod +x /usr/local/bin/confd
#COPY ./confd /etc/confd
# rewrite command & entrypoint with ours
COPY ./entrypoint.sh /
ENV BACKEND_HOSTNAME="shuffle-backend"
ENTRYPOINT [ "/entrypoint.sh" ]
CMD ["nginx", "-g", "daemon off;"]
+2
View File
@@ -1,3 +1,5 @@
## Lalits frontend magic
## Localhost Certificate info:
+3 -2
View File
@@ -71,7 +71,7 @@ http {
}
location ~ /api/v(1|2) {
proxy_pass http://{{ getenv "BACKEND_HOSTNAME" "shuffle-backend" }}:5001;
proxy_pass http://${BACKEND_HOSTNAME}:5001;
proxy_buffering off;
proxy_http_version 1.1;
@@ -113,7 +113,8 @@ http {
# Get the hostname from environment here?
location ~ /api/v(1|2) {
proxy_pass http://{{ getenv "BACKEND_HOSTNAME" "shuffle-backend" }}:5001;
proxy_pass http://${BACKEND_HOSTNAME}:5001;
proxy_buffering off;
proxy_http_version 1.1;
+3 -4
View File
@@ -1,7 +1,6 @@
#!/bin/bash
#!/usr/bin/env sh
set -eu
# generate configs
/usr/local/bin/confd -backend="env" -confdir="/etc/confd" -onetime
envsubst '${BACKEND_HOSTNAME}' < /etc/nginx/nginx.conf.tmpl > /etc/nginx/nginx.conf
# run main command
exec "$@"
+5 -12
View File
@@ -1,10 +1,9 @@
{
"name": "shuffler",
"homepage": "https://shuffler.io",
"version": "1.4.0",
"version": "2.0.0",
"private": true,
"dependencies": {
"@babel/plugin-proposal-class-properties": "^7.18.6",
"@codemirror/commands": "^6.2.4",
"@codemirror/lang-python": "^6.1.3",
"@emotion/react": "^11.11.1",
@@ -13,7 +12,7 @@
"@metamask/detect-provider": "^1.2.0",
"@mui/icons-material": "^5.14.0",
"@mui/material": "^5.14.0",
"@mui/styles": "^5.14.0",
"@mui/styles": "^6.1.4",
"@mui/x-data-grid": "^5.17.11",
"@mui/x-date-pickers": "^6.11.1",
"@types/algoliasearch": "^3.34.11",
@@ -21,7 +20,6 @@
"@uiw/codemirror-theme-vscode": "^4.21.20",
"@uiw/codemirror-themes": "^4.21.9",
"@uiw/react-codemirror": "^4.21.21",
"@use-it/interval": "^0.1.3",
"algoliasearch": "^4.8.3",
"class-transformer": "^0.2.0",
"codemirror": "^6.0.1",
@@ -49,7 +47,6 @@
"i18next-localstorage-backend": "^4.1.0",
"i18next-xhr-backend": "^3.2.2",
"import": "0.0.6",
"interweave": "^11.2.0",
"is-plain-obj": "^4.1.0",
"json-bigint": "^1.0.0",
"match-sorter": "^6.3.1",
@@ -58,12 +55,12 @@
"moment": "~2.29.4",
"mui-chips-input": "^2.1.3",
"mui-nested-menu": "^3.2.1",
"react": "^18.2.0",
"react": "^18.3.1",
"react-ace": "^10.1.0",
"react-alice-carousel": "^2.6.4",
"react-avatar-editor": "^11.1.0",
"react-beforeunload": "^2.2.1",
"react-chartjs-2": "^2.11.1",
"react-chartjs-2": "^2.11.2",
"react-cookie": "^4.0.1",
"react-cytoscapejs": "^2.0.0",
"react-device-detect": "^2.2.3",
@@ -73,21 +70,18 @@
"react-dropzone": "^14.2.3",
"react-ga4": "^2.0.0",
"react-hotkeys": "^2.0.0",
"react-i18next": "^13.1.2",
"react-instantsearch-dom": "^6.28.0",
"react-json-pretty": "^2.2.0",
"react-json-view": "^1.21.3",
"react-json-view-ssr": "^1.19.1",
"react-markdown": "^8.0.7",
"react-markdown-github": "^3.3.1",
"react-powerhooks": "^0.0.7",
"react-router": "^6.14.1",
"react-router-dom": "^6.14.1",
"react-scripts": "^5.0.1",
"react-social-icons": "^5.15.0",
"react-stripe-elements": "^6.1.2",
"react-toastify": "^9.1.3",
"reaviz": "^14.9.7",
"rehype-raw": "^7.0.0",
"remark-gfm": "^3.0.1",
"remark-html": "^16.0.1",
"remark-images": "^4.0.0",
@@ -134,7 +128,6 @@
"babel-preset-es2015": "^6.24.1",
"postcss": "^8.4.38",
"promise-window": "^1.2.1",
"react-hot-loader": "^4.13.0",
"webpack-cli": "^5.1.4"
}
}
+5
View File
@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="24" height="24" fill="#212121" fill-opacity="0.02"/>
<path d="M14 4H7.6C7.17565 4 6.76869 4.16857 6.46863 4.46863C6.16857 4.76869 6 5.17565 6 5.6V18.4C6 18.8243 6.16857 19.2313 6.46863 19.5314C6.76869 19.8314 7.17565 20 7.6 20H17.2C17.6243 20 18.0313 19.8314 18.3314 19.5314C18.6314 19.2313 18.8 18.8243 18.8 18.4V8.8L14 4Z" stroke="#F1F1F1" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14 4V8.8H18.8" stroke="#F1F1F1" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 598 B

+4
View File
@@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 7.20001H6.6H19.4" stroke="#FD4C62" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M17.7996 7.2V18.4C17.7996 18.8243 17.631 19.2313 17.331 19.5314C17.0309 19.8314 16.624 20 16.1996 20H8.19961C7.77526 20 7.3683 19.8314 7.06824 19.5314C6.76818 19.2313 6.59961 18.8243 6.59961 18.4V7.2M8.99961 7.2V5.6C8.99961 5.17565 9.16818 4.76869 9.46824 4.46863C9.7683 4.16857 10.1753 4 10.5996 4H13.7996C14.224 4 14.6309 4.16857 14.931 4.46863C15.231 4.76869 15.3996 5.17565 15.3996 5.6V7.2" stroke="#FD4C62" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 669 B

+4
View File
@@ -0,0 +1,4 @@
<svg width="16" height="11" viewBox="0 0 16 11" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.99984 6.83329C8.73622 6.83329 9.33317 6.23634 9.33317 5.49996C9.33317 4.76358 8.73622 4.16663 7.99984 4.16663C7.26346 4.16663 6.6665 4.76358 6.6665 5.49996C6.6665 6.23634 7.26346 6.83329 7.99984 6.83329Z" stroke="#C8C8C8" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10.8269 2.67329C11.1988 3.04478 11.4938 3.48593 11.6951 3.97153C11.8964 4.45712 12 4.97763 12 5.50329C12 6.02895 11.8964 6.54946 11.6951 7.03505C11.4938 7.52064 11.1988 7.9618 10.8269 8.33329M5.17354 8.32662C4.80163 7.95513 4.5066 7.51398 4.3053 7.02838C4.104 6.54279 4.00039 6.02228 4.00039 5.49662C4.00039 4.97096 4.104 4.45045 4.3053 3.96486C4.5066 3.47927 4.80163 3.03811 5.17354 2.66662M12.7135 0.786621C13.9633 2.03681 14.6654 3.73219 14.6654 5.49995C14.6654 7.26772 13.9633 8.9631 12.7135 10.2133M3.28687 10.2133C2.03706 8.9631 1.33496 7.26772 1.33496 5.49995C1.33496 3.73219 2.03706 2.03681 3.28687 0.786621" stroke="#C8C8C8" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

+5
View File
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
<svg width="800px" height="800px" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg">
<circle cx="512" cy="512" r="512" style="fill:#0091e2"/>
<path d="M827.3 461.5c-1.6-1.3-16.1-12.2-46.7-12.2-8.1 0-16.2.6-24.2 2.1-5.9-40.7-39.5-60.5-41-61.4l-8.2-4.8-5.4 7.8c-6.8 10.5-11.7 22-14.6 34.2-5.5 23.2-2.2 45 9.6 63.6-14.2 7.9-37.1 9.9-41.7 10H277c-9.9 0-17.9 8-17.9 17.9-.4 33.1 5.2 66 16.5 97.1 13 34.2 32.4 59.3 57.6 74.7 28.2 17.3 74.1 27.2 126.2 27.2 23.5.1 47-2.1 70.1-6.4 32.1-5.9 63-17.1 91.4-33.2 23.4-13.6 44.5-30.8 62.4-51.1 29.9-33.9 47.8-71.7 61.1-105.2h5.3c32.8 0 53-13.1 64.1-24.1 7.4-7 13.2-15.5 16.9-25l2.3-6.9-5.7-4.3zM312 489.9h50.7c2.4 0 4.4-2 4.4-4.4v-45.1c0-2.4-2-4.4-4.4-4.5H312c-2.4 0-4.4 2-4.4 4.4v45.2c0 2.5 2 4.4 4.4 4.4m69.9 0h50.7c2.4 0 4.4-2 4.4-4.4v-45.1c0-2.4-2-4.4-4.4-4.5h-50.7c-2.5 0-4.5 2-4.5 4.5v45.1c0 2.5 2 4.4 4.5 4.4m70.8.1h50.7c2.4 0 4.4-2 4.4-4.4v-45.1c0-2.4-2-4.4-4.4-4.5h-50.7c-2.4 0-4.4 2-4.4 4.4v45.2c0 2.4 2 4.3 4.4 4.4m70.1 0h50.7c2.4 0 4.4-2 4.5-4.4v-45.1c0-2.5-2-4.5-4.5-4.5h-50.7c-2.4 0-4.4 2-4.4 4.4v45.2c0 2.4 1.9 4.4 4.4 4.4m-141-65h50.7c2.4 0 4.4-2 4.4-4.5v-45.1c0-2.4-2-4.4-4.4-4.4h-50.7c-2.5 0-4.4 2-4.5 4.4v45.1c.1 2.5 2.1 4.5 4.5 4.5m70.9 0h50.7c2.4 0 4.4-2 4.4-4.5v-45.1c0-2.4-2-4.4-4.4-4.4h-50.7c-2.4 0-4.4 2-4.4 4.4v45.1c0 2.5 2 4.5 4.4 4.5m70.1 0h50.7c2.5 0 4.4-2 4.5-4.5v-45.1c0-2.5-2-4.4-4.5-4.4h-50.7c-2.4 0-4.4 2-4.4 4.4v45.1c0 2.5 1.9 4.5 4.4 4.5m0-64.9h50.7c2.5 0 4.5-2 4.5-4.5v-45.2c0-2.4-2-4.4-4.5-4.4h-50.7c-2.4 0-4.4 2-4.4 4.4v45.2c0 2.5 1.9 4.5 4.4 4.5M593.4 490h50.7c2.4 0 4.4-2 4.4-4.4v-45.1c0-2.5-2-4.4-4.4-4.5h-50.7c-2.4 0-4.4 2-4.4 4.4v45.2c0 2.4 2 4.4 4.4 4.4" style="fill:#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

+4
View File
@@ -0,0 +1,4 @@
<svg width="14" height="17" viewBox="0 0 14 17" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.75 1H2.5C2.10218 1 1.72064 1.15804 1.43934 1.43934C1.15804 1.72064 1 2.10218 1 2.5V14.5C1 14.8978 1.15804 15.2794 1.43934 15.5607C1.72064 15.842 2.10218 16 2.5 16H11.5C11.8978 16 12.2794 15.842 12.5607 15.5607C12.842 15.2794 13 14.8978 13 14.5V6.25L7.75 1Z" stroke="#C8C8C8" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.75 1V6.25H13" stroke="#C8C8C8" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 569 B

+6
View File
@@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="24" height="24" fill="#212121" fill-opacity="0.02"/>
<path d="M8.22559 16.4467L11.7788 20L15.3321 16.4467" stroke="#F1F1F1" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.7793 12.0052V20" stroke="#F1F1F1" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M19.6676 17.415C20.4399 16.8719 21.019 16.0968 21.321 15.2023C21.6229 14.3078 21.632 13.3403 21.3468 12.4403C21.0617 11.5402 20.4971 10.7545 19.7352 10.197C18.9732 9.6396 18.0534 9.33948 17.1092 9.34021H15.99C15.7228 8.299 15.2229 7.33196 14.5279 6.5119C13.8329 5.69184 12.961 5.04013 11.9777 4.60583C10.9944 4.17153 9.92534 3.96596 8.85109 4.00459C7.77684 4.04322 6.72535 4.32505 5.77578 4.82886C4.82621 5.33267 4.00331 6.04534 3.36902 6.9132C2.73474 7.78106 2.30559 8.78151 2.11391 9.83922C1.92222 10.8969 1.97297 11.9844 2.26236 13.0196C2.55174 14.0549 3.07221 15.011 3.78459 15.816" stroke="#F1F1F1" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.1038 4.66848C16.3158 4.45654 16.5674 4.28843 16.8443 4.17373C17.1212 4.05903 17.418 4 17.7177 4C18.0174 4 18.3142 4.05903 18.5911 4.17373C18.868 4.28843 19.1196 4.45654 19.3315 4.66848C19.5435 4.88041 19.7116 5.13201 19.8263 5.40891C19.941 5.68582 20 5.9826 20 6.28232C20 6.58204 19.941 6.87882 19.8263 7.15573C19.7116 7.43263 19.5435 7.68423 19.3315 7.89617L8.43807 18.7896L4 20L5.21038 15.5619L16.1038 4.66848Z" stroke="#F1F1F1" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 596 B

+4
View File
@@ -0,0 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="12" transform="matrix(-1 0 0 1 24 0)" fill="#2F2F2F"/>
<path d="M8 6L14 12L8 18" stroke="#F1F1F1" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 296 B

@@ -0,0 +1,3 @@
<svg width="22" height="20" viewBox="0 0 22 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 10H17L14 19L8 1L5 10H1" stroke="#F1F1F1" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 225 B

+132 -40
View File
@@ -14,7 +14,9 @@ import HealthPage from "./components/HealthPage.jsx";
//import Header from "./components/Header.jsx";
import theme from "./theme";
import Apps from "./views/Apps";
import Apps2 from "./views/Apps2.jsx";
import AppCreator from "./views/AppCreator";
import DetectionDashBoard from "./views/DetectionDashboard.jsx";
import Welcome from "./views/Welcome.jsx";
import Dashboard from "./views/Dashboard.jsx";
@@ -22,6 +24,7 @@ import DashboardView from "./views/DashboardViews.jsx";
import AdminSetup from "./views/AdminSetup";
import Admin from "./views/Admin";
import Docs from "./views/Docs.jsx";
import Usecases2 from "./views/Usecases2.jsx";
//import Introduction from "./views/Introduction";
import SetAuthentication from "./views/SetAuthentication";
import SetAuthenticationSSO from "./views/SetAuthenticationSSO";
@@ -42,11 +45,20 @@ import AlertTemplate from "./components/AlertTemplate";
import { isMobile } from "react-device-detect";
import RuntimeDebugger from "./components/RuntimeDebugger.jsx"
import MFASetUp from './components/MFASetUP.jsx';
import ApiExplorerWrapper from './views/ApiExplorerWrapper.jsx';
import LeftSideBar from './components/LeftSideBar.jsx';
import CodeWorkflow from './views/CodeWorkflow.jsx';
import NotFound from './views/404.jsx';
import { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import Drift from "react-driftjs";
import { AppContext } from './context/ContextApi.jsx';
import Workflows2 from "./views/Workflows2.jsx";
// Production - backend proxy forwarding in nginx
var globalUrl = window.location.origin;
@@ -194,28 +206,38 @@ const App = (message, props) => {
/>
}
<div style={{ minHeight: 68, maxHeight: 68, }}>
<Header
billingInfo={{}}
{curpath.includes("/workflows") && curpath.includes("/run") ?
<div style={{ height: 60, }} />
:
isLoggedIn ?
<div style={{ position: 'fixed', top: 16, left: 10, zIndex: 100000 }}>
<LeftSideBar userdata={userdata} globalUrl={globalUrl} serverside={false} notifications={notifications} />
</div>
:
<div style={{ minHeight: 68, maxHeight: 68, }}>
<Header
billingInfo={{}}
notifications={notifications}
setNotifications={setNotifications}
checkLogin={checkLogin}
cookies={cookies}
removeCookie={removeCookie}
isLoaded={isLoaded}
globalUrl={globalUrl}
setIsLoggedIn={setIsLoggedIn}
isLoggedIn={isLoggedIn}
userdata={userdata}
notifications={notifications}
setNotifications={setNotifications}
checkLogin={checkLogin}
cookies={cookies}
removeCookie={removeCookie}
isLoaded={isLoaded}
globalUrl={globalUrl}
setIsLoggedIn={setIsLoggedIn}
isLoggedIn={isLoggedIn}
userdata={userdata}
curpath={curpath}
serverside={false}
isMobile={false}
curpath={curpath}
serverside={false}
isMobile={false}
{...props}
/>
</div>
{...props}
/>
</div>
}
{/*
<div style={{ height: 60 }} />
@@ -375,6 +397,19 @@ const App = (message, props) => {
{...props}
/>
}
/>
<Route
exact
path="/usecases2"
element={
<Usecases2
userdata={userdata}
isLoaded={isLoaded}
isLoggedIn={isLoggedIn}
globalUrl={globalUrl}
{...props}
/>
}
/>
<Route
exact
@@ -401,6 +436,23 @@ const App = (message, props) => {
{...props}
/>
}
/>
<Route
exact
path="/apps2"
element={
<Apps2
serverside={false}
isLoaded={isLoaded}
isLoggedIn={isLoggedIn}
checkLogin={checkLogin}
userdata={userdata}
globalUrl={globalUrl}
surfaceColor={theme.palette.surfaceColor}
inputColor={theme.palette.inputColor}
{...props}
/>
}
/>
<Route
exact
@@ -414,6 +466,12 @@ const App = (message, props) => {
/>
}
/>
<Route exact path="/apis/:appid" element={<ApiExplorerWrapper serverside={false} userdata={userdata} isLoggedIn={isLoggedIn} isMobile={false} selectedApp={undefined} isLoaded={isLoaded} isLoggedIn={isLoggedIn} globalUrl={globalUrl} surfaceColor={theme.palette.surfaceColor} inputColor={theme.palette.inputColor} checkLogin={checkLogin} {...props} />} />
<Route
exact
path="/detections/sigma"
element={<DetectionDashBoard globalUrl={globalUrl} />}
/>
<Route
exact
path="/workflows"
@@ -430,6 +488,23 @@ const App = (message, props) => {
{...props}
/>
}
/>
<Route
exact
path="/workflows2"
element={
<Workflows2
checkLogin={checkLogin}
cookies={cookies}
removeCookie={removeCookie}
isLoaded={isLoaded}
isLoggedIn={isLoggedIn}
globalUrl={globalUrl}
cookies={cookies}
userdata={userdata}
{...props}
/>
}
/>
<Route
exact
@@ -463,8 +538,14 @@ const App = (message, props) => {
/>
}
/>
<Route exact path="/workflows/:key/code" element={<CodeWorkflow serverside={false} userdata={userdata} globalUrl={globalUrl} isLoaded={isLoaded} isLoggedIn={isLoggedIn} surfaceColor={theme.palette.surfaceColor} inputColor={theme.palette.inputColor}{...props} />} />
<Route exact path="/workflows/:key/run" element={<RunWorkflow userdata={userdata} globalUrl={globalUrl} isLoaded={isLoaded} isLoggedIn={isLoggedIn} surfaceColor={theme.palette.surfaceColor} inputColor={theme.palette.inputColor}{...props} /> } />
<Route exact path="/workflows/:key/execute" element={<RunWorkflow userdata={userdata} globalUrl={globalUrl} isLoaded={isLoaded} isLoggedIn={isLoggedIn} surfaceColor={theme.palette.surfaceColor} inputColor={theme.palette.inputColor}{...props} /> } />
<Route exact path="/forms" element={<RunWorkflow serverside={false} userdata={userdata} globalUrl={globalUrl} isLoaded={isLoaded} isLoggedIn={isLoggedIn} surfaceColor={theme.palette.surfaceColor} inputColor={theme.palette.inputColor}{...props} />} />
<Route exact path="/forms/:key/run" element={<RunWorkflow serverside={false} userdata={userdata} globalUrl={globalUrl} isLoaded={isLoaded} isLoggedIn={isLoggedIn} surfaceColor={theme.palette.surfaceColor} inputColor={theme.palette.inputColor}{...props} />} />
<Route exact path="/forms/:key" element={<RunWorkflow serverside={false} userdata={userdata} globalUrl={globalUrl} isLoaded={isLoaded} isLoggedIn={isLoggedIn} surfaceColor={theme.palette.surfaceColor} inputColor={theme.palette.inputColor}{...props} />} />
<Route
exact
path="/docs/:key"
@@ -520,6 +601,7 @@ const App = (message, props) => {
/>
}
/>
<Route exact path="/login/:key/mfa-setup" element={<MFASetUp setCookie={setCookie} serverside={false} mainColor={theme.palette.backgroundColor} userdata={userdata} stripeKey={undefined} globalUrl={globalUrl} inputColor={theme.palette.inputColor} isLoaded={isLoaded} {...props} />} />
<Route
exact
path="/login_sso"
@@ -598,30 +680,40 @@ const App = (message, props) => {
/>
}
/>
</Routes>
<Route
exact
path="/*"
element={
<NotFound />
}
/>
</Routes>
</div>
return (
<ThemeProvider theme={theme}>
<CssBaseline />
<CookiesProvider>
<BrowserRouter>
{includedData}
</BrowserRouter>
<ToastContainer
position="bottom-center"
autoClose={5000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
theme="dark"
/>
</CookiesProvider>
</ThemeProvider>
<AppContext>
<ThemeProvider theme={theme}>
<CssBaseline />
<CookiesProvider>
<BrowserRouter>
{includedData}
</BrowserRouter>
<ToastContainer
position="bottom-center"
autoClose={5000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
theme="dark"
/>
</CookiesProvider>
</ThemeProvider>
</AppContext>
);
};
File diff suppressed because it is too large Load Diff
+52 -17
View File
@@ -36,6 +36,7 @@ import edgehandles from "cytoscape-edgehandles";
import cytoscape from "cytoscape";
import { toast } from 'react-toastify';
import { isMobile } from 'react-device-detect';
cytoscape.use(edgehandles)
@@ -52,13 +53,15 @@ export const findSpecificApp = (framework, inputcategory) => {
}
const category = inputcategory.toLowerCase().split(":")[0].trim()
//console.log("findSpecificApp: ", category, framework)
if (category === "edr" || category === "eradication" || category === "edr & av") {
if (framework["EDR & AV"] !== undefined && framework["EDR & AV"].name !== undefined) {
if (framework["EDR & AV"] !== undefined && framework["EDR & AV"].name !== undefined && framework["EDR & AV"].name !== "") {
return framework["EDR & AV"]
}
if (framework["edr"] !== undefined && framework["edr"].name !== undefined && framework["edr"].name !== "") {
return framework["edr"]
}
return {
name: "EDR :default",
large_image: parsedDatatypeImages()["EDR & AV"],
@@ -67,10 +70,14 @@ export const findSpecificApp = (framework, inputcategory) => {
id: "",
}
} else if (category === "communication" || category === "comms") {
if (framework["Comms"] !== undefined && framework["Comms"].name !== undefined) {
if (framework["Comms"] !== undefined && framework["Comms"].name !== undefined && framework["Comms"].name !== "") {
return framework["Comms"]
}
if (framework["communication"] !== undefined && framework["communication"].name !== undefined && framework["communication"].name !== "") {
return framework["communication"]
}
return {
name: "COMMS :default",
large_image: parsedDatatypeImages()["COMMS"],
@@ -79,10 +86,14 @@ export const findSpecificApp = (framework, inputcategory) => {
id: "",
}
} else if (category === "email") {
if (framework["Email"] !== undefined && framework["Email"].name !== undefined) {
if (framework["Email"] !== undefined && framework["Email"].name !== undefined && framework["Email"].name !== "") {
return framework["Email"]
}
if (framework["email"] !== undefined && framework["email"].name !== undefined && framework["email"].name !== "") {
return framework["email"]
}
return {
name: "COMMS :default",
large_image: parsedDatatypeImages()["COMMS"],
@@ -91,10 +102,14 @@ export const findSpecificApp = (framework, inputcategory) => {
id: "",
}
} else if (category === "assets") {
if (framework["Assets"] !== undefined && framework["Assets"].name !== undefined) {
if (framework["Assets"] !== undefined && framework["Assets"].name !== undefined && framework["Assets"].name !== "") {
return framework["Assets"]
}
if (framework["assets"] !== undefined && framework["assets"].name !== undefined && framework["assets"].name !== "") {
return framework["assets"]
}
return {
name: "ASSETS :default",
large_image: parsedDatatypeImages()["ASSETS"],
@@ -103,10 +118,14 @@ export const findSpecificApp = (framework, inputcategory) => {
id: "",
}
} else if (category === "cases") {
if (framework["Cases"] !== undefined && framework["Cases"].name !== undefined) {
if (framework["Cases"] !== undefined && framework["Cases"].name !== undefined && framework["Cases"].name !== "") {
return framework["Cases"]
}
if (framework["cases"] !== undefined && framework["cases"].name !== undefined && framework["cases"].name !== "") {
return framework["cases"]
}
return {
name: "CASES :default",
large_image: parsedDatatypeImages()["CASES"],
@@ -115,10 +134,14 @@ export const findSpecificApp = (framework, inputcategory) => {
id: "",
}
} else if (category === "iam") {
if (framework["IAM"] !== undefined && framework["IAM"].name !== undefined) {
if (framework["IAM"] !== undefined && framework["IAM"].name !== undefined && framework["IAM"].name !== "") {
return framework["IAM"]
}
if (framework["iam"] !== undefined && framework["iam"].name !== undefined && framework["iam"].name !== "") {
return framework["iam"]
}
return {
name: "IAM :default",
large_image: parsedDatatypeImages()["IAM"],
@@ -127,10 +150,14 @@ export const findSpecificApp = (framework, inputcategory) => {
id: "",
}
} else if (category === "network") {
if (framework["Network"] !== undefined && framework["Network"].name !== undefined) {
if (framework["Network"] !== undefined && framework["Network"].name !== undefined && framework["Network"].name !== "") {
return framework["Network"]
}
if (framework["network"] !== undefined && framework["network"].name !== undefined && framework["network"].name !== "") {
return framework["network"]
}
return {
name: "Network :default",
large_image: parsedDatatypeImages()["NETWORK"],
@@ -139,10 +166,14 @@ export const findSpecificApp = (framework, inputcategory) => {
id: "",
}
} else if (category === "intel") {
if (framework["Intel"] !== undefined && framework["Intel"].name !== undefined) {
if (framework["Intel"] !== undefined && framework["Intel"].name !== undefined && framework["Intel"].name !== "") {
return framework["Intel"]
}
if (framework["intel"] !== undefined && framework["intel"].name !== undefined && framework["intel"].name !== "") {
return framework["intel"]
}
return {
name: "INTEL :default",
large_image: parsedDatatypeImages()["INTEL"],
@@ -151,9 +182,13 @@ export const findSpecificApp = (framework, inputcategory) => {
id: "",
}
} else if (category === "siem") {
if (framework["SIEM"] !== undefined && framework["SIEM"].name !== undefined) {
if (framework["SIEM"] !== undefined && framework["SIEM"].name !== undefined && framework["SIEM"].name !== "") {
return framework["SIEM"]
}
if (framework["siem"] !== undefined && framework["siem"].name !== undefined && framework["siem"].name !== "") {
return framework["siem"]
}
return {
name: "SIEM :default",
@@ -1094,7 +1129,7 @@ const AppFramework = (props) => {
}, [newSelectedApp])
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true");
const imgSize = 50;
var parsedFrameworkData = frameworkData === undefined ? {} : frameworkData
@@ -1937,14 +1972,14 @@ const AppFramework = (props) => {
{data.name}
</Typography>
<div style={{display: "flex", width: 200, margin: "auto", marginTop: 15, }}>
<div style={{backgroundColor: theme.palette.inputColor, height: 75, width: 75, borderRadius: theme.palette.borderRadius, border: "1px solid rgba(255,255,255,0.7)", marginRight: 15, position: "relative",}}>
<div style={{backgroundColor: theme.palette.inputColor, height: 75, width: 75, borderRadius: theme.palette?.borderRadius, border: "1px solid rgba(255,255,255,0.7)", marginRight: 15, position: "relative",}}>
{parsedLeftImage}
{parsedLeftText}
</div>
<div style={{backgroundColor: theme.palette.inputColor, maxHeight: 30, maxWidth: 30, height: 30, width: 30, borderRadius: theme.palette.borderRadius, border: "1px solid rgba(255,255,255,0.7)", marginTop: 22, padding: "10px 0px 0px 9px",}}>
<div style={{backgroundColor: theme.palette.inputColor, maxHeight: 30, maxWidth: 30, height: 30, width: 30, borderRadius: theme.palette?.borderRadius, border: "1px solid rgba(255,255,255,0.7)", marginTop: 22, padding: "10px 0px 0px 9px",}}>
{svgIcon}
</div>
<div style={{backgroundColor: theme.palette.inputColor, height: 75, width: 75, borderRadius: theme.palette.borderRadius, border: "1px solid rgba(255,255,255,0.7)", marginLeft: 15, position: "relative",}}>
<div style={{backgroundColor: theme.palette.inputColor, height: 75, width: 75, borderRadius: theme.palette?.borderRadius, border: "1px solid rgba(255,255,255,0.7)", marginLeft: 15, position: "relative",}}>
{parsedRightImage}
{parsedRightText}
</div>
@@ -2152,7 +2187,7 @@ const AppFramework = (props) => {
{
Object.getOwnPropertyNames(discoveryData).length > 0 ?
<Paper style={{width: 300, maxHeight: 400, overflow: "hidden", zIndex: 12500, padding: 25, paddingRight: 25, backgroundColor: theme.palette.surfaceColor, border: "1px solid rgba(255,255,255,0.2)", position: "absolute", top: -50, left: 50, }}>
<Paper style={{width: 300, maxHeight: 400, overflow: "hidden", zIndex: 12500, padding: 25, paddingRight: 25, backgroundColor: theme.palette.surfaceColor, border: "1px solid rgba(255,255,255,0.2)", position: "absolute", top: -50, left: isMobile?-50: 50, }}>
{paperTitle.length > 0 ?
<span>
<Typography variant="h6" style={{textAlign: "center"}}>
@@ -2321,7 +2356,7 @@ const AppFramework = (props) => {
elements={elements}
minZoom={0.35}
maxZoom={2.00}
style={{width: 560*scale, height: 560*scale, backgroundColor: theme.palette.backgroundColor, margin: "auto",}}
style={{width: isMobile?null:560*scale, height: 560*scale, backgroundColor: theme.palette.backgroundColor, margin: isMobile?null:"auto",}}
stylesheet={frameworkStyle}
boxSelectionEnabled={false}
panningEnabled={false}
+97 -13
View File
@@ -79,6 +79,7 @@ const AppGrid = (props) => {
const [formMail, setFormMail] = React.useState("");
const [message, setMessage] = React.useState("");
const [formMessage, setFormMessage] = React.useState("");
const [deactivatedIndexes, setDeactivatedIndexes] = React.useState([]);
const buttonStyle = {
borderRadius: 30,
@@ -232,6 +233,11 @@ const AppGrid = (props) => {
removeQuery("q");
refine(event.currentTarget.value);
}}
onKeyDown={(event) => {
if(event.key === "Enter") {
event.preventDefault();
}
}}
limit={5}
/>
{/*isSearchStalled ? 'My search is stalled' : ''*/}
@@ -293,7 +299,7 @@ const AppGrid = (props) => {
useEffect(() => {
var baseurl = globalUrl;
fetch(baseurl + "/api/v1/getinfo", {
fetch(baseurl + "/api/v1/me", {
credentials: "include",
headers: {
'Content-Type': 'application/json',
@@ -347,7 +353,7 @@ const AppGrid = (props) => {
if (responseJson.success === false) {
toast.error(responseJson.reason);
} else {
toast.success(`App ${type}d Successfully!`);
//toast.success(`App ${type}d Successfully!`);
if (type === 'activate') {
setAllActivatedAppIds(prev => [...prev, data.objectID]);
setIsAnyAppActivated(true);
@@ -390,7 +396,7 @@ const AppGrid = (props) => {
{!isLoading ? (
<div>
{hits.length === 0 && searchQuery.length >= 0 && showNoAppFound ? (
<Typography variant="body1" style={{ marginTop: '30%' }}>No App Found</Typography>
<Typography variant="body1" style={{ marginTop: '30%' }}>No Apps Found</Typography>
) : (
<Grid item spacing={2} justifyContent="flex-start">
<div
@@ -458,8 +464,17 @@ const AppGrid = (props) => {
}}
>
<img
id={`image_${index}`}
alt={data.name}
src={data.image_url ? data.image_url : "/images/no_image.png"}
onError={(e) => {
// Replace the image with the default image
const foundImage = document.getElementById(`image_${index}`)
if (foundImage !== undefined && foundImage !== null) {
foundImage.src = theme.palette.defaultImage
data.image_url = theme.palette.defaultImage
}
}}
style={{
width: 80,
height: 80,
@@ -989,11 +1004,16 @@ const AppGrid = (props) => {
}}
autoComplete="off"
color="primary"
placeholder="Search your Activated or Self-built apps"
placeholder="Search your Activated or self-built apps"
id="shuffle_search_field"
onChange={(event) => {
setSearchQuery(event.currentTarget.value);
}}
onKeyDown={(event) => {
if(event.key === "Enter") {
event.preventDefault();
}
}}
limit={5}
/>
{/*isSearchStalled ? 'My search is stalled' : ''*/}
@@ -1541,9 +1561,13 @@ const AppGrid = (props) => {
<Typography variant="h5" style={{ marginBottom: 30, marginTop: 30, fontWeight: "400", fontSize: 24 }}>
Filter By
</Typography>
<FilterUsersAndOrgsAppByCategory selectedCategoryForUsersAndOgsApps={selectedCategoryForUsersAndOgsApps} setselectedCategoryForUsersAndOgsApps={setselectedCategoryForUsersAndOgsApps} />
<FilterUsersAndOrgsAppByActionLabel selectedTagsForUserAndOrgApps={selectedTagsForUserAndOrgApps} setSelectedTagsForUserAndOrgApps={setSelectedTagsForUserAndOrgApps} />
<FilterUsersAndOrgsAppByCreatedWith selectedOptionOfCreatedWith={selectedOptionOfCreatedWith} setSelectedOptionOfCreatedWith={setSelectedOptionOfCreatedWith} />
<FilterUsersAndOrgsAppCreatedBy />
</div>
)}
@@ -1712,6 +1736,10 @@ const AppGrid = (props) => {
? `/apps/${data.id}`
: `https://shuffler.io/apps/${data.id}`;
if (data.name === "" && data.id === "") {
return null
}
return (
<Zoom
key={index}
@@ -1808,17 +1836,71 @@ const AppGrid = (props) => {
width: 230,
textAlign: 'start',
marginLeft: 8,
color: "rgba(158, 158, 158, 1)"
color: "rgba(158, 158, 158, 1)",
display: "flex",
}}
>
{data.tags &&
data.tags.map((tag, tagIndex) => (
<span key={tagIndex}>
{normalizedString(tag)}
{tagIndex < data.tags.length - 1 ? ", " : ""}
</span>
))}
</div>
<div style={{minWidth: 120, overflow: "hidden", }}>
{data.generated !== true ?
<div>
{data.tags &&
data.tags.slice(0,2).map((tag, tagIndex) => (
<span key={tagIndex}>
{normalizedString(tag)}
{tagIndex < data.tags.length - 1 ? ", " : ""}
</span>
))
}
</div>
: null}
</div>
{currTab === 1 && !deactivatedIndexes.includes(index) && mouseHoverIndex === index && data.generated === true ?
<Button style={{
marginLeft: 15,
width: 102,
height: 35,
borderRadius: 200,
backgroundColor: "rgba(73, 73, 73, 1)",
color: "rgba(241, 241, 241, 1)",
textTransform: "none",
}}
onClick={(event) => {
//deactivatedIndexes.push(index)
//setDeactivatedIndexes(deactivatedIndexes)
event.preventDefault();
event.stopPropagation();
//handleActivateButton(event, data, "deactivate");
// FIXME: Put this in a function lol
const url = `${globalUrl}/api/v1/apps/${data.id}/deactivate`;
fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
credentials: "include",
})
.then((response) => response.json())
.then((responseJson) => {
if (responseJson.success === false) {
toast.error(responseJson.reason);
} else {
toast.success("App Deactivated Successfully. Reload UI to see updated changes.")
//const updatedIds = allActivatedAppIds.filter(id => id !== data.objectID);
//setAllActivatedAppIds(updatedIds);
}
})
.catch(error => {
console.log("app error: ", error.toString());
})
}}>
Deactivate
</Button>
: null}
</div>
{/* )} */}
</div>
</ButtonBase>
@@ -1936,6 +2018,7 @@ const AppGrid = (props) => {
selectedOptionOfCreatedWith={selectedOptionOfCreatedWith}
/>
)}
<AppTab
selectedCategoryForUsersAndOgsApps={selectedCategoryForUsersAndOgsApps}
selectedTagsForUserAndOrgApps={selectedTagsForUserAndOrgApps}
@@ -1944,6 +2027,7 @@ const AppGrid = (props) => {
setSelectedTagsForUserAndOrgApps={setSelectedTagsForUserAndOrgApps}
setSelectedOptionOfCreatedWith={setSelectedOptionOfCreatedWith}
/>
</div>
<Configure clickAnalytics />
</InstantSearch>
+759
View File
@@ -0,0 +1,759 @@
import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router';
import {
Dialog,
DialogTitle,
DialogContent,
IconButton,
Typography,
Box,
Button,
Stack,
Avatar,
} from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';
import EditIcon from '@mui/icons-material/Edit';
import Search from '@mui/icons-material/Search';
import AddIcon from '@mui/icons-material/Add';
import ForkRightIcon from '@mui/icons-material/ForkRight';
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
import LaunchIcon from '@mui/icons-material/Launch';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import { CloudDownloadOutlined } from '@mui/icons-material';
import { findSpecificApp } from './AppFramework';
import theme from "../theme";
import YAML from 'yaml';
import { toast } from 'react-toastify';
import { Link } from 'react-router-dom';
const AppModal = ({ open, onClose, app, globalUrl }) => {
const [frameworkData, setFrameworkData] = useState({})
const [userdata, setUserdata] = useState({})
const [usecases, setUsecases] = useState([])
const [workflows, setWorkflows] = useState([])
const [prevSubcase, setPrevSubcase] = useState({})
const [inputUsecase, setInputUsecase] = useState({})
const [latestUsecase, setLatestUsecase] = useState([])
const [foundAppUsecase, setFoundAppUsecase] = useState({})
const navigate = useNavigate();
const parseUsecase = (subcase) => {
const srcdata = findSpecificApp(frameworkData, subcase.type)
const dstdata = findSpecificApp(frameworkData, subcase.last)
if (srcdata !== undefined && srcdata !== null) {
subcase.srcimg = srcdata.large_image
subcase.srcapp = srcdata.name
}
if (dstdata !== undefined && dstdata !== null) {
subcase.dstimg = dstdata.large_image
subcase.dstapp = dstdata.name
}
return subcase
}
useEffect(() => {
var baseurl = globalUrl;
fetch(baseurl + "/api/v1/me", {
credentials: "include",
headers: {
'Content-Type': 'application/json',
},
})
.then(response => response.json())
.then(responseJson => {
if (responseJson.success) {
setUserdata(responseJson)
}
})
.catch(error => {
console.log("Failed login check: ", error);
});
}, [app]);
const getFramework = () => {
fetch(globalUrl + "/api/v1/apps/frameworkConfiguration", {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for framework!");
}
return response.json();
})
.then((responseJson) => {
if (responseJson.success === false) {
const preparedData = {
"siem": findSpecificApp({}, "SIEM"),
"communication": findSpecificApp({}, "COMMUNICATION"),
"assets": findSpecificApp({}, "ASSETS"),
"cases": findSpecificApp({}, "CASES"),
"network": findSpecificApp({}, "NETWORK"),
"intel": findSpecificApp({}, "INTEL"),
"edr": findSpecificApp({}, "EDR"),
"iam": findSpecificApp({}, "IAM"),
"email": findSpecificApp({}, "EMAIL"),
}
setFrameworkData(preparedData)
} else {
setFrameworkData(responseJson)
}
})
.catch((error) => {
console.log("Error getting framework: ", error)
})
}
const fetchUsecases = (workflows) => {
fetch(globalUrl + "/api/v1/workflows/usecases", {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for usecases");
}
return response.json();
})
.then((responseJson) => {
const newUsecases = [...usecases]
newUsecases.forEach((category, index) => {
category.list.forEach((subcase, subindex) => {
getUsecase(subcase, index, subindex)
})
})
setLatestUsecase(newUsecases)
// Matching workflows with usecases
if (responseJson.success !== false) {
if (workflows !== undefined && workflows !== null && workflows.length > 0) {
var categorydata = responseJson
var newcategories = []
for (var key in categorydata) {
var category = categorydata[key]
category.matches = []
for (var subcategorykey in category.list) {
var subcategory = category.list[subcategorykey]
subcategory.matches = []
for (var workflowkey in workflows) {
const workflow = workflows[workflowkey]
if (workflow.usecase_ids !== undefined && workflow.usecase_ids !== null) {
for (var usecasekey in workflow.usecase_ids) {
if (workflow.usecase_ids[usecasekey].toLowerCase() === subcategory?.name?.toLowerCase()) {
category.matches.push({
"workflow": workflow.id,
"category": subcategory?.name,
})
subcategory.matches.push(workflow)
break
}
}
}
if (subcategory.matches.length > 0) {
break
}
}
}
newcategories.push(category)
}
if (newcategories !== undefined && newcategories !== null && newcategories.length > 0) {
setUsecases(newcategories)
} else {
setUsecases(responseJson)
}
} else {
setUsecases(responseJson)
}
}
})
.catch((error) => {
//toast("ERROR: " + error.toString());
console.log("ERROR: " + error.toString());
});
};
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) {
fetchUsecases()
console.log("Status not 200 for workflows :O!: ", response.status);
return;
}
return response.json();
})
.then((responseJson) => {
fetchUsecases(responseJson)
if (responseJson !== undefined) {
setWorkflows(responseJson);
}
})
.catch((error) => {
fetchUsecases()
//toast(error.toString());
});
}
const getUsecase = (subcase, index, subindex) => {
subcase = parseUsecase(subcase)
setPrevSubcase(subcase)
fetch(`${globalUrl}/api/v1/workflows/usecases/${escape(subcase?.name?.replaceAll(" ", "_"))}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for framework!");
}
return response.json();
})
.then((responseJson) => {
var parsedUsecase = responseJson
if (responseJson.success === false) {
parsedUsecase = subcase
} else {
parsedUsecase = responseJson
parsedUsecase.srcimg = subcase.srcimg
parsedUsecase.srcapp = subcase.srcapp
parsedUsecase.dstimg = subcase.dstimg
parsedUsecase.dstapp = subcase.dstapp
}
// Look for the type of app and fill in img1, srcapp...
setInputUsecase(parsedUsecase)
})
.catch((error) => {
//toast(error.toString());
setInputUsecase(subcase)
console.log("Error getting usecase: ", error)
})
}
useEffect(() => {
getAvailableWorkflows()
getFramework()
}, [app])
useEffect(() => {
const foundCategory = latestUsecase?.find((category) =>
category?.list?.some((subcase) => subcase?.srcapp === app?.name || subcase?.dstapp === app?.name)
);
const foundSubcase = foundCategory?.list?.find(
(subcase) => subcase?.srcapp === app?.name || subcase?.dstapp === app?.name
);
setFoundAppUsecase(foundSubcase);
}, [latestUsecase])
const downloadApp = (inputdata) => {
const id = inputdata.id;
toast("Downloading..");
fetch(globalUrl + "/api/v1/apps/" + id + "/config", {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
window.location.pathname = "/apps";
}
return response.json();
})
.then((responseJson) => {
if (!responseJson.success) {
toast("Failed to download file");
} else {
console.log(responseJson);
const basedata = atob(responseJson.openapi);
console.log("BASE: ", basedata);
var inputdata = JSON.parse(basedata);
console.log("POST INPUT: ", inputdata);
inputdata = JSON.parse(inputdata.body);
const newpaths = {};
if (inputdata["paths"] !== undefined) {
Object.keys(inputdata["paths"]).forEach(function (key) {
newpaths[key.split("?")[0]] = inputdata.paths[key];
});
}
inputdata.paths = newpaths;
console.log("INPUT: ", inputdata);
var name = inputdata.info.title;
name = name.replace(/ /g, "_", -1);
name = name.toLowerCase();
delete inputdata.id;
delete inputdata.editing;
const data = YAML.stringify(inputdata);
var blob = new Blob([data], {
type: "application/octet-stream",
});
var url = URL.createObjectURL(blob);
var link = document.createElement("a");
link.setAttribute("href", url);
link.setAttribute("download", `${name}.yaml`);
var event = document.createEvent("MouseEvents");
event.initMouseEvent(
"click",
true,
true,
window,
1,
0,
0,
0,
0,
false,
false,
false,
false,
0,
null
);
link.dispatchEvent(event);
//link.parentNode.removeChild(link)
}
})
.catch((error) => {
console.log(error);
toast(error.toString());
});
};
const isCloud =
window.location.host === "localhost:3002" ||
window.location.host === "shuffler.io" || window.location.host === "localhost:3000"
? true
: false;
var newAppname = app?.name;
if (newAppname === undefined) {
newAppname = "Undefined";
} else {
newAppname = newAppname.charAt(0).toUpperCase() + newAppname.substring(1);
newAppname = newAppname?.replaceAll("_", " ");
}
var canEditApp = userdata.admin === "true" || userdata?.id === app?.owner || app?.owner === "" || (userdata.admin === "true" && userdata.active_org.id === app?.reference_org) || !app?.generated
return (
<Dialog
open={open}
onClose={onClose}
maxWidth="sm"
fullWidth
PaperProps={{
sx: {
borderRadius: 2,
border: "1px solid #494949",
minWidth: '440px',
fontFamily: theme?.typography?.fontFamily,
backgroundColor: "#212121",
'& .MuiDialogContent-root': {
backgroundColor: "#212121",
},
'& .MuiDialogTitle-root': {
backgroundColor: "#212121",
},
'& .MuiTypography-root': {
fontFamily: theme?.typography?.fontFamily,
},
'& .MuiButton-root': {
fontFamily: theme?.typography?.fontFamily,
},
}
}}
>
<DialogTitle
sx={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
pb: 2,
pt: 2,
pl: 3,
pr: 2,
fontFamily: theme?.typography?.fontFamily
}}
>
<Typography component="div" sx={{ fontWeight: 500, color: "#F1F1F1", fontSize: "22px" }}>
About {app?.name.replace(/_/g, ' ').replace(/\b\w/g, char => char.toUpperCase())}
</Typography>
<IconButton
onClick={onClose}
sx={{
color: 'rgba(255, 255, 255, 0.7)',
'&:hover': { bgcolor: 'rgba(255, 255, 255, 0.1)' }
}}
>
<CloseIcon />
</IconButton>
</DialogTitle>
<DialogContent sx={{ py: 3, px: 3 }}>
<Box sx={{ display: 'flex', alignItems: 'space-between', justifyContent: 'space-between' }}>
<div style={{ display: "flex", flexDirection: "row", gap: 10, fontFamily: theme?.typography?.fontFamily }}>
<img
alt={app?.name}
src={app?.large_image || app?.image_url}
style={{
borderRadius: 4,
maxWidth: 100,
minWidth: 100,
maxHeight: "100%",
display: "block",
margin: "0 auto",
boxShadow: "0px 0px 10px 0px rgba(0, 0, 0, 0.2)"
}}
/>
<div style={{ display: "flex", flexDirection: "column", justifyContent: "center", paddingLeft: 8 }}>
<div style={{
display: "flex",
flexDirection: "row",
}}>
<Typography variant="h5" component="div" sx={{ fontWeight: 600 }}>
{newAppname}
</Typography>
{
isCloud && (
<Link
to={"/apps/" + (app?.id || app?.objectID)}
style={{ textDecoration: "none", color: "#f85a3e", marginTop: "-2px" }}
>
<IconButton
style={{
color: "#f85a3e",
fontSize: 20,
}}
>
<OpenInNewIcon />
</IconButton>
</Link>
)
}
</div>
<Typography
variant="body2"
color="textSecondary"
>
{app?.categories ? app.categories.join(", ") : "Communication"}
</Typography>
</div>
</div>
<div style={{ display: "flex", flexDirection: "row", justifyContent: "center", alignItems: "center", gap: 10 }}>
{app?.activated &&
app?.private_id !== undefined &&
app?.private_id?.length > 0 &&
app?.generated ? (
<Button
variant="contained"
sx={{
bgcolor: '#494949',
'&:hover': { bgcolor: '#494949' },
textTransform: 'none',
borderRadius: 1,
minWidth: '45px',
width: '45px',
height: '40px',
padding: 2,
color: "#fff",
fontFamily: theme?.typography?.fontFamily
}}
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
downloadApp(app);
}}
>
<CloudDownloadOutlined />
</Button>) : null}
<Button
variant="contained"
sx={{
bgcolor: "#494949",
'&:hover': { bgcolor: '#494949' },
textTransform: 'none',
borderRadius: 1,
py: 1,
px: 3,
height: '40px',
color: "#fff",
fontFamily: theme?.typography?.fontFamily
}}
startIcon={canEditApp ? <EditIcon /> :
(app?.generated && app?.activated && userdata?.id !== app?.owner && isCloud ?
<ForkRightIcon /> : null
)}
onClick={() => {
if (canEditApp) {
const editUrl = "/apps/edit/" + (app?.id || app?.objectID);
navigate(editUrl)
}else{
const forkUrl = "/apps/new?id=" + (app?.id || app?.objectID);
navigate(forkUrl)
}
}}
>
{canEditApp ? "Edit" : "Fork"}
</Button>
</div>
</Box>
<div style={{
display: "flex",
justifyContent: "space-between",
fontFamily: theme?.typography?.fontFamily,
padding: "26px 0px"
}}>
<div style={{
textAlign: "start",
flex: 1,
}}>
<Typography
variant="h6"
sx={{
fontFamily: theme?.typography?.fontFamily,
fontSize: '24px',
fontWeight: 600,
mb: 0.3,
color: '#fff'
}}
>
20
</Typography>
<Typography
variant="body2"
sx={{
color: 'rgba(255, 255, 255, 0.7)',
fontFamily: theme?.typography?.fontFamily,
fontSize: '14px'
}}
>
Public Workflow
</Typography>
</div>
<div style={{
flex: 1,
textAlign: "start",
borderLeft: "1px solid rgba(255, 255, 255, 0.12)",
paddingLeft: "10px",
height: "100%",
}}>
<Typography variant="h6"
sx={{
fontWeight: 600,
mb: 0.3,
color: '#fff'
}}>
{Array.isArray(app?.actions) ? app.actions.length : app?.actions}
</Typography>
<Typography variant="body2" sx={{ color: 'rgba(255, 255, 255, 0.7)' }}>
Actions
</Typography>
</div>
<div style={{
borderLeft: "1px solid rgba(255, 255, 255, 0.12)",
flex: 1,
paddingLeft: "10px",
paddingTop: "5px"
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: '4px', marginBottom: "5px", fontFamily: theme?.typography?.fontFamily, fontSize: "14px", fontWeight: 600, color: 'white' }}>
{
app?.collection ? (
<>
<CheckCircleIcon sx={{ color: '#4CAF50' }} />
<Typography variant="body1" sx={{
fontWeight: 500,
color: '#fff',
marginTop: "1px",
fontFamily: theme?.typography?.fontFamily,
fontSize: "16px"
}}>
app.collection
</Typography>
</>
) : (
<Typography sx={{
fontSize: "16px",
fontWeight: 500,
marginTop: "1px",
color: 'rgba(255, 255, 255, 0.7)',
fontFamily: theme?.typography?.fontFamily
}}>
No collection yet
</Typography>
)
}
</div>
<Typography variant="body2" sx={{ color: 'rgba(158, 158, 158, 1)' }}>
Part of a collection
</Typography>
</div>
</div>
<div style={{
display: "flex",
flexDirection: "column",
justifyContent: "start",
width: "100%"
}}>
<div style={{
fontFamily: theme?.typography?.fontFamily,
fontSize: "16px",
color: "#fff",
marginBottom: "16px",
fontWeight: 600
}}>
{
(foundAppUsecase?.srcapp !== undefined && foundAppUsecase?.dstapp !== undefined) ? (
"Connect " + foundAppUsecase?.srcapp?.replaceAll("_", " ") + " to " + foundAppUsecase?.dstapp?.replaceAll("_", " ")
) : (
"Connect " + app?.name + " to any tool"
)
}
</div>
<Box sx={{
bgcolor: '#2F2F2F',
p: 2,
borderRadius: 2,
display: 'flex',
alignItems: 'center',
mb: 3
}}>
<Stack direction="row" spacing={-1}>
{
foundAppUsecase === undefined ? (
<Avatar sx={{ width: 32, height: 32, bgcolor: 'background.paper', border: 1, borderColor: 'divider' }}>
<Search sx={{ color: 'text.primary', zIndex: 10, fontSize: 18 }} />
</Avatar>
) : (
<Avatar
src={foundAppUsecase?.srcimg}
sx={{
width: 32,
height: 32,
bgcolor: 'background.paper',
border: 1,
borderColor: 'divider',
zIndex: 10
}}
/>
)
}
{
foundAppUsecase === undefined ? (
<Avatar sx={{ width: 32, height: 32, bgcolor: 'background.paper', border: 1, borderColor: 'divider' }}>
<AddIcon sx={{ color: 'text.primary', zIndex: 10, fontSize: 18 }} />
</Avatar>
) : (
<Avatar
src={foundAppUsecase?.dstimg}
sx={{
width: 32,
height: 32,
bgcolor: 'background.paper',
border: 1,
borderColor: 'divider',
zIndex: 10
}}
/>
)
}
</Stack>
<Typography sx={{ ml: 2, fontSize: "16px", letterSpacing: "0.5px" }}>
{foundAppUsecase?.name || "Search for a Usecase"}
</Typography>
</Box>
</div>
<div style={{ display: "flex", justifyContent: "center", fontFamily: theme?.typography?.fontFamily }}>
<Button
variant="contained"
sx={{
bgcolor: '#FF8544',
'&:hover': { bgcolor: '#FF8544' },
textTransform: 'none',
borderRadius: 1,
py: 1,
px: 7,
fontSize: "14px",
letterSpacing: "0.5px",
color: "black",
fontFamily: theme?.typography?.fontFamily,
minWidth: '200px'
}}
onClick={() => {
navigate("/usecases2")
}}
>
Find a Usecase
</Button>
</div>
</DialogContent>
</Dialog>
);
};
export default AppModal;
+21 -6
View File
@@ -2,7 +2,7 @@ import React, { useState, useEffect, useRef } from "react";
import theme from '../theme.jsx';
import ReactGA from 'react-ga4';
import { useNavigate, Link } from 'react-router-dom';
import { isMobile } from 'react-device-detect';
import { Search as Searchicon, CloudQueue as CloudQueueicon, Code as Codeicon, Close as Closeicon, Folder as Foldericon, LibraryBooks as LibraryBooksicon, Delete as DeleteIcon, Close as CloseIcon, } from '@mui/icons-material';
import aa from 'search-insights'
import Deleteicon from '@mui/icons-material/Delete';
@@ -47,6 +47,8 @@ const AppSearchButtons = (props) => {
const [newSelectedApp, setNewSelectedApp] = useState(undefined)
useEffect(() => {
console.log("UPDATED APP: ", newSelectedApp)
if (newSelectedApp !== undefined && setMissing != undefined) {
const submitAppFramework = {
"description": newSelectedApp.description,
@@ -136,8 +138,14 @@ const AppSearchButtons = (props) => {
const icon = foundApp.large_image
var foundAppImage = AppImage
if (foundApp.name !== undefined && foundApp.name !== null && !foundApp.name.includes(":default")) {
foundAppImage = foundApp.large_image
if (foundApp.name !== undefined && foundApp.name !== null && foundApp.name.length > 0 && !foundApp.name.includes(":default")) {
if (AppImage === undefined || AppImage === null || AppImage.length < 10) {
foundAppImage = foundApp.large_image
}
} else {
const newapp = findSpecificApp(appFramework, appType)
// const { userdata, globalUrl, appFramework, moreButton, finishedApps, appType, totalApps, index, onNodeSelect, setDiscoveryData, appName, AppImage, setDefaultSearch, discoveryData, checkLogin, setMissing, getAppFramework, } = props
}
let xsValue = 12;
@@ -181,7 +189,7 @@ const AppSearchButtons = (props) => {
width: 319,
height: 395,
flexShrink: 0,
marginLeft: 70,
marginLeft: isMobile? null:70,
marginTop: 68,
position: "absolute",
zIndex: 100,
@@ -204,9 +212,14 @@ const AppSearchButtons = (props) => {
<IconButton
style={{
flex: 1,
position: "absolute",
right: 0,
top: 10,
height: 10,
// width: 224,
marginLeft: discoveryData === ("communication") ? 112 : 200,
width: "100%",
//marginLeft: discoveryData === ("communication") ? 112 : 200,
//width: "100%",
marginBottom: 23,
fontSize: 16,
background: "rgba(33, 33, 33, 1)",
@@ -220,6 +233,7 @@ const AppSearchButtons = (props) => {
<Closeicon style={{ width: 16 }} />
</IconButton>
</Tooltip>
{/*
<Tooltip
title="Delete app"
placement="bottom"
@@ -259,6 +273,7 @@ const AppSearchButtons = (props) => {
<DeleteIcon style={{ height: 15, width: 15, }} />
</IconButton>
</Tooltip>
*/}
</div>
</div>
<div
+348 -294
View File
@@ -23,6 +23,7 @@ import {
Paper,
TextField,
Collapse,
Fade,
IconButton,
Avatar,
ButtonBase,
@@ -41,10 +42,13 @@ const AppSelection = props => {
userdata,
globalUrl,
appFramework,
setAppFramework,
setActiveStep,
defaultSearch,
setDefaultSearch,
checkLogin,
isAppPage=false
} = props;
const [discoveryData, setDiscoveryData] = React.useState({})
const [selectionOpen, setSelectionOpen] = React.useState(false)
@@ -57,9 +61,10 @@ const AppSelection = props => {
const [moreButton, setMoreButton] = useState(false);
// const [mouseHoverIndex, setMouseHoverIndex] = useState(-1)
document.title = "Choose your apps"
const ref = useRef()
let navigate = useNavigate();
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true");
useEffect(() => {
if (newSelectedApp === undefined || newSelectedApp.objectID === undefined || newSelectedApp.objectID === undefined || newSelectedApp.objectID.length === 0) {
@@ -98,74 +103,75 @@ const AppSelection = props => {
else if (discoveryData === "IAM") {
appFramework.iam = submitNewApp
}
setFrameworkItem(submitNewApp);
setSelectionOpen(false);
console.log("Selected app changed (effect)");
setFrameworkItem(submitNewApp)
setSelectionOpen(false)
if (setAppFramework !== undefined) {
setAppFramework(appFramework)
}
GetApps()
}, [newSelectedApp]);
const reloadAppButtons = (framework) => {
var tempApps = []
const lastApps = {}
let endTypes = ["network", "assets", "iam"]
if (framework === undefined || framework === null || Object.keys(framework).length === 0) {
//window.location.href = "/welcome"
return
}
Object.entries(framework).forEach(([key, value]) => {
// Overwrrite email properly
if (key.toLowerCase() === "communication") {
value["type"] = "email"
framework["email"] = value
return
}
if (key.toLowerCase() === "other") {
return
}
value.type = key;
if (endTypes.includes(value.type.toLowerCase())) {
lastApps[value.type] = value
return
}
if (lastPosted.type === value.type) {
value = lastPosted
}
tempApps.push(JSON.parse(JSON.stringify(value)));
});
tempApps.sort((a, b) => {
if (a.type.length > b.type.length) {
return -1;
} else if (a.type.length < b.type.length) {
return 1;
}
});
let lastType = lastPosted.type === undefined ? "" : lastPosted.type.toLowerCase()
if (endTypes.includes(lastType)) {
lastApps[lastPosted.type] = lastPosted
}
if (moreButton) {
tempApps.push(JSON.parse(JSON.stringify(lastApps["network"])))
tempApps.push(JSON.parse(JSON.stringify(lastApps["assets"])))
tempApps.push(JSON.parse(JSON.stringify(lastApps["iam"])))
}
setAppButtons(tempApps)
}
useEffect(() => {
var tempApps = []
if (tempApps.length === 0) {
// Object.entries(appFramework).forEach(([key, value]) => {
// value.type = key;
// tempApps.push(value);
// });
// // Define the custom sorting order
// const customSortingOrder = ["CASES", "SIEM", "ENDPOINT", "INTEL", "EMAIL"];
const lastApps = {}
let endTypes = ["network", "assets", "iam"]
if (appFramework === undefined || appFramework === null || Object.keys(appFramework).length === 0) {
//window.location.href = "/welcome"
return
}
Object.entries(appFramework).forEach(([key, value]) => {
if (key.toLowerCase() === "other" || key.toLowerCase() === "communication") {
return
}
value.type = key;
if (endTypes.includes(value.type.toLowerCase())) {
lastApps[value.type] = value
return
}
if (lastPosted.type === value.type) {
value = lastPosted
}
tempApps.push(JSON.parse(JSON.stringify(value)));
});
tempApps.sort((a, b) => {
if (a.type.length > b.type.length) {
return -1;
} else if (a.type.length < b.type.length) {
return 1;
}
});
let lastType = lastPosted.type === undefined ? "" : lastPosted.type.toLowerCase()
if (endTypes.includes(lastType)) {
lastApps[lastPosted.type] = lastPosted
}
if (moreButton) {
tempApps.push(JSON.parse(JSON.stringify(lastApps["network"])))
tempApps.push(JSON.parse(JSON.stringify(lastApps["assets"])))
tempApps.push(JSON.parse(JSON.stringify(lastApps["iam"])))
}
setAppButtons(tempApps)
console.log("Updated appButtons: ", appButtons)
GetApps()
}
reloadAppButtons(appFramework)
}, [lastPosted, moreButton])
if (appFramework === undefined || appFramework === null || Object.keys(appFramework).length === 0) {
@@ -174,11 +180,6 @@ const AppSelection = props => {
}
const setFrameworkItem = (data) => {
console.log("Setting framework item: ", data, isCloud)
// if (!isCloud) {
// activateApp(data.id)
// }
fetch(globalUrl + "/api/v1/apps/frameworkConfiguration", {
method: "POST",
headers: {
@@ -241,31 +242,39 @@ const AppSelection = props => {
body: JSON.stringify(data),
credentials: "include",
})
.then((responseJson) => {
if (responseJson === null) {
console.log("null-response from server")
const pretend_apps = [{
"description": "TBD",
"id": "TBD",
"large_image": "",
"name": "TBD",
"type": "TBD"
}]
.then((response) => {
return response.json()
})
.then((responseJson) => {
if (responseJson === null || responseJson === undefined) {
console.log("null-response from server")
const pretend_apps = [{
"description": "TBD",
"id": "TBD",
"large_image": "",
"name": "TBD",
"type": "TBD"
}]
setApps(pretend_apps)
return
}
setApps(pretend_apps)
return
}
if (responseJson.success === false) {
console.log("error loading apps: ", responseJson)
return
}
if (responseJson.success === false) {
console.log("error loading apps: ", responseJson)
return
}
setApps(responseJson);
})
.catch((error) => {
console.log("App loading error: " + error.toString());
})
if (setAppFramework !== undefined) {
setAppFramework(responseJson)
}
setApps(responseJson)
reloadAppButtons(responseJson)
})
.catch((error) => {
console.log("App loading error: " + error.toString());
})
}
const onNodeSelect = (label) => {
@@ -277,8 +286,6 @@ const AppSelection = props => {
});
}
console.log("NODESELECT: ", label)
setDiscoveryData(label)
setSelectionOpen(true)
setDefaultSearch(label.charAt(0).toUpperCase() + (label.substring(1)).toLowerCase())
@@ -304,209 +311,256 @@ const AppSelection = props => {
};
return (
<Collapse in={true}>
<Tooltip
title="Back"
placement="top"
style={{ zIndex: 10011 }}
>
<IconButton
style={{
}}
onClick={() => {
navigate('/welcome');
window.location.reload();
}}
>
<ArrowBackIcon style={{ width: 20 }} />
<Typography style={{fontSize : 16, marginLeft : 2}}>Back</Typography>
</IconButton>
</Tooltip>
<div
style={{
// minHeight: sizing,
// maxHeight: sizing,
marginTop: 10,
width: isMobile ? 350 : 500,
marginBottom: 25,
textAlign: isMobile ? "center" : null,
}}
>
{selectionOpen ? (
<div
style={{
width: isMobile ? 225 : 319,
height: 395,
flexShrink: 0,
marginLeft: 70,
marginTop: 68,
position: "absolute",
zIndex: 100,
borderRadius: 6,
border: "1px solid var(--Container-Stroke, #494949)",
background: "var(--Container, #212121)",
boxShadow: "8px 8px 32px 24px rgba(0, 0, 0, 0.16)",
}}
>
<div style={{ display: "flex" }}>
<div style={{ display: "flex", textAlign: "center", textTransform: "capitalize" }}>
<Typography style={{ padding: 16, color: "#FFFFFF", textTransform: "capitalize" }}> {discoveryData} </Typography>
</div>
<div style={{ display: "flex" }}>
<Tooltip
title="Close"
placement="top"
style={{ zIndex: 10011 }}
>
<IconButton
style={{
flex: 1,
// width: 224,
marginLeft: discoveryData === ("communication") ? 112 : 200,
width: "100%",
marginBottom: 23,
fontSize: 16,
background: "rgba(33, 33, 33, 1)",
borderColor: "rgba(33, 33, 33, 1)",
borderRadius: 8,
}}
onClick={() => {
setSelectionOpen(false)
}}
>
<CloseIcon style={{ width: 16 }} />
</IconButton>
</Tooltip>
<Tooltip
title="Delete app"
placement="bottom"
style={{ zIndex: 10011 }}
>
<IconButton
style={{ zIndex: 12501, position: "absolute", top: 32, right: 16 }}
onClick={(e) => {
e.preventDefault();
setSelectionOpen(false)
setDefaultSearch("")
const submitDeletedApp = {
"description": "",
"id": "remove",
"name": "",
"type": discoveryData
}
setFrameworkItem(submitDeletedApp)
setNewSelectedApp({})
setTimeout(() => {
setDiscoveryData({})
setFrameworkItem(submitDeletedApp)
setNewSelectedApp({})
}, 1000)
//setAppName(discoveryData.cases.name)
}}
>
<DeleteIcon style={{ color: "white", height: 15, width: 15, }} />
</IconButton>
</Tooltip>
</div>
</div>
<div
style={{ width: "100%", border: "1px #494949 solid" }}
/>
<AppSearch
defaultSearch={defaultSearch}
newSelectedApp={newSelectedApp}
setNewSelectedApp={setNewSelectedApp}
userdata={userdata}
// cy={cy}
/>
</div>
) : null}
<Typography
variant="h4"
style={{
marginLeft: 8,
marginTop: isMobile ? null : 40,
marginRight: 30,
marginBottom: 0,
}}
color="rgba(241, 241, 241, 1)"
>
Find your apps
</Typography>
<Typography
variant="body2"
style={{
marginLeft: 8,
marginTop: 10,
marginRight: 30,
marginBottom: 40,
}}
color="rgba(158, 158, 158, 1)"
>
Select the apps you work with and we will connect them for you.
</Typography>
<Grid rowSpacing={1} columnSpacing={2} container >
{appButtons.map((appData, index) => {
// This is here due to a memory issue with setting apps properly
if (appData.id === "remove") {
console.log("Removed as appdata is overridden: ", appData)
<Fade in={true} timeout={1250}>
<div>
{/*
<Tooltip
title="Back"
placement="top"
style={{ zIndex: 10011 }}
>
<IconButton
style={{
}}
onClick={() => {
navigate('/welcome');
window.location.reload();
}}
>
<ArrowBackIcon style={{ width: 20 }} />
<Typography style={{fontSize : 16, marginLeft : 2}}>Back</Typography>
</IconButton>
</Tooltip>
*/}
<div
style={{
// minHeight: sizing,
// maxHeight: sizing,
marginTop: 10,
width: isMobile ? 350 : 500,
marginBottom: 25,
textAlign: isMobile ? "center" : null,
}}
>
{selectionOpen ? (
<div
style={{
width: isMobile ? 225 : 319,
height: 395,
flexShrink: 0,
marginLeft: 70,
marginTop: 68,
position: "absolute",
zIndex: 100,
borderRadius: 6,
border: "1px solid var(--Container-Stroke, #494949)",
background: "var(--Container, #212121)",
boxShadow: "8px 8px 32px 24px rgba(0, 0, 0, 0.16)",
}}
>
<div style={{ display: "flex" }}>
<div style={{ display: "flex", textAlign: "center", textTransform: "capitalize" }}>
<Typography style={{ padding: 16, color: "#FFFFFF", textTransform: "capitalize" }}> {discoveryData} </Typography>
</div>
<div style={{ display: "flex" }}>
<Tooltip
title="Close"
placement="top"
style={{ zIndex: 10011 }}
>
<IconButton
style={{
flex: 1,
appData = {
"count": 0,
"description": "",
"id": "",
"large_image": "",
"name": "",
"type": appData.type,
}
}
position: "absolute",
top: 0,
right: 6,
}}
onClick={() => {
setSelectionOpen(false)
}}
>
<CloseIcon style={{ width: 16 }} />
</IconButton>
</Tooltip>
<Tooltip
title="Remove selection"
placement="bottom"
style={{ zIndex: 10011 }}
>
<IconButton
style={{ zIndex: 12501, position: "absolute", top: 26, right: 6, }}
onClick={(e) => {
e.preventDefault();
setSelectionOpen(false)
setDefaultSearch("")
const submitDeletedApp = {
"description": "",
"id": "remove",
"name": "",
"type": discoveryData
}
const appName = appData.name
const AppImage = appData.large_image
const appType = appData.type
setFrameworkItem(submitDeletedApp)
setNewSelectedApp({})
setTimeout(() => {
setDiscoveryData({})
setFrameworkItem(submitDeletedApp)
setNewSelectedApp({})
}, 200)
//setAppName(discoveryData.cases.name)
}}
>
<DeleteIcon style={{ color: "white", height: 15, width: 15, }} />
</IconButton>
</Tooltip>
</div>
</div>
<div
style={{ width: "100%", border: "1px #494949 solid" }}
/>
<AppSearch
defaultSearch={defaultSearch}
newSelectedApp={newSelectedApp}
setNewSelectedApp={setNewSelectedApp}
userdata={userdata}
// cy={cy}
/>
</div>
) : null}
{
!isAppPage && (
<>
<Typography
variant="h4"
style={{
marginLeft: 8,
marginTop: isMobile ? null : 40,
marginRight: 30,
marginBottom: 0,
}}
color="rgba(241, 241, 241, 1)"
>
Find your apps
</Typography>
<Typography
variant="body2"
style={{
marginLeft: 8,
marginTop: 10,
marginRight: 30,
marginBottom: 40,
}}
color="rgba(158, 158, 158, 1)"
>
Select the apps you work with and we will connect them for you.
</Typography>
</>
)
}
{
isAppPage && (
<div style={{marginBottom: 20}}>
<span
style={{
fontSize: 16,
color: "rgba(158, 158, 158, 1)",
fontFamily: theme?.typography?.fontFamily,
}}
>
Your organization has no apps yet, select your starting apps here
or discover more apps using the <span
onClick={() => {
navigate("/apps2?tab=all_apps")
}}
style={{ color: "#FF8444", fontWeight: "medium", fontSize: 16, cursor:"pointer" }}>App Library</span>
</span>
</div>
)
}
<Grid rowSpacing={1} columnSpacing={2} container >
{appButtons.map((appData, index) => {
// This is here due to a memory issue with setting apps properly
if (appData.id === "remove") {
appData = {
"count": 0,
"description": "",
"id": "",
"large_image": "",
"name": "",
"type": appData.type,
}
}
return (
<AppSearchButtons
appFramework={appFramework}
index={index}
totalApps={appButtons.length}
appName={appName}
appType={appType}
AppImage={AppImage}
defaultSearch={defaultSearch}
finishedApps={finishedApps}
onNodeSelect={onNodeSelect}
discoveryData={discoveryData}
setDiscoveryData={setDiscoveryData}
setDefaultSearch={setDefaultSearch}
apps={apps}
setMoreButton={setMoreButton}
moreButton={moreButton}
/>
)
})}
</Grid>
</div>
{!moreButton ? (
<div style={{ width: "100%", marginLeft: isMobile ? 80 : 200, marginBottom: 20, textAlign: isMobile ? "center" : null }}>
<Link style={{ color: "#FF8444" }} onClick={() => {
setMoreButton(true)
setTimeout(() => {
navigate("/welcome?tab=2")
}, 250)
}}
>See More Apps</Link>
</div>) : ""}
<div style={{ flexDirection: "row", width: isMobile ? 340 : null, textAlign: isMobile ? "center" : null }}>
<Button variant="contained" type="submit" fullWidth style={bottomButtonStyle} onClick={() => {
navigate("/welcome?tab=3")
setActiveStep(2)
}}>
Continue
</Button>
</div>
</Collapse>
if (appData === undefined || appData === null || appData.name === undefined || appData.name === "") {
appData = {
"count": 0,
"description": "",
"id": "",
"large_image": "",
"name": "",
"type": appData.type,
}
}
//console.log("APP: ", appData)
const appName = appData.name
const AppImage = appData.large_image
const appType = appData.type
return (
<AppSearchButtons
appFramework={appFramework}
index={index}
totalApps={appButtons.length}
appName={appName}
appType={appType}
AppImage={AppImage}
defaultSearch={defaultSearch}
finishedApps={finishedApps}
onNodeSelect={onNodeSelect}
discoveryData={discoveryData}
setDiscoveryData={setDiscoveryData}
setDefaultSearch={setDefaultSearch}
apps={apps}
setMoreButton={setMoreButton}
moreButton={moreButton}
/>
)
})}
</Grid>
</div>
{
!isAppPage && (
<>
{!moreButton ? (
<div style={{ width: "100%", marginLeft: isMobile ? 80 : 200, marginBottom: 20, textAlign: isMobile ? "center" : null }}>
<Link style={{ color: "#FF8444" }} onClick={() => {
setMoreButton(true)
setTimeout(() => {
navigate("/welcome?tab=2")
}, 250)
}}
>See More Apps</Link>
</div>) : ""}
<div style={{ flexDirection: "row", width: isMobile ? 340 : null, textAlign: isMobile ? "center" : null }}>
<Button variant="contained" type="submit" fullWidth style={bottomButtonStyle} onClick={() => {
navigate("/usecases2")
setActiveStep(2)
}}>
See usecases
</Button>
</div>
</>
)
}
</div>
</Fade>
)
}
+1 -4
View File
@@ -24,12 +24,9 @@ const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e52
const Appsearch = props => {
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, newSelectedApp, setNewSelectedApp, defaultSearch, showSearch, ConfiguredHits, userdata, cy, isCreatorPage, actionImageList, setActionImageList, setUserSpecialzedApp } = props
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true");
const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows
const xs = parsedXs === undefined || parsedXs === null ? 12 : parsedXs
//const theme = useTheme();
//const [apps, setApps] = React.useState([]);
//const [filteredApps, setFilteredApps] = React.useState([]);
const [formMail, setFormMail] = React.useState("");
const [message, setMessage] = React.useState("");
const [formMessage, setFormMessage] = React.useState("");
@@ -45,21 +45,21 @@ const AuthenticationItem = (props) => {
data.fields = [
{
key: "url",
value: "Secret. Replaced during app execution!",
value: "URL Secret. Replaced during runtime",
},
{
key: "client_id",
value: "Secret. Replaced during app execution!",
value: "ClientID Secret. Replaced during runtime.",
},
{
key: "client_secret",
value: "Secret. Replaced during app execution!",
value: "Client Secret. Replaced during runtime.",
},
{
key: "scope",
value: "Secret. Replaced during app execution!",
value: "Scope Secret. Replaced during runtime.",
},
];
]
}
const deleteAuthentication = (data) => {
@@ -152,7 +152,7 @@ const AuthenticationItem = (props) => {
src={data.app.large_image}
style={{
maxWidth: 50,
borderRadius: theme.palette.borderRadius,
borderRadius: theme.palette?.borderRadius,
}}
/>
style={{ minWidth: 75, maxWidth: 75 }}
@@ -222,7 +222,7 @@ const AuthenticationData = (props) => {
<TextField
style={{
backgroundColor: theme.palette.inputColor,
borderRadius: theme.palette.borderRadius,
borderRadius: theme.palette?.borderRadius,
}}
InputProps={{
style: {
@@ -304,7 +304,7 @@ const AuthenticationData = (props) => {
<TextField
style={{
backgroundColor: theme.palette.inputColor,
borderRadius: theme.palette.borderRadius,
borderRadius: theme.palette?.borderRadius,
}}
InputProps={{
style: {
@@ -344,12 +344,13 @@ const AuthenticationData = (props) => {
onClick={() => {
setAuthenticationModalOpen(false);
}}
color="primary"
color="secondary"
>
Cancel
</Button>
<Button
style={{ borderRadius: "0px" }}
variant="outlined"
onClick={() => {
setAuthenticationOptions(authenticationOption);
handleSubmitCheck();
@@ -104,14 +104,19 @@ const AuthenticationData = (props) => {
toast("Failed to set app auth: " + responseJson.reason);
}
} else {
setSubmitSuccessful(true)
if (getAppAuthentication !== undefined) {
getAppAuthentication(true, false);
}
setSubmitSuccessful(true)
if (getAppAuthentication !== undefined) {
if (setAuthenticationModalOpen !== undefined) {
setAuthenticationModalOpen(false)
}
if (workflow !== undefined && workflow !== null && workflow.org_id !== undefined && workflow.org_id !== null && workflow.org_id.length > 0) {
getAppAuthentication(true, false, undefined, workflow.org_id)
} else {
getAppAuthentication(true, false)
}
}
if (setAuthenticationModalOpen !== undefined) {
setAuthenticationModalOpen(false)
}
}
})
.catch((error) => {
@@ -289,7 +294,7 @@ const AuthenticationData = (props) => {
<TextField
style={{
backgroundColor: theme.palette.inputColor,
borderRadius: theme.palette.borderRadius,
borderRadius: theme.palette?.borderRadius,
}}
InputProps={{
style: {
@@ -323,10 +328,10 @@ const AuthenticationData = (props) => {
const authenticationButtons = <span>
<Button
style={{ borderRadius: theme.palette.borderRadius, marginTop: authFieldsOnly ? 20 : 0 }}
style={{ borderRadius: theme.palette?.borderRadius, marginTop: authFieldsOnly ? 20 : 0 }}
onClick={() => {
setAuthenticationOptions(authenticationOption);
handleSubmitCheck();
setAuthenticationOptions(authenticationOption)
handleSubmitCheck()
}}
variant={"contained"}
disabled={submitSuccessful}
@@ -432,7 +437,7 @@ const AuthenticationData = (props) => {
<TextField
style={{
backgroundColor: theme.palette.inputColor,
borderRadius: theme.palette.borderRadius,
borderRadius: theme.palette?.borderRadius,
}}
InputProps={{
style: {
File diff suppressed because it is too large Load Diff
+56 -12
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from 'react';
import React, { useState, useEffect, useContext, memo, useMemo } from 'react';
import theme from '../theme.jsx';
import classNames from "classnames";
@@ -37,13 +37,14 @@ import {
} from 'reaviz';
import { typecost, typecost_single, } from "../views/HandlePaymentNew.jsx";
import { Context } from '../context/ContextApi.jsx';
const LineChartWrapper = ({keys, inputname, height, width}) => {
const [hovered, setHovered] = useState("");
const inputdata = keys.data === undefined ? keys : keys.data
return (
<div style={{color: "white", border: "1px solid rgba(255,255,255,0.3)", borderRadius: theme.palette.borderRadius, padding: 30, marginTop: 15, backgroundColor: theme.palette.platformColor, overflow: "hidden", }}>
<div style={{color: "white", border: "1px solid rgba(255,255,255,0.3)", borderRadius: theme.palette?.borderRadius, padding: 30, marginTop: 15, backgroundColor: theme.palette.platformColor, overflow: "hidden", }}>
<Typography variant="h6" style={{marginBotton: 15, }}>
{inputname}
</Typography>
@@ -82,8 +83,8 @@ const AppStats = (defaultprops) => {
const [workflows, setWorkflows] = useState(inputWorkflows === undefined ? [] : inputWorkflows)
const [resultRows, setResultRows] = useState([])
const [resultLoading, setResultLoading] = useState(true)
const includedExecutions = selectedOrganization.sync_features.app_executions !== undefined ? selectedOrganization.sync_features.app_executions.limit : 0
const includedExecutions = selectedOrganization?.sync_features?.app_executions !== undefined ? selectedOrganization?.sync_features?.app_executions?.limit : 0
useEffect(() => {
if (workflows === undefined || workflows === null || workflows.length === 0) {
@@ -92,7 +93,6 @@ const AppStats = (defaultprops) => {
}, [])
const getWorkflowStats = async (workflow, startTime, endTime) => {
if (!userdata.support) {
return workflow
@@ -134,6 +134,9 @@ const AppStats = (defaultprops) => {
Accept: "application/json",
},
credentials: "include",
}).catch((error) => {
console.log("Error getting workflow stats: " + error);
return workflow
})
if (response.status !== 200) {
@@ -159,8 +162,6 @@ const AppStats = (defaultprops) => {
const loadWorkflowStats = (foundWorkflows, startTime, endTime) => {
if (!userdata.support) {
console.log("Not support")
return
}
@@ -485,8 +486,13 @@ const AppStats = (defaultprops) => {
setApprunCosts(appcostRuns)
}
const getStats = () => {
fetch(`${globalUrl}/api/v1/orgs/${selectedOrganization.id}/stats`, {
const getStats = (orgid) => {
if (orgid === undefined || orgid === null) {
return
}
fetch(`${globalUrl}/api/v1/orgs/${orgid}/stats`, {
method: "GET",
headers: {
"Content-Type": "application/json",
@@ -516,8 +522,10 @@ const AppStats = (defaultprops) => {
}
useEffect(() => {
getStats()
}, [])
if(selectedOrganization?.id?.length > 0) {
getStats(selectedOrganization.id)
}
}, [selectedOrganization])
const paperStyle = {
textAlign: "center",
@@ -636,7 +644,7 @@ const AppStats = (defaultprops) => {
const data = (
<div className="content" style={{width: "100%", margin: "auto", }}>
<Typography variant="body1" style={{margin: "auto", marginLeft: 10, marginBottom: 20, }} color="textSecondary">
<Typography style={{margin: "auto", marginLeft: 10, marginBottom: 20, fontSize: 16}} color="textSecondary">
All shown statistics are gathered from <a
href={`${globalUrl}/api/v1/orgs/${selectedOrganization.id}/stats`}
target="_blank"
@@ -719,6 +727,40 @@ const AppStats = (defaultprops) => {
</div>
: null}
{clickedFromOrgTab? (
<LocalizationProvider dateAdapter={AdapterDayjs} style={{ flex: 1 }}>
<div style={{ display: "flex", flexDirection: "column", gap: "10px" }}>
<div style={{ flex: 1, maxWidth: "200px", }}>
<DateTimePicker
sx={{
marginTop: 1,
marginLeft: 1,
}}
ampm={false}
label="Search from"
format="YYYY-MM-DD HH:mm:ss"
value={startTime}
onChange={handleStartTimeChange}
renderInput={(params) => <TextField {...params} />}
/>
</div>
<div style={{ flex: 1, maxWidth: "200px",}}>
<DateTimePicker
sx={{
marginTop: 1,
marginLeft: 1,
}}
ampm={false}
label="Search until"
format="YYYY-MM-DD HH:mm:ss"
value={endTime}
onChange={handleEndTimeChange}
renderInput={(params) => <TextField {...params} />}
/>
</div>
</div>
</LocalizationProvider>
):(
<LocalizationProvider dateAdapter={AdapterDayjs} style={{flex: 1, }}>
<div style={{display: "flex", flexDirection: "column", }}>
<DateTimePicker
@@ -751,6 +793,8 @@ const AppStats = (defaultprops) => {
/>
</div>
</LocalizationProvider>
)}
</div>
+94 -30
View File
@@ -1,17 +1,28 @@
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useContext } from "react";
import ReactGA from 'react-ga4';
import theme from "../theme.jsx";
import { ToastContainer, toast } from "react-toastify"
import {
CheckCircle as CheckCircleIcon,
} from "@mui/icons-material";
import {
Paper,
Typography,
Divider,
Button,
Tooltip,
Grid,
Card,
} from "@mui/material";
import {
red,
green,
} from "../views/AngularWorkflow.jsx"
import { Context } from "../context/ContextApi.jsx";
//import { useAlert
const Branding = (props) => {
@@ -20,7 +31,8 @@ const Branding = (props) => {
const [publishingInfo, setPublishingInfo] = useState("");
const [publishRequirements, setPublishRequirements] = useState([])
const { leftSideBarOpenByClick } = useContext(Context)
const handleEditOrg = (joinStatus) => {
const data = {
"org_id": selectedOrganization.id,
@@ -45,7 +57,7 @@ const Branding = (props) => {
toast("Failed updating org: ", responseJson.reason);
} else {
if (joinStatus == "join") {
setPublishingInfo("Your organization is now part of the Creator Incentive Program. You can now create and publish content to your organization's page. You can also create a creator account to manage your organization's content.")
setPublishingInfo("Your organization is now part of the Partner Program. You can now create, publish and manage content for your organization's public page.")
} else {
setPublishingInfo("Your organization is no longer part of the Creator Incentive Program. You can still create a creator account to manage your organization's content.")
}
@@ -70,7 +82,15 @@ const Branding = (props) => {
}
const isOrganizationReady = () => {
console.log("Is organization ready?")
// Check if it's a suborg
if (selectedOrganization.creator_org !== "") {
const comment = "Child orgs can't become creators"
if (!publishRequirements.includes(comment)) {
setPublishRequirements([...publishRequirements, comment])
}
return false;
}
// A simple checklist to ensure the button shows up properly
if (selectedOrganization.name === selectedOrganization.org) {
@@ -82,15 +102,6 @@ const Branding = (props) => {
return false;
}
// Check if it's a suborg
if (selectedOrganization.creator_org !== "") {
const comment = "Child orgs can't become creators"
if (!publishRequirements.includes(comment)) {
setPublishRequirements([...publishRequirements, comment])
}
return false;
}
if (selectedOrganization.large_image === "" || selectedOrganization.large_image === theme.palette.defaultImage) {
const comment = "Add a logo for your organization"
if (!publishRequirements.includes(comment)) {
@@ -102,38 +113,89 @@ const Branding = (props) => {
return true
}
const isPublished = selectedOrganization.creator_id === ""
const leadinfo = selectedOrganization.lead_info === undefined || selectedOrganization.lead_info === null || selectedOrganization.lead_info === "" ? "" : JSON.stringify(selectedOrganization.lead_info)
const isPartner = leadinfo.includes("partner")
return (
<div style={{ width: clickedFromOrgTab? 1030: "auto", padding: 27, height: "auto", backgroundColor: '#212121', borderRadius: '16px', }}>
<h2 style={{marginTop: clickedFromOrgTab ?0:null,}}>
Branding
</h2>
<Typography variant="body1" color="textSecondary" style={{ marginTop: 20, marginBottom: 10 }}>
<div style={{ width: clickedFromOrgTab? "100%": "auto", height: "100%", minHeight: 1100, boxSizing: 'border-box', transition: "width 0.3s ease", padding: "27px 10px 19px 27px", height: "auto", backgroundColor: '#212121', borderRadius: '16px', }}>
<div style={{height: 843, overflowY: "auto",}}>
<div style={{width: "100%", overflowX: 'hidden', }}>
<Typography style={{fontSize: 24, fontWeight: "bold", marginTop: clickedFromOrgTab ?0:null,}}>
Partner Status & Branding
</Typography>
<Typography variant="body1" color="textSecondary" style={{ marginTop: 10, marginBottom: 10, fontSize: 16 }}>
You can customize your organization's branding by uploading a logo, changing the color scheme and a lot more.
</Typography>
<Typography variant="body1" color="textSecondary" style={{display: 'flex', marginTop: 20, marginBottom: 10 }}>
{isPublished ? <CheckCircleIcon style={{color: red, }} /> : <CheckCircleIcon style={{color: green, }} />}
<span style={{marginLeft: 10, color: isPublished ? red : green, fontSize: 16 }}>{isPublished ? "Not Published" : "Published"}</span>
</Typography>
<a href="https://shuffler.io/partners" target="_blank" style={{ textDecoration: "none", }}>
<Typography variant="body1" color="textSecondary" style={{display: 'flex', marginTop: 20, marginBottom: 10 }}>
{!isPartner ? <CheckCircleIcon style={{color: red, }} /> : <CheckCircleIcon style={{color: green, }} />}
<Tooltip title="Official Partner Program (manual verification)" placement="top" arrow>
<span style={{marginLeft: 10, color: !isPartner ? red : green, fontSize: 16}}>{!isPartner? "Not Officially Partnered" : "Officially Partnered"}</span>
</Tooltip>
</Typography>
</a>
{!isPublished ? (
<a
href={`/partners/${selectedOrganization.creator_id}/edit`}
target="_blank"
style={{ textDecoration: "none" }} // Optional: remove underline
>
<Button
variant="contained"
style={{
marginTop: 20,
marginBottom: 10,
textTransform: 'none',
fontSize: 16
}}
>
Modify Public Partner Details
</Button>
</a>
) : (
<Button
disabled
variant="contained"
style={{
marginTop: 20,
marginBottom: 10,
textTransform: 'none',
fontSize: 16
}}
>
Modify Public Partner Details
</Button>
)}
<Divider style={{marginTop: 50, marginBottom: 50, }} />
<h2>
Creator Incentive Program
</h2>
<div style={{ display: "flex", width: 900, }}>
<Typography style={{fontSize: 24, fontWeight: "bold"}}>
Partner Program
</Typography>
<div style={{ display: "flex", width: 900, marginTop: 10}}>
<div>
<span>
<Typography variant="body1" color="textSecondary">
By changing publishing settings, you agree to our <a href="/docs/terms_of_service" target="_blank" style={{ textDecoration: "none", color: "#f86a3e"}}>Terms of Service</a>, and acknowledge that your organization's non-sensitive data will be added as a <a target="_blank" style={{ textDecoration: "none", color: "#f86a3e"}} href="https://shuffler.io/creators">creator account</a>. None of your existing workflows, apps, or other stored data will be published. Any admin in your organization can manage the creator configuration. Becoming a creator organization is reversible.<div/>Support: <a href="mailto:support@shuffler.io"target="_blank" style={{ textDecoration: "none", color: "#f86a3e"}}>support@shuffler.io</a>
<Typography variant="body1" color="textSecondary" style={{fontSize: 16}}>
By changing publishing settings, you agree to our <a href="/docs/terms_of_service" target="_blank" style={{ textDecoration: "none", color: "#f86a3e"}}>Terms of Service</a>, and acknowledge that your organization's non-sensitive data will be added as a <a target="_blank" style={{ textDecoration: "none", color: "#f86a3e"}} href="https://shuffler.io/creators">creator account</a>. None of your existing workflows, apps, or other stored data will be published. Any admin in your organization can manage the creator configuration. Becoming a creator organization IS reversible.<div/>Support: <a href="mailto:support@shuffler.io"target="_blank" style={{ textDecoration: "none", color: "#f86a3e"}}>support@shuffler.io</a>
</Typography>
{selectedOrganization.creator_id == "" ?
<Typography variant="h6" color="textSecondary" style={{ marginTop: 20, marginBottom: 10, color: "grey", }}>
&nbsp;
</Typography>
:
<Typography variant="h6" color="textSecondary" style={{ marginTop: 20, marginBottom: 10, color: "grey", }}>
<a href={`/creators/${selectedOrganization.creator_id}`} target="_blank" style={{ textDecoration: "none", color: "#f86a3e"}}>Modify your creator organization</a>
</Typography>
null
}
<Button
style={{ height: 40, marginTop: 10, width: 300, }}
style={{ height: 40, marginTop: 10, width: 300, textTransform: 'none', fontSize: 18, backgroundColor: "#ff8544", color: "#1a1a1a" }}
variant={selectedOrganization.creator_id == "" ? "contained" : "outlined"}
color={selectedOrganization.creator_id == "" ? "primary" : "secondary"}
disabled={!isOrganizationReady()}
@@ -141,7 +203,7 @@ const Branding = (props) => {
handleChangePublishing();
}}
>
{selectedOrganization.creator_id == "" ? "Join" : "Leave"} Creators
{selectedOrganization.creator_id == "" ? "Join" : "Leave"} Partner Program
</Button>
<Typography variant="body1" color="textSecondary" style={{ marginTop: 20, marginBottom: 10, color: "white", }}>
@@ -159,6 +221,8 @@ const Branding = (props) => {
</span>
</div>
</div>
</div>
</div>
</div>
)
}
+187 -78
View File
@@ -1,7 +1,7 @@
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useContext, memo } from "react";
import theme from "../theme.jsx";
import { toast } from 'react-toastify';
import ReactJson from "react-json-view";
import ReactJson from "react-json-view-ssr";
import {
Typography,
@@ -19,6 +19,7 @@ import {
Dialog,
DialogTitle,
DialogActions,
Skeleton,
} from "@mui/material";
import {
@@ -47,6 +48,7 @@ import {
VisibilityOff as VisibilityOffIcon,
} from "@mui/icons-material";
import { validateJson, } from "../views/Workflows.jsx";
import { Context } from "../context/ContextApi.jsx";
const scrollStyle1 = {
height: 100,
@@ -65,7 +67,7 @@ const scrollStyle2 = {
}
const CacheView = (props) => {
const CacheView = memo((props) => {
const { globalUrl, userdata, serverside, orgId, isSelectedDataStore } = props;
const [orgCache, setOrgCache] = React.useState("");
const [listCache, setListCache] = React.useState([]);
@@ -78,11 +80,13 @@ const CacheView = (props) => {
const [cacheCursor, setCacheCursor] = React.useState("");
const [dataValue, setDataValue] = React.useState({});
const [editCache, setEditCache] = React.useState(false);
const [cachedLoaded, setCachedLoaded] = React.useState(false);
const [show, setShow] = useState({});
useEffect(() => {
listOrgCache(orgId);
}, []);
if(orgId?.length >0){
listOrgCache(orgId);
}
}, [orgId]);
const listOrgCache = (orgId) => {
fetch(globalUrl + `/api/v1/orgs/${orgId}/list_cache`, {
@@ -104,6 +108,7 @@ const CacheView = (props) => {
.then((responseJson) => {
if (responseJson.success === true) {
setListCache(responseJson.keys);
setCachedLoaded(true);
}
if (responseJson.cursor !== undefined && responseJson.cursor !== null && responseJson.cursor !== "") {
@@ -232,6 +237,34 @@ const CacheView = (props) => {
}
}
const handleReactJsonClipboard = (copy) => {
const elementName = "copy_element_shuffle";
let copyText = document.getElementById(elementName);
if (copyText) {
if (copy.namespace && copy.name && copy.src) {
copy = copy.src;
}
const clipboard = navigator.clipboard;
if (!clipboard) {
toast("Can only copy over HTTPS (port 3443)");
return;
}
let stringified = JSON.stringify(copy);
if (stringified.startsWith('"') && stringified.endsWith('"')) {
stringified = stringified.slice(1, -1);
}
navigator.clipboard.writeText(stringified);
toast("Copied value to clipboard, NOT json path.");
} else {
console.log("Failed to copy from " + elementName + ": ", copyText);
}
};
const modalView = (
// console.log("key:", dataValue.key),
//console.log("value:",dataValue.value),
@@ -241,11 +274,23 @@ const CacheView = (props) => {
setModalOpen(false);
}}
PaperProps={{
style: {
backgroundColor: theme.palette.surfaceColor,
color: "white",
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,
},
},
}}
>
@@ -254,7 +299,7 @@ const CacheView = (props) => {
{ editCache ? "Edit Cache" : "Add Cache" }
</span>
</DialogTitle>
<div style={{ paddingLeft: "30px", paddingRight: '30px' }}>
<div style={{ paddingLeft: "30px", paddingRight: '30px', backgroundColor: "#212121", }}>
Key
<TextField
color="primary"
@@ -278,7 +323,7 @@ const CacheView = (props) => {
onChange={(e) => setKey(e.target.value)}
/>
</div>
<div style={{ paddingLeft: 30, paddingRight: 30 }}>
<div style={{ paddingLeft: 30, paddingRight: 30, backgroundColor: "#212121" }}>
<div style={{display: "flex", }}>
<Typography style={{marginTop: 25, marginBottom: 0, flex: 20, }}>
Value - ({isValidJson.valid === true ? "Valid" : "Invalid"} JSON)
@@ -320,7 +365,7 @@ const CacheView = (props) => {
</div>
<DialogActions style={{ paddingLeft: "30px", paddingRight: '30px' }}>
<Button
style={{ borderRadius: "0px" }}
style={{ borderRadius: "2px", fontSize: 16, color: "#ff8544", textTransform:"none" }}
onClick={() => {
setModalOpen(false)
setValue("")
@@ -332,7 +377,7 @@ const CacheView = (props) => {
</Button>
<Button
variant="contained"
style={{ borderRadius: "0px" }}
style={{ borderRadius: "2px", backgroundColor: "#ff8544",color: "#1a1a1a", textTransform:"none" }}
onClick={() => {
{editCache ? editOrgCache(orgId) : addOrgCache(orgId)}
@@ -348,10 +393,11 @@ const CacheView = (props) => {
);
return (
<div style={{paddingBottom: isSelectedDataStore?null:250, width: isSelectedDataStore?1030:null, padding:isSelectedDataStore?27:null, height: isSelectedDataStore?"auto":null, color: isSelectedDataStore?'#ffffff':null, backgroundColor: isSelectedDataStore?'#212121':null, borderRadius: isSelectedDataStore?'16px':null, }}>
<div style={{paddingBottom: isSelectedDataStore?null:250, minHeight: 1000, boxSizing: "border-box", width: isSelectedDataStore? "100%" :null, transition: "width 0.3s ease", padding:isSelectedDataStore?"27px 10px 27px 27px":null, height: isSelectedDataStore?"100%":null, color: isSelectedDataStore?'#ffffff':null, backgroundColor: isSelectedDataStore?'#212121':null, borderTopRightRadius: isSelectedDataStore?'8px':null, borderBottomRightRadius: isSelectedDataStore?'8px':null, borderLeft: "1px solid #494949" }}>
{modalView}
<div style={{ marginTop: isSelectedDataStore?null:20, marginBottom: 20 }}>
<div style={{height: "100%", maxHeight: 1700, overflowY: "auto", scrollbarColor: '#494949 transparent', scrollbarWidth: 'thin'}}>
<div style={{ height: "100%", width: "calc(100% - 20px)", scrollbarColor: '#494949 transparent', scrollbarWidth: 'thin' }}>
<div style={{ marginTop: isSelectedDataStore?null:20, marginBottom: 20 }}>
<h2 style={{ display: isSelectedDataStore?null: "inline" }}>Shuffle Datastore</h2>
<span style={{ marginLeft: isSelectedDataStore?null:25, color:isSelectedDataStore?"#9E9E9E":null}}>
Datastore is a permanent key-value database for storing data that can be used cross-workflow. <br/>You can store anything from lists of IPs to complex configurations.&nbsp;
@@ -366,7 +412,7 @@ const CacheView = (props) => {
</span>
</div>
<Button
style={{backgroundColor: isSelectedDataStore?'rgba(255, 132, 68, 0.2)':null, boxShadow: isSelectedDataStore ? "none":null,textTransform: isSelectedDataStore ? 'capitalize':null, color:isSelectedDataStore?"#FF8444":null, borderRadius:isSelectedDataStore?200:null, width:isSelectedDataStore?162:null, height:isSelectedDataStore?40:null}}
style={{backgroundColor: isSelectedDataStore? "#ff8544":null, fontSize: 16, boxShadow: isSelectedDataStore ? "none":null,textTransform: isSelectedDataStore ? 'capitalize':null, color:isSelectedDataStore?"#1a1a1a":null, borderRadius:isSelectedDataStore?8:null, width:isSelectedDataStore?162:null, height:isSelectedDataStore?40:null}}
variant="contained"
color="primary"
onClick={() =>{
@@ -379,7 +425,7 @@ const CacheView = (props) => {
Add Cache
</Button>
<Button
style={{ marginLeft: 5, marginRight: 15, backgroundColor: isSelectedDataStore?"#2F2F2F":null, boxShadow: isSelectedDataStore ? "none":null,textTransform: isSelectedDataStore ? 'capitalize':null,borderRadius:isSelectedDataStore?200:null, width:isSelectedDataStore?81:null, height:isSelectedDataStore?40:null, }}
style={{ marginLeft: 5, marginRight: 15, backgroundColor: isSelectedDataStore?"#2F2F2F":null, boxShadow: isSelectedDataStore ? "none":null,textTransform: isSelectedDataStore ? 'capitalize':null,borderRadius:isSelectedDataStore?8:null, width:isSelectedDataStore?81:null, height:isSelectedDataStore?40:null, }}
variant="contained"
color="primary"
onClick={() => listOrgCache(orgId)}
@@ -392,28 +438,79 @@ const CacheView = (props) => {
marginBottom: 20,
}}
/>}
<List style={{borderRadius: isSelectedDataStore?8:null, border:isSelectedDataStore?"1px solid #494949":null, marginTop:isSelectedDataStore?24:null}}>
<ListItem style={{width: isSelectedDataStore?"100%":null, borderBottom:isSelectedDataStore?"1px solid #494949":null}}>
<ListItemText
primary="Key"
style={{ minWidth: isSelectedDataStore?200:250, maxWidth: isSelectedDataStore?200:250, }}
/>
<ListItemText
primary="Value"
style={{ minWidth: isSelectedDataStore?300:400, maxWidth: isSelectedDataStore?300:400, overflowX: "auto", overflowY: "hidden", }}
/>
<ListItemText
primary="Actions"
style={{ minWidth: 150, maxWidth: 150, marginLeft: isSelectedDataStore?80:null,}}
/>
<ListItemText
style={{textAlign:isSelectedDataStore?"center":null}}
primary="Updated"
/>
<div
style={{
borderRadius: 8,
marginTop: 24,
border: "1px solid #494949",
width: "100%",
overflowX: "auto",
paddingBottom: 0,
}}
>
<List
style={{
borderRadius: 8,
paddingBottom: 0,
tableLayout: "auto",
display: "table",
width: '100%',
minWidth: 800,
overflowX: "auto",
}}>
<ListItem style={{width: isSelectedDataStore?"100%":null, borderBottom:isSelectedDataStore?"1px solid #494949":null, display: "table-row"}}>
{["Key", "Value", "Actions", "Updated"].map((header, index) => (
<ListItemText
key={index}
primary={header}
style={{
display: "table-cell",
padding: "0px 8px 8px 8px",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
borderBottom: "1px solid #494949"
}}
/>
))}
</ListItem>
{listCache === undefined || listCache === null
? null
: listCache.map((data, index) => {
{cachedLoaded === false
? [...Array(6)].map((_, rowIndex) => (
<ListItem
key={rowIndex}
style={{
display: "table-row",
backgroundColor: "#212121",
}}
>
{Array(4)
.fill()
.map((_, colIndex) => (
<ListItemText
key={colIndex}
style={{
display: "table-cell",
padding: "8px",
}}
>
<Skeleton
variant="text"
animation="wave"
sx={{
backgroundColor: "#1a1a1a",
height: "20px",
borderRadius: "4px",
}}
/>
</ListItemText>
))}
</ListItem>
))
: listCache?.length === 0 ? (
<Typography style={{ textAlign: "center", marginTop: 20, marginBottom: 20, minWidth: 1000, }}>
No Keys Found
</Typography>
): listCache?.map((data, index) => {
var bgColor = isSelectedDataStore? "#212121":"#27292d";
if (index % 2 === 0) {
bgColor = isSelectedDataStore? "#1A1A1A":"#1f2023";
@@ -421,49 +518,61 @@ const CacheView = (props) => {
const validate = validateJson(data.value);
return (
<ListItem key={index} style={{ backgroundColor: bgColor, maxHeight: 300, overflow: "auto", }}>
<ListItem key={index} style={{display:'table-row', backgroundColor: bgColor, maxHeight: 300, overflow: "auto", borderBottomLeftRadius: listCache?.length - 1 === index ? 8 : 0, borderBottomRightRadius: listCache?.length - 1 === index ? 8 : 0}}>
<ListItemText
style={{
maxWidth: 200,
minWidth: 200,
display: "table-cell",
overflow: "hidden",
padding: 8,
verticalAlign: "middle",
}}
primary={data.key}
/>
<ListItemText
style={{
minWidth: 300,
maxWidth: 300,
// height:200,
overflowX: "hidden",
display: "table-cell",
overflowY: "auto",
overflowX: "auto",
border: "1px solid rgba(255,255,255,0.7)",
borderRadius: 6,
backgroundColor: "#151515",
maxHeight: 300,
verticalAlign: "middle",
}}
primary={validate.valid ?
<ReactJson
src={validate.result}
theme={theme.palette.jsonTheme}
style={theme.palette.reactJsonStyle}
collapsed={true}
enableClipboard={(copy) => {
//handleReactJsonClipboard(copy);
}}
displayDataTypes={false}
onSelect={(select) => {
//HandleJsonCopy(showResult, select, data.action.label);
//console.log("SELECTED!: ", select);
}}
name={"value"}
/>
primary={validate.valid ?
<ReactJson
src={validate.result}
theme={theme.palette.jsonTheme}
style={{
padding: 5,
maxHeight: 300,
overflowY: "auto",
}}
collapsed={true}
enableClipboard={(copy) => {
// handleReactJsonClipboard(copy);
}}
collapseStringsAfterLength={theme.palette.jsonCollapseStringsAfterLength}
iconStyle={theme.palette.jsonIconStyle}
displayDataTypes={false}
onSelect={(select) => {
// HandleJsonCopy(showResult, select, data.action.label);
console.log("SELECTED!: ", select);
}}
name={"value"}
/>
:
data.value
}
/>
<ListItemText
style={{
maxWidth: 200,
minWidth: 200,
marginLeft: 50,
display: "table-cell",
verticalAlign: "middle",
padding: 8
}}
primary=<span style={{ display: "inline" }}>
primary={(
<span style={{ display: "inline" }}>
<Tooltip
title="Edit item"
style={{}}
@@ -482,9 +591,7 @@ const CacheView = (props) => {
setModalOpen(true)
}}
>
<EditIcon
style={{ color: "white" }}
/>
<img src="/icons/editIcon.svg" alt="edit" />
</IconButton>
</span>
</Tooltip>
@@ -508,7 +615,6 @@ const CacheView = (props) => {
</Tooltip>
<Tooltip
title={"Delete item"}
style={{ marginLeft: 25, }}
aria-label={"Delete"}
>
<span>
@@ -519,18 +625,18 @@ const CacheView = (props) => {
//deleteFile(orgId);
}}
>
<DeleteIcon
style={{ color: "white" }}
/>
<img src="/icons/deleteIcon.svg" alt="delete" />
</IconButton>
</span>
</Tooltip>
</span>
)}
/>
<ListItemText
style={{
maxWidth: 225,
minWidth: 225,
display: "table-cell",
verticalAlign: "middle",
padding: 8
}}
primary={new Date(data.edited * 1000).toISOString()}
/>
@@ -538,8 +644,11 @@ const CacheView = (props) => {
);
})}
</List>
</div>
</div>
</div>
</div>
);
}
export default CacheView;
});
export default memo(CacheView);
+36 -28
View File
@@ -2,6 +2,7 @@ import React, { useState, useEffect } from "react";
import { useInterval } from "react-powerhooks";
import { toast } from 'react-toastify';
import theme from "../theme.jsx";
import WorkflowValidationTimeline from "../components/WorkflowValidationTimeline.jsx"
import {
InputAdornment,
@@ -79,7 +80,7 @@ const ConfigureWorkflow = (props) => {
useEffect(() => {
if (requiredActions.length === 0) {
if (setConfigurationFinished !== undefined) {
setConfigurationFinished(true)
setConfigurationFinished(true)
}
}
}, [requiredActions])
@@ -141,17 +142,18 @@ const ConfigureWorkflow = (props) => {
// Where is this from?
if (workflow === undefined || workflow === null || workflow.id === undefined) {
return null;
//console.log("Workflow is undefined or null: ", workflow)
return null
}
if (apps === undefined || apps === null) {
console.log("Apps is undefined or null: ", apps)
return null;
//console.log("Apps is undefined or null: ", apps)
return null
}
if (appAuthentication === undefined || appAuthentication === null) {
console.log("App authentication is undefined or null: ", appAuthentication)
return null;
//console.log("App authentication is undefined or null: ", appAuthentication)
return null
}
const getApp = (actionId, appId) => {
@@ -310,7 +312,7 @@ const ConfigureWorkflow = (props) => {
}
}
if (action.authentication_id === "" && app.authentication.required === true && action.parameters !== undefined && action.parameters !== null) {
if (action?.authentication_id === "" && app?.authentication?.required === true && action.parameters !== undefined && action.parameters !== null) {
// Check if configuration is filled or not
var filled = true;
for (let [key,keyval] in Object.entries(action.parameters)) {
@@ -322,7 +324,7 @@ const ConfigureWorkflow = (props) => {
}
}
if (app.authentication.type === "oauth2" || app.authentication.type === "oauth2-app") {
if (app?.authentication?.type === "oauth2" || app?.authentication?.type === "oauth2-app") {
filled = false
action.auth_type = "oauth2"
@@ -425,10 +427,8 @@ const ConfigureWorkflow = (props) => {
trigger.index = key;
if (trigger.trigger_type === "WEBHOOK") {
console.log("Found webhook: ", trigger)
if (trigger.app_association !== undefined && trigger.app_association.name !== null && trigger.app_association.name !== "") {
console.log("Actions: ", newactions)
const findapp = trigger.app_association.name.toLowerCase()
const foundindex = newactions.findIndex(action => action.app_name.toLowerCase() === findapp)
@@ -449,9 +449,6 @@ const ConfigureWorkflow = (props) => {
newactions[foundindex].show_steps = true
console.log("CHANGED ACTION: ", newactions[foundindex])
//console.log("Index: ", newactions[foundindex])
continue
}
}
@@ -601,7 +598,7 @@ const ConfigureWorkflow = (props) => {
<TextField
style={{
backgroundColor: theme.palette.inputColor,
borderRadius: theme.palette.borderRadius,
borderRadius: theme.palette?.borderRadius,
}}
InputProps={{
endAdornment: <InputAdornment position="end"></InputAdornment>,
@@ -796,15 +793,13 @@ const ConfigureWorkflow = (props) => {
}
parsedName = (parsedName.charAt(0).toUpperCase() + parsedName.slice(1)).replaceAll("_", " ");
console.log("AUTH Action: ", action)
return (
<ListItem
style={{padding: 0, display: "flex", flexDirection: "column", }}
>
<div
style={{
border: filled ? `1px solid ${theme.palette.green}` : "1px solid rgba(255,255,255,0.3)", borderRadius: theme.palette.borderRadius, width: "100%", padding: 12, cursor: "pointer",
border: filled ? `1px solid ${theme.palette.green}` : "1px solid rgba(255,255,255,0.3)", borderRadius: theme.palette?.borderRadius, width: "100%", padding: 12, cursor: "pointer",
}}
id="app-config"
>
@@ -853,7 +848,7 @@ const ConfigureWorkflow = (props) => {
{opened ?
<div style={{padding: 12, }}>
{action.app.authentication.type === "oauth2-app" || action.app.authentication.type === "oauth2" || action.auth_type === "oauth2" ?
{action.app?.authentication?.type === "oauth2-app" || action.app?.authentication?.type === "oauth2" || action.auth_type === "oauth2" ?
<div>
<AuthenticationOauth2
selectedApp={action.app}
@@ -950,7 +945,7 @@ const ConfigureWorkflow = (props) => {
)
})}
{action.app.authentication.type !== "oauth2-app" && action.app.authentication.type !== "oauth2" ?
{action.app?.authentication?.type !== "oauth2-app" && action.app?.authentication?.type !== "oauth2" ?
<Button
variant="contained"
color="primary"
@@ -1001,7 +996,7 @@ const ConfigureWorkflow = (props) => {
justifyContent: "flex-start",
backgroundColor: action.auth_done ? theme.palette.surfaceColor : theme.palette.inputColor,
color: action.auth_done ? "#686a6c" : "#ffffff",
borderRadius: theme.palette.borderRadius,
borderRadius: theme.palette?.borderRadius,
minWidth: 350,
maxHeight: 50,
overflow: "hidden",
@@ -1041,7 +1036,7 @@ const ConfigureWorkflow = (props) => {
>
<img
alt={action.app_name}
style={{ margin: 4, minHeight: 30, maxHeight: 30, borderRadius: theme.palette.borderRadius, }}
style={{ margin: 4, minHeight: 30, maxHeight: 30, borderRadius: theme.palette?.borderRadius, }}
src={action.large_image}
/>
<Typography style={{ margin: 0, marginLeft: 10 }} variant="body1">
@@ -1061,7 +1056,7 @@ const ConfigureWorkflow = (props) => {
justifyContent: "flex-start",
backgroundColor: action.auth_done ? theme.palette.surfaceColor : theme.palette.inputColor,
color: action.auth_done ? "#686a6c" : "#ffffff",
borderRadius: theme.palette.borderRadius,
borderRadius: theme.palette?.borderRadius,
minWidth: 350,
maxHeight: 50,
overflow: "hidden",
@@ -1092,7 +1087,7 @@ const ConfigureWorkflow = (props) => {
>
<img
alt={action.app_name}
style={{ margin: 4, minHeight: 30, maxHeight: 30, borderRadius: theme.palette.borderRadius, }}
style={{ margin: 4, minHeight: 30, maxHeight: 30, borderRadius: theme.palette?.borderRadius, }}
src={action.large_image}
/>
<Typography style={{ margin: 0, marginLeft: 10 }} variant="body1">
@@ -1112,7 +1107,7 @@ const ConfigureWorkflow = (props) => {
justifyContent: "flex-start",
backgroundColor: action.auth_done ? theme.palette.surfaceColor : theme.palette.inputColor,
color: action.auth_done ? "#686a6c" : "#ffffff",
borderRadius: theme.palette.borderRadius,
borderRadius: theme.palette?.borderRadius,
minWidth: 350,
maxHeight: 50,
overflow: "hidden",
@@ -1127,7 +1122,7 @@ const ConfigureWorkflow = (props) => {
>
<img
alt={action.app_name}
style={{ margin: 4, minHeight: 30, maxHeight: 30, borderRadius: theme.palette.borderRadius, }}
style={{ margin: 4, minHeight: 30, maxHeight: 30, borderRadius: theme.palette?.borderRadius, }}
src={action.large_image}
/>
<Typography style={{ margin: 0, marginLeft: 10 }} variant="body1">
@@ -1279,7 +1274,7 @@ const ConfigureWorkflow = (props) => {
const [finishCount, setFinishCount] = useState(0)
return (
<div style={{backgroundColor: hovered ? theme.palette.inputColor : "inherit", border: "1px solid rgba(255,255,255,0.3)", borderRadius: theme.palette.borderRadius, cursor: "pointer", }}
<div style={{backgroundColor: hovered ? theme.palette.inputColor : "inherit", border: "1px solid rgba(255,255,255,0.3)", borderRadius: theme.palette?.borderRadius, cursor: "pointer", }}
>
<div style={{display: "flex", marginLeft: 15, marginTop: 15, marginBottom: 15, }}
onClick={() => {
@@ -1321,7 +1316,6 @@ const ConfigureWorkflow = (props) => {
}
if (step.type === "authenticate") {
console.log("AUTH STEP: ", step)
if (data.must_authenticate === true ) {
filled = false
} else {
@@ -1393,9 +1387,23 @@ const ConfigureWorkflow = (props) => {
: null
}
<div style={{marginTop: 10, }} />
{/*
<WorkflowValidationTimeline
workflow={workflow}
apps={apps}
getParents={undefined}
execution={undefined}
/>
<div style={{marginBottom: 10, }} />
*/}
{requiredActions.length > 0 ? (
<span>
<Typography variant="body2" style={{}}>
<Typography variant="body2" color="textSecondary">
Please configure the following steps to help us complete your workflow. This can also be done later.
</Typography>
+6
View File
@@ -136,6 +136,11 @@ const CreatorGrid = props => {
removeQuery("q")
refine(event.currentTarget.value)
}}
onKeyDown={(event) => {
if(event.key === "Enter") {
event.preventDefault();
}
}}
/>
{/*isSearchStalled ? 'My search is stalled' : ''*/}
</form>
@@ -147,6 +152,7 @@ const CreatorGrid = props => {
flexWrap: "wrap",
alignContent: "space-between",
marginTop: 5,
padding:"0px 180px",
}
const Hits = ({ hits }) => {
@@ -0,0 +1,144 @@
import React from 'react';
import { Bar } from 'react-chartjs-2';
import { toast } from "react-toastify";
export const LoadStats = (globalUrl, cachekey) => {
if (globalUrl === undefined) {
console.log("Error: Global URL is undefined")
return
}
if (cachekey === undefined) {
console.log("Error: Cachekey is undefined")
return
}
var basedata = {
"key": cachekey,
"total": 0,
"available_keys": [],
"labels": [],
"datasets": [
{
"label": "",
"data": [],
"backgroundColor": [],
"barThickness": 15,
}
]
}
//const url = `${globalUrl}/api/v1/stats/app_executions_test2`
//cachekey = cachekey.replace(" ", "_", -1)
const url = `${globalUrl}/api/v1/stats/${cachekey}`
return fetch(url, {
method: "GET",
credentials: "include",
})
.then((resp) => {
return resp.json()
}).then((respJson) => {
const selectedIndex = 0
if (respJson.success === true) {
for (let entryKey in respJson.entries) {
const entry = respJson.entries[entryKey]
basedata.labels.push(entry.date)
basedata.datasets[0].data.push(entry.value)
basedata.datasets[0].backgroundColor.push(entry.value > 0 ? "rgba(255,255,255,0.4)" : "red")
}
basedata.available_keys = respJson.available_keys
basedata.total = respJson.total
return basedata
} else {
console.log("Failed to get stats")
return basedata
}
})
.catch((err) => {
toast("Failed to get stats")
return basedata
})
}
const DashboardBarchart = (props) => {
const { timelineData, title, height, } = props;
var inputHeight = 15
if (height !== undefined && height !== null) {
inputHeight = height
}
const barOptions = {
plugins: {
tooltip: {
enabled: true, // Ensure tooltips are enabled
},
},
tooltips: {
mode: 'index',
intersect: false,
},
legend: {
display: false
},
layout: {
padding: {
top: 0, // Adjust the top padding as needed
bottom: -10, // Adjust the bottom padding as needed
left: 0, // Adjust the left padding as needed
right: 0, // Adjust the right padding as needed
},
},
scales: {
y: {
beginAtZero: false,
},
yAxes: [{
ticks: {
display: false
},
beginAtZero: false,
}],
xAxes: [{
ticks: {
display: false
},
beginAtZero: false,
}]
},
tooltips: {
callbacks: {
label: function (tooltipItem, data) {
const label = data.labels[tooltipItem.index]
return label.split('\n')[0]
},
afterLabel: function (tooltipItem, data) {
const amount = tooltipItem.value === undefined || tooltipItem.value === null ? 0 : tooltipItem.value
return `Amount: ${amount}`
},
title: function () {
return title === undefined ? '' : title
}
}
}
}
return (
<Bar
data={timelineData}
options={barOptions}
height={inputHeight}
getElementAtEvent={(elements) => {
if (elements && elements.length > 0) {
//toast("Click event")
console.log("Clicked: ", elements)
}
}}
/>
)
}
export default DashboardBarchart;
+210
View File
@@ -0,0 +1,210 @@
import React, { useState } from "react";
import {
Container,
Box,
TextField,
Switch,
Typography,
Button,
CircularProgress,
Paper,
} from "@mui/material";
import { toast } from "react-toastify";
import theme from '../theme.jsx';
import DetectionRuleCard from "../components/DetectionRuleCard.jsx";
const handleDirectoryChange = (folderDisabled, setFolderDisabled, globalUrl, isTenzirActive) => {
if (!isTenzirActive) {
toast("connect to siem first for global enable/disable to work");
return;
}
const action = folderDisabled ? "enable_folder" : "disable_folder";
const url = `${globalUrl}/api/v1/detections/${action}`;
fetch(url, {
method: "PUT",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
})
.then((response) =>
response.json().then((responseJson) => {
if (responseJson["success"] === true) {
if (action === "enable_folder") setFolderDisabled(false);
else setFolderDisabled(true);
} else {
//toast(`failed to disable rule`);
}
})
)
.catch((error) => {
console.log(`Error in ${action} the rule: `, error);
toast(`An error occurred while ${action} the rule`);
});
};
const Detection = (props) => {
const { globalUrl, ruleInfo, folderDisabled, setFolderDisabled, isTenzirActive } = props;
const [searchQuery, setSearchQuery] = useState("");
const [loading, setLoading] = useState(false);
const handleConnectClick = () => {
if (!isTenzirActive) {
setLoading(true);
const url = `${globalUrl}/api/v1/detections/siem/connect`;
fetch(url, {
method: "GET",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
})
.then((response) =>
response.json().then((responseJson) => {
if (responseJson["success"] === true) {
setTimeout(() => {
setLoading(false);
window.location.reload();
}, 15000);
} else {
setLoading(false);
toast("Failed to connect to SIEM");
}
})
)
.catch((error) => {
setLoading(false);
console.log(`Error in connecting to SIEM: `, error);
toast("An error occurred while connecting to SIEM");
});
} else {
console.log("Already connected to SIEM");
}
};
const filteredRules = ruleInfo?.filter((rule) =>
rule.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
rule.description.toLowerCase().includes(searchQuery.toLowerCase())
);
return (
<Container>
<Paper
style={{
marginTop: 50,
width: "100%",
padding: 50,
backgroundColor: theme.palette.backgroundColor,
borderRadius: theme.palette?.borderRadius,
}}
>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
mb: 2,
}}
>
<Typography variant="h6" component="div">
Sigma Detection Rules
</Typography>
<Button
variant="contained"
onClick={handleConnectClick}
disabled={loading} // Disable the button while loading
color={isTenzirActive ? "primary" : "secondary"}
style={{ }}
>
{loading ? <CircularProgress size={24} /> : isTenzirActive ? "Connected to siem" : "Connect to siem"}
</Button>
</Box>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
mb: 2,
}}
>
<Box
sx={{
display: "flex",
}}
>
<TextField
label="Search rules"
variant="outlined"
size="small"
sx={{ mr: 2 }}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
{/* <Button
color="primary"
variant="contained"
onClick={() => uploadRef.current.click()}
>
<PublishIcon /> Upload sigma file
</Button>
<input
hidden
type="file"
multiple
ref={uploadRef}
onChange={(event) => {
uploadFiles(event.target.files);
}}
/> */}
</Box>
<Box sx={{ display: "flex", alignItems: "center" }}>
<Typography variant="body2" sx={{ mr: 1 }}>
Global disable/enable
</Typography>
<Switch
checked={!folderDisabled}
onChange={() =>
handleDirectoryChange(folderDisabled, setFolderDisabled, globalUrl, isTenzirActive)
}
disabled={!isTenzirActive}
/>
</Box>
</Box>
<Box
sx={{
height: "500px",
width: "100%",
overflowY: "auto",
p: 1,
}}
>
{filteredRules?.length > 0 ?
filteredRules.map((card) => {
console.log("RULE CARD: ", card);
return (
<DetectionRuleCard
key={card.file_id}
ruleName={card.title}
description={card.description}
file_id={card.file_id}
globalUrl={globalUrl}
folderDisabled={folderDisabled}
isTenzirActive={isTenzirActive}
{...card}
/>
)
})
: null }
</Box>
</Paper>
</Container>
);
};
export default Detection;
@@ -0,0 +1,455 @@
import React, { useState, useEffect, } from "react";
import {
Container,
Box,
TextField,
Switch,
Typography,
Button,
CircularProgress,
Paper,
Divider,
IconButton,
Tooltip,
} from "@mui/material";
import {
OpenInNew as OpenInNewIcon,
FmdGood as FmdGoodIcon,
} from "@mui/icons-material"
import { toast } from "react-toastify";
import theme from '../theme.jsx';
import DetectionRuleCard from "../components/DetectionRuleCard.jsx";
import {
green,
red,
grey,
} from "../views/AngularWorkflow.jsx"
import WorkflowValidationTimeline from "../components/WorkflowValidationTimeline.jsx"
const handleDirectoryChange = (folderDisabled, setFolderDisabled, globalUrl, isDetectionActive) => {
if (!isDetectionActive) {
toast.warn("Connect to siem first for global enable/disable to work");
return;
}
const action = folderDisabled ? "enable_folder" : "disable_folder";
const url = `${globalUrl}/api/v1/detections/${action}`;
fetch(url, {
method: "PUT",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
})
.then((response) =>
response.json().then((responseJson) => {
if (responseJson["success"] === true) {
if (action === "enable_folder") setFolderDisabled(false);
else setFolderDisabled(true);
} else {
//toast(`failed to disable rule`);
}
})
)
.catch((error) => {
console.log(`Error in ${action} the rule: `, error);
toast(`An error occurred while ${action} the rule`);
});
};
const DetectionExplorer = (props) => {
const { globalUrl, userdata, ruleInfo, folderDisabled, setFolderDisabled, detectionInfo, importDetectionFromUrl, rulesLoading, isDetectionActive, setIsDetectionActive, ruleMapping, setRuleMapping, } = props;
const [searchQuery, setSearchQuery] = useState("");
const [loading, setLoading] = useState(false);
const [workflow, setWorkflow] = useState({})
const [detectionWorkflowId, setDetectionWorkflowId] = useState("")
const [isDetectionValid, setIsDetectionValid] = useState(false)
const [availableDetection, setAvailableDetection] = React.useState([]);
const [environmentList, setEnvironmentList] = React.useState([])
const loadUsecases = () => {
const url = `${globalUrl}/api/v1/workflows/usecases`
fetch(url, {
method: "GET",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
})
.then((response) =>
response.json().then((responseJson) => {
if (responseJson.success === false) {
return
}
if (responseJson.length == 0) {
return
}
for (var usecaseCategory in responseJson) {
const category = responseJson[usecaseCategory]
if (!category.name.toLowerCase().includes("respond") && !category.name.toLowerCase().includes("response")) {
continue
}
setAvailableDetection(category.list)
break
}
})
)
.catch((error) => {
console.log(`Error in loading usecases: `, error);
//toast(`An error occurred while loading usecases`);
})
}
const loadWorkflow = (workflowId) => {
const url = `${globalUrl}/api/v1/workflows/${workflowId}`
fetch(url, {
method: "GET",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
})
.then((response) =>
response.json().then((responseJson) => {
if (responseJson.id === workflowId) {
setWorkflow(responseJson)
} else {
toast(`Failed to load workflow ${workflowId}`);
}
}))
.catch((error) => {
console.log(`Error in loading workflow ${workflowId}: `, error);
toast(`An error occurred while loading workflow ${workflowId}`);
})
}
const handleConnectClick = () => {
if (detectionWorkflowId !== "") {
// FIXME: Show the Usecase UI for how to fix the workflow(s)
// Instead loading full workflow and showing it directly? Hmm
//toast.warn("Please reload the UI to load the detection status")
return
}
if (isDetectionActive) {
return
}
if (detectionInfo.category === undefined || detectionInfo.category === null) {
toast.warn("Detection category not found. Please try again or contact support@shuffler.io if you think this is a bug.")
return
}
setLoading(true);
const url = `${globalUrl}/api/v1/detections/${detectionInfo?.category}/connect`
fetch(url, {
method: "GET",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
})
.then((response) =>
response.json().then((responseJson) => {
if (responseJson["success"] === true) {
setLoading(false)
if (setIsDetectionActive !== undefined) {
setIsDetectionActive(true)
}
if (responseJson.workflow_id !== undefined && responseJson.workflow_id !== null) {
setDetectionWorkflowId(responseJson.workflow_id)
loadWorkflow(responseJson.workflow_id)
}
if (responseJson.workflow_valid !== undefined && responseJson.workflow_valid !== null) {
setIsDetectionValid(responseJson.workflow_valid)
}
} else {
if (responseJson.reason !== undefined && responseJson.reason !== null) {
toast(responseJson.reason)
} else {
if (responseJson.workflow_id === "" && responseJson.workflow_valid === false) {
toast.info(`Sent job to generate a Detection Workflow and enable ${detectionInfo?.category}. Please wait a minute and reload this UI.`);
} else {
toast.error(`Failed to connect to ${detectionInfo?.category}`);
}
}
if (responseJson.action !== undefined && responseJson.actio !== null && responseJson.action.length > 0) {
//if (responseJson.action === "environment_create") {
// navigate("/admin?tab=environments")
//}
}
setLoading(false);
}
})
)
.catch((error) => {
setLoading(false);
console.log(`Error in connecting to ${detectionInfo?.category}: `, error);
toast.error(`An error occurred while connecting to ${detectionInfo?.category}`);
});
}
const loadEnvironments = () => {
const url = `${globalUrl}/api/v1/getenvironments`
fetch(url, {
method: "GET",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
})
.then((response) => {
return response.json()
})
.then((responseJson) => {
if (responseJson.success === false) {
return
}
if (responseJson.length == 0) {
return
}
setEnvironmentList(responseJson)
})
.catch((error) => {
console.log(`Error in loading environments: `, error);
})
}
useEffect(() => {
loadUsecases()
loadEnvironments()
}, [])
useEffect(() => {
if (detectionInfo === undefined || detectionInfo === null) {
return
}
if (detectionInfo.category === undefined || detectionInfo.category === null || detectionInfo.category === "") {
return
}
handleConnectClick()
}, [detectionInfo])
const filteredRules = ruleInfo === "default" ? [] : ruleInfo?.filter((rule) =>
rule.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
rule.description.toLowerCase().includes(searchQuery.toLowerCase())
)
const lakeNodes = environmentList !== undefined && environmentList !== null ? environmentList.filter((env) => env?.data_lake?.enabled === true).length : 0
return (
<Container>
<Paper
style={{
marginTop: 50,
width: "100%",
padding: 50,
backgroundColor: theme.palette.backgroundColor,
borderRadius: theme.palette?.borderRadius,
}}
>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
mb: 2,
}}
>
<Typography variant="h6" component="div">
{detectionInfo?.title} {filteredRules === undefined || filteredRules === null ? null : `(${filteredRules?.length} rules)`}
</Typography>
<div style={{display: "flex", }}>
{workflow !== undefined && workflow !== null && workflow.id !== undefined && workflow.id !== null && workflow.id.length > 0 ?
<div style={{display: "flex", }}>
<div style={{minWidth: 400, maxWidth: 400, }}>
<WorkflowValidationTimeline
originalWorkflow={workflow}
apps={[]}
getParents={undefined}
execution={undefined}
workflow={workflow}
showHoverColor={true}
globalUrl={globalUrl}
userdata={userdata}
/>
</div>
<IconButton
variant="contained"
color="secondary"
onClick={() => {
window.open(`/workflows/${workflow.id}`, "_blank")
}}
>
<OpenInNewIcon />
</IconButton>
</div>
:
<Button
variant="contained"
onClick={() => {
handleConnectClick()
}}
disabled={loading} // Disable the button while loading
style={{
// Red = workflow exists, validation is false
// Green = workflow exists, validation is true
// Grey = workflow does not exist
backgroundColor: detectionWorkflowId === "" ? grey : isDetectionValid ? green : red,
}}
>
{loading ? <CircularProgress size={24} /> :
detectionWorkflowId === "" ? `Connect to ${detectionInfo?.category}` :
isDetectionValid ? `Connected to ${detectionInfo?.category}` : `Fix ${detectionInfo?.category} connection`}
</Button>
}
{detectionInfo?.category === "SIGMA" || detectionInfo?.category === "SIEM" ?
<Tooltip title={`You have ${lakeNodes} available Data Lake node(s)`}>
<a href="/admin?tab=environments" style={{textDecoration: "none", color: "inherit", }} target="_blank" rel="noreferrer">
<FmdGoodIcon style={{marginLeft: 15, marginTop: 5, color: lakeNodes > 0 ? green : red}} />
</a>
</Tooltip>
: null}
</div>
</Box>
{filteredRules?.length > 0 ?
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
mb: 2,
}}
>
<Box
sx={{
display: "flex",
minHeight: 50,
maxHeight: 50,
}}
>
<TextField
label="Search rules"
variant="outlined"
size="small"
sx={{ mr: 2 }}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</Box>
<Box sx={{ display: "flex", alignItems: "center" }}>
<Typography variant="body2" sx={{ mr: 1 }}>
Global disable/enable
</Typography>
<Switch
checked={!folderDisabled}
onChange={() =>
handleDirectoryChange(folderDisabled, setFolderDisabled, globalUrl, isDetectionActive)
}
/>
</Box>
</Box>
: null}
<Divider />
<Box
sx={{
height: "500px",
width: "100%",
overflowY: "auto",
p: 1,
}}
>
{filteredRules?.length > 0 ?
ruleMapping !== undefined && ruleMapping !== null && ruleMapping.value !== undefined && ruleMapping.value !== null ?
filteredRules.map((rule, index) => {
return (
<div style={{marginTop: 5, }}>
<DetectionRuleCard
globalUrl={globalUrl}
key={index}
ruleName={rule.file_name}
description={rule.description}
file_id={rule.file_id}
globalUrl={globalUrl}
folderDisabled={folderDisabled}
isDetectionActive={isDetectionActive}
ruleMapping={ruleMapping}
setRuleMapping={setRuleMapping}
availableDetection={availableDetection}
{...rule}
/>
</div>
)
})
: null
:
<div style={{textAlign: "center", }}>
{rulesLoading === true ?
<Container style={{ display: "flex", justifyContent: "center", alignItems: "center", marginTop: 25, }}>
<div>
<CircularProgress />
<Typography variant="h6" style={{ marginTop: 20 }}>Downloading rules, please wait...</Typography>
</div>
</Container>
:
<div>
<Typography variant="h6" color="textSecondary" style={{marginTop: 50, }}>
No rules loaded yet
</Typography>
<Button
style={{marginTop: 20, }}
variant="contained"
color="primary"
onClick={() => {
if (importDetectionFromUrl !== undefined) {
importDetectionFromUrl(true, detectionInfo.download_repo)
} else {
toast("Import function not found. Please contact support@shuffler.io")
}
}}
>
Load Default Rules
</Button>
</div>
}
</div>
}
</Box>
</Paper>
</Container>
);
};
export default DetectionExplorer;
@@ -0,0 +1,299 @@
import React, { useState, useEffect, } from "react";
import {
Card,
CardContent,
IconButton,
Typography,
Switch,
Tooltip,
Select,
MenuItem,
Divider,
FormLabel,
} from "@mui/material";
import DashboardBarchart, { LoadStats } from '../components/DashboardBarchart.jsx';
import {
Edit as EditIcon,
} from "@mui/icons-material";
import { toast } from "react-toastify";
import ShuffleCodeEditor from "../components/ShuffleCodeEditor1.jsx";
import theme from '../theme.jsx';
const RuleCard = ({ ruleName, description, file_id, globalUrl, folderDisabled, isTenzirActive, availableDetection, ruleMapping, setRuleMapping, ...otherProps }) => {
const [openCodeEditor, setOpenCodeEditor] = React.useState(false);
const [fileData, setFileData] = React.useState("");
const [isEnabled, setIsEnabled] = React.useState(otherProps.is_enabled);
const [filteredBarchart, setFilteredBarchart] = React.useState(null)
const [responseValue, setResponseValue] = React.useState("No response action")
const isCloud = ["localhost:3002", "shuffler.io"].includes(window.location.host);
console.log("Rulemapping: ", ruleMapping)
useEffect(() => {
//const url = `${globalUrl}/api/v1/stats/app_executions_test2`
//const resp = LoadStats(globalUrl, ruleName)
//const resp = LoadStats(globalUrl, "app_executions_test2")
const resp = LoadStats(globalUrl, "app_executions_cloud")
resp.then((data) => {
if (data === undefined) {
setFilteredBarchart([])
} else {
setFilteredBarchart(data)
}
})
if (ruleMapping !== undefined && ruleMapping !== null && ruleMapping.value !== undefined && ruleMapping.value !== null) {
console.log("FIX MAPPING FROM ruleMapping.value: ", ruleMapping)
}
}, [])
console.log("Response Value: ", responseValue)
const handleSwitchChange = (event) => {
if (folderDisabled) {
toast.warn("Enable the directory to enable individual rules");
return;
}
if (!isTenzirActive) {
toast.warn("Connect to the siem first to enable/disable the rule");
return;
}
const newIsEnabled = event.target.checked;
toggleRule(file_id, !newIsEnabled, globalUrl, () => {
setIsEnabled(newIsEnabled);
})
}
const UpdateText = (text) => {
fetch(`${globalUrl}/api/v1/files/${file_id}/edit`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: text,
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Can't update file");
}
return response.json();
})
.then((responseJson) => {
if (responseJson.success === true) {
toast("Successfully updated rule");
}
})
.catch((error) => {
toast("Error updating file: " + error.toString());
});
};
return (
<Card style={{
borderRadius: theme.palette?.borderRadius,
minHeight: 100,
marginBottom: 10,
paddingBottom: 0,
}}>
<CardContent
style={{
padding: "10px 30px 0px 30px",
}}
>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center',
color: "white",
}}
>
<Typography variant="h6">{ruleName.replaceAll("_", " ")} ({filteredBarchart === null || filteredBarchart.total === undefined ? 0 : filteredBarchart.total})</Typography>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Select
MenuProps={{
disableScrollLock: true,
}}
labelId="Response Action"
value={responseValue}
SelectDisplayProps={{
style: {
color: "rgba(255,255,255,0.4)",
},
}}
fullWidth
onChange={(e) => {
toast("Changing response: " + e.target.value)
console.log("Target: ", e.target.value)
setResponseValue(e.target.value)
// FIXME: Handle:
// 1. Get the current cache for the detection
// 2. Create a new mapping for Detection -> Response
}}
style={{
backgroundColor: theme.palette.inputColor,
color: "white",
height: 40,
borderRadius: theme.palette?.borderRadius,
}}
>
<MenuItem
style={{
backgroundColor: theme.palette.inputColor,
color: "white",
}}
value="No response action"
>
<em>No selected response</em>
</MenuItem>
<Divider />
{availableDetection === undefined || availableDetection === null ? null : availableDetection.map((data, index) => {
return (
<MenuItem
key={index}
style={{
backgroundColor: theme.palette.inputColor,
color: "white",
overflowX: "auto",
}}
value={data.name}
>
{data.name}
</MenuItem>
)
})}
</Select>
<Tooltip title="Edit Rule" placement="top">
<IconButton onClick={() => openEditBar(file_id, setOpenCodeEditor, setFileData, globalUrl)}>
<EditIcon />
</IconButton>
</Tooltip>
<Tooltip title={isEnabled && !folderDisabled ? "Disable Rule" : "Enable Rule"} placement="top">
<Switch
checked={isEnabled && !folderDisabled}
onChange={handleSwitchChange}
disabled={false}
/>
</Tooltip>
</div>
</div>
<div style={{
overflow: 'visible',
zIndex: 10,
//border: "1px solid rgba(255,255,255,0.3)",
borderRadius: theme.palette?.borderRadius,
marginTop: 5,
minHeight: 40,
maxHeight: 40,
}}>
{filteredBarchart === null ? null :
<DashboardBarchart
timelineData={filteredBarchart}
/>
}
</div>
{/*
<Typography variant="body2" style={{ marginTop: '2%' }}>
{description}
</Typography>
*/}
<ShuffleCodeEditor
isCloud={isCloud}
expansionModalOpen={openCodeEditor}
setExpansionModalOpen={setOpenCodeEditor}
setcodedata={setFileData}
codedata={fileData}
isFileEditor={true}
key={fileData} // https://reactjs.org/docs/reconciliation.html#recursing-on-children
runUpdateText={UpdateText}
/>
</CardContent>
</Card>
);
}
const toggleRule = (fileId, isCurrentlyEnabled, globalUrl, callback) => {
const action = isCurrentlyEnabled ? "disable" : "enable";
const url = `${globalUrl}/api/v1/detections/${fileId}/${action}_rule`;
fetch(url, {
method: "PUT",
credentials: "include",
headers: {
"Content-Type": "application/json",
},
})
.then((response) =>
response.json().then((responseJson) => {
if (responseJson["success"] === false) {
toast(`Failed to ${action} the rule`);
} else {
toast(`Rule ${action}d successfully`);
callback();
}
})
)
.catch((error) => {
console.log(`Error in ${action}ing the rule: `, error);
toast(`An error occurred while ${action}ing the rule`);
});
};
const openEditBar = (file_id, setOpenCodeEditor, setFileData, globalUrl) => {
getFileContent(file_id, setFileData, globalUrl)
setOpenCodeEditor(true);
};
const getFileContent = (file_id, setFileData, globalUrl) => {
setFileData("");
fetch(globalUrl + "/api/v1/files/" + file_id + "/content", {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for file :O!");
return "";
}
return response.text();
})
.then((respdata) => {
if (respdata.length === 0) {
toast("Failed getting file. Is it deleted?");
return;
}
return respdata
})
.then((responseData) => {
setFileData(responseData);
})
.catch((error) => {
toast(error.toString());
});
};
export default RuleCard;
+7 -2
View File
@@ -75,7 +75,12 @@ const DiscordChat = props => {
fullWidth
value={currentRefinement}
onChange={(event) => refine(event.currentTarget.value)}
placeholder="Search Discord Chats"
onKeyDown={(event) => {
if(event.key === "Enter") {
event.preventDefault();
}
}}
placeholder="Search Discord Chats..."
style={{ backgroundColor: theme.palette.inputColor, borderRadius: borderRadius, margin: 10, width: "100%", }}
InputProps={{
style: {
@@ -143,7 +148,7 @@ const DiscordChat = props => {
const CustomHits = connectHits(Hits);
return (
<div style={{ width: "100%", textAlign:"center", position: "relative", height: "100%", }}>
<div style={{textAlign:"center", position: "relative", height: "100%", padding:"0px 240px" }}>
<InstantSearch searchClient={searchClient} indexName="discord_chat">
<div style={{ maxWidth: 450, margin: "auto", marginTop: 15, marginBottom: 5, }}>
<CustomSearchBox />
+6 -1
View File
@@ -124,6 +124,11 @@ const DocsGrid = props => {
removeQuery("q")
refine(event.currentTarget.value)
}}
onKeyDown={(event) => {
if(event.key === "Enter") {
event.preventDefault();
}
}}
limit={5}
/>
{/*isSearchStalled ? 'My search is stalled' : ''*/}
@@ -274,7 +279,7 @@ const DocsGrid = props => {
</Button>
</div>
*/}
<div style={{width: "100%", position: "relative", height: "100%",}}>
<div style={{width: "100%", position: "relative", height: "100%", padding: "0px 180px"}}>
<InstantSearch searchClient={searchClient} indexName="documentation">
<div style={{maxWidth: 450, margin: "auto", marginTop: 15, marginBottom: 15, }}>
<CustomSearchBox />
File diff suppressed because it is too large Load Diff
+546
View File
@@ -0,0 +1,546 @@
import React, { useState, useEffect, useCallback } from 'react';
import { Box, Typography, IconButton, CircularProgress, Tooltip } from '@mui/material';
import { CheckCircle, Error, ArrowBack, Close, Cached as CachedIcon, Pause as PauseIcon } from '@mui/icons-material';
import theme from '../theme.jsx';
import ReactJson from "react-json-view-ssr";
import { toast } from 'react-toastify';
import { validateJson } from "../views/Workflows.jsx";
// import HandleJsonCopy from "./ShuffleCodeEditor1";
const STATUS_CONFIG = {
EXECUTING: {
color: '#64B5F6',
icon: () => <CircularProgress size={16} thickness={4} sx={{ color: '#64B5F6' }} />,
label: 'Executing'
},
SUCCESS: {
color: '#4CAF50',
icon: () => <CheckCircle sx={{ color: '#4CAF50', fontSize: 16 }} />,
label: 'Success'
},
FINISHED: {
color: '#4CAF50',
icon: () => <CheckCircle sx={{ color: '#4CAF50', fontSize: 16 }} />,
label: 'Finished'
},
ABORTED: {
color: '#F44336',
icon: () => <Error sx={{ color: '#F44336', fontSize: 16 }} />,
label: 'Aborted'
}
};
let to_be_copied = ""
const handleReactJsonClipboard = (copy) => {
toast("Copied JSON path to clipboard, NOT Path")
};
const HandleJsonCopy = (base, copy, base_node_name) => {
if (typeof copy.name === "string") {
copy.name = copy.name.replaceAll(" ", "_");
}
//lol
if (typeof base === 'object' || typeof base === 'dict') {
base = JSON.stringify(base)
}
if (base_node_name === "execution_argument" || base_node_name === "Execution Argument") {
base_node_name = "exec"
}
console.log("COPY: ", base_node_name, copy);
//var newitem = JSON.parse(base);
var newitem = validateJson(base).result
to_be_copied = "$" + base_node_name.toLowerCase().replaceAll(" ", "_");
for (let copykey in copy.namespace) {
if (copy.namespace[copykey].includes("Results for")) {
continue;
}
if (newitem !== undefined && newitem !== null) {
newitem = newitem[copy.namespace[copykey]];
if (!isNaN(copy.namespace[copykey])) {
to_be_copied += ".#";
} else {
to_be_copied += "." + copy.namespace[copykey];
}
}
}
if (newitem !== undefined && newitem !== null) {
newitem = newitem[copy.name];
if (!isNaN(copy.name)) {
to_be_copied += ".#";
} else {
to_be_copied += "." + copy.name;
}
}
to_be_copied.replaceAll(" ", "_");
const elementName = "copy_element_shuffle";
var copyText = document.getElementById(elementName);
if (copyText !== null && copyText !== undefined) {
console.log("NAVIGATOR: ", navigator);
const clipboard = navigator.clipboard;
if (clipboard === undefined) {
toast("Can only copy over HTTPS (port 3443)");
return;
}
navigator.clipboard.writeText(to_be_copied);
copyText.select();
copyText.setSelectionRange(0, 99999); /* For mobile devices *
/* Copy the text inside the text field */
document.execCommand("copy");
toast("Copied JSON path to clipboard.")
console.log("COPYING!");
} else {
console.log("Couldn't find element ", elementName);
}
}
const ExecuteWorkflow = async (executionData, globalUrl) => {
try {
const workflowData = executionData.workflow;
// Execute workflow with original parameters
await fetch(`${globalUrl}/api/v1/workflows/${workflowData.id}/execute`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json'
},
credentials: 'include',
body: workflowData
}).then(response => {
window.location.href = `/workflows/${workflowData.id}/code?execution_id=` + response.json().execution_id;
});
} catch (error) {
console.error('Error re-executing workflow:', error);
}
};
const ExecutionsList = ({ executions, onSelectExecution, activeExecutionId }) => {
return (
<Box>
{executions.map((execution) => {
const status = STATUS_CONFIG[execution.status] || STATUS_CONFIG.ABORTED;
return (
<Box
key={execution.execution_id}
onClick={() => onSelectExecution(execution)}
sx={{
display: 'flex',
alignItems: 'center',
cursor: 'pointer',
py: 1,
px: 2,
borderBottom: '1px solid #2A2A2A',
backgroundColor: activeExecutionId === execution.execution_id ?
'rgba(255,255,255,0.05)' : 'transparent',
'&:hover': {
backgroundColor: 'rgba(255,255,255,0.05)'
}
}}
>
{status.icon()}
<Box sx={{ ml: 2, flex: 1, overflow: 'hidden' }}>
<Typography variant="body2" sx={{
color: '#E0E0E0',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}}>
{new Date(execution.started_at * 1000).toLocaleString()}
</Typography>
<Typography variant="caption" sx={{
color: status.color
}}>
{status.label}
</Typography>
</Box>
</Box>
);
})}
</Box>
);
};
const ExecutionDetail = ({ execution: initialExecution, onBack, globalUrl, onExecutionUpdate, selectedAction, executeWorkflow }) => {
const [execution, setExecution] = useState(initialExecution);
const [status, setStatus] = useState(STATUS_CONFIG[execution.status] || STATUS_CONFIG.EXECUTING);
const [validResult, setValidResult] = useState("{}")
const abortExecution = async () => {
try {
await fetch(`${globalUrl}/api/v1/workflows/${execution.workflow.id}/executions/${execution.execution_id}/abort`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
}).then((response) => {
if (response.ok) {
const updatedExecution = {
...execution,
status: "ABORTED",
};
setExecution(updatedExecution);
onExecutionUpdate(updatedExecution);
}
});
} catch (error) {
console.log("Abort error:", error);
}
};
useEffect(() => {
setStatus(STATUS_CONFIG[execution.status] || STATUS_CONFIG.EXECUTING);
}, [execution]);
const pollExecutionStatus = useCallback(async () => {
try {
const response = await fetch(`${globalUrl}/api/v1/streams/results`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include',
body: JSON.stringify({
execution_id: execution.execution_id,
authorization: execution.authorization,
}),
});
if (response.ok) {
const data = await response.json();
const currentStatus = data.results?.[0]?.status || 'EXECUTING';
const updatedExecution = {
...execution,
...data,
status: currentStatus
};
setExecution(updatedExecution);
onExecutionUpdate(updatedExecution);
}
} catch (error) {
console.error('Polling error:', error);
}
}, [execution, globalUrl, onExecutionUpdate]);
useEffect(() => {
let pollTimeout;
if (execution.status === 'EXECUTING') {
pollTimeout = setTimeout(() => pollExecutionStatus(), 3000);
}
if (execution?.results?.length === 1) {
setValidResult(JSON.parse(execution?.results[0]?.result || "{}"))
}
return () => clearTimeout(pollTimeout);
}, [execution.status, pollExecutionStatus]);
return (
<Box sx={{ height: '100%', display: 'flex', flexDirection: 'column' }}>
<Box sx={{
display: 'flex',
alignItems: 'center',
p: 2,
borderBottom: '1px solid #454545'
}}>
<IconButton onClick={onBack} size="small" sx={{ mr: 2 }}>
<ArrowBack fontSize="small" />
</IconButton>
<Typography variant="subtitle2" sx={{ color: '#E0E0E0', mr: 2 }}>
Execution Details
</Typography>
{status.icon()}
<Typography variant="body2" sx={{ ml: 1, color: status.color }}>
{status.label}
</Typography>
<Box sx={{ ml: 'auto' }}>
{execution.status === "EXECUTING" && (
<Tooltip title="Abort workflow" placement="top">
<IconButton
size="small"
onClick={abortExecution}
sx={{ color: theme.palette.error.main }}
>
<PauseIcon fontSize="small" />
</IconButton>
</Tooltip>
)}
<Tooltip title="Rerun workflow (uses same startnode as the original)" placement="top">
<IconButton
size="small"
onClick={() => {
ExecuteWorkflow(
execution,
globalUrl
);
}}
sx={{ color: theme.palette.primary.main }}
>
<CachedIcon fontSize="small" />
</IconButton>
</Tooltip>
</Box>
</Box>
<Box sx={{ flex: 1, overflow: 'auto', p: 2 }}>
<Box sx={{ mb: 3 }}>
<Typography variant="caption" sx={{ color: '#888', display: 'block', mb: 1 }}>
Started at
</Typography>
<Typography variant="body2">
{new Date(execution.started_at * 1000).toLocaleString()}
</Typography>
</Box>
<Box sx={{ mb: 3 }}>
<Typography variant="caption" sx={{ color: '#888', display: 'block', mb: 1 }}>
Execution ID
</Typography>
<Typography variant="body2" sx={{
backgroundColor: '#2A2A2A',
p: 1,
borderRadius: 1,
fontFamily: 'monospace'
}}>
{execution.execution_id}
</Typography>
</Box>
<Box>
<Box>
<Typography variant="caption" sx={{ color: '#888', display: 'block', mb: 1 }}>
Result
</Typography>
<Box sx={{
backgroundColor: '#2A2A2A',
borderRadius: 1,
overflow: 'auto',
maxHeight: '200px'
}}>
<pre style={{
margin: 0,
padding: '1rem',
fontSize: '12px',
color: status.color,
fontFamily: "'JetBrains Mono', monospace"
}}>
{execution?.status === 'EXECUTING' ? (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', p: 2 }}>
<CircularProgress size={24} />
<Typography sx={{ ml: 2, color: '#888' }}>Executing...</Typography>
</Box>
) : (
execution?.results?.length === 1 ?
<ReactJson
src={validResult}
theme={theme.palette.jsonTheme}
style={{
borderRadius: 5,
border: `2px solid ${theme.palette.inputColor}`,
padding: 10,
maxHeight: 450,
minheight: 450,
overflow: "auto",
minWidth: 450,
maxWidth: "100%",
zIndex: 1200
}}
enableClipboard={(copy) => {
handleReactJsonClipboard(copy);
}}
collapsed={false}
displayDataTypes={false}
onSelect={(select) => {
var basename = "exec"
if (selectedAction !== undefined && selectedAction !== null && Object.keys(selectedAction).length !== 0) {
basename = selectedAction.label.toLowerCase().replaceAll(" ", "_")
}
HandleJsonCopy(validResult, select, basename)
}}
name={"JSON autocompletion"}
/> :
<ReactJson
src={{}}
theme={theme.palette.jsonTheme}
style={{
borderRadius: 5,
border: `2px solid ${theme.palette.inputColor}`,
padding: 10,
maxHeight: 450,
minheight: 450,
overflow: "auto",
minWidth: 450,
maxWidth: "100%",
zIndex: 1200
}}
collapsed={false}
enableClipboard={(copy) => { }}
displayDataTypes={false}
name={"JSON autocompletion"}
/>
)}
</pre>
</Box>
</Box>
</Box>
</Box>
</Box>
);
};
const ExecutionPanel = ({
workflow,
globalUrl,
onClose,
currentExecution,
mainAction
}) => {
const [executions, setExecutions] = useState([]);
const [selectedExecution, setSelectedExecution] = useState(null);
const [loading, setLoading] = useState(true);
const handleExecutionUpdate = useCallback((updatedExecution) => {
setExecutions(prevExecutions => {
const updatedExecutions = [...prevExecutions];
const index = updatedExecutions.findIndex(
e => e.execution_id === updatedExecution.execution_id
);
if (index !== -1) {
updatedExecutions[index] = updatedExecution;
}
return updatedExecutions;
});
}, []);
const fetchExecutions = useCallback(async () => {
setLoading(true);
try {
const response = await fetch(`${globalUrl}/api/v2/workflows/${workflow.id}/executions`, {
credentials: 'include',
});
if (response.ok) {
const data = await response.json();
setExecutions(data.executions);
const urlParams = new URLSearchParams(window.location.search);
const executionId = urlParams.get('execution_id');
if (executionId) {
const execution = data.executions.find(e => e.execution_id === executionId);
if (execution) {
setSelectedExecution(execution);
}
}
}
} catch (error) {
console.error('Failed to fetch executions:', error);
} finally {
setLoading(false);
}
}, [workflow.id, globalUrl]);
useEffect(() => {
fetchExecutions();
}, [fetchExecutions]);
useEffect(() => {
if (currentExecution?.execution_id) {
setExecutions(prev => {
const existingIndex = prev.findIndex(e => e.execution_id === currentExecution.execution_id);
if (existingIndex === -1) {
return [currentExecution, ...prev];
}
const updated = [...prev];
updated[existingIndex] = currentExecution;
return updated;
});
setSelectedExecution(currentExecution);
}
}, [currentExecution]);
return (
<Box
sx={{
position: 'relative',
height: "fit-content",
backgroundColor: '#1E1E1E',
borderTop: '1px solid #454545',
overflow: 'hidden',
color: '#CCCCCC',
fontFamily: "'JetBrains Mono', monospace",
display: 'flex',
flexDirection: 'column',
}}
>
{loading && !executions.length ? (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' }}>
<CircularProgress size={24} />
</Box>
) : selectedExecution ? (
<ExecutionDetail
execution={selectedExecution}
onBack={() => {
setSelectedExecution(null);
const url = new URL(window.location);
url.searchParams.delete('execution_id');
window.history.pushState({}, '', url);
fetchExecutions();
}}
globalUrl={globalUrl}
selecteAction={mainAction}
onExecutionUpdate={handleExecutionUpdate}
/>
) : (
<>
<Box sx={{
p: 2,
borderBottom: '1px solid #454545',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}>
<Typography variant="subtitle2" sx={{ color: '#E0E0E0' }}>
Execution History
</Typography>
<IconButton size="small" onClick={onClose} sx={{ color: '#888' }}>
<Close fontSize="small" />
</IconButton>
</Box>
<Box sx={{ flex: 1, overflow: 'auto' }}>
<ExecutionsList
executions={executions}
onSelectExecution={(execution) => {
setSelectedExecution(execution);
const url = new URL(window.location);
url.searchParams.set('execution_id', execution.execution_id);
window.history.pushState({}, '', url);
}}
activeExecutionId={currentExecution?.execution_id}
/>
</Box>
</>
)}
</Box>
);
};
export default ExecutionPanel;
+2 -2
View File
@@ -227,7 +227,7 @@ const ExploreWorkflow = (props) => {
<ArrowBackIosNewIcon />
</IconButton>
</Tooltip>
<div style={{ minWidth: 554, maxWidth: 554, borderRadius: theme.palette.borderRadius, }}>
<div style={{ minWidth: 554, maxWidth: 554, borderRadius: theme.palette?.borderRadius, }}>
<AliceCarousel
style={{ backgroundColor: theme.palette.surfaceColor, minHeight: 750, maxHeight: 750, }}
items={formattedCarousel}
@@ -320,7 +320,7 @@ const ExploreWorkflow = (props) => {
<div style={{ marginTop: 0, }}>
<div className="thumbs" style={{ display: "flex" }}>
<div style={{ minWidth: isMobile ? 300 : 554, maxWidth: isMobile ? 300 : 554, borderRadius: theme.palette.borderRadius, }}>
<div style={{ minWidth: isMobile ? 300 : 554, maxWidth: isMobile ? 300 : 554, borderRadius: theme.palette?.borderRadius, }}>
<Grid item xs={11} style={{}}>
{suggestedUsecases.length === 0 && usecasesSet ?
<Typography variant="h6" style={{ marginTop: 30, marginBottom: 50, }} color="rgba(158, 158, 158, 1)">
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,687 @@
import React, { useState, useEffect } from "react";
import { toast } from "react-toastify"
import theme from '../theme.jsx';
import AuthenticationOauth2 from "../components/Oauth2Auth.jsx";
import { validateJson, GetIconInfo } from "../views/Workflows.jsx";
import {
Tooltip,
Typography,
Button,
Divider,
MenuItem,
Select,
Chip,
TextField,
CircularProgress,
} from "@mui/material"
import {
CheckCircleOutline as CheckCircleOutlineIcon,
ErrorOutline as ErrorOutlineIcon,
} from "@mui/icons-material"
import {
green,
red,
} from "../views/AngularWorkflow.jsx"
const FixWorkflowValidationErrors = (props) => {
const { globalUrl, workflow, setWorkflow, setUpdateParent, } = props;
const [appsLoading, setAppsLoading] = useState(false)
const [apps, setApps] = useState([])
const [appAuth, setAppAuth] = useState([])
const [_, setUpdate] = useState(0)
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io" || window.location.host === "migration.shuffler.io";
if (workflow === undefined || workflow === null) {
console.error("Workflow is undefined")
return null
}
if (workflow.validation === undefined || workflow.validation === null) {
console.error("Workflow validation is undefined")
return null
}
if (workflow.validation.valid === true) {
console.error("Workflow is valid - nothing to do for errors")
return null
}
if (setWorkflow === undefined || setWorkflow === null) {
console.error("No setWorkflow")
return null
}
const fetchApps = () => {
if (appsLoading === true) {
return
}
setAppsLoading(true)
const url = `${globalUrl}/api/v1/apps`
fetch(url,{
method: "GET",
credentials: "include"
})
.then(response => response.json())
.then(data => {
setAppsLoading(false)
if (data.success === false) {
return
}
setApps(data)
})
.catch(error => {
setAppsLoading(false)
console.error("Error: ", error)
})
}
// Save the workflow as well
const saveWorkflow = (workflow) => {
if (workflow.id === undefined || workflow.id === null) {
toast("Workflow ID is missing during save. Please try again")
return
}
const url = `${globalUrl}/api/v1/workflows/${workflow.id}`
fetch(url, {
method: "PUT",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
body: JSON.stringify(workflow),
})
.then(response => response.json())
.then(data => {
if (data.success === false) {
toast("Failed to save workflow")
return
}
})
.catch(error => {
toast("Failed to save workflow: " + error.toString())
})
}
const fetchAuthentication = (reset, updateAction, closeMenu, action_id) => {
if (appsLoading === true) {
return
}
const url = `${globalUrl}/api/v1/apps/authentication`
fetch(url,{
method: "GET",
credentials: "include"
})
.then(response => response.json())
.then(data => {
if (data.success === false) {
return
}
const authlist = data.data
setAppAuth(authlist)
if (updateAction === true) {
console.log("Updating action: ", action_id)
// Find the action in the workflow and set auth for it
var foundActionIndex = -1
for (var i = 0; i < workflow.actions.length; i++) {
if (workflow.actions[i].id === action_id) {
foundActionIndex = i
break
}
}
if (foundActionIndex === -1) {
console.error("Failed to find action in workflow")
return
}
const appId = workflow.actions[foundActionIndex].app_id
var lastauth = -1
for (var authKey in authlist) {
if (authlist[authKey].app_id !== appId) {
continue
}
if (authlist[authKey].created > lastauth) {
lastauth = authlist[authKey].created
} else {
continue
}
console.log("FOUND AUTH: ", authlist[authKey])
workflow.actions[foundActionIndex].authentication_id = authlist[authKey].id
}
if (setWorkflow !== undefined) {
setWorkflow(workflow)
}
}
})
.catch(error => {
console.error("Auth loading error: ", error)
})
}
if (apps !== undefined && apps !== null && apps.length === 0 && appsLoading === false) {
fetchApps()
fetchAuthentication()
}
const setSelectedAction = (action) => {
if (workflow === undefined || workflow === null) {
return null
}
if (workflow.actions === undefined || workflow.actions === null || workflow.actions.length === 0) {
return null
}
if (setWorkflow === undefined || setWorkflow === null) {
return null
}
for (var i = 0; i < workflow.actions.length; i++) {
if (workflow.actions[i].id === action.id) {
workflow.actions[i] = action
// Update any action with the same app_id to have same auth
for (var j = 0; j < workflow.actions.length; j++) {
if (workflow.actions[j].app_id === action.app_id) {
workflow.actions[j].authentication_id = action.authentication_id
workflow.actions[j].selectedAuthentication = action.selectedAuthentication
}
}
break
}
}
setWorkflow(workflow)
}
const ErrorItem = (props) => {
const { apps, error, index } = props
const [validating, setValidating] = useState(false)
const [actionRunInfo, setActionRunInfo] = useState({})
if (error === undefined || error === null) {
return null
}
if (apps === undefined || apps === null || apps.length === 0) {
return null
}
const validateApp = (app, action) => {
if (validating) {
return
}
setValidating(true)
// FIXME: Run execution:
// 1. Should set app authentication validation
if (isCloud) {
action.environment = "Cloud"
} else {
action.environment = "Shuffle"
}
/*
setExecutionResult({
valid: false,
result: baseResult,
})
setExecuting(true);
*/
setActionRunInfo({})
const url = `${globalUrl}/api/v1/apps/${app.id}/run?validation=true`
fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(action),
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for stream results :O!");
}
return response.json();
})
.then((responseJson) => {
setValidating(false)
setActionRunInfo(responseJson)
//console.log("RESPONSE: ", responseJson)
if (
responseJson.success === true &&
responseJson.result !== null &&
responseJson.result !== undefined &&
responseJson.result.length > 0
) {
//toast("
}
})
.catch((error) => {
toast("Execution error: " + error.toString());
setValidating(false)
})
}
var authReturn = null
var validationReturn = null
var foundApp = {
"name": "",
"id": "",
}
var foundAction = {
"name": "",
"label": "",
"id": "",
"app_id": "",
"app_name": "",
}
var selectedImage = null
var resolveButton = null
if (error.app_id !== undefined && error.app_id !== null) {
for (var i = 0; i < apps.length; i++) {
if (apps[i].id === error.app_id) {
foundApp = apps[i]
break
}
}
if (!foundApp) {
toast("Couldn't find relevant app. Is it activated?")
return "Failed to find app"
}
selectedImage =
<img
src={foundApp.large_image}
style={{
width: 25,
height: 25,
marginRight: 10,
borderRadius: theme.palette?.borderRadius,
border: `1px solid ${theme.palette.borderColor}`,
}}
/>
}
if (error.action_id !== undefined && error.action_id !== null && workflow.actions !== undefined && workflow.actions !== null && workflow.actions.length > 0) {
for (var i = 0; i < workflow.actions.length; i++) {
if (workflow.actions[i].id === error.action_id) {
foundAction = workflow.actions[i]
break
}
}
}
const validationIcon = Object.getOwnPropertyNames(actionRunInfo).length === 0 ? null :
<Tooltip
title={
<Typography variant="body1">
{actionRunInfo.result}
</Typography>
}
placement="bottom"
>
{actionRunInfo.validation.valid === true ?
<CheckCircleOutlineIcon style={{color: green, marginRight: 10, }} />
:
<ErrorOutlineIcon style={{color: red, marginRight: 10, }} />
}
</Tooltip>
const authenticationType = foundApp.authentication
if (error.type === "configuration" || error.type === "authentication") {
// FIXME: Check the error
if (appAuth === undefined || appAuth === null) {
return "Loading auth"
}
var relevantAuthentication = []
var foundAuth = {}
for (var key in appAuth) {
if (appAuth[key].app.id !== error.app_id) {
continue
}
foundAuth = appAuth[key]
relevantAuthentication.push(appAuth[key])
}
if (foundAction.selectedAuthentication === undefined || foundAction.selectedAuthentication === null) {
foundAction.selectedAuthentication = {}
}
console.log("FOUNDACTION: ", foundAction, foundAuth)
var authFound = false
if (foundAuth.id !== undefined && foundAuth.id !== null && foundAuth.id.length > 0) {
var authGroups = []
// Choose from a dropdown
authReturn = <Select
MenuProps={{
disableScrollLock: true,
}}
labelId="select-app-auth"
value={
foundAction.authentication_id === "authgroups" ? "authgroups" :
Object.getOwnPropertyNames(foundAction.selectedAuthentication).length === 0
? "No selection"
: foundAction.selectedAuthentication
}
SelectDisplayProps={{
style: {
},
}}
fullWidth
onChange={(e) => {
if (e.target.value === "No selection") {
foundAction.selectedAuthentication = {};
foundAction.authentication_id = "";
for (let [key,keyval] in Object.entries(foundAction.parameters)) {
if (foundAction.parameters[key].configuration === false) {
//console.log("FIELDSKIP: ", foundAction.parameters[key].name)
continue
}
if (foundAction.parameters[key].name === "url" && authenticationType?.type === "oauth2-app" && foundAction.parameters[key].value.includes("http")) {
continue
}
if (foundAction.parameters[key].example !== undefined && foundAction.parameters[key].example !== null && foundAction.parameters[key].example !== "") {
if (foundAction.parameters[key].example.toLowerCase().includes("apik") || foundAction.parameters[key].example.toLowerCase().includes("key") || foundAction.parameters[key].example.toLowerCase().includes("pass") || foundAction.parameters[key].example.toLowerCase().includes("****")) {
foundAction.parameters[key].value = ""
} else {
foundAction.parameters[key].value = foundAction.parameters[key].example
}
} else {
foundAction.parameters[key].value = ""
}
}
setSelectedAction(foundAction)
setUpdate(Math.random());
} else if (e.target.value === "authgroups") {
if (authGroups !== undefined && authGroups !== null && authGroups.length === 0) {
toast("No auth groups created. Opening window to create one")
setTimeout(() => {
window.open("/admin?tab=app_auth", "_blank")
}, 2500)
} else {
foundAction.selectedAuthentication = {};
foundAction.authentication_id = "authgroups"
for (let [key,keyval] in Object.entries(foundAction.parameters)) {
//console.log(foundAction.parameters[key])
if (foundAction.parameters[key].configuration) {
if (foundAction.parameters[key].name === "url" && authenticationType?.type === "oauth2-app") {
} else {
foundAction.parameters[key].value = "authgroup controlled"
}
}
}
setSelectedAction(foundAction)
setUpdate(Math.random())
}
} else {
foundAction.selectedAuthentication = e.target.value
foundAction.authentication_id = e.target.value.id
setSelectedAction(foundAction)
setUpdate(Math.random())
}
}}
style={{
backgroundColor: theme.palette.inputColor,
color: "white",
height: 40,
borderRadius: theme.palette?.borderRadius,
}}
>
<MenuItem
style={{
backgroundColor: theme.palette.inputColor,
color: "white",
}}
value="No selection"
>
{selectedImage}
<em>No selection</em>
</MenuItem>
{relevantAuthentication.map((data) => {
if (data.last_modified === true) {
//console.log("LAST MODIFIED: ", data.label)
}
if (foundAction.authentication_id === data.id) {
authFound = true
}
return (
<MenuItem
key={data.id}
disabled={data.id === foundAction.authentication_id}
style={{
backgroundColor: theme.palette.inputColor,
color: "white",
overflowX: "auto",
}}
value={data}
>
{selectedImage}
{data.label}
</MenuItem>
)
})}
{/*
<Divider style={{marginTop: 10, marginBottom: 10, }}/>
<MenuItem
disabled
style={{
backgroundColor: theme.palette.inputColor,
color: "white",
}}
value="authgroups"
>
<em>Auth Groups</em>
</MenuItem>
*/}
</Select>
}
// FIXME: Validate the CURRENT authentication that has been chosen?
if (foundApp.authentication === undefined || foundApp.authentication === null) {
toast("Authentication error: No authentication found")
authReturn = "Failed to find auth"
}
if (authReturn === null && foundApp.authentication.type === "oauth2" || foundApp.authentication.type === "oauth2-app") {
authReturn =
<AuthenticationOauth2
globalUrl={globalUrl}
authenticationType={foundApp.authentication}
selectedAction={foundAction}
selectedApp={foundApp}
getAppAuthentication={fetchAuthentication}
isCloud={true}
authButtonOnly={true}
/>
} else if (authReturn === null) {
authReturn = "Other auth - Not implemented"
}
validationReturn = authReturn === null || foundAuth.id === undefined || foundAuth.id === null || foundAuth.id.length === 0 || !authFound ? null :
<Button
fullWidth
variant="outlined"
color="secondary"
style={{
height: 35,
justifyContent: !validating ? "flex-start" : "center",
textTransform: "none",
fontSize: 18,
borderRadius: theme.palette?.borderRadius,
}}
onClick={() => {
toast("Validating app")
validateApp(foundApp, foundAction)
}}
>
{validationIcon}
{validating ?
<CircularProgress
color="secondary"
style={{width: 30, height: 30, }}
/>
:
<span>
{selectedImage}
Validate {foundApp.name.replace("_", " ", -1)}
</span>
}
</Button>
//resolveButton = !(Object.getOwnPropertyNames(actionRunInfo).length === 0 || actionRunInfo.validation.valid === true) ? null :
resolveButton =
<Button
fullWidth
variant="outlined"
color="primary"
style={{
height: 35,
textTransform: "none",
backgroundColor: green,
color: "black",
borderRadius: 50,
width: 200,
margin: "auto",
marginTop: 35,
}}
onClick={() => {
toast("Resolving error")
console.log("Error: ", error)
console.log("Errors: ", workflow.validation)
// Remove the error from the validation list
var newErrors = []
for (var workflowErrorKey in workflow.validation.errors) {
if (workflow.validation.errors[workflowErrorKey].error !== error.error) {
newErrors.push(workflow.validation.errors[workflowErrorKey])
continue
}
}
workflow.validation.errors = newErrors
if (workflow.validation.errors.length === 0) {
workflow.validation.valid = true
}
// Sets it in the parent
setWorkflow(workflow)
if (setUpdateParent !== undefined) {
setUpdateParent(Math.random())
}
// Saves the actual workflow with the update(s)
saveWorkflow(workflow)
}}
>
Resolve
</Button>
}
return (
<div>
{authReturn}
<div style={{marginTop: 5, }} />
{validationReturn}
{resolveButton}
</div>
)
}
console.log("Workflow validation: ", workflow.validation)
return (
<div>
{workflow.errors !== undefined && workflow.errors !== null ?
<div>
General errors: {workflow.errors.length}
{workflow.errors.map((error, index) => {
return (
<div>
- {error}
</div>
)
})}
</div>
: null}
<Divider style={{marginTop: 15, marginBottom: 15, }}/>
{workflow.validation.errors !== undefined && workflow.validation.errors !== null ?
<div>
Validation errors: {workflow.validation.errors.length}
{workflow.validation.errors.map((error, index) => {
return (
<div>
<ErrorItem
apps={apps}
error={error}
index={index}
/>
</div>
)
})}
</div>
: null}
<Divider style={{marginTop: 15, marginBottom: 15, }} />
Apps loaded: {apps.length}
</div>
)
}
export default FixWorkflowValidationErrors
+3 -5
View File
@@ -50,8 +50,6 @@ const hoverOutColor = "#e8eaf6"
const Header = props => {
const { globalUrl, setNotifications, notifications, isLoggedIn, removeCookie, homePage, userdata, serverside, } = props;
//const theme = useTheme();
//const alert = useAlert()
const [HomeHoverColor, setHomeHoverColor] = useState(hoverOutColor);
@@ -829,7 +827,7 @@ const { globalUrl, setNotifications, notifications, isLoggedIn, removeCookie, ho
},
}}
style={{
borderRadius: theme.palette.borderRadius,
borderRadius: theme.palette?.borderRadius,
backgroundColor: theme.palette.surfaceColor,
marginRight: 15,
color: "white",
@@ -904,7 +902,7 @@ const { globalUrl, setNotifications, notifications, isLoggedIn, removeCookie, ho
<Tooltip color="primary" title={parsedTitle} placement="left">
<div style={{display: "flex"}}>
{isCloud?<Typography variant="body2" style={{borderRadius: theme.palette.borderRadius, float: "left", margin: "0 0 0 0", marginRight: 25, }}>{regiontag}</Typography>:null} {image} <span style={{marginLeft: 8}}>{data.name}</span>
{isCloud?<Typography variant="body2" style={{borderRadius: theme.palette?.borderRadius, float: "left", margin: "0 0 0 0", marginRight: 25, }}>{regiontag}</Typography>:null} {image} <span style={{marginLeft: 8}}>{data.name}</span>
</div>
</Tooltip>
</MenuItem>
@@ -935,7 +933,7 @@ const { globalUrl, setNotifications, notifications, isLoggedIn, removeCookie, ho
null
:
<Tooltip title={`Amount of executions left: ${userdata.app_execution_usage} / ${userdata.app_execution_limit}. When the limit is reached, you can still use Shuffle normally, but your Workflow triggers may stop working. Reach out to support@shuffler.io to extend this limit.`}>
<div style={{maxHeight: 30, minHeight: 30, padding: 8, textAlign: "center", cursor: "pointer", borderRadius: theme.palette.borderRadius, marginRight: 10, marginTop: 12, backgroundColor: theme.palette.surfaceColor, minWidth: 60, maxWidth: 60, border: userdata.app_execution_usage/userdata.app_execution_limit >= 0.9 ? "#f86a3e" : null, }} onClick={() => {
<div style={{maxHeight: 30, minHeight: 30, padding: 8, textAlign: "center", cursor: "pointer", borderRadius: theme.palette?.borderRadius, marginRight: 10, marginTop: 12, backgroundColor: theme.palette.surfaceColor, minWidth: 60, maxWidth: 60, border: userdata.app_execution_usage/userdata.app_execution_limit >= 0.9 ? "#f86a3e" : null, }} onClick={() => {
console.log(userdata.appe_execution_usage/userdata.app_execution_limit)
if (window.drift !== undefined) {
window.drift.api.startInteraction({ interactionId: 326905 })
+2 -2
View File
@@ -11,14 +11,14 @@ import {
import HealthBarChart from '../components/HealthBarChart.jsx';
const HealthPage = (props) => {
const { userdata } = props;
const { globalUrl, userdata } = props;
const [healthData, setHealthData] = useState(null);
const [selectedRange, setSelectedRange] = useState('30d');
const [filteredData, setFilteredData] = useState([]);
const [averageUptime, setAverageUptime] = useState(0);
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
const globalUrl = `https://shuffler.io`
//const globalUrl = `https://shuffler.io`
console.log("HEALTHPAGE 1")
File diff suppressed because it is too large Load Diff
+16 -17
View File
@@ -2,7 +2,6 @@ import React, { useState, useEffect } from "react";
import ReactGA from 'react-ga4';
import theme from "../theme.jsx";
import { useTheme } from "@mui/styles";
import countries from "../components/Countries.jsx";
import {
Box,
@@ -88,7 +87,7 @@ const LicencePopup = (props) => {
const paperStyle = {
padding: 20,
borderRadius: theme.palette.borderRadius,
borderRadius: theme.palette?.borderRadius,
height: "100%",
}
@@ -194,7 +193,7 @@ const LicencePopup = (props) => {
return (
<Tooltip
style={{ borderRadius: theme.palette.borderRadius, }}
style={{ borderRadius: theme.palette?.borderRadius, }}
placement="bottom"
>
<div style={{}}>
@@ -320,7 +319,7 @@ const LicencePopup = (props) => {
margin: "auto",
width: 100,
backgroundColor: "white",
// borderRadius: theme.palette.borderRadius,
// borderRadius: theme.palette?.borderRadius,
}}
/>
: null}
@@ -390,7 +389,7 @@ const LicencePopup = (props) => {
value={feature.split("Worker License: ")[1]}
style={{
// backgroundColor: theme.palette.inputColor,
// borderRadius: theme.palette.borderRadius,
// borderRadius: theme.palette?.borderRadius,
}}
id={fieldId}
onClick={() => { }}
@@ -515,16 +514,16 @@ const LicencePopup = (props) => {
}
useEffect(() => {
console.log("New variant: ", shuffleVariant)
console.log("New variant: ", shuffleVariant)
if (shuffleVariant === 1) {
setCalculatedCost("$600")
setSelectedValue(8)
} else {
setCalculatedCost("$540")
setSelectedValue(300)
}
}, [shuffleVariant])
if (shuffleVariant === 1) {
setCalculatedCost("$960")
setSelectedValue(8)
} else {
setCalculatedCost("$960")
setSelectedValue(300)
}
}, [shuffleVariant])
if (typeof window === 'undefined' || window.location === undefined) {
return null
@@ -680,7 +679,7 @@ const LicencePopup = (props) => {
color: "white",
}
console.log("Priceitem: ", shuffleVariant)
const isLoggedInHandler = () => {
if (calculatedCost === payasyougo) {
handlePayasyougo(props.userdata)
@@ -690,7 +689,7 @@ const LicencePopup = (props) => {
const priceItem = window.location.origin === "https://shuffler.io" ?
shuffleVariant === 0 ? "app_executions" : "cores"
:
shuffleVariant === 0 ? "price_1MROFrDzMUgUjxHShcSxgHO1" : "price_1NXjQqDzMUgUjxHSg690R4FP"
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`
@@ -824,7 +823,7 @@ const LicencePopup = (props) => {
{errorMessage.length > 0 ? <Typography variant="h4">Error: {errorMessage}</Typography> : null}
<Card style={{
padding: 20,
borderRadius: theme.palette.borderRadius,
borderRadius: theme.palette?.borderRadius,
border: "1px solid #f85a3e",
}}>
<div>
+197
View File
@@ -0,0 +1,197 @@
import React, { useEffect, useState } from "react";
import { Paper, Typography, Box, CircularProgress, TextField, Button } from "@mui/material";
import { toast } from "react-toastify";
const MFASetup = ({ isLoaded, globalUrl, setCookie }) => {
const [image2FA, setImage2FA] = useState("");
const [secret2FA, setSecret2FA] = useState("");
const [mfaCode, setMfaCode] = useState("");
const [code, setCode] = useState(null);
useEffect(() => {
handleGet2FACode();
}, []);
useEffect(() => {
if (isLoaded) {
const code = window.location.pathname.split("/")[2];
setMfaCode(code);
}
}, [isLoaded]);
const handleGet2FACode = () => {
if (mfaCode === "") {
return;
}
fetch(`${globalUrl}/api/v1/users/${mfaCode}/get2fa`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status === 404) {
toast("User not found. Redirecting to login page in 3 seconds...");
setTimeout(() => {
window.location.pathname = "/login";
return;
}, 3000);
}
if (response.status !== 200) {
console.log("Status not 200 for apps :O!");
}
return response.json();
})
.then((responseJson) => {
if (responseJson.success === true) {
setImage2FA(responseJson.reason);
setSecret2FA(responseJson.extra);
}
})
.catch((error) => {
toast(error.toString());
});
};
useEffect(() => {
if (mfaCode) {
handleGet2FACode();
}
}, [mfaCode]);
const handleVerify2FA = (mfaCode, code, changeMFAActive) => {
const data = {
code: code,
changeMFAActive: changeMFAActive,
};
toast("Verifying 2fa code. Please wait...");
fetch(`${globalUrl}/api/v1/users/${mfaCode}/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 === 500) {
toast("Wrong code sent. Please try again.");
return;
}
return response.json();
})
.then((responseJson) => {
if (responseJson.success === true) {
toast.success("Successfully setup 2fa. Redirecting in 3 seconds...");
for (var key in responseJson["cookies"]) {
setCookie(responseJson["cookies"][key].key, responseJson["cookies"][key].value, { path: "/" });
}
const tmpView = new URLSearchParams(window.location.search).get("view");
if (tmpView !== undefined && tmpView !== null) {
var newUrl = `/${tmpView}`;
if (tmpView.startsWith("/")) {
newUrl = `${tmpView}`;
}
window.location.pathname = newUrl;
return;
}
if (responseJson.tutorials !== undefined && responseJson.tutorials !== null) {
const welcome = responseJson.tutorials.find((element) => element.name === "welcome");
if (welcome === undefined || welcome === null) {
setTimeout(() => {
window.location.pathname = "/welcome";
}, 3000);
}
}
setTimeout(() => {
window.location.pathname = "/workflows";
}, 3000);
} else {
toast("Failed to setup 2fa. Please try again.");
}
})
.catch((error) => {
console.error("Error:", error);
});
};
return (
<div style={{ paddingTop: 50, margin: "0px auto", width: "500px" }}>
<Paper elevation={3} style={{ padding: "30px", backgroundColor: "#212121" }}>
<Typography variant="h5" style={{ color: "white", marginBottom: 10, textAlign: "center" }}>
Multi-Factor Authentication Setup
</Typography>
<div style={{ marginTop: 15 }}>
<QRCodeSection secret2FA={secret2FA} image2FA={image2FA} />
</div>
<Typography variant="body1" style={{ color: "white", marginTop: 15 }}>
Enter the code from your authenticator app below.
</Typography>
<TextField
variant="outlined"
margin="normal"
required
fullWidth
id="code"
label="Code"
name="code"
autoComplete="code"
autoFocus
onChange={(e) => setCode(e.target.value)}
onKeyPress={(e) => {
if (e.key === "Enter" && code !== null && code !== "" && code.length === 6) {
handleVerify2FA(mfaCode, code, true);
}
}}
/>
<Button
type="submit"
fullWidth
variant="contained"
color="primary"
onClick={() => handleVerify2FA(mfaCode, code, true)}
style={{
backgroundColor: code === null || code === "" || code.length !== 6 ? "gray" : "#f86743",
marginTop: 10,
color: "#fff",
cursor: code === null || code === "" || code.length !== 6 ? "" : "pointer",
}}
disabled={code === null || code === "" || code.length !== 6}
>
Submit
</Button>
</Paper>
</div>
);
};
const QRCodeSection = ({ secret2FA, image2FA }) => {
return (
<div style={{ display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", height: "100%", width: "100%" }}>
{secret2FA && image2FA ? (
<div style={{ textAlign: "center" }}>
<Typography variant="body2" style={{ color: "white", textAlign: "justify", marginBottom: 10, fontSize: 16 }}>
Scan the image below with the two-factor authentication app on your phone. If you cant use a QR code, use the code {secret2FA} instead.
</Typography>
<img alt="2FA QR code" src={image2FA} style={{ maxHeight: 200, maxWidth: 200, }} />
</div>
) : (
<CircularProgress style={{ margin: "15px auto" }} />
)}
</div>
);
};
export default MFASetup;
File diff suppressed because it is too large Load Diff
+1 -2
View File
@@ -1,7 +1,7 @@
import React, {useState} from 'react';
import { useTheme } from '@mui/styles';
import {isMobile} from "react-device-detect";
import ReactGA from 'react-ga4';
import theme from '../theme.jsx';
import {
TextField,
@@ -12,7 +12,6 @@ import {
const Newsletter = (props) => {
const { globalUrl, } = props;
const theme = useTheme();
const [email, setEmail] = useState("");
const [msg, setMsg] = useState("");
const [buttonActive, setButtonActive] = useState(true);
+69 -51
View File
@@ -96,7 +96,7 @@ const AuthenticationOauth2 = (props) => {
autoAuth,
authButtonOnly,
isLoggedIn,
org_id,
setFinalized,
} = props;
@@ -148,16 +148,14 @@ const AuthenticationOauth2 = (props) => {
navigate(`/login?view=${window.location.pathname}&message=Log in to authenticate this app`)
}
console.log("Should automatically click the auto-auth button?: ", autoAuth)
if (autoAuth === true && selectedApp !== undefined) {
startOauth2Request()
}
}, [])
if (selectedApp.authentication === undefined) {
return null;
}
if (selectedApp.authentication === undefined) {
return null;
}
const startOauth2Request = (admin_consent) => {
// Admin consent also means to add refresh tokens
@@ -167,7 +165,7 @@ const AuthenticationOauth2 = (props) => {
//console.log("APP: ", selectedApp)
if (selectedApp.name.toLowerCase() == "outlook_graph" || selectedApp.name.toLowerCase() == "outlook_office365") {
handleOauth2Request(
"efe4c3fe-84a1-4821-a84f-23a6cfe8e72d",
"fd55c175-aa30-4fa6-b303-09a29fb3f750",
"",
"https://graph.microsoft.com",
["Mail.ReadWrite", "Mail.Send", "offline_access"],
@@ -299,12 +297,11 @@ const AuthenticationOauth2 = (props) => {
const handleOauth2Request = (client_id, client_secret, oauth_url, scopes, admin_consent, prompt, skipScopeReplace) => {
console.log("SKIP SCOPE: ", skipScopeReplace)
if (skipScopeReplace === false || skipScopeReplace === undefined) {
console.log("Selected scopes: ", selectedScopes)
if (selectedScopes !== undefined && selectedScopes !== null && selectedScopes.length > 0) {
toast("Using your scopes instead of the default ones")
//toast("Using your scopes instead of the default ones")
scopes = selectedScopes
}
}
@@ -453,6 +450,8 @@ const AuthenticationOauth2 = (props) => {
if (orgId !== undefined && orgId !== null && orgId.length > 0) {
console.log("Adding org_id from user side")
state += `%26org_id%3d${orgId}`;
}else{
state += `%26org_id%3d${org_id}`
}
if (oauth_url !== undefined && oauth_url !== null && oauth_url.length > 0) {
@@ -461,42 +460,56 @@ const AuthenticationOauth2 = (props) => {
}
if (
authenticationType.refresh_uri !== undefined &&
authenticationType.refresh_uri !== null &&
authenticationType.refresh_uri.length > 0
) {
state += `%26refresh_uri%3d${authenticationType.refresh_uri}`;
if (authenticationType.refresh_uri !== undefined && authenticationType.refresh_uri !== null && authenticationType.refresh_uri.length > 0) {
state += `%26refresh_uri%3d${authenticationType.refresh_uri}`
} else {
state += `%26refresh_uri%3d${authentication_url}`;
state += `%26refresh_uri%3d${authentication_url}`
}
// No prompt forcing
//var url = `${authenticationType.redirect_uri}?client_id=${client_id}&redirect_uri=${redirectUri}&response_type=code&prompt=login&scope=${resources}&state=${state}&access_type=offline`;
if (workflow?.org_id !== undefined && workflow?.org_id !== null && workflow?.org_id.length > 0) {
state += `%26org_id%3d${workflow.org_id}`
}
// FIXME: Should this be =consent?
var defaultPrompt = "login"
if (prompt !== undefined && prompt !== null && prompt.length > 0) {
defaultPrompt = prompt
}
defaultPrompt = prompt
}
var url = `${authenticationType.redirect_uri}?client_id=${client_id}&redirect_uri=${redirectUri}&response_type=code&prompt=${defaultPrompt}&scope=${resources}&state=${state}&access_type=offline`;
var url = `${authenticationType.redirect_uri}?client_id=${client_id}&redirect_uri=${redirectUri}&response_type=code&prompt=${defaultPrompt}&scope=${resources}&state=${state}&access_type=offline`;
if (admin_consent === true) {
console.log("Running Oauth2 WITH admin consent")
//url = `${authenticationType.redirect_uri}?client_id=${client_id}&redirect_uri=${redirectUri}&response_type=code&prompt=consent&scope=${resources}&state=${state}&access_type=offline`;
url = `${authenticationType.redirect_uri}?client_id=${client_id}&redirect_uri=${redirectUri}&response_type=code&prompt=admin_consent&scope=${resources}&state=${state}&access_type=offline`;
}
if (url !== undefined && url !== null && url.length > 0) {
if (url.toLowerCase().includes("{tenant")) {
// Check location of {tenant, then find the next } and replace with 'common'. Make sure next } is AFTER {tenant
try {
const tenantIndex = url.toLowerCase().indexOf("{tenant")
const substring = url.substring(tenantIndex)
const nextBracket = substring.indexOf("}")
const newUrl = url.substring(0, tenantIndex) + "common" + url.substring(tenantIndex + nextBracket + 1)
url = newUrl
} catch (e) {
console.log("Failed to replace {tenant} with common: ", e)
}
if (admin_consent === true) {
console.log("Running Oauth2 WITH admin consent")
//url = `${authenticationType.redirect_uri}?client_id=${client_id}&redirect_uri=${redirectUri}&response_type=code&prompt=consent&scope=${resources}&state=${state}&access_type=offline`;
url = `${authenticationType.redirect_uri}?client_id=${client_id}&redirect_uri=${redirectUri}&response_type=code&prompt=admin_consent&scope=${resources}&state=${state}&access_type=offline`;
}
}
// Force new consent
/*
console.log("OAUTH2 URL: ", url)
return
*/
// Force new consent
//const url = `${authenticationType.redirect_uri}?client_id=${client_id}&redirect_uri=${redirectUri}&response_type=code&scope=${resources}&prompt=consent&state=${state}&access_type=offline`;
// Admin consent
// Admin consent
//const url = `https://accounts.zoho.com/oauth/v2/auth?response_type=code&client_id=${client_id}&scope=AaaServer.profile.Read&redirect_uri=${redirectUri}&prompt=consent`
// &resource=https%3A%2F%2Fgraph.microsoft.com&
// FIXME: Awful, but works for prototyping
// How can we get a callback properly realtime?
// How can we properly try-catch without breaks on error?
try {
var newwin = window.open(url, "", "width=582,height=700");
//console.log(newwin)
@@ -511,7 +524,12 @@ const AuthenticationOauth2 = (props) => {
//alert('"Secure Payment" window closed!');
if (getAppAuthentication !== undefined) {
getAppAuthentication(true, true, true);
// This should be orgId, not action Id as to load auth properly
if (workflow !== undefined && workflow !== null && workflow.org_id !== undefined && workflow.org_id !== null && workflow.org_id.length > 0) {
getAppAuthentication(true, true, true, workflow.org_id)
} else {
getAppAuthentication(true, true, true)
}
}
toast("Authentication successful!")
@@ -525,7 +543,7 @@ const AuthenticationOauth2 = (props) => {
setFinalized(true)
}
} else {
console.log("Not closed")
//console.log("Not closed")
}
}, 1000);
//do {
@@ -678,7 +696,7 @@ const AuthenticationOauth2 = (props) => {
justifyContent: "flex-start",
backgroundColor: "#ffffff",
color: "#2f2f2f",
borderRadius: theme.palette.borderRadius,
borderRadius: theme.palette?.borderRadius,
minWidth: 300,
maxWidth: 300,
maxHeight: 50,
@@ -703,7 +721,7 @@ const AuthenticationOauth2 = (props) => {
<span style={{display: "flex"}}>
<img
alt={selectedAction.app_name}
style={{ margin: 4, minHeight: 30, maxHeight: 30, borderRadius: theme.palette.borderRadius, }}
style={{ margin: 4, minHeight: 30, maxHeight: 30, borderRadius: theme.palette?.borderRadius, }}
src={selectedAction.large_image}
/>
<Typography style={{ margin: 0, marginLeft: 10, marginTop: 5,}} variant="body1">
@@ -726,7 +744,7 @@ const AuthenticationOauth2 = (props) => {
</DialogTitle>
<DialogContent>
<span style={{}}>
Oauth2 requires a client ID and secret to authenticate, defined in the remote system. {authenticationType.type === "oauth2-app" ? null : <span>Your redirect URL is <b>{window.location.origin}/set_authentication</b>&nbsp;-&nbsp;</span>}
Oauth2 requires a client ID and secret to authenticate, defined in the remote system. <span>Your redirect URL is <b>{window.location.origin}/set_authentication</b>&nbsp;-&nbsp;</span>
<a
target="_blank"
rel="norefferer"
@@ -739,7 +757,7 @@ const AuthenticationOauth2 = (props) => {
<div />
</span>
{isCloud && registeredApps.includes(selectedApp.name.toLowerCase()) ?
{isCloud && registeredApps?.includes(selectedApp?.name?.replaceAll(" ", "_").toLowerCase()) ?
<span>
<span style={{display: "flex"}}>
{autoAuthButton}
@@ -785,7 +803,7 @@ const AuthenticationOauth2 = (props) => {
</span>
: null}
{/*<TextField
style={{backgroundColor: theme.palette.inputColor, borderRadius: theme.palette.borderRadius,}}
style={{backgroundColor: theme.palette.inputColor, borderRadius: theme.palette?.borderRadius,}}
InputProps={{
style:{
},
@@ -817,13 +835,14 @@ const AuthenticationOauth2 = (props) => {
setOauthUrl(data.value);
}
const defaultValue = data.name === "url" && authenticationType.token_uri !== undefined && authenticationType.token_uri !== null && authenticationType.token_uri.length > 0 && (authenticationType.authorizationUrl === undefined || authenticationType.authorizationUrl === null || authenticationType.authorizationUrl.length === 0) && authenticationType.type === "oauth2-app" ? authenticationType.token_uri : data.value === undefined || data.value === null ? "" : data.value
const isNormalOauth = authenticationType.redirect_uri !== undefined && authenticationType.redirect_uri !== null && authenticationType.redirect_uri.length > 0
const defaultValue = !isNormalOauth && data.name === "url" && authenticationType.token_uri !== undefined && authenticationType.token_uri !== null && authenticationType.token_uri.length > 0 && (authenticationType.authorizationUrl === undefined || authenticationType.authorizationUrl === null || authenticationType.authorizationUrl.length === 0) && authenticationType.type === "oauth2-app" ? authenticationType.token_uri : data.value === undefined || data.value === null ? "" : data.value
const fieldname = data.name === "url" && authenticationType.grant_type !== undefined && authenticationType.grant_type !== null && authenticationType.grant_type.length > 0 && authenticationType.type === "oauth2-app" ? "Token URL" : data.name
const fieldname = !isNormalOauth && data.name === "url" && authenticationType.grant_type !== undefined && authenticationType.grant_type !== null && authenticationType.grant_type.length > 0 && authenticationType.type === "oauth2-app" ? "Token URL" : data.name
return (
<div key={index} style={{ marginTop: authenticationType.type === "oauth2-app" ? 10 : 0, }}>
<div key={index} style={{ marginTop: !isNormalOauth && authenticationType.type === "oauth2-app" ? 10 : 0, }}>
<LockOpenIcon style={{ marginRight: 10 }} />
<b>{fieldname}</b>
@@ -875,7 +894,7 @@ const AuthenticationOauth2 = (props) => {
<TextField
style={{
backgroundColor: theme.palette.inputColor,
borderRadius: theme.palette.borderRadius,
borderRadius: theme.palette?.borderRadius,
}}
InputProps={{
style: {
@@ -906,7 +925,7 @@ const AuthenticationOauth2 = (props) => {
style={{
marginTop: 20,
backgroundColor: theme.palette.inputColor,
borderRadius: theme.palette.borderRadius,
borderRadius: theme.palette?.borderRadius,
}}
InputProps={{
style: {
@@ -924,7 +943,7 @@ const AuthenticationOauth2 = (props) => {
<TextField
style={{
backgroundColor: theme.palette.inputColor,
borderRadius: theme.palette.borderRadius,
borderRadius: theme.palette?.borderRadius,
marginBottom: 10,
}}
InputProps={{
@@ -946,7 +965,7 @@ const AuthenticationOauth2 = (props) => {
<TextField
style={{
backgroundColor: theme.palette.inputColor,
borderRadius: theme.palette.borderRadius,
borderRadius: theme.palette?.borderRadius,
}}
InputProps={{
style: {
@@ -964,7 +983,7 @@ const AuthenticationOauth2 = (props) => {
<TextField
style={{
backgroundColor: theme.palette.inputColor,
borderRadius: theme.palette.borderRadius,
borderRadius: theme.palette?.borderRadius,
marginBottom: 10,
}}
InputProps={{
@@ -997,7 +1016,6 @@ const AuthenticationOauth2 = (props) => {
color: "white",
padding: 5,
minWidth: 300,
maxWidth: 300,
}}
onChange={(e, value) => {
//handleScopeChange(e)
@@ -1044,7 +1062,7 @@ const AuthenticationOauth2 = (props) => {
style={{
marginBottom: 40,
marginTop: 20,
borderRadius: theme.palette.borderRadius,
borderRadius: theme.palette?.borderRadius,
}}
disabled={
clientSecret.length === 0 || clientId.length === 0 || buttonClicked || (allscopes.length !== 0 && selectedScopes.length === 0)
@@ -1075,7 +1093,7 @@ const AuthenticationOauth2 = (props) => {
<Button
style={{
marginLeft: 10,
borderRadius: theme.palette.borderRadius,
borderRadius: theme.palette?.borderRadius,
}}
disabled={clientSecret.length === 0 || clientId.length === 0}
variant="text"
+24 -2
View File
@@ -110,8 +110,30 @@ const OrgHeader = (props) => {
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,
},
[],
)
}
File diff suppressed because it is too large Load Diff
+4 -2
View File
@@ -2,7 +2,7 @@ import React, {useState, useEffect, useLayoutEffect} from 'react';
import Draggable from "react-draggable";
import {
Paper
Paper
} from "@mui/material";
const PaperComponent = (props) => {
@@ -11,7 +11,9 @@ const PaperComponent = (props) => {
handle="#draggable-dialog-title"
cancel={'[class*="MuiDialogContent-root"]'}
>
<Paper {...props} />
<Paper
{...props}
/>
</Draggable>
)
}
File diff suppressed because it is too large Load Diff
+250 -190
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useContext, memo } from "react";
import { toast } from "react-toastify";
import theme from "../theme.jsx";
@@ -13,22 +13,26 @@ import {
Card,
Chip,
Switch,
Skeleton,
} from "@mui/material";
import { Context } from "../context/ContextApi.jsx";
import { useNavigate, Link } from "react-router-dom";
import Priority from "../components/Priority.jsx";
import { constrainMatrix } from "reaviz";
//import { useAlert
const Priorities = (props) => {
const Priorities = memo((props) => {
const { globalUrl, userdata,clickedFromOrgTab, serverside, billingInfo, stripeKey, checkLogin, setAdminTab, setCurTab, notifications, setNotifications, } = props;
const [showDismissed, setShowDismissed] = React.useState(false);
const [showRead, setShowRead] = React.useState(false);
const [appFramework, setAppFramework] = React.useState({});
const [selectedWorkflow, setSelectedWorkflow] = React.useState("NO HIGHLIGHT");
const [selectedExecutionId, setSelectedExecutionId] = React.useState("NO HIGHLIGHT");
const [highlightKMS, setHighlightKMS] = React.useState(false)
let navigate = useNavigate();
useEffect(() => {
getFramework()
@@ -36,6 +40,12 @@ const Priorities = (props) => {
const urlParams = new URLSearchParams(window.location.search)
const workflow = urlParams.get("workflow")
const execution_id = urlParams.get("execution_id")
const kms = urlParams.get("kms")
if (kms !== null && kms !== undefined && kms.length > 0 && kms === "true") {
toast.info("KMS-related notifications are highlighted.")
setHighlightKMS(true)
}
if (execution_id !== null) {
setSelectedExecutionId(execution_id)
@@ -209,176 +219,16 @@ const Priorities = (props) => {
const notificationWidth = "100%"
const imagesize = 22
const boxColor = "#86c142"
const NotificationItem = (props) => {
const {data} = props
var image = "";
var orgName = "";
var orgId = "";
const highlighted = selectedExecutionId === "" && selectedWorkflow === "" ? false : data.reference_url === undefined || data.reference_url === null || data.reference_url.length === 0 ? false : data.reference_url.includes(selectedExecutionId) || data.reference_url.includes(selectedWorkflow)
if (userdata.orgs !== undefined) {
const foundOrg = userdata.orgs.find((org) => org.id === data["org_id"]);
if (foundOrg !== undefined && foundOrg !== null) {
//position: "absolute", bottom: 5, right: -5,
const imageStyle = {
width: imagesize,
height: imagesize,
pointerEvents: "none",
marginLeft:
data.creator_org !== undefined && data.creator_org.length > 0
? 20
: 0,
borderRadius: 10,
border:
foundOrg.id === userdata.active_org.id
? `3px solid ${boxColor}`
: null,
cursor: "pointer",
marginRight: 10,
};
image =
foundOrg.image === "" ? (
<img
alt={foundOrg.name}
src={theme.palette.defaultImage}
style={imageStyle}
/>
) : (
<img
alt={foundOrg.name}
src={foundOrg.image}
style={imageStyle}
onClick={() => {}}
/>
);
orgName = foundOrg.name;
orgId = foundOrg.id;
}
}
return (
<Paper
style={{
backgroundColor: theme.palette.platformColor,
width: clickedFromOrgTab ? null :notificationWidth,
padding: 30,
borderBottom: "1px solid rgba(255,255,255,0.4)",
marginBottom: 20,
border: highlighted ? "2px solid #f85a3e" : null,
borderRadius: theme.palette.borderRadius,
}}
>
<div style={{display: "flex", }}>
{data.amount === 1 && data.read === false ?
<Chip
label={"First seen"}
variant="contained"
color="primary"
style={{marginRight: 15, height: 25, }}
/>
: null}
{data.ignored === true ?
<Chip
label={"Disabled"}
variant="outlined"
color="primary"
style={{marginRight: 15, height: 25, }}
/>
: null}
{data.read === false ?
<Chip
label={"Unread"}
variant="outlined"
color="primary"
style={{marginRight: 15, height: 25, }}
/>
:
<Chip
label={"Read"}
variant="outlined"
color="secondary"
style={{marginRight: 15, height: 25, }}
/>
}
<Typography variant="body1" color="textPrimary">
{data.title}
</Typography >
</div>
{data.image !== undefined && data.image !== null && data.image.length > 0 ?
<img alt={data.title} src={data.image} style={{height: 100, width: 100, }} />
:
null
}
<Typography variant="body2" color="textSecondary" style={{marginTop: 10, maxHeight: 200, overflowX: "hidden", overflowY: "auto", }}>
{data.description}
</Typography >
<div style={{ display: "flex" }}>
<ButtonGroup>
<Button
color="secondary"
variant="outlined"
style={{ marginTop: 15 }}
disabled={data.reference_url === undefined || data.reference_url === null || data.reference_url.length === 0}
onClick={() => {
window.open(data.reference_url, "_blank")
}}
>
Explore
</Button>
{data.read === false ? (
<Button
color="secondary"
variant="outlined"
style={{ marginTop: 15 }}
onClick={() => {
dismissNotification(data.id);
}}
>
Dismiss
</Button>
) : null}
<Tooltip title="Disabling a notification makes it so similar notifications to this one will NOT be re-opened. It will NOT forward notifications to your notification workflow, but WILL still keep counting." placement="top">
<Button
color="secondary"
variant={data.ignored === true ? "contained" : "outlined"}
style={{ marginTop: 15, }}
onClick={() => {
if (data.ignored === true) {
dismissNotification(data.id, false)
} else {
dismissNotification(data.id, true)
}
}}
>
{data.ignored === true ? "Re-enable" : "Disable"}
</Button>
</Tooltip>
</ButtonGroup>
<Typography variant="body2" color="textSecondary" style={{marginLeft: 20, marginTop: 20, }}>
<b>First seen</b>: {new Date(data.created_at * 1000).toISOString().slice(0, 19)}
</Typography >
<Typography variant="body2" color="textSecondary" style={{marginLeft: 20, marginTop: 20, }}>
<b>Last seen</b>: {new Date(data.updated_at * 1000).toISOString().slice(0, 19)}
</Typography >
<Typography variant="body2" color="textSecondary" style={{marginLeft: 20, marginTop: 20, }}>
<b>Times seen</b>: {data.amount}
</Typography >
</div>
</Paper>
);
}
return (
<div style={{width: clickedFromOrgTab ? 1030:1000, padding: clickedFromOrgTab ? 27:null, height: clickedFromOrgTab ? "auto":null, backgroundColor: clickedFromOrgTab ? '#212121':null, borderRadius: clickedFromOrgTab ? '16px':null, }}>
<h2 style={{ display: clickedFromOrgTab?null:"inline", marginBottom: clickedFromOrgTab? 8:null, marginTop: clickedFromOrgTab?40:null, color: clickedFromOrgTab?"#ffffff":null }}>Notifications</h2>
<span style={{ marginLeft: clickedFromOrgTab?null:25, color: clickedFromOrgTab?"#9E9E9E":null, }}>
<div style={{width: "100%", height: "100%", boxSizing: 'border-box', transition: 'width 0.3s ease', padding: clickedFromOrgTab ? "27px 10px 19px 27px":null, height: clickedFromOrgTab ? "auto":null, minHeight: 843, backgroundColor: clickedFromOrgTab ? '#212121':null, borderRadius: clickedFromOrgTab ? '16px':null, }}>
<div style={{ maxHeight: 1700, overflowY: "auto", width: '100%', scrollbarColor: '#494949 transparent', scrollbarWidth: 'thin'}}>
<div style={{maxWidth: "calc(100% - 20px)"}}>
<Typography style={{ fontSize: 24, fontWeight: 'bold', display: clickedFromOrgTab?null:"inline", marginBottom: clickedFromOrgTab? 8:null, color: clickedFromOrgTab?"#ffffff":null }}>Notifications ({
notifications?.filter((notification) => showRead === true || notification.read === false).length
})</Typography>
<span style={{ fontSize: 16, marginLeft: clickedFromOrgTab?null:25, color: clickedFromOrgTab?"#9E9E9E":null, }}>
Notifications help you find potential problems with your workflows and apps.&nbsp;
<a
target="_blank"
@@ -411,24 +261,11 @@ const Priorities = (props) => {
</Button>
) : null}
</div>
{notifications === null || notifications === undefined || notifications.length === 0 ? null :
<div>
{notifications.map((notification, index) => {
if (showRead === false && notification.read === true) {
return null
}
return (
<NotificationItem data={notification} key={index} />
)
})}
</div>
}
<NotificationComponent notifications={notifications} showRead={showRead} selectedExecutionId={selectedExecutionId} selectedWorkflow={selectedWorkflow} highlightKMS={highlightKMS} userdata={userdata} imagesize={imagesize} boxColor={boxColor} clickedFromOrgTab={clickedFromOrgTab} notificationWidth={notificationWidth} dismissNotification={dismissNotification}/>
{clickedFromOrgTab? null : <Divider style={{marginTop: 50, marginBottom: 50, }} />}
<h2 style={{ display: clickedFromOrgTab ? null:"inline", marginBottom: clickedFromOrgTab ? 8:null, marginTop: clickedFromOrgTab ?0:null, color: clickedFromOrgTab ? "#ffffff" : null }}>Suggestions</h2>
<span style={{ color: clickedFromOrgTab ?"#9E9E9E":null,marginLeft: clickedFromOrgTab ?null:25 }}>
<h2 style={{ display: clickedFromOrgTab ? null:"inline", marginBottom: clickedFromOrgTab ? 8:null, marginTop: clickedFromOrgTab ? 30 :null, color: clickedFromOrgTab ? "#ffffff" : null }}>Suggestions</h2>
<span style={{ fontSize: 16, color: clickedFromOrgTab ?"#9E9E9E":null,marginLeft: clickedFromOrgTab ?null:25 }}>
Suggestions are tasks identified by Shuffle to help you discover ways to protect your and customers' company. <br/>These range from simple configurations in Shuffle to Usecases you may have missed.&nbsp;
<a
target="_blank"
@@ -470,9 +307,232 @@ const Priorities = (props) => {
)
})
}
</div>
</div>
</div>
)
}
})
export default Priorities;
const NotificationItem = memo((props) => {
const {data, selectedExecutionId, selectedWorkflow, highlightKMS, userdata, imagesize, boxColor, clickedFromOrgTab, notificationWidth, dismissNotification} = props
var image = "";
var orgName = "";
var orgId = "";
var highlighted = selectedExecutionId === "" && selectedWorkflow === "" ? false : data.reference_url === undefined || data.reference_url === null || data.reference_url.length === 0 ? false : data.reference_url.includes(selectedExecutionId) || data.reference_url.includes(selectedWorkflow)
if (!highlighted && highlightKMS) {
if (data.title !== undefined && data.title !== null && data.title.toLowerCase().includes("kms")) {
highlighted = true
} else if (data.description !== undefined && data.description !== null && data.description.toLowerCase().includes("kms")) {
highlighted = true
}
}
if (userdata.orgs !== undefined) {
const foundOrg = userdata.orgs.find((org) => org.id === data["org_id"]);
if (foundOrg !== undefined && foundOrg !== null) {
//position: "absolute", bottom: 5, right: -5,
const imageStyle = {
width: imagesize,
height: imagesize,
pointerEvents: "none",
marginLeft:
data.creator_org !== undefined && data.creator_org.length > 0
? 20
: 0,
borderRadius: 10,
border:
foundOrg.id === userdata.active_org.id
? `3px solid ${boxColor}`
: null,
cursor: "pointer",
marginRight: 10,
};
image =
foundOrg.image === "" ? (
<img
alt={foundOrg.name}
src={theme.palette.defaultImage}
style={imageStyle}
/>
) : (
<img
alt={foundOrg.name}
src={foundOrg.image}
style={imageStyle}
onClick={() => {}}
/>
);
orgName = foundOrg.name;
orgId = foundOrg.id;
}
}
return (
<Paper
style={{
backgroundColor: theme.palette.inputColor.backgroundColor,
width: clickedFromOrgTab ? null :notificationWidth,
padding: 30,
borderBottom: "1px solid rgba(255,255,255,0.4)",
marginBottom: 20,
border: highlighted ? "2px solid #f85a3e" : null,
borderRadius: theme.palette?.borderRadius,
}}
>
<div style={{display: "flex", }}>
{data.amount === 1 && data.read === false ?
<Chip
label={"First seen"}
variant="contained"
color="primary"
style={{marginRight: 15, height: 25, }}
/>
: null}
{data.ignored === true ?
<Chip
label={"Disabled"}
variant="outlined"
color="primary"
style={{marginRight: 15, height: 25, }}
/>
: null}
{data.read === false ?
<Chip
label={"Unread"}
variant="outlined"
color="primary"
style={{marginRight: 15, height: 25, }}
/>
:
<Chip
label={"Read"}
variant="outlined"
color="secondary"
style={{marginRight: 15, height: 25, }}
/>
}
<Typography variant="body1" color="textPrimary" style={{ wordWrap: "break-word", overflow: "hidden", textOverflow: "ellipsis" }}>
{data.title}
</Typography >
</div>
{data.image !== undefined && data.image !== null && data.image.length > 0 ?
<img alt={data.title} src={data.image} style={{height: 100, width: 100, }} />
:
null
}
<Typography variant="body2" color="textSecondary" style={{ marginTop: 10, maxHeight: 200, overflowX: "hidden", overflowY: "auto", wordWrap: "break-word" }}>
{data.description}
</Typography >
<div style={{ display: "flex" }}>
<ButtonGroup>
<Button
color="secondary"
variant="outlined"
style={{ marginTop: 15 }}
disabled={data.reference_url === undefined || data.reference_url === null || data.reference_url.length === 0}
onClick={() => {
window.open(data.reference_url, "_blank")
}}
>
Explore
</Button>
{data.read === false ? (
<Button
color="secondary"
variant="outlined"
style={{ marginTop: 15 }}
onClick={() => {
dismissNotification(data.id);
}}
>
Dismiss
</Button>
) : null}
<Tooltip title="Disabling a notification makes it so similar notifications to this one will NOT be re-opened. It will NOT forward notifications to your notification workflow, but WILL still keep counting." placement="top">
<Button
color="secondary"
variant={data.ignored === true ? "contained" : "outlined"}
style={{ marginTop: 15, }}
onClick={() => {
if (data.ignored === true) {
dismissNotification(data.id, false)
} else {
dismissNotification(data.id, true)
}
}}
>
{data.ignored === true ? "Re-enable" : "Disable"}
</Button>
</Tooltip>
</ButtonGroup>
<Typography variant="body2" color="textSecondary" style={{ marginLeft: 20, marginTop: 20, wordWrap: "break-word", overflow: "hidden", textOverflow: "ellipsis" }}>
<b>First seen</b>: {new Date(data.created_at * 1000).toISOString().slice(0, 19)}
</Typography >
<Typography variant="body2" color="textSecondary" style={{ marginLeft: 20, marginTop: 20, wordWrap: "break-word", overflow: "hidden", textOverflow: "ellipsis" }}>
<b>Last seen</b>: {new Date(data.updated_at * 1000).toISOString().slice(0, 19)}
</Typography >
<Typography variant="body2" color="textSecondary" style={{ marginLeft: 20, marginTop: 20, wordWrap: "break-word", overflow: "hidden", textOverflow: "ellipsis" }}>
<b>Times seen</b>: {data.amount}
</Typography >
</div>
</Paper>
);
})
const NotificationComponent = memo(({notifications, showRead, selectedExecutionId, selectedWorkflow, highlightKMS, userdata, imagesize, boxColor, clickedFromOrgTab, notificationWidth, dismissNotification}) => {
return(
<div>
{notifications === null || notifications === undefined || notifications?.length === 0 ? (
null
) :
<div>
{notifications?.map((notification, index) => {
if (showRead === false && notification.read === true) {
return null
}
return (
<NotificationItem data={notification} key={index} selectedExecutionId={selectedExecutionId} selectedWorkflow={selectedWorkflow} highlightKMS={highlightKMS} userdata={userdata} imagesize={imagesize} boxColor={boxColor} clickedFromOrgTab={clickedFromOrgTab} notificationWidth={notificationWidth} dismissNotification={dismissNotification} />
)
})}
</div>
}
</div>
)
})
// const PaddingWrapper = memo(({children, clickedFromOrgTab}) => {
// const { leftSideBarOpenByClick } = useContext(Context)
// return(
// <div style={{width: leftSideBarOpenByClick ? 950 : 1030,transition: 'width 0.3s ease', padding: clickedFromOrgTab ? "27px 10px 19px 27px":null, height: clickedFromOrgTab ? "auto":null, minHeight: 843, backgroundColor: clickedFromOrgTab ? '#212121':null, borderRadius: clickedFromOrgTab ? '16px':null, }}>
// {children}
// </div>
// )
// })
// const Wrapper = memo(({children, clickedFromOrgTab}) => {
// return(
// <PaddingWrapper clickedFromOrgTab={clickedFromOrgTab}>
// {children}
// </PaddingWrapper>
// )
// })
+25 -4
View File
@@ -24,9 +24,16 @@ import {
const Priority = (props) => {
const { globalUrl, clickedFromOrgTab,userdata, serverside, priority, checkLogin, setAdminTab, setCurTab, appFramework, } = props;
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true");
let navigate = useNavigate();
if (window.location.pathname === "/workflows") {
const hidePriorities = localStorage.getItem("hidePriorities", "true")
if (hidePriorities === "true") {
return null
}
}
var realignedSrc = false
var realignedDst = false
let newdescription = priority.description
@@ -114,7 +121,7 @@ const Priority = (props) => {
const srcSize = realignedSrc ? 35 : 30
const dstSize = realignedDst ? 35 : 30
return (
<div style={{border: priority.active === false ? "1px solid #000000" : priority.severity === 1 ? "1px solid #f85a3e" : clickedFromOrgTab ?null:"1px solid rgba(255,255,255,0.3)", borderRadius: theme.palette.borderRadius, marginTop: 10, marginBottom: 10, padding: clickedFromOrgTab ? 24:15, textAlign: "center", minHeight: isCloud ? 70 : 100, maxHeight: isCloud ? 70 : 100, textAlign: "left", backgroundColor: clickedFromOrgTab ? "#1A1A1A": theme.palette.surfaceColor, display: "flex", }}>
<div style={{border: priority.active === false ? "1px solid #000000" : priority.severity === 1 ? "1px solid #f85a3e" : clickedFromOrgTab ?null:"1px solid rgba(255,255,255,0.3)", borderRadius: theme.palette?.borderRadius, marginTop: 10, marginBottom: 10, padding: clickedFromOrgTab ? 24:15, textAlign: "center", minHeight: isCloud ? 70 : 100, maxHeight: isCloud ? 70 : 100, textAlign: "left", backgroundColor: clickedFromOrgTab ? "#1A1A1A": theme.palette.surfaceColor, display: "flex", }}>
<div style={{flex: 2, overflow: "hidden",}}>
<span style={{display: "flex", }}>
{priority.type === "usecase" || priority.type == "apps" ? <AutoFixHighIcon style={{height: 19, width: 19, marginLeft: 3, marginRight: 10, }}/> : null}
@@ -124,7 +131,7 @@ const Priority = (props) => {
</span>
{priority.type === "usecase" && priority.description.includes("&") ?
<span style={{display: "flex", marginTop: 10, }}>
<img src={newdescription.split("&")[1]} alt={priority.name} style={{height: srcSize, width: srcSize, marginRight: realignedSrc ? isCloud ? 0 : -10 : 10, borderRadius: theme.palette.borderRadius-3, marginTop: realignedSrc ? 5 : 0 }} />
<img src={newdescription.split("&")[1]} alt={priority.name} style={{height: srcSize, width: srcSize, marginRight: realignedSrc ? isCloud ? 0 : -10 : 10, borderRadius: theme.palette?.borderRadius-3, marginTop: realignedSrc ? 5 : 0 }} />
<Typography variant="body2" color="textSecondary" style={{marginTop: 3, }}>
{newdescription.split("&")[0]}
</Typography>
@@ -132,7 +139,7 @@ const Priority = (props) => {
{newdescription.split("&").length > 3 ?
<span style={{display: "flex", }}>
<ArrowForwardIcon style={{marginLeft: 15, marginRight: 15, }}/>
<img src={newdescription.split("&")[3]} alt={priority.name+"2"} style={{height: dstSize, width: dstSize, marginRight: realignedDst ? -5 : 10, borderRadius: theme.palette.borderRadius-3, marginTop: realignedDst ? 5 : 0 }} />
<img src={newdescription.split("&")[3]} alt={priority.name+"2"} style={{height: dstSize, width: dstSize, marginRight: realignedDst ? -5 : 10, borderRadius: theme.palette?.borderRadius-3, marginTop: realignedDst ? 5 : 0 }} />
<Typography variant="body2" color="textSecondary" style={{marginTop: 3}}>
{newdescription.split("&")[2]}
</Typography>
@@ -176,6 +183,20 @@ const Priority = (props) => {
<Button style={{borderRadius: 25, fontSize:16, boxShadow: clickedFromOrgTab ? "none":null,textTransform: clickedFromOrgTab ? 'capitalize':null, width: 100, height: 50, marginTop: 8, }} variant="text" color="secondary" onClick={() => {
// dismiss -> get envs
changeRecommendation(priority, "dismiss")
// Check window location if it's /workflows
if (window.location.pathname === "/workflows") {
// Set local storage to hide priorities for now
localStorage.setItem("hidePriorities", "true")
}
if (isCloud) {
ReactGA.event({
category: "recommendation",
action: `dismiss_${priority.name}`,
label: "",
})
}
}}>
Dismiss
</Button>
+155
View File
@@ -0,0 +1,155 @@
import React from "react"
import { Link } from "react-router-dom";
import {
Avatar,
Box,
Button,
Typography,
Tooltip,
} from "@mui/material"
import { useNavigate } from "react-router";
import theme from "../theme.jsx";
import {
Lock as LockIcon,
} from '@mui/icons-material';
// onclickHandler = function override from parent onclick
const RecentWorkflow = ({ workflow, onclickHandler, leftNavOpen, currentWorkflowId, }) => {
const navigate = useNavigate();
const [hovered, setHovered] = React.useState(false)
if (workflow === undefined || workflow === null) {
console.log("No workflow")
return null
}
/*
* Note for @Lalit:
*
* When you want to make a list of something that is complex,
* make a component. This way, you can easily manage
* the logic, and we can actually reuse it. This component is used
* multiple places, so do make sure to not break it randomly.
*/
const expandLeftNav = leftNavOpen === true || leftNavOpen === undefined ? true : false
// Check if workflow.input_markdown has an image in it
// If it does, show it as the main thing
var relevantImageUrl = ""
if (workflow?.form_control?.input_markdown !== undefined && workflow?.form_control?.input_markdown !== null && workflow?.form_control?.input_markdown !== "") {
// Look for <img> tag or ![alt](src) markdown
// html > markdown
const imgTag = workflow?.form_control?.input_markdown.match(/<img[^>]+>/g)
if (imgTag !== null) {
const src = imgTag[0].match(/src="([^"]+)"/)
if (src !== null) {
relevantImageUrl = src[1]
}
} else {
const markdownTag = workflow?.form_control?.input_markdown.match(/!\[.*\]\(.*\)/g)
if (markdownTag !== null) {
const src = markdownTag[0].match(/\(([^)]+)\)/)
if (src !== null) {
relevantImageUrl = src[1]
}
}
}
}
return (
<div
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
<Link to={`/workflows/` + workflow?.id} style={{textDecoration: "none"}}>
<Button
onClick={(e) => {
if (onclickHandler !== undefined) {
e.preventDefault()
e.stopPropagation()
onclickHandler()
}
}}
style={{
display: "flex",
flexDirection: "column",
textTransform: "none",
width: "100%",
justifyContent: "flex-start",
textAlign: "left",
opacity: expandLeftNav ? 1 : 0,
transition: "opacity 0.1s",
borderRadius: theme.palette?.borderRadius,
backgroundColor: hovered || currentWorkflowId === workflow.id ? "#1f1f1f" : "transparent",
}}
disableRipple
>
<Box
style={{
display: "flex",
marginRight: "auto",
alignItems: "center",
}}
>
{relevantImageUrl !== undefined && relevantImageUrl !== null && relevantImageUrl !== "" ?
<Avatar
alt={workflow?.name}
src={relevantImageUrl}
style={{ width: 24, height: 24, marginRight: 5, }}
/>
:
workflow?.apps?.slice(0, 2).map((data, index) => (
<Box
key={index}
style={{
position: "relative",
marginLeft: index === 1 ? -8 : 0,
}}
>
<Avatar
alt={data.app_name}
src={
data.large_image
? data.large_image
: "/images/no_image.png"
}
style={{ width: 24, height: 24 }}
/>
</Box>
))}
<Typography
style={{
color: "#CDCDCD",
fontSize: 16,
marginLeft: 8,
maxWidth: 180,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{workflow?.name}
</Typography>
{onclickHandler !== undefined && workflow.sharing !== "form" ?
<Tooltip title="Private Org Form" placement="right">
<LockIcon style={{height: 15, width: 15, color: "grey", position: "absolute", left: -17, }}/>
</Tooltip>
: null
}
</Box>
</Button>
</Link>
</div>
)
}
export default RecentWorkflow
+31 -17
View File
@@ -35,6 +35,7 @@ import {
PlayArrow as PlayArrowIcon,
Insights as InsightsIcon,
Replay as ReplayIcon,
EditNote as EditNoteIcon,
} from '@mui/icons-material';
import { DataGrid, GridColDef, GridValueGetterParams } from '@mui/x-data-grid'
@@ -76,6 +77,10 @@ const RuntimeDebugger = (props) => {
{"id": "", "name": "All Workflows",}
])
if (document != undefined) {
document.title = "Workflow Run Debugger"
}
// Shitty workflow search on purpose :)
const handleWorkflowUsageCount = (workflows) => {
if (workflows === undefined || workflows === null || workflows.length === 0) {
@@ -219,7 +224,7 @@ const RuntimeDebugger = (props) => {
}
const getAvailableWorkflows = () => {
const getAvailableWorkflows = (workflowId) => {
fetch(globalUrl + "/api/v1/workflows", {
method: "GET",
headers: {
@@ -240,6 +245,15 @@ const RuntimeDebugger = (props) => {
var foundWorkflows = [{"id": "", "name": "All Workflows",}]
foundWorkflows.push(...responseJson)
setWorkflows(foundWorkflows)
if (workflowId !== undefined && workflowId !== null && workflowId !== "" && workflowId.length === 36) {
for (var key in responseJson) {
if (responseJson[key].id === workflowId) {
setWorkflow(responseJson[key])
break
}
}
}
}
})
.catch((error) => {
@@ -248,7 +262,6 @@ const RuntimeDebugger = (props) => {
}
useEffect(() => {
getAvailableWorkflows()
// Find workflow_id in url query
const urlParams = new URLSearchParams(window.location.search);
@@ -265,6 +278,8 @@ const RuntimeDebugger = (props) => {
}
}
getAvailableWorkflows(workflowId)
const foundStatus = urlParams.get('status');
if (foundStatus !== undefined && foundStatus !== null && foundStatus !== "") {
setStatus(foundStatus)
@@ -314,15 +329,17 @@ const RuntimeDebugger = (props) => {
var source = params.row.execution_source
if (source === "schedule") {
foundSource = <img src={alltriggers[1].large_image} alt="schedule" style={{borderRadius: theme.palette.borderRadius, height: imageSize, width: imageSize, }} />
foundSource = <img src={alltriggers[1].large_image} alt="schedule" style={{borderRadius: theme.palette?.borderRadius, height: imageSize, width: imageSize, }} />
} else if (source === "webhook") {
foundSource = <img src={alltriggers[0].large_image} alt="webhook" style={{borderRadius: theme.palette.borderRadius, height: imageSize, width: imageSize, }} />
foundSource = <img src={alltriggers[0].large_image} alt="webhook" style={{borderRadius: theme.palette?.borderRadius, height: imageSize, width: imageSize, }} />
} else if (source === "subflow" || source.length === 36) {
foundSource = <img src={alltriggers[4].large_image} alt="subflow" style={{borderRadius: theme.palette.borderRadius, height: imageSize, width: imageSize, }} />
foundSource = <img src={alltriggers[3].large_image} alt="subflow" style={{borderRadius: theme.palette?.borderRadius, height: imageSize, width: imageSize, }} />
source = "subflow"
} else if (source === "rerun" || source.length === 36) {
foundSource = <ReplayIcon style={{color: theme.palette.primary.secondary, height: imageSize, width: imageSize, }} />
source = "rerun of a previous run"
} else if (source === "form") {
foundSource = <EditNoteIcon style={{color: theme.palette.primary.secondary, height: imageSize, width: imageSize, }} />
} else {
source = "manual"
}
@@ -888,13 +905,13 @@ const RuntimeDebugger = (props) => {
{userdata.support === true ?
<Button
variant={ignoreOrg ? "contained" : "outlined"}
color="primary"
style={{maxHeight: 40, marginTop: 25, }}
color="secondary"
style={{marginLeft: 100, maxHeight: 40, marginTop: 25, }}
onClick={() => {
setIgnoreOrg(!ignoreOrg)
}}
>
{ignoreOrg ? "Ignoring Org" : "Ignore Org"}
{ignoreOrg ? "Ignoring Org" : "Ignore Org (Support Only)"}
</Button>
: null}
</div>
@@ -942,11 +959,8 @@ const RuntimeDebugger = (props) => {
},
}}
getOptionLabel={(option) => {
if (
option === undefined ||
option === null ||
option.name === undefined ||
option.name === null
if (option === undefined || option === null ||
option.name === undefined || option.name === null
) {
return "No Workflow Selected";
}
@@ -961,7 +975,7 @@ const RuntimeDebugger = (props) => {
style={{
backgroundColor: theme.palette.inputColor,
height: 50,
borderRadius: theme.palette.borderRadius,
borderRadius: theme.palette?.borderRadius,
marginTop: 5,
marginLeft: 5,
}}
@@ -995,13 +1009,13 @@ const RuntimeDebugger = (props) => {
<Tooltip arrow placement="left" title={
<span style={{}}>
{data.image !== undefined && data.image !== null && data.image.length > 0 ?
<img src={data.image} alt={data.name} style={{ backgroundColor: theme.palette.surfaceColor, maxHeight: 200, minHeigth: 200, borderRadius: theme.palette.borderRadius, }} />
<img src={data.image} alt={data.name} style={{ backgroundColor: theme.palette.surfaceColor, maxHeight: 200, minHeigth: 200, borderRadius: theme.palette?.borderRadius, }} />
: null}
<Typography>
Choose {data.name}
</Typography>
</span>
} placement="bottom">
}>
<MenuItem
style={{
backgroundColor: theme.palette.inputColor,
@@ -1023,7 +1037,7 @@ const RuntimeDebugger = (props) => {
<TextField
style={{
backgroundColor: theme.palette.inputColor,
borderRadius: theme.palette.borderRadius,
borderRadius: theme.palette?.borderRadius,
}}
{...params}
label="Workflow"
+19 -17
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useRef } from 'react';
import React, { useState, useEffect, useRef, useContext } from 'react';
import theme from '../theme.jsx';
import { useNavigate, Link, useParams } from "react-router-dom";
@@ -33,6 +33,8 @@ import {
AvatarGroup,
} from "@mui/material"
import { Context } from '../context/ContextApi.jsx';
import { Search as SearchIcon, Close as CloseIcon, Folder as FolderIcon, Code as CodeIcon, LibraryBooks as LibraryBooksIcon } from '@mui/icons-material'
import algoliasearch from 'algoliasearch/lite';
@@ -47,8 +49,9 @@ const chipStyle = {
const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240")
const SearchData = props => {
const { serverside, globalUrl, userdata, setModalOpen, modalOpen } = props
const { serverside, globalUrl, userdata } = props
let navigate = useNavigate();
const { searchBarModalOpen, setSearchBarModalOpen } = useContext(Context);
const borderRadius = 3
const node = useRef()
const [searchOpen, setSearchOpen] = useState(false)
@@ -56,8 +59,8 @@ const SearchData = props => {
const [value, setValue] = useState("");
const handleLinkClick = () => {
if (modalOpen) {
setModalOpen(false); // Assuming setModalOpen is defined correctly
if (searchBarModalOpen) {
setSearchBarModalOpen(false); // Assuming setModalOpen is defined correctly
} else {
console.log("Condition not met, staying on the same page");
}
@@ -71,7 +74,7 @@ const SearchData = props => {
// return null
//}
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true");
// if (window.location.pathname === "/docs" || window.location.pathname === "/apps" || window.location.pathname === "/usecases" ) {
// setModalOpen(false)
// }
@@ -88,14 +91,14 @@ const SearchData = props => {
const textFieldRef = useRef(null);
const keyPressHandler = (e) => {
if (e.which === 13) {
if (e.key === "Enter") {
e.preventDefault();
// navigate(`/search?q=${currentRefinement}`, { state: value, replace: true });
// setModalOpen(false);
const trimmedValue = inputValue.trim();
if (trimmedValue !== '') {
e.preventDefault();
navigate(`/search?q=${trimmedValue}`, { state: trimmedValue, replace: true });
setModalOpen(false);
setSearchBarModalOpen(false);
}
}
};
@@ -249,7 +252,7 @@ const SearchData = props => {
<Link key={hit.objectID} to={parsedUrl} rel="noopener noreferrer" style={{ textDecoration: "none", color: "white", }} onClick={(event) => {
//console.log("CLICK")
setSearchOpen(true)
setModalOpen(false)
setSearchBarModalOpen(false)
aa('init', {
appId: searchClient.appId,
apiKey: searchClient.transporter.queryParameters["x-algolia-api-key"]
@@ -498,7 +501,7 @@ const SearchData = props => {
return (
<Link key={hit.objectID} to={parsedUrl} style={{ textDecoration: "none", color: "white", }} onClick={(event) => {
setSearchOpen(true)
setModalOpen(false)
setSearchBarModalOpen(false)
aa('init', {
appId: searchClient.appId,
apiKey: searchClient.transporter.queryParameters["x-algolia-api-key"]
@@ -547,8 +550,6 @@ const SearchData = props => {
e.preventDefault()
e.stopPropagation()
console.log("OBJECT CHANGE: ", hit.objectID)
// This does nothing rofl
if (userdata.active_apps === undefined || userdata.active_apps === null) {
activateApp(hit.name, hit.objectID, "activate")
@@ -702,7 +703,7 @@ const SearchData = props => {
console.log("CLICK")
setSearchOpen(true)
setModalOpen(false)
setSearchBarModalOpen(false)
}}>
<ListItem key={hit.objectID} style={innerlistitemStyle} onMouseOver={() => {
setMouseHoverIndex(index)
@@ -873,10 +874,11 @@ const SearchData = props => {
</List>
</Grid>
<Grid style={{ textAlign: "end", width: "100%", textTransform: 'capitalize', }}>
<Button style={{ textAlign: "center", textTransform: 'capitalize' }}
onClick={() => { window.location = "/search"; }} >
See More
</Button>
<Link to="/search" style={{ textDecoration: "none", color: "#f85a3e" }}>
<Button style={{ textAlign: "center", textTransform: 'capitalize' }}>
See More
</Button>
</Link>
</Grid>
</Grid>
) : null
+15 -11
View File
@@ -1,9 +1,11 @@
import React, { useState, useEffect, useRef } from 'react';
import React, { useState, useEffect, useRef, useContext } from 'react';
import theme from '../theme.jsx';
import { useNavigate, Link, useParams } from "react-router-dom";
import SearchBox from "../components/SearchData.jsx";
import { Context } from '../context/ContextApi.jsx';
import {
Chip,
IconButton,
@@ -45,20 +47,22 @@ const chipStyle = {
const SearchField = props => {
const { serverside, userdata, isMobile, isLoaded, globalUrl, isHeader, isLoggedIn, small, rounded } = props
const {searchBarModalOpen, setSearchBarModalOpen} = useContext(Context);
let navigate = useNavigate();
const borderRadius = 3
const node = useRef()
const [searchOpen, setSearchOpen] = useState(false)
const [modalOpen, setModalOpen] = React.useState(false);
// const [modalOpen, setModalOpen] = React.useState(false);
const [oldPath, setOldPath] = useState("")
const [value, setValue] = useState("");
useEffect(() => {
Mousetrap.bind(['command+k', 'ctrl+k'], () => {
setModalOpen(true);
setSearchBarModalOpen(true);
return false; // Prevent the default action
});
Mousetrap.bind(['esc'], () => {
setModalOpen(false);
setSearchBarModalOpen(false);
return false; // Prevent the default action
});
@@ -72,9 +76,9 @@ const SearchField = props => {
// console.log("key:", dataValue.key),
//console.log("value:",dataValue.value),
<Dialog
open={modalOpen}
open={searchBarModalOpen}
onClose={() => {
setModalOpen(false);
setSearchBarModalOpen(false);
}}
PaperProps={{
style: {
@@ -92,12 +96,12 @@ const SearchField = props => {
{isHeader ? <div style={{ display: "flex"}}>
<DialogTitle style={{ marginTop: 15, marginLeft: 5, color: "var(--Paragraph-text, #C8C8C8)" }} >Search for Docs, Apps, Workflows and more</DialogTitle>
<Button color="secondary" fullWidth style={{ marginLeft:180, }} onClick={() => {
setModalOpen(false);
setSearchBarModalOpen(false);
}}><CloseIcon /></Button>
</div>
: null}
<DialogContent className='dialog-content' style={{}}>
<SearchBox globalUrl={globalUrl} setModalOpen={setModalOpen} modalOpen={modalOpen} serverside={serverside} userdata={userdata} />
<SearchBox globalUrl={globalUrl} serverside={serverside} userdata={userdata} />
</DialogContent>
<Divider style={{overflow: "hidden"}}/>
<span style={{display:"flex", width:"100%", height:30}}>
@@ -124,10 +128,10 @@ const SearchField = props => {
);
return (
<div style={{ marginTop: "auto", marginLeft: !isLoggedIn ? 0 : 130 }}>
<div style={{ marginTop: "auto", marginLeft: !isLoggedIn ? 0: "auto", marginRight: !isLoggedIn ? 0 : "auto", width: !isLoggedIn ? "auto" : 410, }}>
{modalView}
<TextField
style={{ backgroundColor: "#212121", height: 48, borderRadius: rounded === true ? 25 : theme.palette.borderRadius, minWidth: fieldWidth, maxWidth: fieldWidth, }}
style={{ backgroundColor: "#212121", height: 48, borderRadius: rounded === true ? 25 : theme.palette?.borderRadius, minWidth: fieldWidth, maxWidth: fieldWidth, }}
InputProps={{
style: {
color: "white",
@@ -155,7 +159,7 @@ const SearchField = props => {
color="primary"
placeholder="Search Apps, Workflows, Docs..."
onClick={(event) => {
setModalOpen(true)
setSearchBarModalOpen(true)
}}
limit={5}
/>
+189 -43
View File
@@ -40,11 +40,12 @@ import {
Close as CloseIcon,
DragIndicator as DragIndicatorIcon,
RestartAlt as RestartAltIcon,
} from '@mui/icons-material';
import { validateJson } from "../views/Workflows.jsx";
import ReactJson from "react-json-view";
import ReactJson from "react-json-view-ssr";
import PaperComponent from "../components/PaperComponent.jsx";
import { padding, textAlign } from '@mui/system';
@@ -56,6 +57,7 @@ import { tags as t } from '@lezer/highlight';
import AceEditor from "react-ace";
import ace from "ace-builds";
import 'ace-builds/src-noconflict/mode-python';
import 'ace-builds/src-noconflict/mode-json';
//import 'ace-builds/src-noconflict/theme-twilight';
//import 'ace-builds/src-noconflict/theme-solarized_dark';
import 'ace-builds/src-noconflict/theme-gruvbox';
@@ -105,12 +107,16 @@ const CodeEditor = (props) => {
selectedAction ,
workflowExecutions,
getParents,
activeDialog,
setActiveDialog,
fieldname,
contentLoading,
editorData,
setAiQueryModalOpen,
fullScreenMode
} = props
const [localcodedata, setlocalcodedata] = React.useState(codedata === undefined || codedata === null || codedata.length === 0 ? "" : codedata);
//const { setContainer } = useCodeMirror({
@@ -163,6 +169,11 @@ const CodeEditor = (props) => {
setMenuPosition(null);
}
useEffect(() => {
highlight_variables(localcodedata)
expectedOutput(localcodedata)
}, [localcodedata])
let navigate = useNavigate();
useEffect(() => {
@@ -627,8 +638,8 @@ const CodeEditor = (props) => {
newMarkers.push({
startRow: i,
startCol: startCh,
endRow: i+1,
endCol: endCh+1,
endRow: i,
endCol: endCh,
className: correctVariable ? "good-marker" : "bad-marker",
type: "text",
})
@@ -811,11 +822,7 @@ const CodeEditor = (props) => {
}
const handleItemClick = (values) => {
if (
values === undefined ||
values === null ||
values.length === 0
) {
if (values === undefined || values === null || values.length === 0) {
return;
}
@@ -830,7 +837,11 @@ const CodeEditor = (props) => {
toComplete += values[key].autocomplete;
}
setlocalcodedata(localcodedata+toComplete)
handleClick({
"value": toComplete
})
//setlocalcodedata(localcodedata+toComplete)
setMenuPosition(null)
}
@@ -839,10 +850,37 @@ const CodeEditor = (props) => {
return
}
if (!item.value.includes("{%") && !item.value.includes("{{")) {
setlocalcodedata(localcodedata+" | "+item.value+" }}")
} else {
setlocalcodedata(localcodedata+item.value)
// Injects it in the right spot instead of random
var edited = false
if (currentCharacter !== undefined && currentCharacter !== null && currentCharacter !== -1 && currentLine !== undefined && currentLine !== null && currentLine !== -1) {
// Input at the right spot
var codedatasplit = localcodedata.split('\n')
if (codedatasplit.length > currentLine) {
var currentLineData = codedatasplit[currentLine]
// Remove newlines from item.value
if (item.value.includes("% python %")) {
item.value = item.value.replaceAll("\n", ";")
item.value = item.value.replaceAll("python %};", "python %}")
} else {
item.value = item.value.replaceAll("\n", "")
}
currentLineData = currentLineData.slice(0, currentCharacter) + item.value + currentLineData.slice(currentCharacter)
codedatasplit[currentLine] = currentLineData
setlocalcodedata(codedatasplit.join('\n'))
edited = true
}
}
if (edited === false) {
if (!item.value.includes("{%") && !item.value.includes("{{")) {
setlocalcodedata(localcodedata+" | "+item.value+" }}")
} else {
setlocalcodedata(localcodedata+item.value)
}
}
setAnchorEl(null)
@@ -858,8 +896,8 @@ const CodeEditor = (props) => {
// Shuffle Tools 1.2.0 (in most cases?)
const appid = toolsAppId !== undefined && toolsAppId !== null && toolsAppId.length > 0 ? toolsAppId : "3e2bdf9d5069fe3f4746c29d68785a6a"
const actionname = selectedAction.name === "execute_python" && !inputdata.replaceAll(" ", "").includes("{%python%}") ? "execute_python" : "repeat_back_to_me"
const params = actionname === "execute_python" ? [{"name": "code", "value":inputdata}] : [{"name":"call", "value": inputdata}]
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":{}}
@@ -946,23 +984,77 @@ const CodeEditor = (props) => {
// Define a custom completer for the Ace Editor
const customVariables = availableVariables
const customCompleter = {
getCompletions: function(editor, session, pos, prefix, callback) {
callback(null, customVariables.map((variable) => ({
caption: variable,
value: variable,
meta: 'custom',
})));
console.log("CUSTOM COMPLETER: ", prefix)
callback(null, availableVariables.map((variable) => {
console.log("CUSTOM VAR: ", variable)
return ({
caption: variable,
value: variable,
meta: 'custom',
})
}))
}
}
if (fullScreenMode) {
return (
<AceEditor
mode="python"
theme="gruvbox"
value={localcodedata}
onChange={(value, editor) => {
// setlocalcodedata(value)
// expectedOutput(value)
// highlight_variables(value,editor)
setlocalcodedata(value)
setcodedata(value)
}}
name="python-editor"
fontSize={14}
width="100%"
height="100%"
showPrintMargin={false}
showGutter={true}
markers={markers}
highlightActiveLine={false}
enableBasicAutocompletion={true}
completers={[customCompleter]}
style={{
wordBreak: "break-word",
marginTop: 0,
paddingBottom: 10,
overflowY: "auto",
whiteSpace: "pre-wrap",
wordWrap: "break-word",
backgroundColor: "rgba(40,40,40,1)",
zIndex: activeDialog === "codeeditor" ? 1200 : 1100,
}}
setOptions={{
enableBasicAutocompletion: true,
enableLiveAutocompletion: true,
enableSnippets: true,
showLineNumbers: true,
tabSize: 4,
fontFamily: "'JetBrains Mono', Consolas, monospace",
useSoftTabs: true
}}
/>
)
}
return (
<Dialog
aria-labelledby="draggable-code-modal"
disableBackdropClick={true}
aria-labelledby="draggable-dialog-title"
// disableBackdropClick={true}
disableEnforceFocus={true}
//style={{ pointerEvents: "none" }}
style={{ pointerEvents: "none", zIndex: activeDialog === "codeeditor" ? 1200 : 1100}}
hideBackdrop={true}
open={expansionModalOpen}
onClose={() => {
@@ -977,8 +1069,14 @@ const CodeEditor = (props) => {
}}
PaperComponent={PaperComponent}
PaperProps={{
onClick: () => {
if (setActiveDialog !== undefined) {
setActiveDialog("codeeditor")
}
},
style: {
zIndex: 12501,
// zIndex: 12501,
pointerEvents: "auto",
color: "white",
minWidth: isMobile ? "100%" : isFileEditor ? 650 : "80%",
maxWidth: isMobile ? "100%" : isFileEditor ? 650 : 1100,
@@ -986,9 +1084,22 @@ const CodeEditor = (props) => {
maxHeight: isMobile ? "100%" : 700,
border: theme.palette.defaultBorder,
padding: isMobile ? "25px 10px 25px 10px" : 25,
zoom: 0.8,
backgroundColor: "black",
},
}}
>
{contentLoading === true ?
<Tooltip
color="primary"
title={`The File content is loading. Please wait a moment.`}
placement="top"
>
<CircularProgress style={{position: "absolute", right: 106, top: 6, }}/>
</Tooltip>
: null}
<Tooltip
color="primary"
title={`Move window`}
@@ -1071,10 +1182,15 @@ const CodeEditor = (props) => {
*/}
{ isFileEditor ? null :
<div style={{display: "flex", maxHeight: 40, }}>
{selectedAction.name === "execute_python" ?
{selectedAction?.name === "execute_python" ?
<Typography variant="body1" style={{marginTop: 5, }}>
Run Python Code
</Typography>
:
selectedAction.name === "execute_bash" ?
<Typography variant="body1" style={{marginTop: 5, }}>
Run Bash Code
</Typography>
:
<div style={{display: "flex", }}>
<Button
@@ -1236,7 +1352,7 @@ const CodeEditor = (props) => {
maxHeight: 650,
}}
>
{actionlist.map((innerdata) => {
{actionlist?.map((innerdata) => {
const icon =
innerdata.type === "action" ? (
<AppsIcon style={{ marginRight: 10 }} />
@@ -1321,6 +1437,8 @@ const CodeEditor = (props) => {
onClick={() => {
console.log("CLICKED: ", innerdata);
console.log(innerdata.example)
//const handleClick = (item) => {
handleItemClick([innerdata]);
}}
>
@@ -1465,14 +1583,37 @@ const CodeEditor = (props) => {
width: 50,
marginLeft: 100,
}}
disabled={editorData === undefined || editorData.example === undefined || editorData.example === null || editorData.example.length === 0}
onClick={() => {
setlocalcodedata(editorData.example)
}}
color="secondary"
>
<Tooltip
title={"Reset to example body"}
placement="top"
>
<RestartAltIcon />
</Tooltip>
</IconButton>
<IconButton
style={{
height: 50,
width: 50,
marginLeft: 0,
}}
disabled={isAiLoading}
onClick={() => {
autoFormat(localcodedata)
if (setAiQueryModalOpen !== undefined) {
setAiQueryModalOpen(true)
} else {
autoFormat(localcodedata)
}
}}
>
<Tooltip
color="primary"
title={"Auto format data"}
title={"Format with AI"}
placement="top"
>
{isAiLoading ?
@@ -1487,7 +1628,7 @@ const CodeEditor = (props) => {
}
<div style={{
borderRadius: theme.palette.borderRadius,
borderRadius: theme.palette?.borderRadius,
position: "relative",
paddingTop: 0,
// minHeight: 548,
@@ -1495,8 +1636,10 @@ const CodeEditor = (props) => {
}}>
{(availableVariables !== undefined && availableVariables !== null && availableVariables.length > 0) || isFileEditor ? (
<AceEditor
id="shuffle-codeeditor"
name="shuffle-codeeditor"
value={localcodedata}
mode={selectedAction === undefined ? "" : selectedAction.name === "execute_python" ? "python" : ""}
mode={selectedAction === undefined ? "json" : selectedAction.name === "execute_python" ? "python" : selectedAction.name === "execute_bash" ? "bash" : "json"}
theme="gruvbox"
height={isFileEditor ? 450 : 550}
width={isFileEditor ? 650 : "100%"}
@@ -1515,17 +1658,16 @@ const CodeEditor = (props) => {
whiteSpace: "pre-wrap",
wordWrap: "break-word",
backgroundColor: "rgba(40,40,40,1)",
zIndex: activeDialog === "codeeditor" ? 1200 : 1100,
}}
onLoad={(editor) => {
highlight_variables(localcodedata)
}}
onCursorChange={(cursorPosition, editor, value) => {
setCurrentCharacter(cursorPosition.column)
setCurrentLine(cursorPosition.row)
setCurrentCharacter(cursorPosition.cursor.column)
setCurrentLine(cursorPosition.cursor.row)
findIndex(cursorPosition.row, cursorPosition.column)
//highlight_variables(localcodedata)
//console.log("VALUE CURSOR: ", value)
}}
onChange={(value, editor) => {
// setlocalcodedata(value)
@@ -1559,7 +1701,7 @@ const CodeEditor = (props) => {
</div>
{isFileEditor ? null :
<div style={{flex: 1, marginLeft: 5, borderLeft: "1px solid rgba(255,255,255,0.3)", paddingLeft: 5, }}>
<div style={{flex: 1, marginLeft: 5, borderLeft: "1px solid rgba(255,255,255,0.3)", paddingLeft: 5, overflow: "hidden", }}>
<div>
{isMobile ? null :
<DialogTitle
@@ -1567,11 +1709,13 @@ const CodeEditor = (props) => {
paddingLeft: 10,
paddingTop: 0,
display: "flex",
cursor: "move"
}}
>
<div>
<span style={{color: "white"}}>
Expected Output
{selectedAction === undefined ? "" : selectedAction.name === "execute_python" || selectedAction.name === "execute_bash" ? "Code to run" : `Expected Output for '${selectedAction.name}'`}
</span>
</div>
@@ -1588,7 +1732,7 @@ const CodeEditor = (props) => {
border: `1px solid ${theme.palette.primary.main}`,
position: "absolute",
top: 24,
right: 65,
right: 100,
maxHeight: 35,
minWidth: 70,
}}
@@ -1599,7 +1743,7 @@ const CodeEditor = (props) => {
{executing ?
<CircularProgress style={{height: 18, width: 18, }} />
:
<span>Try it <PlayArrowIcon style={{height: 18, width: 18, marginBottom: -4, marginLeft: 5, }} /> </span>
<span>{selectedAction === undefined ? "" : selectedAction.name === "execute_python" ? "Run Python Code" : selectedAction.name === "execute_bash" ? "Run Bash" : "Try it"}<PlayArrowIcon style={{height: 18, width: 18, marginBottom: -4, marginLeft: 5, }} /> </span>
}
</Button>
</Tooltip>
@@ -1618,6 +1762,7 @@ const CodeEditor = (props) => {
overflow: "auto",
minWidth: 450,
maxWidth: "100%",
zIndex: activeDialog === "codeeditor" ? 1200 : 1100,
}}
collapsed={false}
enableClipboard={(copy) => {
@@ -1645,11 +1790,12 @@ const CodeEditor = (props) => {
padding: 10,
marginTop: -2,
border: `2px solid ${theme.palette.inputColor}`,
borderRadius: theme.palette.borderRadius,
borderRadius: theme.palette?.borderRadius,
maxHeight: 450,
minHeight: 450,
overflow: "auto",
wordWrap: "anywhere",
zIndex: activeDialog === "codeeditor" ? 1200 : 1100,
}}
>
{expOutput}
@@ -102,7 +102,7 @@ const SuggestedWorkflows = (props) => {
placement="top"
style={{ zIndex: 10011 }}
>
<div key={index} style={{cursor: finished ? "auto" : "pointer", marginTop: 10, padding: 10, borderRadius: theme.palette.borderRadius, border: `1px solid ${bordercolor}`, display: "flex", backgroundColor: hovering === true ? theme.palette.inputColor : theme.palette.surfaceColor, }} onMouseOver={() => {
<div key={index} style={{cursor: finished ? "auto" : "pointer", marginTop: 10, padding: 10, borderRadius: theme.palette?.borderRadius, border: `1px solid ${bordercolor}`, display: "flex", backgroundColor: hovering === true ? theme.palette.inputColor : theme.palette.surfaceColor, }} onMouseOver={() => {
setHovering(true)
}} onMouseOut={() => {
setHovering(false)
@@ -140,7 +140,7 @@ const SuggestedWorkflows = (props) => {
//<Paper style={{width: 275, maxHeight: 400, overflow: "hidden", zIndex: 12500, padding: 25, paddingRight: 35, backgroundColor: theme.palette.surfaceColor, border: "1px solid rgba(255,255,255,0.2)", position: "absolute", top: -50, left: 50, }}>
return (
<Paper style={{margin: "auto", position: "relative", backgroundColor: theme.palette.surfaceColor, borderRadius: theme.palette.borderRadius, zIndex: foundZindex, border: "1px solid rgba(255,255,255,0.2)", top: 100, left: 85,}}>
<Paper style={{margin: "auto", position: "relative", backgroundColor: theme.palette.surfaceColor, borderRadius: theme.palette?.borderRadius, zIndex: foundZindex, border: "1px solid rgba(255,255,255,0.2)", top: 100, left: 85,}}>
<Dialog
open={usecaseSearch.length > 0 && usecaseSearchType.length > 0}
onClose={() => {
@@ -193,7 +193,7 @@ const SuggestedWorkflows = (props) => {
apps={apps}
/>
</Dialog>
<div style={{minWidth: 250, maxWidth: 250, padding: 15, borderRadius: theme.palette.borderRadius, position: "relative", }}>
<div style={{minWidth: 250, maxWidth: 250, padding: 15, borderRadius: theme.palette?.borderRadius, position: "relative", }}>
<Typography variant="body1" style={{textAlign: "center"}}>
Suggested Workflows ({finishedUsecases.length}/{usecaseSuggestions.length})
</Typography>
+7 -5
View File
@@ -349,7 +349,7 @@ const UsecaseSearch = (props) => {
const [selectedAction, setSelectedAction] = React.useState({});
const [firstRequest, setFirstRequest] = React.useState(true);
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true");
//const alert = useAlert()
useEffect(() => {
@@ -594,7 +594,7 @@ const UsecaseSearch = (props) => {
width: 30,
height: 30,
border: "2px solid rgba(255,255,255,0.6)",
borderRadius: theme.palette.borderRadius,
borderRadius: theme.palette?.borderRadius,
maxHeight: 30,
maxWidth: 30,
overflow: "hidden",
@@ -616,7 +616,7 @@ const UsecaseSearch = (props) => {
width: 30,
height: 30,
border: "2px solid rgba(255,255,255,0.6)",
borderRadius: theme.palette.borderRadius,
borderRadius: theme.palette?.borderRadius,
maxWidth: 30,
maxHeight: 30,
overflow: "hidden",
@@ -1266,9 +1266,11 @@ const UsecaseSearch = (props) => {
.then((responseJson) => {
if (responseJson.success === false) {
var msgString = "Failed to activate the app"
if (responseJson.reason !== undefined) {
msgString += ": " + responseJson.reason
}
toast(msgString)
} else {
//toast("App activated for your organization! Refresh the page to use the app.")
@@ -1388,7 +1390,7 @@ const UsecaseSearch = (props) => {
<Typography variant="body1" style={{color: "rgba(255,255,255,0.5)", marginRight: 20, marginTop: 13, }}>
{startText}
</Typography>
<div style={{border: `1px solid ${borderColor}`, backgroundColor: theme.palette.surfaceColor, width: miditem === true ? "65%" : "85%", marginLeft: miditem === true ? 125 : 0, borderRadius: expanded ? theme.palette.borderRadius : 50, maxHeight: expanded || hasError ? 500 : 50, minHeight: 50, }}>
<div style={{border: `1px solid ${borderColor}`, backgroundColor: theme.palette.surfaceColor, width: miditem === true ? "65%" : "85%", marginLeft: miditem === true ? 125 : 0, borderRadius: expanded ? theme.palette?.borderRadius : 50, maxHeight: expanded || hasError ? 500 : 50, minHeight: 50, }}>
{selectionOpen === true ?
<AppsearchPopout
@@ -1560,7 +1562,7 @@ const UsecaseSearch = (props) => {
// <b>{defaultSearch}: {allusecases[usecaseIndex].name}</b>
//console.log("UseCase: ", usecases)
return (
<div style={{maxWidth: "100%", minWidth: "100%", border: "1px solid rgba(255,255,255,0)", borderRadius: theme.palette.borderRadius,}}>
<div style={{maxWidth: "100%", minWidth: "100%", border: "1px solid rgba(255,255,255,0)", borderRadius: theme.palette?.borderRadius,}}>
{configureWorkflowModal}
{authenticationModal}
{showTitle !== false && defaultSearch !== undefined ?
+4 -16
View File
@@ -64,6 +64,7 @@ const WelcomeForm = (props) => {
discoveryWrapper,
setDiscoveryWrapper,
appFramework,
setAppFramework,
getFramework,
activeStep,
setActiveStep,
@@ -160,7 +161,7 @@ const WelcomeForm = (props) => {
const [clickdiff, setclickdiff] = useState(0);
const [mouseHoverIndex, setMouseHoverIndex] = useState(-1)
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true");
//const alert = useAlert();
let navigate = useNavigate();
@@ -435,20 +436,6 @@ const WelcomeForm = (props) => {
setSkipped(newSkipped);
};
const handleBack = () => {
setActiveStep((prevActiveStep) => prevActiveStep - 1);
if (activeStep === 2) {
setDiscoveryWrapper({});
if (getFramework !== undefined) {
getFramework();
}
navigate("/welcome?tab=2");
} else if (activeStep === 1) {
navigate("/welcome?tab=1");
}
};
const handleReset = () => {
setActiveStep(0);
@@ -645,6 +632,7 @@ const WelcomeForm = (props) => {
globalUrl={globalUrl}
userdata={userdata}
appFramework={appFramework}
setAppFramework={setAppFramework}
setActiveStep={setActiveStep}
defaultSearch={defaultSearch}
setDefaultSearch={setDefaultSearch}
@@ -660,7 +648,7 @@ const WelcomeForm = (props) => {
<div style={{ marginTop: 0, }}>
<div className="thumbs" style={{ display: "flex" }}>
<div style={{ minWidth: isMobile ? 300 : 554, maxWidth: isMobile ? 300 : 554, borderRadius: theme.palette.borderRadius, }}>
<div style={{ minWidth: isMobile ? 300 : 554, maxWidth: isMobile ? 300 : 554, borderRadius: theme.palette?.borderRadius, }}>
<ExploreWorkflow
globalUrl={globalUrl}
isLoggedIn={isLoggedIn}
+11 -1
View File
@@ -28,7 +28,7 @@ const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e52
const AppGrid = props => {
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, alternativeView, onlyResults, inputsearch } = props
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true");
const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows
const xs = parsedXs === undefined || parsedXs === null ? isMobile ? 6 : 4 : parsedXs
//const [apps, setApps] = React.useState([]);
@@ -204,6 +204,9 @@ const AppGrid = props => {
style={{backgroundColor: theme.palette.inputColor, borderRadius: borderRadius, margin: 10, width: "100%",}}
InputProps={{
style:{
color: "white",
fontSize: "1em",
height: 50,
},
startAdornment: (
<InputAdornment position="start">
@@ -221,6 +224,11 @@ const AppGrid = props => {
removeQuery("q")
refine(event.currentTarget.value)
}}
onKeyDown={(event) => {
if(event.key === "Enter") {
event.preventDefault();
}
}}
limit={5}
/>
: null}
@@ -233,6 +241,8 @@ const AppGrid = props => {
flexWrap: "wrap",
alignContent: "space-between",
marginTop: 5,
padding: "0px 180px",
width:"auto"
}
var workflowDelay = -50
@@ -47,7 +47,7 @@ const WorkflowTemplatePopup = (props) => {
const [requestSent, setRequestSent] = React.useState(false)
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true");
let navigate = useNavigate();
useEffect(() => {
if (modalOpen !== true) {
@@ -585,7 +585,7 @@ const WorkflowTemplatePopup = (props) => {
{/*errorMessage === "" && configurationFinished === true && workflow.id !== undefined && workflowLoading === false ?
<Tooltip title="Click to explore the workflow" placement="top">
<span
style={{position: "fixed", display: "flex", right: "10%", top: "20%", border: "1px solid rgba(255,255,255,0.3)", borderRadius: theme.palette.borderRadius, padding: "15px 30px 15px 30px", backgroundColor: theme.palette.platformColor, cursor: "pointer", }}
style={{position: "fixed", display: "flex", right: "10%", top: "20%", border: "1px solid rgba(255,255,255,0.3)", borderRadius: theme.palette?.borderRadius, padding: "15px 30px 15px 30px", backgroundColor: theme.palette.platformColor, cursor: "pointer", }}
onClick={() => {
// Open in new tab
window.open("/workflows/" + workflow.id, "_blank")
@@ -0,0 +1,901 @@
import React, { useState, useEffect } from "react";
import { toast } from "react-toastify"
import theme from '../theme.jsx';
import { useNavigate, Link, useParams } from "react-router-dom";
import AppSearchButtons from "../components/AppSearchButtons.jsx";
import { isMobile } from "react-device-detect";
import RenderCytoscape from "../components/RenderCytoscape.jsx";
import {
Button,
Typography,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Drawer,
CircularProgress,
Fade,
IconButton,
Tooltip,
} from "@mui/material";
import {
Check as CheckIcon,
TrendingFlat as TrendingFlatIcon,
Close as CloseIcon,
East as EastIcon,
Interests as InterestsIcon,
} from '@mui/icons-material';
import {
green,
yellow,
red,
grey,
} from "../views/AngularWorkflow.jsx"
import WorkflowTemplatePopup2 from "./WorkflowTemplatePopup.jsx";
import ConfigureWorkflow from "../components/ConfigureWorkflow.jsx";
import WorkflowValidationTimeline from "../components/WorkflowValidationTimeline.jsx";
import FixWorkflowValidationErrors from "../components/FixWorkflowValidationErrors.jsx";
const WorkflowTemplatePopup = (props) => {
const {
userdata, appFramework, globalUrl, img1, srcapp, img2, dstapp, title, description, visualOnly, apps, isLoggedIn, isHomePage, getAppFramework, showTryit, shownColor, workflowBuilt, usecaseDetails,
isModalOpenDefault,
setIsClicked,
inputWorkflowId,
} = props;
const [isActive, setIsActive] = useState(workflowBuilt === true);
const [isHovered, setIsHovered] = useState(false);
const [modalOpen, setModalOpen] = useState(isModalOpenDefault === true ? true : false)
const [errorMessage, setErrorMessage] = useState("");
const [workflowLoading, setWorkflowLoading] = useState(false)
const [showLoginButton, setShowLoginButton] = useState(false);
const [appAuthentication, setAppAuthentication] = React.useState(undefined);
const [missingSource, setMissingSource] = React.useState(undefined)
const [missingDestination, setMissingDestination] = React.useState(undefined);
const [configurationFinished, setConfigurationFinished] = React.useState(false)
const [appSetupDone, setAppSetupDone] = React.useState(false)
const [requestSent, setRequestSent] = React.useState(false)
const [showTryitOut, setShowTryitout] = React.useState(showTryit === true ? true : false)
const [loadingWorkflow, setLoadingWorkflow] = React.useState(false)
const [workflow, setWorkflow] = useState({});
const [_, setUpdate] = useState(0)
const fetchWorkflow = (id) => {
if (id === undefined || id === null || id === "") {
return
}
if (loadingWorkflow === true) {
return
}
setLoadingWorkflow(true)
const url = `${globalUrl}/api/v1/workflows/${id}`
fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
setLoadingWorkflow(false)
if (response.status !== 200) {
console.log("Status not 200 for framework!");
}
return response.json();
})
.then((responseJson) => {
if (responseJson.success === false) {
console.log("Error in workflow loading for ID ", id)
} else {
setWorkflow(responseJson)
}
})
.catch((error) => {
console.log("err in framework: ", error.toString());
setLoadingWorkflow(false)
})
}
if (inputWorkflowId !== undefined && inputWorkflowId !== null && inputWorkflowId !== "" && workflow.id !== inputWorkflowId) {
fetchWorkflow(inputWorkflowId)
}
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
let navigate = useNavigate();
useEffect(() => {
if (modalOpen !== true) {
if (workflowLoading === true) {
setWorkflowLoading(false)
}
//console.log("Modal is not open, so we are not doing anything.")
return
}
if (workflowLoading !== true) {
//console.log("Workflow loading is false, so we can try to get the workflow.")
return
}
console.log("DEBUG: Skipped direct generation without Try it for now.")
/*
if (!srcapp.includes(":default") && !dstapp.includes(":default")) {
if (appSetupDone === false && setAppSetupDone !== undefined) {
setAppSetupDone(true)
}
getGeneratedWorkflow()
}
if (missingSource !== undefined && missingDestination !== undefined) {
if (appSetupDone === false && setAppSetupDone !== undefined) {
setAppSetupDone(true)
}
}
if (getAppFramework !== undefined) {
setTimeout(() => {
getAppFramework()
}, 500)
}
*/
}, [modalOpen, missingSource, missingDestination])
useEffect(() => {
//console.log("IN USEEFFECT FOR CONFIG: ", configurationFinished)
if (configurationFinished === true && workflow.id !== undefined && workflow.id !== null && workflow.id !== "") {
//toast.success("Generation Successful")
/*
setTimeout(() => {
navigate("/workflows/" + workflow.id)
}, 2000)
*/
}
}, [configurationFinished, workflow])
const imageSize = 32
const defaultBorder = "1px solid rgba(255,255,255,0.6)"
const imagestyleWrapper = {
height: imageSize,
width: imageSize,
borderRadius: imageSize,
border: isHomePage ? null : defaultBorder,
overflow: "hidden",
display: "flex",
backgroundColor: theme.palette.inputColor,
}
const imagestyleWrapperDefault = {
height: imageSize,
width: imageSize,
borderRadius: imageSize,
border: isHomePage ? null : defaultBorder,
overflow: "hidden",
display: "flex",
backgroundColor: theme.palette.inputColor,
}
const imagestyle = {
height: imageSize,
width: imageSize,
borderRadius: imageSize,
//border: isHomePage ? null : defaultBorder,
overflow: "hidden",
backgroundColor: theme.palette.inputColor,
}
const imagestyleDefault = {
display: "block",
marginLeft: 9,
marginTop: 9,
height: imageSize,
width: "auto",
backgroundColor: theme.palette.inputColor,
}
if (modalOpen === false && (title === undefined || title === null || title === "")) {
if (setIsClicked !== undefined) {
setIsClicked(false)
}
console.log("No title for workflow template popup!");
return null
}
const loadAppAuth = () => {
// Check if it exists, and has keys
//
if (userdata === undefined || userdata === null || Object.keys(userdata).length === 0) {
setErrorMessage("You need to be logged in to try the pre-built Workflow Templates.")
setShowLoginButton(true)
// Send the user to the login screen after 3 seconds
setTimeout(() => {
// Make it cancel if the state modalOpen changes
if (modalOpen === false) {
return
}
navigate("/login?view=" + window.location.pathname + window.location.search)
}, 4500)
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 setting app auth :O!");
}
return response.json();
})
.then((responseJson) => {
if (!responseJson.success) {
toast("Failed to get app auth: " + responseJson.reason);
return
}
var newauth = [];
for (let authkey in responseJson.data) {
if (responseJson.data[authkey].defined === false) {
continue;
}
newauth.push(responseJson.data[authkey]);
}
setAppAuthentication(newauth);
})
.catch((error) => {
//toast(error.toString());
console.log("New auth error: ", error.toString());
});
}
// Can create and set workflows
const reloadWorkflow = (workflow_id) => {
const new_url = `${globalUrl}/api/v1/workflows/${workflow_id}`
return fetch(new_url, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for workflows :O!");
return;
}
//setSubmitLoading(false);
return response.json();
})
.then((responseJson) => {
if (responseJson.success === false) {
if (responseJson.reason !== undefined) {
toast("Error setting workflow: ", responseJson.reason)
} else {
toast("Error setting workflow.")
}
return
} else if (responseJson.id !== undefined && responseJson.id !== null && responseJson.id !== "") {
setWorkflow(responseJson)
}
return responseJson;
})
.catch((error) => {
toast("Failed reloading configured workflow: ", error.toString());
});
};
// Can create and set workflows
const saveWorkflow = (workflowdata) => {
const new_url = `${globalUrl}/api/v1/workflows?set_auth=true`
return fetch(new_url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(workflowdata),
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for workflows :O!");
return;
}
//setSubmitLoading(false);
return response.json();
})
.then((responseJson) => {
if (responseJson.success === false) {
if (responseJson.reason !== undefined) {
toast("Error setting workflow: ", responseJson.reason)
} else {
toast("Error setting workflow.")
}
return
}
// In case it got a new id, this is to make sure it loads with the correct config
if (responseJson.id !== undefined && responseJson.id !== null && responseJson.id !== "") {
reloadWorkflow(responseJson.id)
}
return responseJson;
})
.catch((error) => {
toast("Failed generating workflow: ", error.toString());
});
};
const getGeneratedWorkflow = () => {
// POST
// https://shuffler.io/api/v1/workflows/merge
// destination: {app_id: "b9c2feaf99b6309dabaeaa8518c61d3d", app_name: "Servicenow_API", app_version: "",}
// id: ""
// middle:[]
// name: "Email analysis"
// source:{app_id: "accdaaf2eeba6a6ed43b2efc0112032d", app_name
if (requestSent === true) {
return
}
console.log("SRCAPP: ", srcapp, "DSTAPP: ", dstapp)
if (srcapp === undefined || srcapp === null) {
srcapp = ""
}
if ((srcapp !== undefined && srcapp !== null && srcapp.includes(":default")) || (dstapp !== undefined && dstapp !== null && dstapp.includes(":default"))) {
toast("You need to select both a source and destination app before generating this workflow.")
if (srcapp !== undefined && srcapp !== null && srcapp.includes(":default")) {
setMissingSource({
"type": srcapp.split(":")[0],
})
}
if (dstapp !== undefined && dstapp !== null && dstapp.includes(":default")) {
setMissingDestination({
"type": dstapp.split(":")[0],
})
}
return
}
setWorkflowLoading(true)
const newsrcapp = srcapp
const newdstapp = dstapp
const mergedata = {
name: title,
id: "",
source: {
app_name: newsrcapp,
},
middle: [],
destination: {
app_name: newdstapp,
},
}
setRequestSent(true)
const url = isCloud ? `${globalUrl}/api/v1/workflows/merge` : `https://shuffler.io/api/v1/workflows/merge`
fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
body: JSON.stringify(mergedata),
})
.then((response) => {
if (response.status !== 200) {
//console.log("Status not 200 for framework!");
setRequestSent(false)
}
setWorkflowLoading(false)
return response.json();
})
.then((responseJson) => {
if (responseJson.id !== undefined && responseJson.id !== null && responseJson.id !== "" && responseJson.name !== undefined && responseJson.name !== null && responseJson.name !== "") {
console.log("Success in workflow template (prebuilt): ", responseJson);
setWorkflow(responseJson)
// Sets it in the database properly
saveWorkflow(responseJson)
return
}
if (responseJson.success === false) {
//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."
if (responseJson.reason !== undefined && responseJson.reason !== null && responseJson.reason !== "") {
setErrorMessage(defaultMessage + "\n\n" + responseJson.reason)
} else {
setErrorMessage(defaultMessage)
}
setIsActive(true)
//setTimeout(() => {
// setModalOpen(false)
//}, 5000)
} else {
console.log("Success in workflow template: ", responseJson);
setIsActive(true)
if (responseJson.workflow_id === "") {
console.log("Failed to build workflow for these tools. Closing in 3 seconds.")
return
}
fetchWorkflow(responseJson.workflow_id)
}
})
.catch((error) => {
console.log("err in framework: ", error.toString());
setRequestSent(false)
setWorkflowLoading(false)
})
}
if (modalOpen === true && !srcapp?.includes(":default") && !dstapp?.includes(":default")) {
if (appSetupDone === false && setAppSetupDone !== undefined) {
setAppSetupDone(true)
}
// No autoruns anymore without clicking "Try it"
if (workflow.id === undefined && workflowLoading === false && errorMessage === "") {
//getGeneratedWorkflow()
}
}
const isFinished = () => {
// Look for configuration fields being done in the current modal
// 1. Start by finding the modal
const template = document.getElementById("workflow-template")
if (template === null || template == undefined) {
return true
}
// Find item in template with id app-config
const appconfig = template.getElementsByClassName("app-config")
if (appconfig === null || appconfig == undefined) {
return true
}
return false
}
const ModalView = () => {
if (modalOpen === false) {
return null
}
const divHeight = 500
const divWidth = 500
return (
<Drawer
anchor={"right"}
open={modalOpen}
onClose={() => {
setModalOpen(false);
if (setIsClicked !== undefined) {
setIsClicked(false)
}
}}
PaperProps={{
style: {
backgroundColor: "black",
color: "white",
minWidth: isHomePage ? null : isMobile ? 300 : 850,
maxWidth: isHomePage ? null : isMobile ? 300 : 850,
paddingTop: isMobile ? null : 75,
itemAlign: "center",
},
}}
>
<IconButton
style={{
zIndex: 5000,
position: "absolute",
top: 14,
right: 14,
color: "white",
}}
onClick={() => {
setModalOpen(false);
}}
>
<CloseIcon />
</IconButton>
<DialogContent style={{marginTop: 0, marginLeft: isHomePage ? null : isMobile ? null : 75, maxWidth: 470, }}>
<Typography variant="h4" style={{ fontSize: isMobile ? 20 : null}}>
<b>Configure Workflow</b>
</Typography>
{title === undefined || title === null || title === "" ? null :
<span>
<Typography variant="body2" color="textSecondary" style={{marginTop: 25, }}>
Selected Workflow:
</Typography>
<div style={{marginBottom: 0, }} id="workflow-template">
<WorkflowTemplatePopup2
globalUrl={globalUrl}
img1={img1}
srcapp={srcapp}
img2={img2}
dstapp={dstapp}
title={title}
description={description}
visualOnly={true}
workflowBuilt={workflowBuilt}
shownColor={shownColor}
/>
</div>
</span>
}
<div style={{marginTop: 15, }}>
{/* Fix the timeline when errors are fixed.. how? */}
<WorkflowValidationTimeline
workflow={workflow}
/>
<FixWorkflowValidationErrors
globalUrl={globalUrl}
workflow={workflow}
setWorkflow={setWorkflow}
setUpdateParent={setUpdate}
/>
</div>
{workflowLoading === true ?
<div style={{marginTop: 75, textAlign: "center", }}>
<Typography variant="h4"> Generating the Workflow...
</Typography>
<CircularProgress style={{marginLeft: 0, marginTop: 25, }}/>
</div>
:
<div>
{usecaseDetails === undefined ? null :
<Typography variant="h6" style={{marginTop: 75, }}>
{usecaseDetails?.description}
</Typography>
}
<Typography variant="h6" style={{marginTop: 75, }}>
{errorMessage !== "" ? errorMessage : ""}
</Typography>
{showLoginButton ?
<Link to="/register?message=Please login to create workflows&view=usecases"
style={{
textDecoration: 'none',
marginBottom: 50,
}}
>
<Typography
style={{
display: "flex",
fontSize: 18,
color: "rgba(255, 132, 68, 1)",
marginTop: 32,
fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)",
fontWeight: 550,
}}
>
Sign up
<EastIcon style={{ marginTop: 3, marginLeft: 7 }} />
</Typography>
</Link>
:
!showTryitOut && !isActive ?
<Button
variant="outlined"
style={{
textTransform: "none",
}}
onClick={() => {
//setWorkflowLoading(true)
getGeneratedWorkflow()
loadAppAuth()
}}
>
Try this usecase <TrendingFlatIcon style={{ }} />
</Button>
: null}
</div>
}
{!isLoggedIn ? null :
<div>
{(appSetupDone === false && missingSource !== undefined || missingDestination !== undefined) ?
<Typography variant="body1" style={{marginTop: 75, marginBottom: 10, }}>
{"Find relevant Apps for this Usecase"}
</Typography>
: null}
{(missingSource !== undefined) ?
<div style={{}}>
<AppSearchButtons
globalUrl={globalUrl}
appFramework={appFramework}
appType={missingSource.type}
AppImage={missingSource.image}
setMissing={setMissingSource}
getAppFramework={getAppFramework}
/>
</div>
: null}
{(missingDestination !== undefined) ?
<div style={{}}>
<AppSearchButtons
globalUrl={globalUrl}
appFramework={appFramework}
appType={missingDestination.type}
AppImage={missingDestination.image}
setMissing={setMissingDestination}
getAppFramework={getAppFramework}
/>
</div>
: null}
</div>
}
<ConfigureWorkflow
userdata={userdata}
theme={theme}
globalUrl={globalUrl}
appAuthentication={appAuthentication}
setAppAuthentication={setAppAuthentication}
workflow={workflow}
apps={apps}
setConfigurationFinished={setConfigurationFinished}
/>
</DialogContent>
</Drawer>
)
}
if (isModalOpenDefault === true) {
return <ModalView />
}
var parsedTitle = title !== undefined && title !== null ? title : ""
const maxlength = 50
if (title !== undefined && title !== null && title.length > maxlength) {
parsedTitle = title.substring(0, maxlength) + "..."
}
parsedTitle = parsedTitle.replaceAll("_", " ")
const parsedDescription = description !== undefined && description !== null ? description.replaceAll("_", " ") : ""
const boxHeight = 104
const highlightColor = shownColor !== undefined && shownColor !== null && shownColor !== "" ? shownColor : "#f85a3e"
var hasInterest = false
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 === "") {
continue
}
if (modalOpen) {
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)
}
hasInterest = true
break
}
}
}
const borderStyle = isHomePage ? null : isHovered && isActive ? errorMessage !== "" ? "1px solid red" : `2px solid ${theme.palette.green}` : isHovered ? `1px solid ${highlightColor}` : "1px solid rgba(33, 33, 33, 1)"
return (
<div style={{ display: "flex", height: boxHeight, borderRadius: theme.palette?.borderRadius, justifyContent: isMobile ? null : "center" }}
>
<ModalView />
<div
// variant={isActive === 1 ? "contained" : "outlined"}
color="secondary"
disabled={visualOnly === true}
style={{
width: isHomePage? isMobile ? null : "100%" : "99%",
borderRadius: 8,
textTransform: "none",
backgroundColor: isHomePage ? null : theme.palette.inputColor,
border: borderStyle,
cursor: isActive ? errorMessage !== "" ? "not-allowed" : "pointer" : "pointer",
position: "relative",
}}
onMouseEnter={() => {
setIsHovered(true)
setShowTryitout(true)
}}
onMouseLeave={() => {
setIsHovered(false)
if (showTryit !== true) {
setShowTryitout(false)
}
}}
onClick={() => {
if (visualOnly === true) {
console.log("Not showing more than visuals.")
return
}
if (!isLoggedIn) {
loadAppAuth()
setModalOpen(true)
} else if (isLoggedIn && errorMessage !== "") {
toast.error("Already failed to generate a workflow for this usecase. Please try again later or contact support@shuffler.io.")
setModalOpen(true)
} else if (isActive) {
// toast.success("Workflow already generated. Please try another workflow template!")
// FIXME: Remove these?
loadAppAuth()
setModalOpen(true)
//getGeneratedWorkflow()
} else {
setModalOpen(true)
//setWorkflowLoading(false)
}
}}
>
<div style={{display: "flex", }}>
{shownColor !== undefined && shownColor !== null && shownColor !== "" ?
<div style={{position: "absolute", left: 0, height: boxHeight-2, width: 4, backgroundColor: shownColor, borderTopLeftRadius: 8, borderBottomLeftRadius: 8, }} />
: null}
<div style={{ display: "flex", itemAlign: "left", textAlign: "left", }}>
<div style={{display: "flex", flex: 1, marginLeft: 25, marginTop: showTryitOut && !isActive ? 14 : 30, }}>
<div style={{zIndex: 51}}>
{img1 !== undefined && img1 !== "" && srcapp !== undefined && srcapp !== "" ?
<Tooltip title={srcapp.replaceAll(":default", "").replaceAll("_", " ").replaceAll(" API", "")} placement="top">
<div style={srcapp !== undefined && srcapp.includes(":default") ? imagestyleWrapperDefault : imagestyleWrapper}>
<img src={img1} style={srcapp !== undefined && srcapp.includes(":default") ? imagestyleDefault : imagestyle} />
</div>
</Tooltip>
:
<div style={{width: 50, }} />
}
</div>
{img2 !== undefined && img2 !== "" && dstapp !== undefined && dstapp !== "" ?
<Tooltip title={dstapp.replaceAll(":default", "").replaceAll("_", " ").replaceAll(" API", "")} placement="top">
<div style={{display: "flex", position: "relative", left: -10, }}>
<div style={dstapp !== undefined && dstapp.includes(":default") ? imagestyleWrapperDefault : imagestyleWrapper}>
<img src={img2} style={dstapp !== undefined && dstapp.includes(":default") ? imagestyleDefault : imagestyle} />
</div>
</div>
</Tooltip>
:
<div style={{width: 0, }} />
}
</div>
<div style={{ marginLeft: 20, overflow: "hidden", maxHeight: 30, marginTop: showTryitOut && !isActive ? 8 : 23, }}>
<Typography variant="body1" style={{ marginTop: parsedDescription.length === 0 ? 10 : 0, fontSize: isMobile ? 13 : 16, fontWeight: isHomePage ? 600 : null, textTransform: 'capitalize', color: isHomePage ? "var(--White-text, #F1F1F1)" : "rgba(241, 241, 241, 1)"}} >
<b>{parsedTitle}</b>
</Typography>
</div>
</div>
</div>
<div>
{isActive === true && errorMessage === "" ?
<Tooltip title="You already have workflows that are based on this usecase" placement="top">
<CheckIcon color="primary" sx={{ borderRadius: 4 }} style={{ position: "absolute", color: theme.palette.green, top: 10, right: 10, }} />
</Tooltip>
: ""}
{!isActive && hasInterest === true ?
<Tooltip title="Your team has shown interest in this usecase previously." placement="top">
<InterestsIcon color="primary" sx={{ borderRadius: 4 }} style={{ position: "absolute", color: "rgba(254, 204, 0, 0.5)", top: 10, right: 10, }} />
</Tooltip>
: null}
</div>
{showTryitOut && !isActive ?
<Fade in={showTryitOut} timeout={300}>
<Button
variant="text"
style={{
textTransform: "none",
marginTop: 8,
marginLeft: 15,
}}
onClick={() => {
//setWorkflowLoading(true)
getGeneratedWorkflow()
loadAppAuth()
}}
>
Try it out <TrendingFlatIcon style={{ }} />
</Button>
</Fade>
: null}
</div>
</div>
)
}
export default WorkflowTemplatePopup
@@ -0,0 +1,694 @@
import React, { useState, } from "react";
import { makeStyles, createStyles } from "@mui/styles";
import { toast } from "react-toastify"
import {
Tooltip,
Chip,
Typography,
IconButton,
Avatar,
AvatarGroup,
} from "@mui/material"
import {
ErrorOutline as ErrorOutlineIcon,
} from "@mui/icons-material"
import {
green,
yellow,
red,
grey,
} from "../views/AngularWorkflow.jsx"
import WorkflowTemplatePopup2 from "../components/WorkflowTemplatePopup2.jsx"
import { validateJson, GetIconInfo } from "../views/Workflows.jsx";
import theme from "../theme.jsx";
const itemHeight = 24
export const getParentNodes = (workflow, action) => {
if (action === undefined || action === null) {
return []
}
if (workflow.actions === undefined || workflow.actions === null) {
workflow.actions = []
}
if (workflow.triggers === undefined || workflow.triggers === null) {
workflow.triggers = []
}
if (workflow.branches === undefined || workflow.branches === null) {
workflow.branches = []
}
var allkeys = [action.id];
var handled = [];
var results = [];
// maxiter = max amount of parent nodes to loop
// also handles breaks if there are issues
var iterations = 0;
var maxiter = 10;
while (true) {
for (let parentkey in allkeys) {
if (allkeys[parentkey] === undefined) {
continue
}
var currentnode = workflow.actions.find((element) => element.id === allkeys[parentkey])
if (currentnode === undefined) {
currentnode = workflow.triggers.find((element) => element.id === allkeys[parentkey])
if (currentnode === undefined) {
//console.log("Could not find parent node for: ", allkeys[parentkey])
continue
}
}
if (handled.includes(currentnode.id)) {
continue
} else {
handled.push(currentnode.id);
results.push(currentnode);
}
// Get the name / label here too?
if (currentnode.length === 0) {
continue;
}
// FIXME: This part is only handling first level,
// but needs to recurse
var incomingEdges = []
for (var branchkey in workflow.branches) {
const branch = workflow.branches[branchkey]
if (branch.destination_id !== currentnode.id) {
continue
}
// Go up in the levels
const parents = getParentNodes(workflow, {
id: branch.source_id,
})
if (parents.length > 0) {
incomingEdges = incomingEdges.concat(parents)
}
incomingEdges.push(branch)
}
for (let i = 0; i < incomingEdges.length; i++) {
var tmp = incomingEdges[i];
if (tmp.decorator === true) {
continue
}
if (!allkeys.includes(tmp.source_id)) {
allkeys.push(tmp.source_id)
}
}
}
if (results.length === allkeys.length || iterations === maxiter) {
break
}
iterations += 1
}
// Remove on the end as we don't want to remove everything
results = results.filter((data) => data.id !== action.id)
results = results.filter((data) => data.type === "ACTION" || data.app_name === "Shuffle Workflow" || data.app_name === "User Input" || data.app_name === "shuffle-subflow")
results.push({ label: "Execution Argument", type: "INTERNAL" })
return results
}
const WorkflowValidationTimeline = (props) => {
const { globalUrl, userdata, workflow, originalWorkflow, apps, getParents, execution, showHoverColor, } = props
const [hovering, setHovering] = useState(false)
const [decidedColor, setDecidedColor] = useState(grey)
const [isClicked, setIsClicked] = useState(false)
const showMiddle = false
if (workflow === undefined || workflow === null) {
return null
}
if (workflow.validation === undefined || workflow.validation === null) {
return null
}
if (workflow.actions === undefined || workflow.actions === null) {
workflow.actions = []
}
if (workflow.triggers === undefined || workflow.triggers === null) {
workflow.triggers = []
}
if (workflow.branches === undefined || workflow.branches === null) {
workflow.branches = []
}
var results = []
if (execution !== undefined) {
results = execution.results
}
// 1. Find startnode
// 2. Map childnodes from it
var startnodeId = workflow.start
if (execution !== undefined && execution !== null) {
startnodeId = execution.start
}
// Find parent of startnodeId and if it's a webhook
var relevantactions = []
for (var key in workflow.branches) {
const branch = workflow.branches[key]
if (branch.destination_id !== startnodeId) {
continue
}
for (var triggerkey in workflow.triggers) {
const trigger = workflow.triggers[triggerkey]
if (trigger.trigger_type !== "WEBHOOK") {
continue
}
if (trigger.id === branch.source_id) {
trigger.order = -1
relevantactions.push(trigger)
break
}
}
}
for (var key in workflow.actions) {
const action = workflow.actions[key]
if (action.id === startnodeId) {
action.order = 0
relevantactions.push(action)
continue
}
var parents = []
if (getParents !== undefined) {
parents = getParents(action)
} else {
parents = getParentNodes(workflow, action)
}
if (action.app_name === "Integration Framework" || action.app_name === "Integration") {
for (var paramkey in action.parameters) {
const param = action.parameters[paramkey]
if (param.name === "app_name") {
action.app_name = param.value.charAt(0).toUpperCase() + param.value.slice(1)
}
}
}
//const parents = getParentNodes(workflow, action)
//console.log("PARENTS", key, parents)
if (parents !== undefined && parents !== null && parents.length > 0) {
const parentfound = parents.find((element) => element.id === startnodeId)
if (parentfound !== undefined) {
// FIXME: add order here based on how many steps away from the startnode
// This just has the parent count
action.order = parents.length
relevantactions.push(action)
}
}
}
if (getParents === undefined) {
var newactions = []
for (var key in workflow.triggers) {
const trigger = workflow.triggers[key]
if (trigger.trigger_type !== "SUBFLOW" && trigger.trigger_type !== "USERINPUT") {
continue
}
for (var branchkey in workflow.branches) {
const branch = workflow.branches[branchkey]
// Checking for OUTBOUND branches from it.
// This will mean it's NOT the last node and is easy to visualize
if (branch.source_id !== trigger.id) {
continue
}
trigger.order = 2
}
// Just in case (:
if (workflow.actions.find((element) => element.id === trigger.id) === undefined) {
newactions.push(trigger)
//workflow.actions.push(trigger)
}
}
relevantactions.push(...newactions)
}
if (relevantactions.length <= 1) {
return null
}
// Sort according to how many parents a node has. MAY be wrong~
relevantactions.sort((a, b) => {
if (a.order === undefined) {
return 1
}
if (b.order === undefined) {
return -1
}
return a.order - b.order
})
// FIXME: Add other relevant items as well from subflows (?)
var nodecolor = grey
var branchcolor = grey
var skipped = false
var previousTools = false
var scheduleNotStarted = false
if (workflow.validation !== undefined && workflow.validation !== null && workflow.validation.validation_ran === false) {
console.log("Validation didn't run. Why?")
return null
}
if (workflow.validation !== undefined && workflow.validation !== null && workflow.validation.errors !== undefined && workflow.validation.errors !== null && workflow.validation.errors.length > 0) {
var newErrors = []
for (var key in workflow.validation.errors) {
const error = workflow.validation.errors[key]
if (error.type === "SCHEDULE") {
scheduleNotStarted = true
continue
}
newErrors.push(error)
}
workflow.validation.errors = newErrors
}
// Use this variable to control visualization
//const showMiddle = false
// border: workflow.validation.valid ? `2px solid ${green}` : "1px solid rgba(255,255,255,0.4)",
var middleError = ""
var startBranchColor = ""
var middleBranchColor = ""
const showHoverForClick = showHoverColor === true ? true : false
return (
<div
style={{
padding: "10px 5px 10px 5px",
borderRadius: theme.palette?.borderRadius,
border: hovering === true && showHoverForClick === true ? `1px solid ${decidedColor}` : "1px solid rgba(255,255,255,0.0)",
cursor: hovering === true && showHoverForClick === true ? "pointer" : "default",
}}
onMouseEnter={() => {
if (isClicked === false) {
setHovering(true)
}
}}
onMouseLeave={() => {
if (isClicked === false) {
setHovering(false)
}
}}
onClick={() => {
if (showHoverForClick === true) {
setIsClicked(true)
}
}}
>
{isClicked === false ? null :
<WorkflowTemplatePopup2
globalUrl={globalUrl}
userdata={userdata}
isModalOpenDefault={isClicked}
workflowBuilt={true}
setIsClicked={setIsClicked}
inputWorkflowId={workflow.id}
/>
}
<div style={{display: "flex", justifyContent: "center", alignItems: "center"}}>
{scheduleNotStarted === true ?
null
: null}
{relevantactions.map((action, index) => {
action.result = {}
if (results !== undefined) {
const foundResult = results.find((element) => element.action.id === action.id)
if (foundResult !== undefined) {
action.result = foundResult
action.status = foundResult.status
}
}
const lastitem = index === relevantactions.length - 1
if (!lastitem) {
if (action.app_name === "Shuffle Tools") {
if (action.status === "SUCCESS") {
branchcolor = red
// Check action.result for the actual status
const validate = validateJson(action.result.result)
if (validate.valid) {
if (validate.result.success === true) {
nodecolor = green
branchcolor = green
} else {
nodecolor = grey
branchcolor = grey
}
}
} else if (action.status === "SKIPPED") {
branchcolor = grey
} else {
// FIXME: How do we handle this?
if (action.status === undefined) {
nodecolor = grey
branchcolor = grey
} else {
nodecolor = red
branchcolor = red
}
}
previousTools = true
if (startnodeId !== action.id) {
//return null
}
} else {
if (action.status === "SUCCESS") {
nodecolor = green
} else if (action.status === "SKIPPED") {
nodecolor = grey
} else {
if (action.status === undefined) {
nodecolor = green
} else {
nodecolor = red
}
}
}
} else {
nodecolor = grey
}
if (action.status === "SKIPPED") {
skipped = true
}
var image = ""
if (action.large_image !== undefined && action.large_image !== null && action.large_image !== "") {
image = action.large_image
} else {
if (originalWorkflow !== undefined) {
for (var key in originalWorkflow.actions) {
if (originalWorkflow.actions[key].id === action.id) {
image = originalWorkflow.actions[key].large_image
break
}
}
if (image === "") {
for (var key in originalWorkflow.triggers) {
if (originalWorkflow.triggers[key].id === action.id) {
image = originalWorkflow.triggers[key].large_image
break
}
}
}
}
}
var founderror = ""
//console.log("WORKFLOW VALIDATION: ", workflow.validation)
if (workflow.validation !== undefined && workflow.validation !== null && workflow.validation.errors !== undefined && workflow.validation.errors !== null) {
const foundError = workflow.validation.errors.find((element) => element.action_id === action.id)
if (foundError !== undefined) {
founderror = foundError.error
nodecolor = red
branchcolor = red
} else {
//console.log("NO ERROR: ", action.id)
}
}
var appgroup = []
if (action.app_name === "shuffle-subflow") {
if (action.status === "SUCCESS") {
nodecolor = green
branchcolor = green
}
if (workflow.validation.subflow_apps !== undefined && workflow.validation.subflow_apps !== null && workflow.validation.subflow_apps.length > 0) {
nodecolor = red
branchcolor = red
for (var subflowkey in workflow.validation.subflow_apps) {
const subflowApp = workflow.validation.subflow_apps[subflowkey]
founderror += "- " + subflowApp.error+"\n"
if (subflowApp.error === action.id) {
appgroup.push(subflowApp)
}
}
}
}
if (!showMiddle && relevantactions.length > 2 && index > 0 && index === relevantactions.length - 2) {
if (founderror.length > 0) {
middleError += founderror+"\n"
middleBranchColor = branchcolor
}
if (index === relevantactions.length-2 && relevantactions.length > 2) {
const selectedIcon = middleError.length > 0 ?
<Tooltip title={
<Typography variant="body1" style={{margin: 5, whiteSpace: "pre-line", }}>
{middleError}
</Typography>
}>
<IconButton style={{width: 30, height: 30, backgroundColor: "rgba(255,255,255,0.0)", borderRadius: 30, marginTop: 2, }}>
<ErrorOutlineIcon style={{color: "red", }} />
</IconButton>
</Tooltip>
: null
return selectedIcon
} else {
return null
}
}
// Returns for anything non-middle
if (relevantactions.length > 2 && index >= 1 && index < relevantactions.length - 2) {
if (founderror.length > 0) {
middleError += founderror+"\n"
}
return null
}
if (skipped && !lastitem) {
nodecolor = grey
branchcolor = grey
}
if (previousTools) {
previousTools = false
} else {
branchcolor = nodecolor
}
if (action.trigger_type === "WEBHOOK") {
nodecolor = green
branchcolor = green
}
var flex = index !== 0 && index !== relevantactions.length - 1 ? 1 : 3
if (nodecolor === green) {
branchcolor = green
} else if (nodecolor === yellow) {
branchcolor = yellow
} else if (nodecolor === red) {
branchcolor = red
}
if (index === 0) {
startBranchColor = branchcolor
} else if (index !== 0 && index !== relevantactions.length - 1) {
// FIXME: This doesn't work yet
middleBranchColor = branchcolor
}
if (lastitem) {
if (middleError.length === 0) {
branchcolor = startBranchColor
} else {
//branchcolor = middleBranchColor
}
if (founderror === "") {
nodecolor = green
}
}
// FIXME: This could mean the workflow hasn't ran yet
if (workflow.validation.valid === false && (workflow.validation.errors === undefined || workflow.validation.errors === null || workflow.validation.errors.length == 0) && (workflow.validation.subflow_apps === undefined || workflow.validation.subflow_apps === null || workflow.validation.subflow_apps.length == 0)) {
nodecolor = grey
branchcolor = grey
}
const branchTooltip = branchcolor === yellow ? "Check nodes for errors" : ""
const appname = action.app_name.replaceAll('_', ' ').slice(0, 16)
const chipBackground = nodecolor === green ? "rgba(2,203,112, 0.2)" : nodecolor === grey ? "#494949": "rgba(245,52,52,0.8)"
const chipColor = nodecolor === green ? "#02cb70" : nodecolor === grey ? "#CDCDCD" : "white"
//const ballcolor = lastitem ? nodecolor : branchcolor
const ballcolor = branchcolor
const ballsize = 8
const topMargin = 20
const chipStyle = {
height: 40,
minWidth: 125,
maxWidth: 125,
borderRadius: 50,
color: chipColor,
backgroundColor: chipBackground,
//border: `2px solid ${chipBackground}`,
//backgroundColor: "rgba(0,0,0,0.0)",
}
if (image === "") {
console.log("MISSING IMAGE: ", appname, image, action)
}
if (decidedColor === grey && nodecolor === green) {
setDecidedColor(red)
}
if (decidedColor !== red && nodecolor === red) {
setDecidedColor(red)
}
return (
<div style={{display: "flex", flex: flex, justifyContent: "right", }}>
{lastitem ?
<Tooltip title={branchTooltip}>
<div style={{display: "flex", width: "100%", position: "relative",}}>
<div style={{marginLeft: 0, marginRight: 0, marginTop: topMargin, height: 3, width: "100%", backgroundColor: branchcolor, }} />
<div style={{position: "absolute", right: -3, top: topMargin-2.5, backgroundColor: ballcolor, width: ballsize, height: ballsize, borderRadius: 10, }}/>
</div>
</Tooltip>
: null}
{appgroup.length > 0 ?
<AvatarGroup max={4} style={{height: itemHeight+8, }}>
{appgroup.map((subflowApp, subflowIndex) => {
var appimage = ""
if (apps !== undefined && apps !== null && apps.length > 0) {
for (var key in apps) {
const app = apps[key]
if (app.name === subflowApp.app_name) {
appimage = apps[key].large_image
break
}
}
}
return (
<Tooltip title={`App: ${subflowApp.app_name} - subflow app`}>
<Avatar
style={{
border: `4px solid ${nodecolor}`,
}}
src={appimage}
/>
</Tooltip>
)
})}
</AvatarGroup>
:
<Tooltip title={
<Typography variant="body1" style={{margin: 5, color: "white", }}>
{founderror.length > 0 ? founderror : `App: ${appname} - Action: ${action.label}`}
</Typography>
} placement="top">
<Chip label={`${appname}`} style={chipStyle} icon={
<Avatar
variant="round"
sx={{ backgroundColor: nodecolor, width: itemHeight, height: itemHeight, borderRadius: 50, border: `1px solid ${nodecolor}`,}}
>
{image !== "" ?
<img
src={image}
style={{
width: itemHeight,
height: itemHeight,
}}
/>
: null}
</Avatar>
} />
</Tooltip>
}
{lastitem ? null :
<Tooltip title={branchTooltip}>
<div style={{display: "flex", width: "100%", position: "relative",}}>
<div style={{position: "absolute", left: -3, top: topMargin-2.5, backgroundColor: ballcolor, color: ballcolor, width: ballsize, height: ballsize, borderRadius: 10, }}/>
<div style={{marginLeft: 0, marginRight: 0, marginTop: 20, height: 3, width: "100%", backgroundColor: branchcolor, }} />
</div>
</Tooltip>
}
</div>
)
})}
</div>
</div>
)
}
export default WorkflowValidationTimeline
+37
View File
@@ -0,0 +1,37 @@
import { createContext, useState, useEffect } from 'react';
export const Context = createContext();
export const AppContext =(props) => {
// Left side bar global states
const [searchBarModalOpen, setSearchBarModalOpen] = useState(false);
const [leftSideBarOpenByClick, setLeftSideBarOpenByClick] = useState(false);
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
//Calculate window width
useEffect(() => {
const handleResize = () => {
setWindowWidth(window?.innerWidth);
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return (
<Context.Provider value={{
searchBarModalOpen,
setSearchBarModalOpen,
leftSideBarOpenByClick,
setLeftSideBarOpenByClick,
windowWidth
}}>
{props.children}
</Context.Provider>
)
}
+94 -16
View File
@@ -13,6 +13,30 @@ const data = [
return elementname
},
"text-valign": "center",
"text-margin-x": function(element) {
// Attempt at bottom-positioning
// Required text-valign: bottom
// FIXME: Disabled for now.
return "15px"
const name = element.data("label")
console.log("Name: ", name)
if (name === null || name === undefined || name == "" || document=== undefined || document === null) {
return "0px"
}
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d')
context.font = '18px Segoe UI, Tahoma, Geneva, Verdana, sans-serif, sans-serif'
const textWidth = context.measureText(name).width
return textWidth + "px"
//return -1*(textWidth) + "px"
},
"font-family": "Segoe UI, Tahoma, Geneva, Verdana, sans-serif, sans-serif",
"font-weight": "lighter",
"font-size": "18px",
@@ -23,7 +47,6 @@ const data = [
padding: "10px",
margin: "5px",
"border-width": "1px",
"text-margin-x": "10px",
"z-index": 5001,
},
},
@@ -35,7 +58,7 @@ const data = [
"curve-style": "unbundled-bezier",
label: "data(label)",
"text-margin-y": "-15px",
width: "5px",
width: "3px",
color: "white",
"line-fill": "linear-gradient",
"line-gradient-stop-positions": ["0.0", "100"],
@@ -119,10 +142,10 @@ const data = [
},
},
{
selector: `node[app_name="Shuffle Tools"]`,
selector: `node[app_name="Shuffle Tools"], node[app_name="email"], node[app_name="http"]`,
css: {
width: "30px",
height: "30px",
width: "35px",
height: "35px",
"z-index": 5000,
"font-size": "0px",
"background-width": "75%",
@@ -258,7 +281,9 @@ const data = [
{
selector: "node[?isStartNode]",
css: {
shape: "ellipse",
shape: function(element) {
return "ellipse"
},
"border-color": "#80deea",
width: "80px",
height: "80px",
@@ -270,8 +295,8 @@ const data = [
{
selector: "node[!is_valid]",
css: {
"border-color": "red",
"border-width": "10px",
"border-color": "#f53434",
"border-width": "5px",
},
},
{
@@ -357,7 +382,7 @@ const data = [
css: {
"background-color": "#f85a3e",
"border-color": "#f85a3e",
"border-width": "12px",
"border-width": "7px",
"transition-property": "border-width",
"transition-duration": "0.25s",
label: "data(label)",
@@ -462,13 +487,66 @@ const data = [
"font-size": "0px",
},
},
{
selector: "node:selected",
css: {
"border-color": "#f86a3e",
"border-width": "7px",
},
},
{
selector: "node:selected",
css: {
"border-color": "#f86a3e",
"border-width": "7px",
},
},
{
selector: `node[buttonType="condition-drag"]`,
css: {
"width": "5px",
"height": "5px",
"background-color": "#f85a3e",
},
},
{
selector: `node[name="switch"]`,
css: {
label: function(element) {
// Load from the actual element
var nodeheight = 400
var conditions = [{
"name": "Condition 1",
"check": "X equals Y",
},
{
"name": "Condition 2",
"check": "X2 equals Y2",
},
{
"name": "Condition 3",
"check": "X3 equals Y3",
}]
conditions.push({
"name": "Else",
"check": "If all else fails",
})
const newlines = nodeheight / conditions.length
console.log("Newlines: ", newlines)
const label = conditions.map((condition) => {
return `${condition.name}\n\n\n`
}).join("\n")
return label
},
color: "white",
"border-color": "#f85a3e",
"background-color": "#1f1f1f",
"font-size": "19px",
"text-margin-x": "-110px",
"text-wrap": "wrap",
shape: "roundrectangle",
width: "100",
height: "300",
},
},
];
//{
+2 -4
View File
@@ -18,11 +18,9 @@ code {
margin-right: -13px;
}
.toc:hover {
background-color: gray;
color: white;
table th, table td {
border: 1px solid;
}
/* .cm-string{
z-index: -1;
}
+19 -7
View File
@@ -1,28 +1,30 @@
import React from "react";
import { createTheme, adaptV4Theme } from "@mui/material/styles";
//const theme = createTheme({
const theme = createTheme(adaptV4Theme({
palette: {
theme: "dark",
main: "#F86743",
main: "#FF8544",
primary: {
main: "#F86743",
main: "#FF8544",
contrastText: "#ffffff",
},
secondary: {
main: "#e8eaf6",
main: "rgba(255,255,255,0.7)",
contrastText: "#000000",
},
text: {
secondary: "rgba(255,255,255,0.7)",
},
type: "dark",
inputColor: "rgba(39,41,45,1)",
//inputColor: "#383B40",
inputColor: "rgba(39,41,45,1)",
surfaceColor: "#27292d",
platformColor: "#1c1c1d",
//platformColor: "#1c1c1d",
platformColor: "#212121",
backgroundColor: "#1a1a1a",
green: "#5cc879",
borderRadius: 10,
defaultBorder: "1px solid rgba(255,255,255,0.3)",
@@ -44,10 +46,20 @@ const theme = createTheme(adaptV4Theme({
overflowX: "auto",
},
textFieldStyle: {
backgroundColor: "#383B40",
backgroundColor: "#212121",
borderRadius: 5,
height: 40,
},
DialogStyle: {
backgroundColor: "#212121",
borderRadius: 2,
boxShadow: "0px 0px 10px 0px rgba(0,0,0,0.75)",
border: "1px solid #494949",
},
innerTextfieldStyle: {
height: 40,
fontSize: 16,
backgroundColor: "#212121",
// Removed since upgrading to mui 18
//color: "white",
//minHeight: 50,
+138
View File
@@ -0,0 +1,138 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { Button, Typography } from '@mui/material';
import theme from "../theme.jsx";
const NotFound = () => {
const navigate = useNavigate();
const buttonStyle = {
borderRadius: 25,
height: 50,
fontSize: 18,
width: "100%",
marginBottom: "10px",
};
const primaryButtonStyle = {
...buttonStyle,
background: "linear-gradient(89.83deg, #FF8444 0.13%, #F2643B 99.84%)",
color: "white",
'&:hover': {
background: "linear-gradient(89.83deg, #FF8444 0.13%, #F2643B 99.84%)",
opacity: 0.9,
}
};
const secondaryButtonStyle = {
...buttonStyle,
background: "#383B40",
border: "1px solid #494949",
color: "white",
'&:hover': {
background: "#434649",
}
};
return (
<div style={{paddingTop: 100, }} className="min-h-screen bg-[#1A1A1A] flex items-center justify-center">
<div
style={{
width: "100%",
maxWidth: "532px",
background: "#212121",
borderRadius: "8px",
padding: "40px",
display: "flex",
flexDirection: "column",
alignItems: "center",
textAlign: "center",
justifySelf: "center",
}}
>
<img
src="/images/logos/orange_logo.svg"
alt="Shuffle Logo"
style={{
height: 44,
width: 44,
marginBottom: 10,
}}
/>
<Typography
variant="h1"
style={{
fontSize: "72px",
fontWeight: 900,
background: "linear-gradient(89.83deg, #FF8444 0.13%, #F2643B 99.84%)",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
marginBottom: "10px",
textAlign: "center",
}}
>
404
</Typography>
<Typography
variant="h4"
style={{
color: "white",
marginBottom: "8px",
fontWeight: 600,
textAlign: "center",
}}
>
Page Not Found
</Typography>
<Typography
variant="body1"
style={{
color: "#9E9E9E",
marginBottom: "32px",
fontSize: "16px",
textAlign: "center",
maxWidth: "400px",
}}
>
Our code doggo couldn't find the page you were looking for.
</Typography>
<iframe
src="https://giphy.com/embed/FY8c5SKwiNf1EtZKGs"
width="125"
height="165"
frameBorder="0"
className="giphy-embed"
allowFullScreen
style={{
marginBottom: 32,
borderRadius: theme.palette.borderRadius,
}}
/>
<div style={{ width: "100%", maxWidth: "400px" }}>
<Button
variant="contained"
onClick={() => navigate('/docs')}
sx={primaryButtonStyle}
>
Documentation
</Button>
<Button
variant="contained"
onClick={() => navigate('/')}
sx={secondaryButtonStyle}
>
Return Home
</Button>
</div>
</div>
</div>
);
};
export default NotFound;
+1436 -865
View File
File diff suppressed because it is too large Load Diff

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