Merge branch '2.0.0'
@@ -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=
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
@@ -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'
|
||||
@@ -4,15 +4,22 @@
|
||||
|
||||
Shuffle Automation
|
||||
|
||||
[](https://github.com/Shuffle/Shuffle/actions/workflows/codeql-analysis.yml)
|
||||
[](https://github.com/Shuffle/Shuffle/actions/workflows/dockerbuild.yaml)
|
||||
[](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).
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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"
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -6,3 +6,4 @@ flask[async]==2.0.2
|
||||
waitress==2.1.0
|
||||
#flask==1.1.2
|
||||
python-dateutil==2.8.1
|
||||
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;"]
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
## Lalits frontend magic
|
||||
|
||||
## Localhost Certificate info:
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 "$@"
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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", }}>
|
||||
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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 />
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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;
|
||||
@@ -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)">
|
||||
|
||||
@@ -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
|
||||
@@ -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 })
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 can’t 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;
|
||||
@@ -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);
|
||||
|
||||
@@ -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> - </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> - </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"
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
[],
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
<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.
|
||||
<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>
|
||||
// )
|
||||
// })
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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  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
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ?
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
//{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||