Merge pull request #996 from Shuffle/launch
v1.1.0 - Creators & Workflow Templates
This commit is contained in:
@@ -32,6 +32,7 @@ SHUFFLE_ENCRYPTION_MODIFIER=
|
||||
|
||||
# Other configs
|
||||
BASE_URL=http://shuffle-backend:5001
|
||||
SSO_REDIRECT_URL=http://localhost:3001
|
||||
BACKEND_HOSTNAME=shuffle-backend
|
||||
BACKEND_PORT=5001
|
||||
FRONTEND_PORT=3001
|
||||
@@ -50,16 +51,17 @@ SHUFFLE_PASS_WORKER_PROXY=TRUE
|
||||
SHUFFLE_PASS_APP_PROXY=FALSE
|
||||
TZ=Europe/Amsterdam # Timezone-handler in Orborus, Worker and Apps
|
||||
ORBORUS_CONTAINER_NAME= # Used to FIND the containername. cgroup v2: issue 501
|
||||
SHUFFLE_ORBORUS_STARTUP_DELAY= # Used for setting up a startup delay for Orborus
|
||||
|
||||
SHUFFLE_BASE_IMAGE_NAME=frikky
|
||||
SHUFFLE_BASE_IMAGE_NAME=shuffle
|
||||
SHUFFLE_BASE_IMAGE_REGISTRY=ghcr.io
|
||||
SHUFFLE_BASE_IMAGE_TAG_SUFFIX="-0.8.80"
|
||||
SHUFFLE_BASE_IMAGE_TAG_SUFFIX="-1.0.0"
|
||||
|
||||
# Used for auto-cleanup of containers. REALLY important at scale.
|
||||
SHUFFLE_CONTAINER_AUTO_CLEANUP=false
|
||||
SHUFFLE_ELASTIC=true
|
||||
SHUFFLE_LOGS_DISABLED=false
|
||||
SHUFFLE_CHAT_DISABLED=false
|
||||
SHUFFLE_CHAT_DISABLED=false # Controls support chat
|
||||
SHUFFLE_RERUN_SCHEDULE=300
|
||||
|
||||
# DATABASE CONFIGURATIONS
|
||||
|
||||
@@ -87,9 +87,13 @@ http://localhost:5001 - REST API - requires [>=go1.13](https://golang.org/dl/)
|
||||
```bash
|
||||
export SHUFFLE_OPENSEARCH_URL="http://localhost:9200"
|
||||
export SHUFFLE_ELASTIC=true
|
||||
export SHUFFLE_OPENSEARCH_USERNAME=admin
|
||||
export SHUFFLE_OPENSEARCH_PASSWORD=admin
|
||||
export SHUFFLE_OPENSEARCH_SKIPSSL_VERIFY=true
|
||||
cd backend/go-app
|
||||
go run *.go
|
||||
go run main.go walkoff.go docker.go
|
||||
```
|
||||
**WINDOWS USERS:** Follow [this guide](https://www.wikihow.com/Create-an-Environment-Variable-in-Windows-10) to add environment variables in your machine.
|
||||
|
||||
Large portions of the backend is written in another repository - [shuffle-shared](https://github.com/frikky/shuffle-shared). If you want to update any of this code and test in realtime, we recommend following these steps:
|
||||
1. Clone shuffle-shared to a local repository
|
||||
@@ -101,8 +105,6 @@ Large portions of the backend is written in another repository - [shuffle-shared
|
||||
4. Make the changes you want, then restart the backend server!
|
||||
5. With your changes made, make a pull request :fire:
|
||||
|
||||
**WINDOWS USERS:** You'll have to to add the "export" part as an environment variable.
|
||||
|
||||
## Database - Opensearch
|
||||
Make sure this is running through the docker-compose, and that the backend points to it with SHUFFLE_OPENSEARCH_URL defined
|
||||
|
||||
@@ -122,6 +124,4 @@ export BASE_URL=http://YOUR-IP:5001
|
||||
export DOCKER_API_VERSION=1.40
|
||||
```
|
||||
|
||||
**WINDOWS USERS:** You'll have to to add the "export" part as an environment variable.
|
||||
|
||||
AND THAT's it - hopefully it worked. If it didn't please email [frikky@shuffler.io](mailto:frikky@shuffler.io)
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
# This can be done in the dockerpush workflow itself
|
||||
# Done manually for now since GHCR isn't being pushed to easily with the current Github action CI. Nightly = Latest IF we run hotfixes on latest
|
||||
|
||||
### Pull latest from ghcr CI/CD
|
||||
#docker pull ghcr.io/shuffle/shuffle-app_sdk:nightly
|
||||
#docker pull ghcr.io/shuffle/shuffle-worker:nightly
|
||||
#docker pull ghcr.io/shuffle/shuffle-orborus:nightly
|
||||
#docker pull ghcr.io/shuffle/shuffle-frontend:nightly
|
||||
#docker pull ghcr.io/shuffle/shuffle-backend:nightly
|
||||
|
||||
### LATEST releases:
|
||||
#docker tag ghcr.io/shuffle/shuffle-app_sdk:nightly ghcr.io/shuffle/shuffle-app_sdk:latest
|
||||
#docker tag ghcr.io/shuffle/shuffle-worker:nightly ghcr.io/shuffle/shuffle-worker:latest
|
||||
#docker tag ghcr.io/shuffle/shuffle-orborus:nightly ghcr.io/shuffle/shuffle-orborus:latest
|
||||
#docker tag ghcr.io/shuffle/shuffle-frontend:nightly ghcr.io/shuffle/shuffle-frontend:latest
|
||||
#docker tag ghcr.io/shuffle/shuffle-backend:nightly ghcr.io/shuffle/shuffle-backend:latest
|
||||
#
|
||||
#docker push ghcr.io/shuffle/shuffle-app_sdk:latest
|
||||
#docker push ghcr.io/shuffle/shuffle-worker:latest
|
||||
#docker push ghcr.io/shuffle/shuffle-orborus:latest
|
||||
#docker push ghcr.io/shuffle/shuffle-frontend:latest
|
||||
#docker push ghcr.io/shuffle/shuffle-backend:latest
|
||||
|
||||
|
||||
### 1.1.0 releases:
|
||||
#docker tag ghcr.io/shuffle/shuffle-app_sdk:nightly ghcr.io/shuffle/shuffle-app_sdk:1.1.0
|
||||
#docker tag ghcr.io/shuffle/shuffle-worker:nightly ghcr.io/shuffle/shuffle-worker:1.1.0
|
||||
#docker tag ghcr.io/shuffle/shuffle-orborus:nightly ghcr.io/shuffle/shuffle-orborus:1.1.0
|
||||
#docker tag ghcr.io/shuffle/shuffle-frontend:nightly ghcr.io/shuffle/shuffle-frontend:1.1.0
|
||||
#docker tag ghcr.io/shuffle/shuffle-backend:nightly ghcr.io/shuffle/shuffle-backend:1.1.0
|
||||
#
|
||||
#docker push ghcr.io/shuffle/shuffle-app_sdk:1.1.0
|
||||
#docker push ghcr.io/shuffle/shuffle-worker:1.1.0
|
||||
#docker push ghcr.io/shuffle/shuffle-orborus:1.1.0
|
||||
#docker push ghcr.io/shuffle/shuffle-frontend:1.1.0
|
||||
#docker push ghcr.io/shuffle/shuffle-backend:1.1.0
|
||||
|
||||
|
||||
|
||||
### Manage worker-scale upload (Requires auth)
|
||||
# This is supposed to be unavailable, and only be downloadable by customers
|
||||
docker pull ghcr.io/shuffle/shuffle-worker-scale:latest
|
||||
docker save ghcr.io/shuffle/shuffle-worker-scale:latest -o shuffle-worker.zip
|
||||
echo "1. Upload shuffle-worker.zip to the shuffler.io public repo. If in Github Dev env, download the file, and upload manually."
|
||||
echo "2. Have customers download it with: $ wget URL"
|
||||
echo "3. Have customers use with with: docker load shuffle-worker.zip"
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
name: dockerbuild
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: launch
|
||||
jobs:
|
||||
main:
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: ${{ matrix.experimental }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- app: frontend
|
||||
path: frontend
|
||||
version: nightly
|
||||
experimental: true
|
||||
- app: backend
|
||||
path: backend
|
||||
version: nightly
|
||||
experimental: true
|
||||
- app: app_sdk
|
||||
path: backend/app_sdk
|
||||
version: nightly
|
||||
experimental: true
|
||||
- app: orborus
|
||||
path: functions/onprem/orborus
|
||||
version: nightly
|
||||
experimental: true
|
||||
- app: worker
|
||||
path: functions/onprem/worker
|
||||
version: nightly
|
||||
experimental: true
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to Ghcr
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Ghcr Build and push
|
||||
id: docker_build
|
||||
uses: docker/build-push-action@v3
|
||||
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 }}:nightly
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/shuffle-${{ matrix.app }}:nightly
|
||||
|
||||
- name: Image digest
|
||||
run: echo ${{ steps.docker_build.outputs.digest }}
|
||||
@@ -11,7 +11,6 @@ name: Snyk Container
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- launch
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
@@ -25,9 +24,12 @@ jobs:
|
||||
snyk:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Build a Docker image
|
||||
run: docker build -t your/image-to-test .
|
||||
run: docker build -t frontend .
|
||||
|
||||
- name: Run Snyk to check Docker image for vulnerabilities
|
||||
# Snyk can be used to break the build when it detects vulnerabilities.
|
||||
# In this case we want to upload the issues to GitHub Code Scanning
|
||||
@@ -41,6 +43,7 @@ jobs:
|
||||
with:
|
||||
image: your/image-to-test
|
||||
args: --file=Dockerfile
|
||||
|
||||
- name: Upload result to GitHub Code Scanning
|
||||
uses: github/codeql-action/upload-sarif@v1
|
||||
with:
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
|
||||
Shuffle Automation
|
||||
|
||||
[](https://github.com/Shuffle/Shuffle/actions/workflows/codeql-analysis.yml)
|
||||
[](https://github.com/Shuffle/Shuffle/actions/workflows/dockerbuild.yaml)
|
||||
|
||||
</h1><h4 align="center">
|
||||
|
||||
[Shuffle](https://shuffler.io) is an automation platform for and by the community, focusing on accessibility for anyone to automate. Security operations is complex, but it doesn't have to be.
|
||||
|
||||
+13
-3
@@ -1,4 +1,4 @@
|
||||
FROM golang:1.17.2-buster as builder
|
||||
FROM golang:1.19.3-buster as builder
|
||||
|
||||
# Add files
|
||||
RUN mkdir /app
|
||||
@@ -15,14 +15,24 @@ ADD ./app_sdk/app_base.py /app_sdk
|
||||
ADD ./app_gen /app_gen
|
||||
|
||||
RUN go get -v
|
||||
RUN go mod tidy
|
||||
RUN go clean -modcache
|
||||
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o webapp .
|
||||
# From November 2022, CGO is enabled due to packages
|
||||
# that we use requiring it. This is a temporary fix
|
||||
# and makes us HAVE to install libc compatibility packages farther down.
|
||||
RUN CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -o webapp .
|
||||
|
||||
# Certificate build - gets required certs
|
||||
FROM alpine:latest as certs
|
||||
RUN apk --update add ca-certificates
|
||||
|
||||
FROM alpine:3.14.2
|
||||
# Sets up the final image
|
||||
FROM alpine:3.17.0
|
||||
|
||||
# FIXME: Install cgo because CGO_ENABLED=1 during build
|
||||
RUN apk add --no-cache libc6-compat
|
||||
RUN apk add --no-cache libstdc++
|
||||
|
||||
COPY --from=builder /app/ /app
|
||||
COPY --from=builder /app_sdk/ /app_sdk
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM peterclemenko/blackarch as base
|
||||
FROM blackarchlinux/blackarch as base
|
||||
|
||||
FROM base as builder
|
||||
|
||||
|
||||
+341
-40
@@ -17,6 +17,7 @@ import http.client
|
||||
import urllib.parse
|
||||
import jinja2
|
||||
import datetime
|
||||
import dateutil
|
||||
from io import StringIO as StringBuffer
|
||||
from io import BytesIO
|
||||
from liquid import Liquid, defaults
|
||||
@@ -104,6 +105,89 @@ def base64_decode(a):
|
||||
except:
|
||||
return base64.b64decode(a)
|
||||
|
||||
@shuffle_filters.register
|
||||
def json_parse(a):
|
||||
return json.loads(str(a))
|
||||
|
||||
@shuffle_filters.register
|
||||
def as_object(a):
|
||||
return json.loads(str(a))
|
||||
|
||||
@shuffle_filters.register
|
||||
def ast(a):
|
||||
return ast.literal_eval(str(a))
|
||||
|
||||
@shuffle_filters.register
|
||||
def escape_string(a):
|
||||
a = str(a)
|
||||
return a.replace("\\\'", "\'", -1).replace("\\\"", "\"", -1).replace("'", "\\\'", -1).replace("\"", "\\\"", -1)
|
||||
|
||||
@shuffle_filters.register
|
||||
def json_escape(a):
|
||||
a = str(a)
|
||||
return a.replace("\\\'", "\'", -1).replace("\\\"", "\"", -1).replace("'", "\\\\\'", -1).replace("\"", "\\\\\"", -1)
|
||||
|
||||
@shuffle_filters.register
|
||||
def escape_json(a):
|
||||
a = str(a)
|
||||
return a.replace("\\\'", "\'", -1).replace("\\\"", "\"", -1).replace("'", "\\\\\'", -1).replace("\"", "\\\\\"", -1)
|
||||
|
||||
# By default using json escape to add all backslashes
|
||||
@shuffle_filters.register
|
||||
def escape(a):
|
||||
a = str(a)
|
||||
return json_escape(a)
|
||||
|
||||
@shuffle_filters.register
|
||||
def flatten(a):
|
||||
a = list(a)
|
||||
|
||||
flat_list = [a for xs in xss for a in xs]
|
||||
return flat_list
|
||||
|
||||
@shuffle_filters.register
|
||||
def csv_parse(a):
|
||||
a = str(a)
|
||||
splitdata = a.split("\n")
|
||||
columns = []
|
||||
if len(splitdata) > 1:
|
||||
columns = splitdata[0].split(",")
|
||||
else:
|
||||
return a.split("\n")
|
||||
|
||||
allitems = []
|
||||
cnt = -1
|
||||
for item in splitdata[1:]:
|
||||
cnt += 1
|
||||
commasplit = item.split(",")
|
||||
|
||||
fullitem = {}
|
||||
fullitem["unparsed"] = item
|
||||
fullitem["index"] = cnt
|
||||
fullitem["parsed"] = {}
|
||||
if len(columns) != len(commasplit):
|
||||
|
||||
if len(commasplit) > len(columns):
|
||||
diff = len(commasplit)-len(columns)
|
||||
|
||||
try:
|
||||
commasplit = commasplit[0:len(commasplit)-diff]
|
||||
except:
|
||||
pass
|
||||
else:
|
||||
for item in range(0, len(columns)-len(commasplit)):
|
||||
commasplit.append("")
|
||||
|
||||
for key in range(len(columns)):
|
||||
try:
|
||||
fullitem["parsed"][columns[key]] = commasplit[key]
|
||||
except:
|
||||
continue
|
||||
|
||||
allitems.append(fullitem)
|
||||
|
||||
return allitems
|
||||
|
||||
#print(standard_filter_manager.filters)
|
||||
#print(shuffle_filters.filters)
|
||||
#print(Liquid("{{ '10' | plus: 1}}", filters=shuffle_filters.filters).render())
|
||||
@@ -261,6 +345,36 @@ class AppBase:
|
||||
|
||||
return new_input
|
||||
|
||||
def prepare_response(self, request):
|
||||
try:
|
||||
parsedheaders = {}
|
||||
for key, value in request.headers.items():
|
||||
parsedheaders[key] = value
|
||||
|
||||
cookies = {}
|
||||
if request.cookies:
|
||||
for key, value in request.cookies.items():
|
||||
cookies[key] = value
|
||||
|
||||
|
||||
jsondata = request.text
|
||||
try:
|
||||
jsondata = json.loads(jsondata)
|
||||
except:
|
||||
pass
|
||||
|
||||
return json.dumps({
|
||||
"success": True,
|
||||
"status": request.status_code,
|
||||
"url": request.url,
|
||||
"headers": parsedheaders,
|
||||
"body": jsondata,
|
||||
"cookies":cookies,
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"[WARNING] Failed in request: {e}")
|
||||
return request.text
|
||||
|
||||
# FIXME: Add more info like logs in here.
|
||||
# Docker logs: https://forums.docker.com/t/docker-logs-inside-the-docker-container/68190/2
|
||||
def send_result(self, action_result, headers, stream_path):
|
||||
@@ -330,7 +444,7 @@ class AppBase:
|
||||
# FIXME: Adding retries here.
|
||||
try:
|
||||
finished = False
|
||||
for i in range (0, 5):
|
||||
for i in range (0, 10):
|
||||
try:
|
||||
ret = requests.post(url, headers=headers, json=action_result, timeout=10)
|
||||
|
||||
@@ -339,26 +453,36 @@ class AppBase:
|
||||
finished = True
|
||||
break
|
||||
else:
|
||||
self.logger.info(f"[DEBUG] RESP: {ret.text}")
|
||||
self.logger.info(f"[ERROR] RESP: {ret.text}")
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
self.logger.info(f"[DEBUG] Request problem: {e}")
|
||||
time.sleep(0.1)
|
||||
|
||||
#time.sleep(5)
|
||||
continue
|
||||
except TimeoutError as e:
|
||||
self.logger.info(f"[DEBUG] Timeout or request: {e}")
|
||||
time.sleep(0.1)
|
||||
|
||||
#time.sleep(5)
|
||||
continue
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
self.logger.info(f"[DEBUG] Connectionerror: {e}")
|
||||
time.sleep(0.1)
|
||||
|
||||
#time.sleep(5)
|
||||
continue
|
||||
except http.client.RemoteDisconnected as e:
|
||||
self.logger.info(f"[DEBUG] Remote: {e}")
|
||||
time.sleep(0.1)
|
||||
|
||||
#time.sleep(5)
|
||||
continue
|
||||
except urllib3.exceptions.ProtocolError as e:
|
||||
self.logger.info(f"[DEBUG] Protocol err: {e}")
|
||||
time.sleep(0.1)
|
||||
|
||||
#time.sleep(5)
|
||||
continue
|
||||
|
||||
@@ -367,17 +491,17 @@ class AppBase:
|
||||
if not finished:
|
||||
# Not sure why this would work tho :)
|
||||
action_result["status"] = "FAILURE"
|
||||
action_result["result"] = f"POST failed to get info!"
|
||||
self.logger.info(f"[DEBUG] Before typeerror stream result - NOT finished")
|
||||
action_result["result"] = json.dumps({"success": False, "reason": "POST error: Failed connecting to %s over 10 retries to the backend" % url})
|
||||
self.logger.info(f"[DEBUG] Before typeerror stream result - NOT finished after 10 requests")
|
||||
ret = requests.post("%s%s" % (self.base_url, stream_path), headers=headers, json=action_result)
|
||||
|
||||
self.logger.info(f"""[DEBUG] Successful request result request: Status= {ret.status_code} & Response= {ret.text}. Action status: {action_result["status"]}""")
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
self.logger.info(f"[DEBUG] Unexpected ConnectionError happened: {e}")
|
||||
except TypeError as e:
|
||||
#self.logger.exception(e)
|
||||
action_result["status"] = "FAILURE"
|
||||
action_result["result"] = f"POST error: {e}"
|
||||
action_result["result"] = json.dumps({"success": False, "reason": "Typeerror when sending to backend URL %s" % url})
|
||||
|
||||
self.logger.info(f"[DEBUG] Before typeerror stream result: {e}")
|
||||
ret = requests.post("%s%s" % (self.base_url, stream_path), headers=headers, json=action_result)
|
||||
#self.logger.info(f"[DEBUG] Result: {ret.status_code}")
|
||||
@@ -971,6 +1095,31 @@ class AppBase:
|
||||
self.logger.info("\nLOOP: %s\nRESULTS: %s" % (loop_wrapper, results))
|
||||
return results
|
||||
|
||||
# Downloads all files from a namespace
|
||||
# Currently only working on local version of Shuffle
|
||||
def get_file_category_ids(self, category):
|
||||
org_id = self.full_execution["workflow"]["execution_org"]["id"]
|
||||
|
||||
get_path = "/api/v1/files/namespaces/%s?execution_id=%s&ids=true" % (category, self.full_execution["execution_id"])
|
||||
headers = {
|
||||
"Authorization": "Bearer %s" % self.authorization
|
||||
}
|
||||
|
||||
ret = requests.get("%s%s" % (self.url, get_path), headers=headers)
|
||||
return ret.json()
|
||||
#if ret1.status_code != 200:
|
||||
# return {
|
||||
# "success": False,
|
||||
# "reason": "Status code is %d from backend for category %s" % category,
|
||||
# "list": [],
|
||||
# }
|
||||
|
||||
#return {
|
||||
# "success": True,
|
||||
# "ids": ret1.json(),
|
||||
#}
|
||||
|
||||
|
||||
# Downloads all files from a namespace
|
||||
# Currently only working on local version of Shuffle
|
||||
def get_file_namespace(self, namespace):
|
||||
@@ -1003,6 +1152,12 @@ class AppBase:
|
||||
|
||||
return myzipfile
|
||||
|
||||
def get_file_namespace_ids(self, namespace):
|
||||
return self.get_file_category_ids(self, namespace)
|
||||
|
||||
def get_file_category(self, category):
|
||||
return self.get_file_namespace(self, category)
|
||||
|
||||
# Things to consider for files:
|
||||
# - How can you download / stream a file?
|
||||
# - Can you decide if you want a stream or the files directly?
|
||||
@@ -1273,36 +1428,53 @@ class AppBase:
|
||||
if isinstance(self.full_execution, str) and len(self.full_execution) == 0:
|
||||
self.logger.info("[DEBUG] NO EXECUTION - LOADING!")
|
||||
try:
|
||||
tmpdata = {
|
||||
"authorization": self.authorization,
|
||||
"execution_id": self.current_execution_id
|
||||
}
|
||||
failed = False
|
||||
rettext = ""
|
||||
for i in range(0, 5):
|
||||
tmpdata = {
|
||||
"authorization": self.authorization,
|
||||
"execution_id": self.current_execution_id
|
||||
}
|
||||
|
||||
self.logger.info("[DEBUG] Before FULLEXEC stream result")
|
||||
ret = requests.post(
|
||||
"%s/api/v1/streams/results" % (self.base_url),
|
||||
headers=headers,
|
||||
json=tmpdata
|
||||
)
|
||||
self.logger.info("[ERROR] Before FULLEXEC stream result")
|
||||
ret = requests.post(
|
||||
"%s/api/v1/streams/results" % (self.base_url),
|
||||
headers=headers,
|
||||
json=tmpdata
|
||||
)
|
||||
|
||||
if ret.status_code == 200:
|
||||
fullexecution = ret.json()
|
||||
else:
|
||||
try:
|
||||
self.logger.info("[DEBUG] Error: Data: ", ret.json())
|
||||
self.logger.info("[DEBUG] Error with status code for results. Crashing because ACTION_RESULTS or WORKFLOW_VARIABLE can't be handled. Status: %d" % ret.status_code)
|
||||
except json.decoder.JSONDecodeError:
|
||||
pass
|
||||
if ret.status_code == 200:
|
||||
fullexecution = ret.json()
|
||||
failed = False
|
||||
break
|
||||
|
||||
elif ret.status_code == 500:
|
||||
self.logger.info("[ERROR] (fails: %d) Error in app with status code %d for results (1). RETRYING because results can't be handled" % (i+1, ret.status_code))
|
||||
|
||||
rettext = ret.text
|
||||
failed = True
|
||||
time.sleep(10)
|
||||
continue
|
||||
|
||||
else:
|
||||
self.logger.info("[ERROR] Error in app with status code %d for results (2). Crashing because results can't be handled" % ret.status_code)
|
||||
|
||||
rettext = ret.text
|
||||
failed = True
|
||||
break
|
||||
|
||||
if failed:
|
||||
self.action_result["result"] = json.dumps({
|
||||
"success": False,
|
||||
"reason": f"Bad result from backend during startup of app: {ret.status_code}",
|
||||
"extended_reason": f"{ret.text}"
|
||||
"extended_reason": f"{rettext}"
|
||||
})
|
||||
|
||||
self.send_result(self.action_result, headers, stream_path)
|
||||
return
|
||||
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
self.logger.info("[DEBUG] FullExec Connectionerror: %s" % e)
|
||||
self.logger.info("[ERROR] FullExec Connectionerror: %s" % e)
|
||||
self.action_result["result"] = json.dumps({
|
||||
"success": False,
|
||||
"reason": f"Connection error during startup: {e}"
|
||||
@@ -1315,7 +1487,7 @@ class AppBase:
|
||||
try:
|
||||
fullexecution = json.loads(self.full_execution)
|
||||
except json.decoder.JSONDecodeError as e:
|
||||
self.logger.info("[WARNING] Json decode execution error: %s" % e)
|
||||
self.logger.info("[ERROR] Json decode execution error: %s" % e)
|
||||
self.action_result["result"] = "Json error during startup: %s" % e
|
||||
self.send_result(self.action_result, headers, stream_path)
|
||||
return
|
||||
@@ -2012,7 +2184,6 @@ class AppBase:
|
||||
errors = False
|
||||
error_msg = ""
|
||||
try:
|
||||
#self.logger.info("In liquid")
|
||||
if len(template) > 10000000:
|
||||
self.logger.info("[DEBUG] Skipping liquid - size too big (%d)" % len(template))
|
||||
return template
|
||||
@@ -2042,10 +2213,87 @@ class AppBase:
|
||||
self.logger.info(f"[ERROR] Liquid Template error: {e}")
|
||||
error = True
|
||||
error_msg = e
|
||||
|
||||
self.action["parameters"].append({
|
||||
"name": "liquid_template_error",
|
||||
"value": f"There was a Liquid input error (1). Details: {e}",
|
||||
})
|
||||
|
||||
self.action_result["action"] = self.action
|
||||
except SyntaxError as e:
|
||||
self.logger.info(f"[ERROR] Liquid Syntax error: {e}")
|
||||
error = True
|
||||
error_msg = e
|
||||
|
||||
self.action["parameters"].append({
|
||||
"name": "liquid_python_syntax_error",
|
||||
"value": f"There was a syntax error in your Liquid input (2). Details: {e}",
|
||||
})
|
||||
|
||||
self.action_result["action"] = self.action
|
||||
except IndentationError as e:
|
||||
self.logger.info(f"[ERROR] Liquid IndentationError: {e}")
|
||||
error = True
|
||||
error_msg = e
|
||||
|
||||
self.action["parameters"].append({
|
||||
"name": "liquid_indentiation_error",
|
||||
"value": f"There was an indentation error in your Liquid input (2). Details: {e}",
|
||||
})
|
||||
|
||||
self.action_result["action"] = self.action
|
||||
except jinja2.exceptions.TemplateSyntaxError as e:
|
||||
self.logger.info(f"[ERROR] Liquid Syntax error: {e}")
|
||||
error = True
|
||||
error_msg = e
|
||||
|
||||
self.action["parameters"].append({
|
||||
"name": "liquid_syntax_error",
|
||||
"value": f"There was a syntax error in your Liquid input (2). Details: {e}",
|
||||
})
|
||||
|
||||
self.action_result["action"] = self.action
|
||||
except json.decoder.JSONDecodeError as e:
|
||||
self.logger.info(f"[ERROR] Liquid JSON Syntax error: {e}")
|
||||
|
||||
replace = False
|
||||
skip_next = False
|
||||
newlines = []
|
||||
thisline = []
|
||||
for line in template.split("\n"):
|
||||
#print("LINE: %s" % repr(line))
|
||||
if "\"\"\"" in line or "\'\'\'" in line:
|
||||
if replace:
|
||||
skip_next = True
|
||||
else:
|
||||
replace = not replace
|
||||
|
||||
if replace == True:
|
||||
thisline.append(line)
|
||||
if skip_next == True:
|
||||
if len(thisline) > 0:
|
||||
#print(thisline)
|
||||
newlines.append(" ".join(thisline))
|
||||
thisline = []
|
||||
|
||||
replace = False
|
||||
else:
|
||||
newlines.append(line)
|
||||
|
||||
new_template = "\n".join(newlines)
|
||||
if new_template != template:
|
||||
#check_template(new_template)
|
||||
return parse_liquid(new_template, self)
|
||||
else:
|
||||
error = True
|
||||
error_msg = e
|
||||
|
||||
self.action["parameters"].append({
|
||||
"name": "liquid_json_error",
|
||||
"value": f"There was a syntax error in your input JSON(2). This is typically an issue with escaping newlines. Details: {e}",
|
||||
})
|
||||
|
||||
self.action_result["action"] = self.action
|
||||
except TypeError as e:
|
||||
try:
|
||||
if "string as left operand" in f"{e}":
|
||||
@@ -2070,6 +2318,13 @@ class AppBase:
|
||||
|
||||
except Exception as e:
|
||||
print(f"SubError in Liquid: {e}")
|
||||
|
||||
self.action["parameters"].append({
|
||||
"name": "liquid_general_error",
|
||||
"value": f"There was general error Liquid input (2). Details: {e}",
|
||||
})
|
||||
|
||||
self.action_result["action"] = self.action
|
||||
#return template
|
||||
|
||||
self.logger.info(f"[ERROR] Liquid TypeError error: {e}")
|
||||
@@ -2081,6 +2336,16 @@ class AppBase:
|
||||
error = True
|
||||
error_msg = e
|
||||
|
||||
self.action["parameters"].append({
|
||||
"name": "liquid_general_exception",
|
||||
"value": f"There was general exception Liquid input (2). Details: {e}",
|
||||
})
|
||||
|
||||
self.action_result["action"] = self.action
|
||||
|
||||
if "fmt" in error_msg and "liquid_date" in error_msg:
|
||||
return template
|
||||
|
||||
self.logger.info("Done in liquid")
|
||||
if error == True:
|
||||
self.action_result["status"] = "FAILURE"
|
||||
@@ -2089,6 +2354,7 @@ class AppBase:
|
||||
"reason": f"Failed to parse LiquidPy: {error_msg}",
|
||||
"input": template,
|
||||
}
|
||||
|
||||
try:
|
||||
self.action_result["result"] = json.dumps(data)
|
||||
except Exception as e:
|
||||
@@ -2227,7 +2493,6 @@ class AppBase:
|
||||
#self.logger.info("STATIC PARSED: %s" % actualitem)
|
||||
#self.logger.info("[INFO] Done with regex matching")
|
||||
if len(actualitem) > 0:
|
||||
#self.logger.info("[DEBUG] Matches: ", actualitem)
|
||||
for replace in actualitem:
|
||||
try:
|
||||
to_be_replaced = replace[0]
|
||||
@@ -2458,6 +2723,12 @@ class AppBase:
|
||||
except KeyError:
|
||||
return True, ""
|
||||
|
||||
# Startnode should always run - no need to check incoming
|
||||
try:
|
||||
if action["id"] == fullexecution["start"]:
|
||||
return True, ""
|
||||
except Exception as error:
|
||||
self.logger.info(f"[WARNING] Failed checking startnode: {error}")
|
||||
|
||||
available_checks = [
|
||||
"=",
|
||||
@@ -2527,7 +2798,7 @@ class AppBase:
|
||||
check, sourcevalue, is_loop = parse_params(action, fullexecution, condition["source"], self)
|
||||
if check:
|
||||
continue
|
||||
return False, {"success": False, "reason": "Failed condition (1): %s %s %s because %s" % (sourcevalue, condition["condition"]["value"], destinationvalue, check)}
|
||||
return False, {"success": False, "reason": "Failed condition: %s %s %s because %s" % (sourcevalue, condition["condition"]["value"], destinationvalue, check)}
|
||||
|
||||
#sourcevalue = sourcevalue.encode("utf-8")
|
||||
sourcevalue = parse_wrapper_start(sourcevalue, self)
|
||||
@@ -2536,7 +2807,7 @@ class AppBase:
|
||||
check, destinationvalue, is_loop = parse_params(action, fullexecution, condition["destination"], self)
|
||||
if check:
|
||||
continue
|
||||
return False, {"success": False, "reason": "Failed condition (2): %s %s %s because %s" % (sourcevalue, condition["condition"]["value"], destinationvalue, check)}
|
||||
return False, {"success": False, "reason": "Failed condition: %s %s %s because %s" % (sourcevalue, condition["condition"]["value"], destinationvalue, check)}
|
||||
|
||||
#destinationvalue = destinationvalue.encode("utf-8")
|
||||
destinationvalue = parse_wrapper_start(destinationvalue, self)
|
||||
@@ -2695,6 +2966,8 @@ class AppBase:
|
||||
if parameter["name"] == "body":
|
||||
bodyindex = counter
|
||||
#self.logger.info("PARAM: %s" % parameter)
|
||||
|
||||
# FIXMe: This should also happen after liquid & param parsing..
|
||||
try:
|
||||
values = parameter["value_replace"]
|
||||
if values != None:
|
||||
@@ -2702,16 +2975,24 @@ class AppBase:
|
||||
for val in values:
|
||||
replace_value = val["value"]
|
||||
replace_key = val["key"]
|
||||
|
||||
if (val["value"].startswith("{") and val["value"].endswith("}")) or (val["value"].startswith("[") and val["value"].endswith("]")):
|
||||
self.logger.info(f"""Trying to parse as JSON: {val["value"]}""")
|
||||
try:
|
||||
value_replace = json.loads(val["value"])
|
||||
# If it gets here, remove the "" infront and behind the key as well since this is preventing the JSON from being loaded
|
||||
newval = val["value"]
|
||||
|
||||
# If it gets here, remove the "" infront and behind the key as well
|
||||
# since this is preventing the JSON from being loaded
|
||||
tmpvalue = json.loads(newval)
|
||||
replace_key = f"\"{replace_key}\""
|
||||
except json.decoder.JSONDecodeError as e:
|
||||
self.logger.info("Failed JSON replacement for OpenAPI %s", val["key"])
|
||||
self.logger.info("[WARNING] Failed JSON replacement for OpenAPI %s", val["key"])
|
||||
|
||||
elif val["value"].lower() == "true" or val["value"].lower() == "false":
|
||||
replace_key = f"\"{replace_key}\""
|
||||
else:
|
||||
if "\"" in replace_value and not "\\\"" in replace_value:
|
||||
replace_value = replace_value.replace("\"", "\\\"", -1)
|
||||
|
||||
action["parameters"][counter]["value"] = action["parameters"][counter]["value"].replace(replace_key, replace_value, 1)
|
||||
|
||||
@@ -2768,6 +3049,9 @@ class AppBase:
|
||||
"exception": f"Value Error: {check}",
|
||||
}))
|
||||
|
||||
if parameter["name"] == "body":
|
||||
self.logger.info(f"[INFO] Should debug field with liquid and other checks as it's BODY: {value}")
|
||||
|
||||
# Custom format for ${name[0,1,2,...]}$
|
||||
#submatch = "([${]{2}([0-9a-zA-Z_-]+)(\[.*\])[}$]{2})"
|
||||
#self.logger.info(f"Returnedvalue: {value}")
|
||||
@@ -3107,6 +3391,13 @@ class AppBase:
|
||||
# FIXME: add this to Multi exec as well.
|
||||
try:
|
||||
for key, value in params.items():
|
||||
if "-" in key:
|
||||
try:
|
||||
newkey = key.replace("-", "_", -1).lower()
|
||||
params[newkey] = params[key]
|
||||
except Exception as e:
|
||||
self.logger.info("[DEBUG] Failed updating key with dash in it: %s" % e)
|
||||
|
||||
try:
|
||||
if isinstance(value, str) and ((value.startswith("{") and value.endswith("}")) or (value.startswith("[") and value.endswith("]"))):
|
||||
params[key] = json.loads(value)
|
||||
@@ -3151,7 +3442,7 @@ class AppBase:
|
||||
errorstring = f"{e}"
|
||||
|
||||
if "the JSON object must be" in errorstring:
|
||||
self.logger.info("[ERROR] Something is wrong with the input for this function. Are lists and JSON data handled parsed properly?")
|
||||
self.logger.info("[ERROR] Something is wrong with the input for this function. Are lists and JSON data handled parsed properly (0)?")
|
||||
try:
|
||||
e = json.loads(f"{e}")
|
||||
except:
|
||||
@@ -3159,7 +3450,7 @@ class AppBase:
|
||||
|
||||
newres = json.dumps({
|
||||
"success": False,
|
||||
"reason": "An exception occurred while running this function. See exception for more details and contact support if this persists (support@shuffler.io)",
|
||||
"reason": "An exception occurred while running this function (1). See exception for more details and contact support if this persists (support@shuffler.io)",
|
||||
"exception": e,
|
||||
})
|
||||
break
|
||||
@@ -3181,7 +3472,7 @@ class AppBase:
|
||||
})
|
||||
break
|
||||
except Exception as e:
|
||||
self.logger.info("[ERROR] Something is wrong with the input for this function. Are lists and JSON data handled parsed properly?")
|
||||
self.logger.info("[ERROR] Something is wrong with the input for this function. Are lists and JSON data handled parsed properly (1)?")
|
||||
|
||||
try:
|
||||
e = json.loads(f"{e}")
|
||||
@@ -3190,7 +3481,7 @@ class AppBase:
|
||||
|
||||
newres = json.dumps({
|
||||
"success": False,
|
||||
"reason": "An exception occurred while running this function. See exception for more details and contact support if this persists (support@shuffler.io)",
|
||||
"reason": "An exception occurred while running this function (2). See exception for more details and contact support if this persists (support@shuffler.io)",
|
||||
"exception": e,
|
||||
})
|
||||
break
|
||||
@@ -3322,12 +3613,22 @@ class AppBase:
|
||||
except TypeError as e:
|
||||
self.logger.info("[ERROR] TypeError issue: %s" % e)
|
||||
self.action_result["status"] = "FAILURE"
|
||||
self.action_result["result"] = "TypeError: %s" % str(e)
|
||||
self.action_result["result"] = json.dumps({
|
||||
"success": False,
|
||||
"reason": f"Typeerror. Most likely due to a list that should've been a string. See details for more info.",
|
||||
"details": e,
|
||||
})
|
||||
#self.action_result["result"] = "TypeError: %s" % str(e)
|
||||
else:
|
||||
self.logger.info("[DEBUG] Function %s doesn't exist?" % action["name"])
|
||||
self.logger.error(f"[ERROR] App {self.__class__.__name__}.{action['name']} is not callable")
|
||||
self.action_result["status"] = "FAILURE"
|
||||
self.action_result["result"] = "Function %s is not callable." % actionname
|
||||
#self.action_result["result"] = "Function %s is not callable." % actionname
|
||||
|
||||
self.action_result["result"] = json.dumps({
|
||||
"success": False,
|
||||
"reason": f"Function %s doesn't exist." % actionname,
|
||||
})
|
||||
|
||||
# https://ptb.discord.com/channels/747075026288902237/882017498550112286/882043773138382890
|
||||
except (requests.exceptions.RequestException, TimeoutError) as e:
|
||||
|
||||
@@ -2,16 +2,21 @@
|
||||
|
||||
### DEFAULT
|
||||
NAME=shuffle-app_sdk
|
||||
VERSION=0.9.70
|
||||
VERSION=1.1.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
|
||||
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
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
urllib3==1.26.5
|
||||
requests==2.25.1
|
||||
MarkupSafe==2.0.1
|
||||
liquidpy==0.7.3
|
||||
liquidpy==0.7.6
|
||||
flask[async]==2.0.2
|
||||
waitress==2.1.0
|
||||
#flask==1.1.2
|
||||
python-dateutil==2.8.1
|
||||
|
||||
@@ -2,15 +2,17 @@ package main
|
||||
|
||||
// Docker
|
||||
import (
|
||||
"archive/tar"
|
||||
|
||||
"github.com/shuffle/shuffle-shared"
|
||||
|
||||
"archive/tar"
|
||||
//"bufio"
|
||||
"path/filepath"
|
||||
//"strconv"
|
||||
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -757,7 +759,7 @@ func getDockerImage(resp http.ResponseWriter, request *http.Request) {
|
||||
tagFound = version.Name
|
||||
}
|
||||
|
||||
buildSwaggerApp(resp, []byte(openApiApp.Body), user)
|
||||
buildSwaggerApp(resp, []byte(openApiApp.Body), user, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -803,6 +805,92 @@ func getDockerImage(resp http.ResponseWriter, request *http.Request) {
|
||||
//resp.WriteHeader(200)
|
||||
}
|
||||
|
||||
// Downloads and activates an app from shuffler.io if possible
|
||||
func handleRemoteDownloadApp(resp http.ResponseWriter, ctx context.Context, user shuffle.User, appId string) {
|
||||
url := fmt.Sprintf("https://shuffler.io/api/v1/apps/%s/config", appId)
|
||||
log.Printf("Downloading API from %s", url)
|
||||
req, err := http.NewRequest(
|
||||
"GET",
|
||||
url,
|
||||
nil,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed auto-downloading app %s: %s", appId, err)
|
||||
resp.WriteHeader(401)
|
||||
resp.Write([]byte(`{"success": false, "reason": "App doesn't exist"}`))
|
||||
return
|
||||
}
|
||||
|
||||
httpClient := &http.Client{}
|
||||
newresp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed running auto-download request for %s: %s", appId, err)
|
||||
resp.WriteHeader(401)
|
||||
resp.Write([]byte(`{"success": false, "reason": "App doesn't exist"}`))
|
||||
return
|
||||
}
|
||||
|
||||
respBody, err := ioutil.ReadAll(newresp.Body)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed setting respbody for workflow download: %s", err)
|
||||
resp.WriteHeader(401)
|
||||
resp.Write([]byte(`{"success": false, "reason": "App doesn't exist"}`))
|
||||
return
|
||||
}
|
||||
|
||||
if len(respBody) > 0 {
|
||||
type tmpapp struct {
|
||||
Success bool `json:"success"`
|
||||
OpenAPI string `json:"openapi"`
|
||||
}
|
||||
|
||||
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)
|
||||
resp.Write([]byte(`{"success": false, "reason": "App doesn't exist"}`))
|
||||
return
|
||||
}
|
||||
|
||||
key, err := base64.StdEncoding.DecodeString(app.OpenAPI)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed auto-setting OpenAPI app: %s", err)
|
||||
resp.WriteHeader(401)
|
||||
resp.Write([]byte(`{"success": false, "reason": "App doesn't exist"}`))
|
||||
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)
|
||||
|
||||
newapp := shuffle.ParsedOpenApi{}
|
||||
err = json.Unmarshal(key, &newapp)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed openapi unmarshal during auto-download: %s", app.Success, len(app.OpenAPI), err)
|
||||
resp.WriteHeader(401)
|
||||
resp.Write([]byte(`{"success": false, "reason": "App doesn't exist"}`))
|
||||
return
|
||||
}
|
||||
|
||||
err = json.Unmarshal(key, &newapp)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed openapi unmarshal during auto-download: %s", app.Success, len(app.OpenAPI), err)
|
||||
resp.WriteHeader(401)
|
||||
resp.Write([]byte(`{"success": false, "reason": "App doesn't exist"}`))
|
||||
return
|
||||
}
|
||||
|
||||
buildSwaggerApp(resp, []byte(newapp.Body), user, true)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func activateWorkflowAppDocker(resp http.ResponseWriter, request *http.Request) {
|
||||
cors := shuffle.HandleCors(resp, request)
|
||||
if cors {
|
||||
@@ -846,9 +934,9 @@ func activateWorkflowAppDocker(resp http.ResponseWriter, request *http.Request)
|
||||
apps, err := shuffle.FindWorkflowAppByName(ctx, appName)
|
||||
//log.Printf("[INFO] Found %d apps for %s", len(apps), appName)
|
||||
if err != nil || len(apps) == 0 {
|
||||
log.Printf("[WARNING] Error getting app %s (app config): %s", appName, err)
|
||||
resp.WriteHeader(401)
|
||||
resp.Write([]byte(`{"success": false, "reason": "App doesn't exist"}`))
|
||||
log.Printf("[WARNING] Error getting app %s (app config). Starting remote download.: %s", appName, err)
|
||||
|
||||
handleRemoteDownloadApp(resp, ctx, user, fileId)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -869,10 +957,12 @@ func activateWorkflowAppDocker(resp http.ResponseWriter, request *http.Request)
|
||||
|
||||
app = &selectedApp
|
||||
} else {
|
||||
log.Printf("[WARNING] Error getting app with ID %s (app config): %s", fileId, err)
|
||||
resp.WriteHeader(401)
|
||||
resp.Write([]byte(`{"success": false, "reason": "App doesn't exist"}`))
|
||||
log.Printf("[WARNING] Error getting app with ID %s (app config): %s. Starting remote download(2)", fileId, err)
|
||||
handleRemoteDownloadApp(resp, ctx, user, fileId)
|
||||
return
|
||||
//resp.WriteHeader(401)
|
||||
//resp.Write([]byte(`{"success": false, "reason": "App doesn't exist"}`))
|
||||
//return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+84
-19
@@ -1,35 +1,100 @@
|
||||
module main
|
||||
|
||||
go 1.16
|
||||
go 1.19
|
||||
|
||||
//replace github.com/shuffle/shuffle-shared => ../../../shuffle-shared
|
||||
|
||||
//replace github.com/frikky/kin-openapi => ../../../../git/kin-openapi
|
||||
//replace github.com/frikky/go-elasticsearch => ../../../../git/go-elasticsearch
|
||||
|
||||
require (
|
||||
cloud.google.com/go/datastore v1.6.0
|
||||
cloud.google.com/go/iam v0.1.1 // indirect
|
||||
cloud.google.com/go/pubsub v1.17.1
|
||||
cloud.google.com/go/storage v1.18.2
|
||||
cloud.google.com/go/datastore v1.10.0
|
||||
cloud.google.com/go/pubsub v1.28.0
|
||||
cloud.google.com/go/storage v1.28.1
|
||||
github.com/basgys/goxml2json v1.1.0
|
||||
github.com/carlescere/scheduler v0.0.0-20170109141437-ee74d2f83d82
|
||||
github.com/docker/docker v20.10.12+incompatible
|
||||
github.com/frikky/kin-openapi v0.41.0
|
||||
github.com/fsouza/go-dockerclient v1.7.7
|
||||
github.com/docker/docker v20.10.21+incompatible
|
||||
github.com/frikky/kin-openapi v0.42.0
|
||||
github.com/fsouza/go-dockerclient v1.9.0
|
||||
github.com/ghodss/yaml v1.0.0
|
||||
github.com/go-git/go-billy/v5 v5.3.1
|
||||
github.com/go-git/go-git/v5 v5.4.2
|
||||
github.com/go-git/go-git/v5 v5.5.0
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/h2non/filetype v1.1.3
|
||||
github.com/nirasan/go-oauth-pkce-code-verifier v0.0.0-20170819232839-0fbfe93532da // indirect
|
||||
github.com/satori/go.uuid v1.2.0
|
||||
github.com/shuffle/shuffle-shared v0.2.42
|
||||
go4.org v0.0.0-20201209231011-d4a079459e60 // indirect
|
||||
golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce
|
||||
google.golang.org/api v0.65.0
|
||||
github.com/shuffle/shuffle-shared v0.3.35
|
||||
golang.org/x/crypto v0.3.0
|
||||
google.golang.org/api v0.103.0
|
||||
google.golang.org/appengine v1.6.7
|
||||
google.golang.org/grpc v1.43.0
|
||||
google.golang.org/grpc v1.51.0
|
||||
gopkg.in/src-d/go-git.v4 v4.13.1
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go v0.105.0 // indirect
|
||||
cloud.google.com/go/compute v1.13.0 // indirect
|
||||
cloud.google.com/go/compute/metadata v0.2.1 // indirect
|
||||
cloud.google.com/go/iam v0.7.0 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
|
||||
github.com/Masterminds/semver v1.5.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.0 // indirect
|
||||
github.com/Microsoft/hcsshim v0.9.3 // indirect
|
||||
github.com/ProtonMail/go-crypto v0.0.0-20221026131551-cf6655e29de4 // indirect
|
||||
github.com/acomagu/bufpipe v1.0.3 // indirect
|
||||
github.com/adrg/strutil v0.2.3 // indirect
|
||||
github.com/algolia/algoliasearch-client-go/v3 v3.18.1 // indirect
|
||||
github.com/bradfitz/gomemcache v0.0.0-20221031212613-62deef7fc822 // indirect
|
||||
github.com/bradfitz/slice v0.0.0-20180809154707-2b758aa73013 // indirect
|
||||
github.com/cloudflare/circl v1.1.0 // indirect
|
||||
github.com/containerd/cgroups v1.0.3 // indirect
|
||||
github.com/containerd/containerd v1.6.6 // indirect
|
||||
github.com/docker/distribution v2.7.1+incompatible // indirect
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/emirpasic/gods v1.18.1 // indirect
|
||||
github.com/frikky/go-elasticsearch/v8 v8.13.1 // indirect
|
||||
github.com/go-git/gcfg v1.5.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
github.com/go-openapi/swag v0.19.5 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||
github.com/golang/protobuf v1.5.2 // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/google/go-github/v28 v28.1.1 // indirect
|
||||
github.com/google/go-querystring v1.0.0 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.2.0 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.7.0 // indirect
|
||||
github.com/imdario/mergo v0.3.13 // indirect
|
||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||
github.com/mailru/easyjson v0.7.0 // indirect
|
||||
github.com/moby/sys/mount v0.3.3 // indirect
|
||||
github.com/moby/sys/mountinfo v0.6.2 // indirect
|
||||
github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect
|
||||
github.com/morikuni/aec v1.0.0 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect
|
||||
github.com/opencontainers/runc v1.1.2 // indirect
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
||||
github.com/pjbgf/sha1cd v0.2.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/sergi/go-diff v1.1.0 // indirect
|
||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||
github.com/skeema/knownhosts v1.1.0 // indirect
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
|
||||
github.com/src-d/gcfg v1.4.0 // indirect
|
||||
github.com/xanzy/ssh-agent v0.3.2 // indirect
|
||||
go.opencensus.io v0.24.0 // indirect
|
||||
go4.org v0.0.0-20201209231011-d4a079459e60 // indirect
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
|
||||
golang.org/x/net v0.2.0 // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783 // indirect
|
||||
golang.org/x/sync v0.1.0 // indirect
|
||||
golang.org/x/sys v0.2.0 // indirect
|
||||
golang.org/x/text v0.4.0 // indirect
|
||||
golang.org/x/tools v0.1.12 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect
|
||||
google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd // indirect
|
||||
google.golang.org/protobuf v1.28.1 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
)
|
||||
|
||||
+148
-32
@@ -691,13 +691,25 @@ func createNewUser(username, password, role, apikey string, org shuffle.OrgMini)
|
||||
|
||||
err = shuffle.SetUser(ctx, newUser, true)
|
||||
if err != nil {
|
||||
log.Printf("Error adding User %s: %s", username, err)
|
||||
log.Printf("[ERROR] Problem adding User %s: %s", username, err)
|
||||
return err
|
||||
}
|
||||
|
||||
neworg, err := shuffle.GetOrg(ctx, org.Id)
|
||||
if err == nil {
|
||||
//neworg.Users = append(neworg.Users, *newUser)
|
||||
for tutorialIndex, tutorial := range neworg.Tutorials {
|
||||
if tutorial.Name == "Invite teammates" {
|
||||
neworg.Tutorials[tutorialIndex].Description = fmt.Sprintf("%d users are in your org. Org name and Image change next.", len(neworg.Users))
|
||||
if len(neworg.Users) > 0 {
|
||||
neworg.Tutorials[tutorialIndex].Done = true
|
||||
neworg.Tutorials[tutorialIndex].Link = "/admin"
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
err = shuffle.SetOrg(ctx, *neworg, neworg.Id)
|
||||
if err != nil {
|
||||
log.Printf("Failed updating org with user %s", newUser.Username)
|
||||
@@ -729,7 +741,7 @@ func handleRegister(resp http.ResponseWriter, request *http.Request) {
|
||||
if err != nil {
|
||||
if (countErr == nil && count > 0) || countErr != nil {
|
||||
resp.WriteHeader(401)
|
||||
resp.Write([]byte(`{"success": false, "reason": "Can't register without being admin"}`))
|
||||
resp.Write([]byte(`{"success": false, "reason": "Users already exist. Please go to /login to log into your admin user."}`))
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -843,6 +855,8 @@ func handleCookie(request *http.Request) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Returns whether the user is logged in or not etc.
|
||||
// Also has more data about the user and org
|
||||
func handleInfo(resp http.ResponseWriter, request *http.Request) {
|
||||
cors := shuffle.HandleCors(resp, request)
|
||||
if cors {
|
||||
@@ -981,7 +995,8 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) {
|
||||
}
|
||||
|
||||
org, err := shuffle.GetOrg(ctx, userInfo.ActiveOrg.Id)
|
||||
if err == nil {
|
||||
//if err == nil {
|
||||
if len(org.Id) > 0 {
|
||||
userInfo.ActiveOrg = shuffle.OrgMini{
|
||||
Id: org.Id,
|
||||
Name: org.Name,
|
||||
@@ -990,6 +1005,7 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) {
|
||||
Image: org.Image,
|
||||
}
|
||||
}
|
||||
//}
|
||||
|
||||
userInfo.ActiveOrg.Users = []shuffle.UserMini{}
|
||||
userOrgs := []shuffle.OrgMini{}
|
||||
@@ -1000,7 +1016,8 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) {
|
||||
}
|
||||
|
||||
org, err := shuffle.GetOrg(ctx, item)
|
||||
if err == nil {
|
||||
_ = err
|
||||
if len(org.Id) > 0 {
|
||||
userOrgs = append(userOrgs, shuffle.OrgMini{
|
||||
Id: org.Id,
|
||||
Name: org.Name,
|
||||
@@ -1035,7 +1052,35 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) {
|
||||
chatDisabled = true
|
||||
}
|
||||
|
||||
tutorialsFinished := []string{}
|
||||
orgPriorities := org.Priorities
|
||||
if len(org.Priorities) < 5 {
|
||||
log.Printf("[WARNING] Should find and add priorities as length is less than 5 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
|
||||
}
|
||||
}
|
||||
|
||||
tutorialsFinished := []shuffle.Tutorial{}
|
||||
for _, tutorial := range userInfo.PersonalInfo.Tutorials {
|
||||
tutorialsFinished = append(tutorialsFinished, shuffle.Tutorial{
|
||||
Name: tutorial,
|
||||
})
|
||||
}
|
||||
|
||||
if len(org.SecurityFramework.SIEM.Name) > 0 || len(org.SecurityFramework.Network.Name) > 0 || len(org.SecurityFramework.EDR.Name) > 0 || len(org.SecurityFramework.Cases.Name) > 0 || len(org.SecurityFramework.IAM.Name) > 0 || len(org.SecurityFramework.Assets.Name) > 0 || len(org.SecurityFramework.Intel.Name) > 0 || len(org.SecurityFramework.Communication.Name) > 0 {
|
||||
tutorialsFinished = append(tutorialsFinished, shuffle.Tutorial{
|
||||
Name: "find_integrations",
|
||||
})
|
||||
}
|
||||
|
||||
for _, tutorial := range org.Tutorials {
|
||||
tutorialsFinished = append(tutorialsFinished, tutorial)
|
||||
}
|
||||
|
||||
returnValue := shuffle.HandleInfo{
|
||||
Success: true,
|
||||
Username: userInfo.Username,
|
||||
@@ -1051,8 +1096,10 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) {
|
||||
},
|
||||
},
|
||||
EthInfo: userInfo.EthInfo,
|
||||
Tutorials: tutorialsFinished,
|
||||
ChatDisabled: chatDisabled,
|
||||
Tutorials: tutorialsFinished,
|
||||
|
||||
Priorities: orgPriorities,
|
||||
}
|
||||
|
||||
returnData, err := json.Marshal(returnValue)
|
||||
@@ -1231,7 +1278,6 @@ func checkAdminLogin(resp http.ResponseWriter, request *http.Request) {
|
||||
|
||||
// Should run calculations
|
||||
if len(org.SSOConfig.OpenIdAuthorization) > 0 {
|
||||
log.Printf("[DEBUG] Found OpenID url (PKCE!!). Extra redirect check: %s", request.URL.String())
|
||||
baseSSOUrl = org.SSOConfig.OpenIdAuthorization
|
||||
|
||||
codeChallenge := uuid.NewV4().String()
|
||||
@@ -1251,6 +1297,10 @@ func checkAdminLogin(resp http.ResponseWriter, request *http.Request) {
|
||||
redirectUrl = url.QueryEscape(fmt.Sprintf("%s/api/v1/login_openid", os.Getenv("BASE_URL")))
|
||||
}
|
||||
|
||||
if len(os.Getenv("SSO_REDIRECT_URL")) > 0 {
|
||||
redirectUrl = url.QueryEscape(fmt.Sprintf("%s/api/v1/login_openid", os.Getenv("SSO_REDIRECT_URL")))
|
||||
}
|
||||
|
||||
state := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("org=%s&challenge=%s&redirect=%s", org.Id, codeChallenge, redirectUrl)))
|
||||
|
||||
// has to happen after initial value is stored
|
||||
@@ -1260,12 +1310,25 @@ func checkAdminLogin(resp http.ResponseWriter, request *http.Request) {
|
||||
|
||||
//log.Printf("[DEBUG] Got challenge value %s (POST state)", codeChallenge)
|
||||
|
||||
baseSSOUrl += fmt.Sprintf("?client_id=%s&response_type=code&scope=openid&redirect_uri=%s&state=%s&code_challenge_method=S256&code_challenge=%s", org.SSOConfig.OpenIdClientId, redirectUrl, state, codeChallenge)
|
||||
if len(org.SSOConfig.OpenIdClientSecret) > 0 {
|
||||
|
||||
//baseSSOUrl += fmt.Sprintf("?client_id=%s&response_type=code&scope=openid&redirect_uri=%s&state=%s&client_secret=%s", org.SSOConfig.OpenIdClientId, redirectUrl, state, org.SSOConfig.OpenIdClientSecret)
|
||||
state := base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("org=%s&redirect=%s&challenge=%s", org.Id, redirectUrl, org.SSOConfig.OpenIdClientSecret)))
|
||||
log.Printf("URL: %s", redirectUrl)
|
||||
|
||||
baseSSOUrl += fmt.Sprintf("?client_id=%s&response_type=id_token&scope=openid&redirect_uri=%s&state=%s&response_mode=form_post&nonce=%s", org.SSOConfig.OpenIdClientId, redirectUrl, state, state)
|
||||
//baseSSOUrl += fmt.Sprintf("&client_secret=%s", org.SSOConfig.OpenIdClientSecret)
|
||||
log.Printf("[DEBUG] Found OpenID url (client secret). Extra redirect check: %s - %s", request.URL.String(), baseSSOUrl)
|
||||
} else {
|
||||
log.Printf("[DEBUG] Found OpenID url (PKCE!!). Extra redirect check: %s", request.URL.String())
|
||||
baseSSOUrl += fmt.Sprintf("?client_id=%s&response_type=code&scope=openid&redirect_uri=%s&state=%s&code_challenge_method=S256&code_challenge=%s", org.SSOConfig.OpenIdClientId, redirectUrl, state, codeChallenge)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
if len(org.SSOConfig.SSOEntrypoint) > 0 {
|
||||
log.Printf("[DEBUG] Found SAML SSO url")
|
||||
log.Printf("[DEBUG] Found SAML SSO url: %s", org.SSOConfig.SSOEntrypoint)
|
||||
baseSSOUrl = org.SSOConfig.SSOEntrypoint
|
||||
break
|
||||
}
|
||||
@@ -1333,8 +1396,23 @@ func handleLogin(resp http.ResponseWriter, request *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// FIXME - have timeout here
|
||||
tutorialsFinished := []shuffle.Tutorial{}
|
||||
for _, tutorial := range Userdata.PersonalInfo.Tutorials {
|
||||
tutorialsFinished = append(tutorialsFinished, shuffle.Tutorial{
|
||||
Name: tutorial,
|
||||
})
|
||||
}
|
||||
returnValue := shuffle.HandleInfo{
|
||||
Success: true,
|
||||
Tutorials: tutorialsFinished,
|
||||
}
|
||||
|
||||
loginData := `{"success": true}`
|
||||
newData, err := json.Marshal(returnValue)
|
||||
if err == nil {
|
||||
loginData = string(newData)
|
||||
}
|
||||
|
||||
if len(Userdata.Session) != 0 {
|
||||
log.Println("[INFO] User session already exists - resetting it")
|
||||
expiration := time.Now().Add(3600 * time.Second)
|
||||
@@ -1345,7 +1423,17 @@ func handleLogin(resp http.ResponseWriter, request *http.Request) {
|
||||
Expires: expiration,
|
||||
})
|
||||
|
||||
returnValue.Cookies = append(returnValue.Cookies, shuffle.SessionCookie{
|
||||
Key: "session_token",
|
||||
Value: Userdata.Session,
|
||||
Expiration: expiration.Unix(),
|
||||
})
|
||||
|
||||
loginData = fmt.Sprintf(`{"success": true, "cookies": [{"key": "session_token", "value": "%s", "expiration": %d}]}`, Userdata.Session, expiration.Unix())
|
||||
newData, err := json.Marshal(returnValue)
|
||||
if err == nil {
|
||||
loginData = string(newData)
|
||||
}
|
||||
//log.Printf("SESSION LENGTH MORE THAN 0 IN LOGIN: %s", Userdata.Session)
|
||||
|
||||
err = shuffle.SetSession(ctx, Userdata, Userdata.Session)
|
||||
@@ -1382,7 +1470,17 @@ func handleLogin(resp http.ResponseWriter, request *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
returnValue.Cookies = append(returnValue.Cookies, shuffle.SessionCookie{
|
||||
Key: "session_token",
|
||||
Value: sessionToken,
|
||||
Expiration: expiration.Unix(),
|
||||
})
|
||||
|
||||
loginData = fmt.Sprintf(`{"success": true, "cookies": [{"key": "session_token", "value": "%s", "expiration": %d}]}`, sessionToken, expiration.Unix())
|
||||
newData, err := json.Marshal(returnValue)
|
||||
if err == nil {
|
||||
loginData = string(newData)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("[INFO] %s SUCCESSFULLY LOGGED IN with session %s", data.Username, Userdata.Session)
|
||||
@@ -2244,7 +2342,7 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) {
|
||||
//start, startok := request.URL.Query()["start"]
|
||||
|
||||
// OrgId: activeOrgs[0].Id,
|
||||
workflowExecution, executionResp, err := handleExecution(item, workflow, newRequest)
|
||||
workflowExecution, executionResp, err := handleExecution(item, workflow, newRequest, hook.OrgId)
|
||||
if err == nil {
|
||||
/*
|
||||
err = increaseStatisticsField(ctx, "total_webhooks_ran", workflowExecution.Workflow.ID, 1, workflowExecution.ExecutionOrg)
|
||||
@@ -3055,18 +3153,19 @@ func handleSwaggerValidation(body []byte) (shuffle.ParsedOpenApi, error) {
|
||||
return parsed, err
|
||||
}
|
||||
|
||||
func buildSwaggerApp(resp http.ResponseWriter, body []byte, user shuffle.User) {
|
||||
func buildSwaggerApp(resp http.ResponseWriter, body []byte, user shuffle.User, skipEdit bool) {
|
||||
type Test struct {
|
||||
Editing bool `datastore:"editing"`
|
||||
Id string `datastore:"id"`
|
||||
Image string `datastore:"image"`
|
||||
Editing bool `json:"editing" datastore:"editing"`
|
||||
Id string `json:"id" datastore:"id"`
|
||||
Image string `json:"image" datastore:"image"`
|
||||
Body string `json:"body" datastore:"body"`
|
||||
}
|
||||
|
||||
var test Test
|
||||
err := json.Unmarshal(body, &test)
|
||||
if err != nil {
|
||||
log.Printf("[WARNING] Failed unmarshalling test: %s", err)
|
||||
resp.WriteHeader(401)
|
||||
log.Printf("[ERROR] Failed unmarshalling in swagger build: %s", err)
|
||||
resp.WriteHeader(400)
|
||||
resp.Write([]byte(`{"success": false}`))
|
||||
return
|
||||
}
|
||||
@@ -3076,13 +3175,13 @@ func buildSwaggerApp(resp http.ResponseWriter, body []byte, user shuffle.User) {
|
||||
hasher.Write(body)
|
||||
newmd5 := hex.EncodeToString(hasher.Sum(nil))
|
||||
|
||||
if test.Editing && len(user.Id) > 0 {
|
||||
if test.Editing && len(user.Id) > 0 && skipEdit != true {
|
||||
// Quick verification test
|
||||
ctx := context.Background()
|
||||
app, err := shuffle.GetApp(ctx, test.Id, user, false)
|
||||
if err != nil {
|
||||
log.Printf("[WARNING] Error getting app when editing: %s", app.Name)
|
||||
resp.WriteHeader(401)
|
||||
log.Printf("[ERROR] Error getting app when editing: %s", app.Name)
|
||||
resp.WriteHeader(400)
|
||||
resp.Write([]byte(`{"success": false}`))
|
||||
return
|
||||
}
|
||||
@@ -3090,7 +3189,7 @@ func buildSwaggerApp(resp http.ResponseWriter, body []byte, user shuffle.User) {
|
||||
// FIXME: Check whether it's in use.
|
||||
if user.Id != app.Owner && user.Role != "admin" {
|
||||
log.Printf("[WARNING] Wrong user (%s) for app %s when verifying swagger", user.Username, app.Name)
|
||||
resp.WriteHeader(401)
|
||||
resp.WriteHeader(400)
|
||||
resp.Write([]byte(`{"success": false}`))
|
||||
return
|
||||
}
|
||||
@@ -3114,12 +3213,13 @@ func buildSwaggerApp(resp http.ResponseWriter, body []byte, user shuffle.User) {
|
||||
}
|
||||
|
||||
if swagger.Info == nil {
|
||||
log.Printf("[ERORR] Info is nil?: %#v", swagger)
|
||||
log.Printf("[ERORR] Info is nil in swagger?")
|
||||
resp.WriteHeader(500)
|
||||
resp.Write([]byte(`{"success": false, "reason": "Info not parsed"}`))
|
||||
return
|
||||
}
|
||||
|
||||
swagger.Info.Title = shuffle.FixFunctionName(swagger.Info.Title, swagger.Info.Title, false)
|
||||
if strings.Contains(swagger.Info.Title, " ") {
|
||||
swagger.Info.Title = strings.Replace(swagger.Info.Title, " ", "_", -1)
|
||||
}
|
||||
@@ -3223,7 +3323,7 @@ func buildSwaggerApp(resp http.ResponseWriter, body []byte, user shuffle.User) {
|
||||
//log.Println(stitched)
|
||||
|
||||
// 3. Zip and stream it directly in the directory
|
||||
_, err = shuffle.StreamZipdata(ctx, identifier, stitched, "requests\nurllib3", "")
|
||||
_, err = shuffle.StreamZipdata(ctx, identifier, stitched, shuffle.GetAppRequirements(), "")
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Zipfile error: %s", err)
|
||||
resp.WriteHeader(500)
|
||||
@@ -3374,7 +3474,7 @@ func verifySwagger(resp http.ResponseWriter, request *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
buildSwaggerApp(resp, body, user)
|
||||
buildSwaggerApp(resp, body, user, false)
|
||||
}
|
||||
|
||||
// Creates osfs from folderpath with a basepath as directory base
|
||||
@@ -3445,7 +3545,6 @@ func handleAppHotload(ctx context.Context, location string, forceUpdate bool) er
|
||||
return err
|
||||
}
|
||||
|
||||
//log.Printf("Reading app folder: %#v", dir)
|
||||
_, _, err = IterateAppGithubFolders(ctx, fs, dir, "", "", forceUpdate)
|
||||
if err != nil {
|
||||
log.Printf("[WARNING] Githubfolders error: %s", err)
|
||||
@@ -3518,7 +3617,7 @@ func handleCloudExecutionOnprem(workflowId, startNode, executionSource, executio
|
||||
Body: ioutil.NopCloser(bytes.NewReader(b)),
|
||||
}
|
||||
|
||||
_, _, err = handleExecution(workflowId, shuffle.Workflow{}, newRequest)
|
||||
_, _, err = handleExecution(workflowId, shuffle.Workflow{}, newRequest, workflow.OrgId)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -3639,7 +3738,7 @@ func handleCloudJob(job shuffle.CloudSyncJob) error {
|
||||
return err
|
||||
}
|
||||
|
||||
_, _, err = handleExecution(job.PrimaryItemId, shuffle.Workflow{}, newRequest)
|
||||
_, _, err = handleExecution(job.PrimaryItemId, shuffle.Workflow{}, newRequest, job.OrgId)
|
||||
if err != nil {
|
||||
log.Printf("Failed continuing workflow from cloud user_input: %s", err)
|
||||
return err
|
||||
@@ -3942,7 +4041,12 @@ func runInitEs(ctx context.Context) {
|
||||
Body: ioutil.NopCloser(strings.NewReader(schedule.WrappedArgument)),
|
||||
}
|
||||
|
||||
_, _, err := handleExecution(schedule.WorkflowId, shuffle.Workflow{}, request)
|
||||
orgId := ""
|
||||
if len(activeOrgs) > 0 {
|
||||
orgId = activeOrgs[0].Id
|
||||
}
|
||||
|
||||
_, _, err := handleExecution(schedule.WorkflowId, shuffle.Workflow{}, request, orgId)
|
||||
if err != nil {
|
||||
log.Printf("[WARNING] Failed to execute %s: %s", schedule.WorkflowId, err)
|
||||
}
|
||||
@@ -4728,7 +4832,6 @@ func runInit(ctx context.Context) {
|
||||
continue
|
||||
}
|
||||
|
||||
log.Printf("ENV: %s", item.Environment)
|
||||
if item.Environment == "cloud" {
|
||||
log.Printf("Skipping cloud schedule")
|
||||
continue
|
||||
@@ -4802,7 +4905,12 @@ func runInit(ctx context.Context) {
|
||||
Body: ioutil.NopCloser(strings.NewReader(schedule.WrappedArgument)),
|
||||
}
|
||||
|
||||
_, _, err := handleExecution(schedule.WorkflowId, shuffle.Workflow{}, request)
|
||||
orgId := ""
|
||||
if len(activeOrgs) > 0 {
|
||||
orgId = activeOrgs[0].Id
|
||||
}
|
||||
|
||||
_, _, err := handleExecution(schedule.WorkflowId, shuffle.Workflow{}, request, orgId)
|
||||
if err != nil {
|
||||
log.Printf("[WARNING] Failed to execute %s: %s", schedule.WorkflowId, err)
|
||||
}
|
||||
@@ -5963,7 +6071,9 @@ func initHandlers() {
|
||||
r.HandleFunc("/api/v1/workflows/collections/{key}", shuffle.HandleGetCollection).Methods("GET", "OPTIONS")
|
||||
|
||||
// Related to use-cases that are not directly workflows.
|
||||
r.HandleFunc("/api/v1/workflows/usecases/{key}", shuffle.HandleGetUsecase).Methods("GET", "OPTIONS")
|
||||
r.HandleFunc("/api/v1/workflows/usecases", shuffle.LoadUsecases).Methods("GET", "OPTIONS")
|
||||
r.HandleFunc("/api/v1/workflows/usecases", shuffle.UpdateUsecases).Methods("POST", "OPTIONS")
|
||||
|
||||
// Legacy app things
|
||||
r.HandleFunc("/api/v1/workflows/apps/validate", validateAppInput).Methods("POST", "OPTIONS")
|
||||
@@ -6041,13 +6151,14 @@ func initHandlers() {
|
||||
r.HandleFunc("/api/v1/orgs/{orgId}/validate_app_values", shuffle.HandleKeyValueCheck).Methods("POST", "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}/stats", shuffle.HandleGetStatistics).Methods("GET", "OPTIONS")
|
||||
r.HandleFunc("/api/v1/apps/{key}/execute", executeSingleAction).Methods("POST", "OPTIONS")
|
||||
|
||||
// Docker orborus specific - downloads an image
|
||||
r.HandleFunc("/api/v1/get_docker_image", getDockerImage).Methods("POST", "OPTIONS")
|
||||
r.HandleFunc("/api/v1/migrate_database", migrateDatabase).Methods("POST", "OPTIONS")
|
||||
r.HandleFunc("/api/v1/login_sso", shuffle.HandleSSO).Methods("GET", "POST", "OPTIONS")
|
||||
r.HandleFunc("/api/v1/login_openid", shuffle.HandleOpenId).Methods("GET", "OPTIONS")
|
||||
r.HandleFunc("/api/v1/login_openid", shuffle.HandleOpenId).Methods("GET", "POST", "OPTIONS")
|
||||
|
||||
// Important for email, IDS etc. Create this by:
|
||||
// PS: For cloud, this has to use cloud storage.
|
||||
@@ -6056,7 +6167,8 @@ func initHandlers() {
|
||||
r.HandleFunc("/api/v1/files/namespaces/{namespace}", shuffle.HandleGetFileNamespace).Methods("GET", "OPTIONS")
|
||||
r.HandleFunc("/api/v1/files/{fileId}/content", shuffle.HandleGetFileContent).Methods("GET", "OPTIONS")
|
||||
r.HandleFunc("/api/v1/files/create", shuffle.HandleCreateFile).Methods("POST", "OPTIONS")
|
||||
r.HandleFunc("/api/v1/files/{fileId}/upload", shuffle.HandleUploadFile).Methods("POST", "OPTIONS")
|
||||
r.HandleFunc("/api/v1/files/{fileId}/upload", shuffle.HandleUploadFile).Methods("POST", "OPTIONS", "PATCH")
|
||||
r.HandleFunc("/api/v1/files/{fileId}/edit", shuffle.HandleEditFile).Methods("PUT", "OPTIONS")
|
||||
r.HandleFunc("/api/v1/files/{fileId}", shuffle.HandleGetFileMeta).Methods("GET", "OPTIONS")
|
||||
r.HandleFunc("/api/v1/files/{fileId}", shuffle.HandleDeleteFile).Methods("DELETE", "OPTIONS")
|
||||
r.HandleFunc("/api/v1/files", shuffle.HandleGetFiles).Methods("GET", "OPTIONS")
|
||||
@@ -6070,6 +6182,10 @@ func initHandlers() {
|
||||
r.HandleFunc("/api/v1/users/notifications/clear", shuffle.HandleClearNotifications).Methods("GET", "OPTIONS")
|
||||
r.HandleFunc("/api/v1/users/notifications/{notificationId}/markasread", shuffle.HandleMarkAsRead).Methods("GET", "OPTIONS")
|
||||
|
||||
//r.HandleFunc("/api/v1/users/notifications/{notificationId}/markasread", shuffle.HandleMarkAsRead).Methods("GET", "OPTIONS")
|
||||
r.HandleFunc("/api/v1/dashboards/{key}/widgets", shuffle.HandleNewWidget).Methods("POST", "OPTIONS")
|
||||
r.HandleFunc("/api/v1/dashboards/{key}/widgets/{widget_id}", shuffle.HandleGetWidget).Methods("GET", "OPTIONS")
|
||||
|
||||
http.Handle("/", r)
|
||||
}
|
||||
|
||||
|
||||
+72
-35
@@ -98,7 +98,7 @@ func createSchedule(ctx context.Context, scheduleId, workflowId, name, startNode
|
||||
Body: ioutil.NopCloser(strings.NewReader(bodyWrapper)),
|
||||
}
|
||||
|
||||
_, _, err := handleExecution(workflowId, shuffle.Workflow{ExecutingOrg: shuffle.OrgMini{Id: orgId}}, request)
|
||||
_, _, err := handleExecution(workflowId, shuffle.Workflow{ExecutingOrg: shuffle.OrgMini{Id: orgId}}, request, orgId)
|
||||
if err != nil {
|
||||
log.Printf("Failed to execute %s: %s", workflowId, err)
|
||||
}
|
||||
@@ -395,6 +395,40 @@ func handleGetStreamResults(resp http.ResponseWriter, request *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
for _, action := range workflowExecution.Workflow.Actions {
|
||||
found := false
|
||||
for _, result := range workflowExecution.Results {
|
||||
if result.Action.ID == action.ID {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
continue
|
||||
}
|
||||
|
||||
//log.Printf("[DEBUG] Maybe not handled yet: %s", action.ID)
|
||||
cacheId := fmt.Sprintf("%s_%s_result", workflowExecution.ExecutionId, action.ID)
|
||||
cache, err := shuffle.GetCache(ctx, cacheId)
|
||||
if err != nil {
|
||||
//log.Printf("[WARNING] Couldn't find in fix exec %s (2): %s", cacheId, err)
|
||||
continue
|
||||
}
|
||||
|
||||
actionResult := shuffle.ActionResult{}
|
||||
cacheData := []byte(cache.([]uint8))
|
||||
|
||||
// Just ensuring the data is good
|
||||
err = json.Unmarshal(cacheData, &actionResult)
|
||||
if err != nil {
|
||||
continue
|
||||
} else {
|
||||
log.Printf("[DEBUG] APPENDING %s result to send to app or something\n\n\n\n", action.ID)
|
||||
workflowExecution.Results = append(workflowExecution.Results, actionResult)
|
||||
}
|
||||
}
|
||||
|
||||
newjson, err := json.Marshal(workflowExecution)
|
||||
if err != nil {
|
||||
resp.WriteHeader(401)
|
||||
@@ -559,7 +593,8 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl
|
||||
}
|
||||
|
||||
//log.Printf("BASE LENGTH: %d", len(workflowExecution.Results))
|
||||
workflowExecution, dbSave, err := shuffle.ParsedExecutionResult(ctx, *workflowExecution, actionResult, false)
|
||||
|
||||
workflowExecution, dbSave, err := shuffle.ParsedExecutionResult(ctx, *workflowExecution, actionResult, false, 0)
|
||||
if err != nil {
|
||||
b, suberr := json.Marshal(actionResult)
|
||||
if suberr != nil {
|
||||
@@ -745,7 +780,7 @@ func deleteWorkflow(resp http.ResponseWriter, request *http.Request) {
|
||||
if item.TriggerType == "SCHEDULE" && item.Status != "uninitialized" {
|
||||
err = deleteSchedule(ctx, item.ID)
|
||||
if err != nil {
|
||||
log.Printf("Failed to delete schedule: %s - is it started?", err)
|
||||
log.Printf("[DEBUG] Failed to delete schedule: %s - is it started?", err)
|
||||
}
|
||||
} else if item.TriggerType == "WEBHOOK" {
|
||||
//err = removeWebhookFunction(ctx, item.ID)
|
||||
@@ -755,7 +790,7 @@ func deleteWorkflow(resp http.ResponseWriter, request *http.Request) {
|
||||
} else if item.TriggerType == "EMAIL" {
|
||||
err = shuffle.HandleOutlookSubRemoval(ctx, user, workflow.ID, item.ID)
|
||||
if err != nil {
|
||||
log.Printf("Failed to delete OUTLOOK email sub (checking gmail after): %s", err)
|
||||
log.Printf("[DEBUG] Failed to delete OUTLOOK email sub (checking gmail after): %s", err)
|
||||
}
|
||||
|
||||
err = shuffle.HandleGmailSubRemoval(ctx, user, workflow.ID, item.ID)
|
||||
@@ -763,14 +798,8 @@ func deleteWorkflow(resp http.ResponseWriter, request *http.Request) {
|
||||
log.Printf("Failed to delete gmail email sub: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
//err = increaseStatisticsField(ctx, "total_workflow_triggers", workflow.ID, -1, workflow.OrgId)
|
||||
//if err != nil {
|
||||
// log.Printf("Failed to increase total workflows: %s", err)
|
||||
//}
|
||||
}
|
||||
|
||||
// FIXME - maybe delete workflow executions
|
||||
err = shuffle.DeleteKey(ctx, "workflow", fileId)
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG]] Failed deleting key %s", fileId)
|
||||
@@ -780,11 +809,6 @@ func deleteWorkflow(resp http.ResponseWriter, request *http.Request) {
|
||||
}
|
||||
log.Printf("[INFO] Should have deleted workflow %s (%s)", workflow.Name, fileId)
|
||||
|
||||
//memcacheName := fmt.Sprintf("%s_%s", user.Username, fileId)
|
||||
//memcache.Delete(ctx, memcacheName)
|
||||
//memcacheName = fmt.Sprintf("%s_workflows", user.Username)
|
||||
//memcache.Delete(ctx, memcacheName)
|
||||
//cacheKey := fmt.Sprintf("%s_workflows", user.Id)
|
||||
cacheKey := fmt.Sprintf("%s_workflows", user.Id)
|
||||
shuffle.DeleteCache(ctx, cacheKey)
|
||||
log.Printf("[DEBUG] Cleared workflow cache for %s (%s)", user.Username, user.Id)
|
||||
@@ -830,7 +854,7 @@ func getWorkflowLocal(fileId string, request *http.Request) ([]byte, error) {
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func handleExecution(id string, workflow shuffle.Workflow, request *http.Request) (shuffle.WorkflowExecution, string, error) {
|
||||
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"))
|
||||
// _ = <-time.After(time.Second * 60)
|
||||
@@ -849,8 +873,12 @@ func handleExecution(id string, workflow shuffle.Workflow, request *http.Request
|
||||
}
|
||||
|
||||
if len(workflow.ExecutingOrg.Id) == 0 {
|
||||
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(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 {
|
||||
@@ -1048,6 +1076,10 @@ func cloudExecuteAction(execution shuffle.WorkflowExecution) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 1. Check CORS
|
||||
// 2. Check authentication
|
||||
// 3. Check authorization
|
||||
// 4. Run the actual function
|
||||
func executeWorkflow(resp http.ResponseWriter, request *http.Request) {
|
||||
cors := shuffle.HandleCors(resp, request)
|
||||
if cors {
|
||||
@@ -1104,8 +1136,8 @@ func executeWorkflow(resp http.ResponseWriter, request *http.Request) {
|
||||
|
||||
executionAuthValid, newOrgId = shuffle.RunExecuteAccessValidation(request, workflow)
|
||||
if !executionAuthValid {
|
||||
log.Printf("[INFO] Api authentication failed in execute workflow: %s", userErr)
|
||||
resp.WriteHeader(401)
|
||||
log.Printf("[INFO] Api authorization failed in execute workflow: %s", userErr)
|
||||
resp.WriteHeader(403)
|
||||
resp.Write([]byte(`{"success": false}`))
|
||||
return
|
||||
} else {
|
||||
@@ -1122,7 +1154,7 @@ func executeWorkflow(resp http.ResponseWriter, request *http.Request) {
|
||||
log.Printf("[AUDIT] Letting user %s execute %s because they're admin of the same org", user.Username, workflow.ID)
|
||||
} else {
|
||||
log.Printf("[AUDIT] Wrong user (%s) for workflow %s (execute)", user.Username, workflow.ID)
|
||||
resp.WriteHeader(401)
|
||||
resp.WriteHeader(403)
|
||||
resp.Write([]byte(`{"success": false}`))
|
||||
return
|
||||
}
|
||||
@@ -1133,7 +1165,7 @@ func executeWorkflow(resp http.ResponseWriter, request *http.Request) {
|
||||
|
||||
user.ActiveOrg.Users = []shuffle.UserMini{}
|
||||
workflow.ExecutingOrg = user.ActiveOrg
|
||||
workflowExecution, executionResp, err := handleExecution(fileId, *workflow, request)
|
||||
workflowExecution, executionResp, err := handleExecution(fileId, *workflow, request, user.ActiveOrg.Id)
|
||||
if err == nil {
|
||||
resp.WriteHeader(200)
|
||||
resp.Write([]byte(fmt.Sprintf(`{"success": true, "execution_id": "%s", "authorization": "%s"}`, workflowExecution.ExecutionId, workflowExecution.Authorization)))
|
||||
@@ -1384,15 +1416,14 @@ func stopScheduleGCP(resp http.ResponseWriter, request *http.Request) {
|
||||
}
|
||||
|
||||
func deleteSchedule(ctx context.Context, id string) error {
|
||||
log.Printf("Should stop schedule %s!", id)
|
||||
log.Printf("[DEBUG] Should stop schedule %s!", id)
|
||||
err := shuffle.DeleteKey(ctx, "schedules", id)
|
||||
if err != nil {
|
||||
log.Printf("Failed to delete schedule: %s", err)
|
||||
log.Printf("[ERROR] Failed to delete schedule: %s", err)
|
||||
return err
|
||||
} else {
|
||||
if value, exists := scheduledJobs[id]; exists {
|
||||
log.Printf("STOPPING THIS SCHEDULE: %s", id)
|
||||
// Looks like this does the trick? Hurr
|
||||
// Stops the schedule properly
|
||||
value.Lock()
|
||||
} else {
|
||||
// FIXME - allow it to kind of stop anyway?
|
||||
@@ -1495,14 +1526,19 @@ func scheduleWorkflow(resp http.ResponseWriter, request *http.Request) {
|
||||
|
||||
// Finds the startnode for the specific schedule
|
||||
startNode := ""
|
||||
for _, branch := range workflow.Branches {
|
||||
if branch.SourceID == schedule.Id {
|
||||
startNode = branch.DestinationID
|
||||
}
|
||||
}
|
||||
if schedule.Start != "" {
|
||||
startNode = schedule.Start
|
||||
} else {
|
||||
|
||||
if startNode == "" {
|
||||
startNode = workflow.Start
|
||||
for _, branch := range workflow.Branches {
|
||||
if branch.SourceID == schedule.Id {
|
||||
startNode = branch.DestinationID
|
||||
}
|
||||
}
|
||||
|
||||
if startNode == "" {
|
||||
startNode = workflow.Start
|
||||
}
|
||||
}
|
||||
|
||||
//log.Printf("Startnode: %s", startNode)
|
||||
@@ -2047,8 +2083,9 @@ func iterateOpenApiGithub(fs billy.Filesystem, dir []os.FileInfo, extra string,
|
||||
workflowapps, err := shuffle.GetAllWorkflowApps(ctx, 1000, 0)
|
||||
appCounter := 0
|
||||
if err != nil {
|
||||
log.Printf("Failed to get existing generated apps")
|
||||
log.Printf("[WARNING] Failed to get existing generated apps for OpenAPI verification: %s", err)
|
||||
}
|
||||
|
||||
for _, file := range dir {
|
||||
if len(onlyname) > 0 && file.Name() != onlyname {
|
||||
continue
|
||||
@@ -2606,7 +2643,7 @@ func executeSingleAction(resp http.ResponseWriter, request *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[INFO] Execution: %s should execute onprem with execution environment \"%s\". Workflow: %s", workflowExecution.ExecutionId, environment, workflowExecution.Workflow.ID)
|
||||
log.Printf("[INFO] Execution (single action): %s should execute onprem with execution environment \"%s\". Workflow: %s", workflowExecution.ExecutionId, environment, workflowExecution.Workflow.ID)
|
||||
|
||||
executionRequest := shuffle.ExecutionRequest{
|
||||
ExecutionId: workflowExecution.ExecutionId,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
#curl http://localhost:5001/api/v1/files/create -H "Authorization: Bearer db0373c6-1083-4dec-a05d-3ba73f02ccd4" -d '{"filename": "file.txt", "org_id": "b199646b-16d2-456d-9fd6-b9972e929466", "workflow_id": "global"}'
|
||||
#
|
||||
#echo
|
||||
curl http://localhost:5001/api/v1/apps/upload -H "Authorization: Bearer db0373c6-1083-4dec-a05d-3ba73f02ccd4" -F 'shuffle_file=@files.sh'
|
||||
#curl http://localhost:5001/api/v1/apps/upload -H "Authorization: Bearer db0373c6-1083-4dec-a05d-3ba73f02ccd4" -F 'shuffle_file=@files.sh'
|
||||
#
|
||||
#curl http://localhost:5001/api/v1/files/1915981b-b897-4db1-8a2e-44bc34cead3b/content -H "Authorization: Bearer db0373c6-1083-4dec-a05d-3ba73f02ccd4"
|
||||
#curl http://localhost:5001/api/v1/files/e19cffe4-e2da-47e9-809e-904f5cb03687 -H "Authorization: Bearer db0373c6-1083-4dec-a05d-3ba73f02ccd4"
|
||||
@@ -16,7 +16,7 @@ curl http://localhost:5001/api/v1/apps/upload -H "Authorization: Bearer db0373c6
|
||||
#r.HandleFunc("/api/v1/files/{fileId}", handleDeleteFile).Methods("DELETE", "OPTIONS")
|
||||
|
||||
|
||||
#curl http://localhost:5001/api/v1/files/create -H "Authorization: Bearer c5b4c827-65ec-47f4-9e8a-234cdba38959" -d '{"filename": "rule2.yar", "org_id": "b4e88fe9-352b-47b4-b280-960181670acf", "workflow_id": "global", "namespace": "yara"}'
|
||||
#curl http://localhost:5001/api/v1/files/5cb941ad-fa1c-4444-a685-92024b1fa31c/upload -H "Authorization: Bearer c5b4c827-65ec-47f4-9e8a-234cdba38959" -F 'shuffle_file=@upload.sh'
|
||||
#curl http://localhost:5001/api/v1/files/create -H "Authorization: Bearer 09627dcb-7e2a-4843-819b-417d268ff840" -d '{"filename": "rule2.yar", "org_id": "11f67b76-6051-4425-b0d6-be23daac6d12", "workflow_id": "global", "namespace": "yara"}'
|
||||
curl http://localhost:5002/api/v1/files/file_366ee8d2-1af6-4270-8639-213af30b4a29/upload -H "Authorization: Bearer 09627dcb-7e2a-4843-819b-417d268ff840" -F 'shuffle_file=@upload.sh'
|
||||
|
||||
#curl http://localhost:5001/api/v1/files/namespaces/yara -H "Authorization: Bearer c5b4c827-65ec-47f4-9e8a-234cdba38959" --output rules.zip
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
#curl -X POST "https://europe-west1-shuffle-241517.cloudfunctions.net/webhook_982995716e67c3a549092d3a3a7921cd" -H "Content-Type:application/json" -H "Authorization: Bearer 144308d0-6aab-4d4f-8bb2-75189281ee26" --data '{"name":"Keyboard Cat"}' -v
|
||||
|
||||
## GET HOOK
|
||||
#curl http://localhost:5001/api/v1/hooks/b4ba07c9-45d4-41f2-b260-83c8e99eba0c -H "Authorization: Bearer 144308d0-6aab-4d4f-8bb2-75189281ee26"
|
||||
#curl http://localhost:5001/api/v1/hooks/b4ba07c9-45d4-41f2-b260-83c8e99eba0c -H "Authorization: Bearer "
|
||||
|
||||
#curl https://shuffler.io/api/v1/hooks/b4ba07c9-45d4-41f2-b260-83c8e99eba0c -H "Authorization: Bearer 144308d0-6aab-4d4f-8bb2-75189281ee26"
|
||||
#curl https://shuffler.io/api/v1/hooks/b4ba07c9-45d4-41f2-b260-83c8e99eba0c -H "Authorization: Bearer "
|
||||
|
||||
|
||||
#curl -X POST "https://europe-west1-shuffle-241517.cloudfunctions.net/webhook_3ceff795-ce9a-43a2-a2f5-d4401a6e772d" -H "Authorization: Bearer 144308d0-6aab-4d4f-8bb2-75189281ee26" --data 'wut'
|
||||
|
||||
@@ -1 +1,4 @@
|
||||
#
|
||||
# hello
|
||||
this is line 2
|
||||
and 3
|
||||
Is it a python problem?
|
||||
|
||||
+11
-9
@@ -1,7 +1,7 @@
|
||||
version: '3'
|
||||
services:
|
||||
frontend:
|
||||
image: ghcr.io/frikky/shuffle-frontend:latest
|
||||
image: ghcr.io/shuffle/shuffle-frontend:latest
|
||||
container_name: shuffle-frontend
|
||||
hostname: shuffle-frontend
|
||||
ports:
|
||||
@@ -15,7 +15,7 @@ services:
|
||||
depends_on:
|
||||
- backend
|
||||
backend:
|
||||
image: ghcr.io/frikky/shuffle-backend:latest
|
||||
image: ghcr.io/shuffle/shuffle-backend:latest
|
||||
container_name: shuffle-backend
|
||||
hostname: ${BACKEND_HOSTNAME}
|
||||
# Here for debugging:
|
||||
@@ -29,11 +29,12 @@ services:
|
||||
- ${SHUFFLE_FILE_LOCATION}:/shuffle-files:z
|
||||
env_file: .env
|
||||
environment:
|
||||
#- DOCKER_HOST=tcp://docker-socket-proxy:2375
|
||||
- SHUFFLE_APP_HOTLOAD_FOLDER=/shuffle-apps
|
||||
- SHUFFLE_FILE_LOCATION=/shuffle-files
|
||||
restart: unless-stopped
|
||||
orborus:
|
||||
image: ghcr.io/frikky/shuffle-orborus:latest
|
||||
image: ghcr.io/shuffle/shuffle-orborus:latest
|
||||
container_name: shuffle-orborus
|
||||
hostname: shuffle-orborus
|
||||
networks:
|
||||
@@ -41,6 +42,7 @@ services:
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
environment:
|
||||
#- DOCKER_HOST=tcp://docker-socket-proxy:2375
|
||||
- SHUFFLE_WORKER_VERSION=latest
|
||||
- ENVIRONMENT_NAME=${ENVIRONMENT_NAME}
|
||||
- BASE_URL=http://${OUTER_HOSTNAME}:5001
|
||||
@@ -52,25 +54,22 @@ services:
|
||||
- HTTPS_PROXY=${HTTPS_PROXY}
|
||||
- SHUFFLE_PASS_WORKER_PROXY=${SHUFFLE_PASS_WORKER_PROXY}
|
||||
- SHUFFLE_PASS_APP_PROXY=${SHUFFLE_PASS_APP_PROXY}
|
||||
- SHUFFLE_SWARM_NETWORK_NAME=shuffle_swarm_executions
|
||||
- SHUFFLE_SCALE_REPLICAS=1
|
||||
- SHUFFLE_SWARM_CONFIG=runn
|
||||
restart: unless-stopped
|
||||
security_opt:
|
||||
- seccomp:unconfined
|
||||
opensearch:
|
||||
image: opensearchproject/opensearch:1.2.4
|
||||
image: opensearchproject/opensearch:2.4.0
|
||||
hostname: shuffle-opensearch
|
||||
container_name: shuffle-opensearch
|
||||
environment:
|
||||
- bootstrap.memory_lock=true
|
||||
- "OPENSEARCH_JAVA_OPTS=-Xms1024m -Xmx1024m" # minimum and maximum Java heap size, recommend setting both to 50% of system RAM
|
||||
- cluster.initial_master_nodes=shuffle-opensearch
|
||||
- cluster.routing.allocation.disk.threshold_enabled=false
|
||||
- cluster.name=shuffle-cluster
|
||||
- node.name=shuffle-opensearch
|
||||
- discovery.seed_hosts=shuffle-opensearch
|
||||
- cluster.initial_master_nodes=shuffle-opensearch
|
||||
- node.store.allow_mmap=false
|
||||
- discovery.seed_hosts=shuffle-opensearch
|
||||
ulimits:
|
||||
memlock:
|
||||
soft: -1
|
||||
@@ -87,6 +86,8 @@ services:
|
||||
restart: unless-stopped
|
||||
#docker-socket-proxy:
|
||||
# image: tecnativa/docker-socket-proxy
|
||||
# container_name: shuffle-frontend
|
||||
# hostname: docker-socket-proxy
|
||||
# privileged: true
|
||||
# environment:
|
||||
# - SERVICES=1
|
||||
@@ -105,6 +106,7 @@ services:
|
||||
# - POST=1
|
||||
# - AUTH=1
|
||||
# - SECRETS=1
|
||||
# - SWARM=1
|
||||
# volumes:
|
||||
# - /var/run/docker.sock:/var/run/docker.sock
|
||||
# networks:
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ ENV PATH /usr/src/app/node_modules/.bin:$PATH
|
||||
COPY package.json /usr/src/app/package.json
|
||||
|
||||
RUN yarn config set "strict-ssl" false -g
|
||||
RUN yarn install
|
||||
RUN yarn install --network-timeout 1000000
|
||||
|
||||
# copy only required files to not trigger rebuilding every time
|
||||
COPY ./certs /usr/src/app/certs/
|
||||
|
||||
+12
-1
@@ -1,4 +1,4 @@
|
||||
# Certificate:
|
||||
## Localhost Certificate info:
|
||||
|
||||
Creating a localhost certificate:
|
||||
|
||||
@@ -7,3 +7,14 @@ openssl genrsa -out privkey.pem 2048
|
||||
openssl req -new -key privkey.pem -out certreq.csr
|
||||
openssl x509 -req -days 3650 -in certreq.csr -signkey privkey.pem -out fullchain.pem
|
||||
```
|
||||
|
||||
## Using your own certificate
|
||||
If you have your own .crt and .key file, you can do it like this:
|
||||
```
|
||||
openssl x509 -in mycert.crt -out fullchain.cert.pem -outform PEM
|
||||
```
|
||||
|
||||
The KEY file has to be named privkey.pem
|
||||
```
|
||||
mv cert.key privkey.pem
|
||||
```
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "shuffler",
|
||||
"homepage": "https://shuffler.io",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.15.8",
|
||||
@@ -9,7 +9,6 @@
|
||||
"@emotion/react": "^11.7.0",
|
||||
"@emotion/styled": "^11.6.0",
|
||||
"@material-ui/core": "^4.5.2",
|
||||
"@material-ui/data-grid": "^4.0.0-alpha.22",
|
||||
"@material-ui/icons": "^4.5.1",
|
||||
"@material-ui/lab": "^4.0.0-alpha.58",
|
||||
"@material-ui/styles": "^4.5.2",
|
||||
@@ -17,8 +16,10 @@
|
||||
"@metamask/detect-provider": "^1.2.0",
|
||||
"@mui/icons-material": "^5.2.1",
|
||||
"@mui/material": "^5.2.3",
|
||||
"@mui/x-data-grid": "^5.17.11",
|
||||
"@uiw/react-codemirror": "^3.2.1",
|
||||
"@use-it/interval": "^1.0.0",
|
||||
"algoliasearch": "^4.13.1",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"class-transformer": "^0.4.0",
|
||||
"create-react-app": "^4.0.3",
|
||||
@@ -46,6 +47,7 @@
|
||||
"react": "^16.14.0",
|
||||
"react-alert": "^5.5.0",
|
||||
"react-alert-template-basic": "^1.0.0",
|
||||
"react-alice-carousel": "^2.6.4",
|
||||
"react-avatar-editor": "^11.1.0",
|
||||
"react-beforeunload": "^2.2.1",
|
||||
"react-chartjs-2": "^2.11.1",
|
||||
@@ -58,6 +60,7 @@
|
||||
"react-dropzone": "^10.1.10",
|
||||
"react-ga": "^2.7.0",
|
||||
"react-iframe": "^1.8.0",
|
||||
"react-instantsearch-dom": "^6.28.0",
|
||||
"react-json-pretty": "^2.2.0",
|
||||
"react-json-view": "^1.19.1",
|
||||
"react-markdown": "^4.2.2",
|
||||
@@ -69,6 +72,7 @@
|
||||
"react-shepherd": "^3.3.6",
|
||||
"reactstrap": "^7.1.0",
|
||||
"reaviz": "^12.1.0",
|
||||
"search-insights": "^2.2.1",
|
||||
"shellwords": "^0.1.1",
|
||||
"simplebar": "^4.2.3",
|
||||
"styled-components": "^4.4.0",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 154 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 69 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 79 KiB |
@@ -6,6 +6,7 @@ docker rm shuffle-frontend
|
||||
echo "Running build for website"
|
||||
#sudo npm run build
|
||||
docker build . -t ghcr.io/frikky/shuffle-frontend:nightly
|
||||
docker tag ghcr.io/frikky/shuffle-frontend:nightly ghcr.io/shuffle/shuffle-frontend:nightly
|
||||
|
||||
echo "Starting server"
|
||||
# Rerun build locally for it to update :)
|
||||
|
||||
+52
-11
@@ -15,13 +15,16 @@ import theme from "./theme";
|
||||
import Apps from "./views/Apps";
|
||||
import AppCreator from "./views/AppCreator";
|
||||
|
||||
import Welcome from "./views/Welcome.jsx";
|
||||
import Dashboard from "./views/Dashboard.jsx";
|
||||
import DashboardView from "./views/DashboardViews.jsx";
|
||||
import AdminSetup from "./views/AdminSetup";
|
||||
import Admin from "./views/Admin";
|
||||
import Docs from "./views/Docs";
|
||||
import Introduction from "./views/Introduction";
|
||||
import SetAuthentication from "./views/SetAuthentication";
|
||||
import SetAuthenticationSSO from "./views/SetAuthenticationSSO";
|
||||
import Search from "./views/Search.jsx";
|
||||
|
||||
import LandingPageNew from "./views/LandingpageNew";
|
||||
import LoginPage from "./views/LoginPage";
|
||||
@@ -40,6 +43,7 @@ import { isMobile } from "react-device-detect";
|
||||
|
||||
import detectEthereumProvider from "@metamask/detect-provider";
|
||||
import Drift from "react-driftjs";
|
||||
import DashboardPage from "./views/TempDashboard.jsx";
|
||||
|
||||
// Production - backend proxy forwarding in nginx
|
||||
var globalUrl = window.location.origin;
|
||||
@@ -50,11 +54,12 @@ if (window.location.port === "3000") {
|
||||
//globalUrl = "http://localhost:5002"
|
||||
}
|
||||
|
||||
if (globalUrl.includes("githubpreview.dev")) {
|
||||
// Development on Github Codespaces
|
||||
if (globalUrl.includes("app.github.dev")) {
|
||||
//globalUrl = globalUrl.replace("3000", "5001")
|
||||
globalUrl = "https://frikky-shuffle-5gvr4xx62w64-5001.githubpreview.dev"
|
||||
globalUrl = "https://frikky-shuffle-5gvr4xx62w64-5001.preview.app.github.dev"
|
||||
}
|
||||
console.log("global: ", globalUrl)
|
||||
//console.log("global: ", globalUrl)
|
||||
|
||||
const App = (message, props) => {
|
||||
|
||||
@@ -64,11 +69,7 @@ const App = (message, props) => {
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false)
|
||||
const [dataset, setDataset] = useState(false)
|
||||
const [isLoaded, setIsLoaded] = useState(false)
|
||||
const [curpath, setCurpath] = useState(
|
||||
typeof window === "undefined" || window.location === undefined
|
||||
? ""
|
||||
: window.location.pathname
|
||||
)
|
||||
const [curpath, setCurpath] = useState(typeof window === "undefined" || window.location === undefined ? "" : window.location.pathname)
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
@@ -130,7 +131,7 @@ const App = (message, props) => {
|
||||
.then((responseJson) => {
|
||||
var userInfo = {};
|
||||
if (responseJson.success === true) {
|
||||
console.log(responseJson);
|
||||
//console.log("USER: ", responseJson);
|
||||
|
||||
userInfo = responseJson;
|
||||
setIsLoggedIn(true);
|
||||
@@ -303,6 +304,7 @@ const App = (message, props) => {
|
||||
>
|
||||
<ScrollToTop
|
||||
getUserNotifications={getUserNotifications}
|
||||
curpath={curpath}
|
||||
setCurpath={setCurpath}
|
||||
/>
|
||||
{!isLoaded ? null :
|
||||
@@ -310,7 +312,7 @@ const App = (message, props) => {
|
||||
<Drift
|
||||
appId="zfk9i7w3yizf"
|
||||
attributes={{
|
||||
name: userdata.username === undefined || userdata.username === null ? "OSS user" : `${userdata.username} - OSS`,
|
||||
name: userdata.username === undefined || userdata.username === null ? "OSS user" : `OSS ${userdata.username}`,
|
||||
}}
|
||||
eventHandlers={[
|
||||
{
|
||||
@@ -319,7 +321,6 @@ const App = (message, props) => {
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
}
|
||||
<Header
|
||||
notifications={notifications}
|
||||
@@ -370,6 +371,7 @@ const App = (message, props) => {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route exact path="/search" element={<Search serverside={false} isLoaded={isLoaded} userdata={userdata} globalUrl={globalUrl} surfaceColor={theme.palette.surfaceColor} inputColor={theme.palette.inputColor} {...props} /> } />
|
||||
<Route
|
||||
exact
|
||||
path="/admin/:key"
|
||||
@@ -648,6 +650,45 @@ const App = (message, props) => {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/testdashboard"
|
||||
element={
|
||||
<DashboardPage
|
||||
isLoaded={isLoaded}
|
||||
globalUrl={globalUrl}
|
||||
{...props}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/dashboards"
|
||||
element={
|
||||
<DashboardView
|
||||
isLoaded={isLoaded}
|
||||
isLoggedIn={isLoggedIn}
|
||||
globalUrl={globalUrl}
|
||||
{...props}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/welcome"
|
||||
element={
|
||||
<Welcome
|
||||
cookies={cookies}
|
||||
removeCookie={removeCookie}
|
||||
isLoaded={isLoaded}
|
||||
isLoggedIn={isLoggedIn}
|
||||
globalUrl={globalUrl}
|
||||
cookies={cookies}
|
||||
userdata={userdata}
|
||||
{...props}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/"
|
||||
|
||||
+463
-75
@@ -1,13 +1,17 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
import { securityFramework } from "./LandingpageUsecases.jsx";
|
||||
import CytoscapeComponent from 'react-cytoscapejs';
|
||||
import frameworkStyle from '../frameworkStyle.jsx';
|
||||
import WorkflowSearch from './Workflowsearch.jsx';
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import theme from '../theme';
|
||||
import { useAlert } from "react-alert";
|
||||
|
||||
import AppSearch from '../components/Appsearch.jsx';
|
||||
import PaperComponent from "../components/PaperComponent.jsx"
|
||||
import { usecaseTypes } from "../components/UsecaseSearch.jsx"
|
||||
import SuggestedWorkflows from "../components/SuggestedWorkflows.jsx"
|
||||
import { securityFramework} from "../components/LandingpageUsecases.jsx";
|
||||
|
||||
import {
|
||||
Paper,
|
||||
Typography,
|
||||
@@ -16,6 +20,7 @@ import {
|
||||
Badge,
|
||||
CircularProgress,
|
||||
Tooltip,
|
||||
Dialog,
|
||||
} from "@material-ui/core";
|
||||
|
||||
import {
|
||||
@@ -517,21 +522,192 @@ export const usecases = {
|
||||
}
|
||||
}
|
||||
|
||||
const Framework = (props) => {
|
||||
const {globalUrl, isLoaded, showOptions, selectedOption, rolling, frameworkData, size, inputUsecase, isLoggedIn, } = props;
|
||||
const AppFramework = (props) => {
|
||||
const { globalUrl, isLoaded, showOptions, selectedOption, rolling, frameworkData, setFrameworkData, size, inputUsecase, isLoggedIn, color, discoveryWrapper, setDiscoveryWrapper, userdata, apps, inputUsecases, setInputUsecases } = props;
|
||||
const [cy, setCy] = React.useState()
|
||||
const [edgesStarted, setEdgesStarted] = React.useState(false)
|
||||
const [graphDone, setGraphDone] = React.useState(false)
|
||||
const [cyDone, setCyDone] = React.useState(false)
|
||||
const [discoveryData, setDiscoveryData] = React.useState({})
|
||||
const [selectionOpen, setSelectionOpen] = React.useState(true)
|
||||
const [frameworkSuggestions, setFrameworkSuggestions] = React.useState([])
|
||||
const [newSelectedApp, setNewSelectedApp] = React.useState({})
|
||||
const [defaultSearch, setDefaultSearch] = React.useState("")
|
||||
const [animationStarted, setAnimationStarted] = React.useState(false)
|
||||
const [paperTitle, setPaperTitle] = React.useState("")
|
||||
const [changedApp, setChangedApp] = React.useState("")
|
||||
|
||||
|
||||
const [usecaseType, setUsecaseType] = React.useState(0)
|
||||
const [selectedUsecase, setSelectedUsecase] = React.useState(selectedOption !== undefined ? selectedOption : "Phishing")
|
||||
|
||||
const scale = size === undefined ? 1 : size > 5 ? 3 : size
|
||||
|
||||
const alert = useAlert()
|
||||
|
||||
const showRecommendations = (changed, frameworkData) => {
|
||||
console.log("Inside recommendation loader")
|
||||
setChangedApp(changed)
|
||||
|
||||
// Alternative changed
|
||||
// This is for secondary values like email = comms
|
||||
var alternativeChanged = changed
|
||||
if (changed == "COMMS") {
|
||||
alternativeChanged = "email"
|
||||
}
|
||||
|
||||
// FIX:
|
||||
// 0. Get workflows loaded in from usecasesearch
|
||||
// 1. Search through workflow templates for matching app types
|
||||
// 2. Validate if template is already in use~ (workflows with same tools)
|
||||
// 3. Generate the workflow(s) - PS: Fix new workflow templates
|
||||
// 4. Moving on!
|
||||
|
||||
// How can we load templates? UsecaseSearch?
|
||||
var showusecases = []
|
||||
//const foundusecase = usecaseTypes.find(data => data.name.toLowerCase() === defaultSearch.toLowerCase())
|
||||
for (var key in usecaseTypes) {
|
||||
for (var subkey in usecaseTypes[key].value) {
|
||||
const usecase = usecaseTypes[key].value[subkey]
|
||||
|
||||
if (usecase.active === false) {
|
||||
continue
|
||||
}
|
||||
|
||||
var potential = false
|
||||
var matches = []
|
||||
for (var itemtype in usecase.items) {
|
||||
var apptype = usecase.items[itemtype].app_type.toLowerCase()
|
||||
if (apptype.toLowerCase() === "email" || apptype.toLowerCase() === "comms" || apptype === "communication") {
|
||||
apptype = "Comms"
|
||||
}
|
||||
//console.log("OLD: ", changed, "USECASE: ", apptype)
|
||||
|
||||
//console.log("APptype, changed, framework: ", apptype.toLowerCase(), alternativeChanged.toLowerCase(), changed.toLowerCase(), frameworkData)
|
||||
if (changed.toLowerCase() === apptype.toLowerCase() || changed.toLowerCase().includes(apptype.toLowerCase()) || alternativeChanged.toLowerCase() === apptype.toLowerCase() || alternativeChanged.toLowerCase().includes(apptype.toLowerCase())) {
|
||||
potential = true
|
||||
console.log("Potential: !", apptype)
|
||||
|
||||
if (frameworkData[apptype] !== undefined && frameworkData[apptype].name !== undefined && frameworkData[apptype].name !== null && frameworkData[apptype].name.length > 0) {
|
||||
usecase.items[itemtype].app = frameworkData[apptype]
|
||||
}
|
||||
|
||||
matches.push(usecase.items[itemtype])
|
||||
} else {
|
||||
// Check if the type is done in frameworkData
|
||||
if (frameworkData[apptype] !== undefined) {
|
||||
//console.log("NOT UNDEFINED: ", frameworkData[apptype])
|
||||
if (frameworkData[apptype].name !== undefined && frameworkData[apptype].name !== null && frameworkData[apptype].name.length > 0) {
|
||||
//console.log("FOUND: ", frameworkData[apptype])
|
||||
usecase.items[itemtype].app = frameworkData[apptype]
|
||||
|
||||
//console.log("Real app!")
|
||||
matches.push(usecase.items[itemtype])
|
||||
}
|
||||
|
||||
//if (frameworkData[apptype] !== undefined) {
|
||||
} else {
|
||||
console.log("UNDEFINED APP (bad name?): ", apptype)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Adds to list if it's all matching and unhandled
|
||||
if (potential) {
|
||||
// Check finished usecases.
|
||||
if (inputUsecases !== undefined && setInputUsecases !== undefined && usecase.usecase_references !== undefined && usecase.usecase_references.length > 0) {
|
||||
var foundUsecase = false
|
||||
for (var usecaseKey in inputUsecases) {
|
||||
const usecaseCategory = inputUsecases[usecaseKey]
|
||||
for (var subUsecaseKey in usecaseCategory.list) {
|
||||
const loopUsecase = usecaseCategory.list[subUsecaseKey]
|
||||
if (loopUsecase.matches === undefined || loopUsecase.matches === null || loopUsecase.matches.length === 0) {
|
||||
//console.log("No matches - continuing")
|
||||
continue
|
||||
}
|
||||
|
||||
if (usecase.usecase_references.includes(loopUsecase.name)) {
|
||||
foundUsecase = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (foundUsecase) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundUsecase) {
|
||||
console.log("Usecase NOT found!")
|
||||
} else {
|
||||
console.log("FOUND usecase existing in ", usecase.usecase_references)
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
console.log("No usecase to try to match it to (usecase.usecase_references in UsecaseSearch)")
|
||||
}
|
||||
|
||||
console.log("Usecase: ", usecase)
|
||||
if (matches.length === usecase.items.length) {
|
||||
usecase.color = "#c51152"
|
||||
usecase.type = usecaseTypes[key].name
|
||||
showusecases.push(usecase)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: Check if a usecase has already been handled
|
||||
console.log("")
|
||||
console.log("GOT USECASES: ", showusecases)
|
||||
|
||||
// FIXME: Just showing one usecase at a time for now
|
||||
if (showusecases.length > 0) {
|
||||
setFrameworkSuggestions(showusecases.slice(0,1))
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
console.log("DISCWRAP CHANG: ", discoveryWrapper)
|
||||
if (discoveryWrapper === undefined || discoveryWrapper.id === "SHUFFLE" || discoveryWrapper.id === undefined || cy === undefined) {
|
||||
setDiscoveryData({})
|
||||
|
||||
if (cy !== undefined) {
|
||||
cy.nodes().unselect()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Find the node and click it?
|
||||
//setTimeout(() => {
|
||||
const nodes = cy.nodes().jsons()
|
||||
for (var key in nodes) {
|
||||
const node = nodes[key]
|
||||
var newSearchName = discoveryWrapper.id.valueOf()
|
||||
if (newSearchName === "EMAIL") {
|
||||
newSearchName = "COMMS"
|
||||
}
|
||||
|
||||
if (newSearchName === "ERADICATION" || newSearchName === "ENDPOINT") {
|
||||
newSearchName = "EDR & AV"
|
||||
}
|
||||
|
||||
if (node.data.id === newSearchName) {
|
||||
const tmpnode = cy.getElementById(node.data.id)
|
||||
if (tmpnode !== undefined) {
|
||||
tmpnode.select()
|
||||
}
|
||||
|
||||
setDefaultSearch(discoveryWrapper.id)
|
||||
setPaperTitle(discoveryWrapper.id)
|
||||
}
|
||||
}
|
||||
//}, 50,)
|
||||
//setDiscoveryData(discoveryWrapper)
|
||||
}, [discoveryWrapper])
|
||||
|
||||
const setUsecaseItem = (inputUsecase) => {
|
||||
var parsedUsecase = inputUsecase
|
||||
const edges = cy.edges().jsons()
|
||||
@@ -576,7 +752,41 @@ const Framework = (props) => {
|
||||
})
|
||||
}
|
||||
|
||||
const activateApp = (appid) => {
|
||||
fetch(globalUrl+"/api/v1/apps/"+appid+"/activate", {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
},
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status !== 200) {
|
||||
console.log("Failed to activate")
|
||||
}
|
||||
|
||||
return response.json()
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson.success === false) {
|
||||
alert.error("Failed to activate the app")
|
||||
} else {
|
||||
//alert.success("App activated for your organization! Refresh the page to use the app.")
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
//alert.error(error.toString())
|
||||
console.log("Activate app error: ", error.toString())
|
||||
});
|
||||
}
|
||||
|
||||
const setFrameworkItem = (data) => {
|
||||
console.log("Setting framework item: ", data, isCloud)
|
||||
if (!isCloud) {
|
||||
activateApp(data.id)
|
||||
}
|
||||
|
||||
fetch(globalUrl + "/api/v1/apps/frameworkConfiguration", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -613,11 +823,22 @@ const Framework = (props) => {
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (discoveryData.id === undefined) {
|
||||
console.log("New selected app: ", newSelectedApp, discoveryData)
|
||||
if (newSelectedApp.objectID === undefined || newSelectedApp.objectID === undefined || newSelectedApp.objectID.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
if (newSelectedApp.objectID === undefined || newSelectedApp.objectID === undefined || newSelectedApp.objectID.length === 0) {
|
||||
//if (paperTitle.length > 0) {
|
||||
// console.log("No papertitle (parent button)")
|
||||
|
||||
// cy.elements().unselect()
|
||||
// return
|
||||
//}
|
||||
|
||||
if (discoveryData.id === undefined) {
|
||||
console.log("No discoverydata (parent button)")
|
||||
|
||||
cy.elements().unselect()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -625,8 +846,11 @@ const Framework = (props) => {
|
||||
"type": discoveryData.id,
|
||||
"name": newSelectedApp.name,
|
||||
"id": newSelectedApp.objectID,
|
||||
"large_image": newSelectedApp.image_url,
|
||||
"description": newSelectedApp.description === undefined ? "" : newSelectedApp.description,
|
||||
}
|
||||
|
||||
|
||||
const foundelement = cy.getElementById(discoveryData.id)
|
||||
if (foundelement !== undefined && foundelement !== null) {
|
||||
foundelement.data("large_image", newSelectedApp.image_url)
|
||||
@@ -637,7 +861,35 @@ const Framework = (props) => {
|
||||
foundelement.data("height", `${85*scale}px`)
|
||||
}
|
||||
|
||||
if (setFrameworkData !== undefined) {
|
||||
// Find discoveryData.id
|
||||
var keys = []
|
||||
for (const [key, value] of Object.entries(frameworkData)) {
|
||||
if (key.toLowerCase() === discoveryData.id.toLowerCase()) {
|
||||
keys.push(key)
|
||||
}
|
||||
}
|
||||
|
||||
if (keys.length === 0) {
|
||||
console.log("Failed to find: ", discoveryData.id, " IN ", frameworkData)
|
||||
} else {
|
||||
for (var key in keys) {
|
||||
frameworkData[keys[key]] = submitValue
|
||||
}
|
||||
|
||||
console.log("Frameworkdata: ", frameworkData)
|
||||
setFrameworkData(frameworkData)
|
||||
|
||||
if (discoveryData.large_image !== undefined && discoveryData.large_image !== null && discoveryData.large_image.includes("storage.googleapis.com")) {
|
||||
showRecommendations(discoveryData.id, frameworkData)
|
||||
} else {
|
||||
console.log("Skipping recommendations during unselect")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setFrameworkItem(submitValue)
|
||||
cy.elements().unselect()
|
||||
|
||||
}, [newSelectedApp])
|
||||
|
||||
@@ -647,52 +899,105 @@ const Framework = (props) => {
|
||||
window.location.host === "shuffler.io";
|
||||
|
||||
const imgSize = 50;
|
||||
var parsedFrameworkData = frameworkData
|
||||
var parsedFrameworkData = frameworkData === undefined ? {} : frameworkData
|
||||
|
||||
// Awful mapping to make sure all access is always there
|
||||
if (frameworkData !== undefined) {
|
||||
if (frameworkData.cases !== undefined) {
|
||||
frameworkData.Cases = frameworkData.cases
|
||||
}
|
||||
if (frameworkData.siem !== undefined) {
|
||||
frameworkData.SIEM = frameworkData.siem
|
||||
}
|
||||
if (frameworkData.assets !== undefined) {
|
||||
frameworkData.Assets = frameworkData.assets
|
||||
}
|
||||
if (frameworkData.intel !== undefined) {
|
||||
frameworkData.Intel = frameworkData.intel
|
||||
}
|
||||
if (frameworkData.communication !== undefined) {
|
||||
frameworkData.Comms = frameworkData.communication
|
||||
}
|
||||
if (frameworkData.network !== undefined) {
|
||||
frameworkData.Network = frameworkData.network
|
||||
}
|
||||
if (frameworkData.iam !== undefined) {
|
||||
frameworkData.IAM = frameworkData.iam
|
||||
}
|
||||
if (frameworkData.edr !== undefined) {
|
||||
frameworkData["EDR & AV"] = frameworkData.edr
|
||||
if (frameworkData.cases.large_image === undefined && frameworkData.cases.large_image === null || frameworkData.cases.large_image === "") {
|
||||
frameworkData.cases = {}
|
||||
}
|
||||
|
||||
parsedFrameworkData.Cases = frameworkData.cases
|
||||
} else {
|
||||
parsedFrameworkData.Cases = {}
|
||||
}
|
||||
|
||||
parsedFrameworkData = frameworkData
|
||||
} else {
|
||||
console.log("No frameworkdata: ")
|
||||
parsedFrameworkData = {
|
||||
"Cases": {},
|
||||
"SIEM": {},
|
||||
"Assets": {},
|
||||
"IAM": {},
|
||||
"Intel": {},
|
||||
"Comms": {},
|
||||
"Network": {},
|
||||
"EDR & AV": {},
|
||||
if (frameworkData.siem !== undefined) {
|
||||
if (frameworkData.siem.large_image === undefined && frameworkData.siem.large_image === null || frameworkData.siem.large_image === "") {
|
||||
frameworkData.siem = {}
|
||||
}
|
||||
|
||||
parsedFrameworkData.SIEM = frameworkData.siem
|
||||
} else {
|
||||
parsedFrameworkData.SIEM = {}
|
||||
}
|
||||
|
||||
if (frameworkData.assets !== undefined) {
|
||||
if (frameworkData.assets.large_image === undefined && frameworkData.assets.large_image === null || frameworkData.assets.large_image === "") {
|
||||
frameworkData.assets = {}
|
||||
}
|
||||
|
||||
parsedFrameworkData.Assets = frameworkData.assets
|
||||
} else {
|
||||
parsedFrameworkData.Assets = {}
|
||||
}
|
||||
|
||||
if (frameworkData.intel !== undefined) {
|
||||
if (frameworkData.intel.large_image === undefined && frameworkData.intel.large_image === null || frameworkData.intel.large_image === "") {
|
||||
frameworkData.intel = {}
|
||||
}
|
||||
|
||||
parsedFrameworkData.Intel = frameworkData.intel
|
||||
} else {
|
||||
parsedFrameworkData.Intel= {}
|
||||
}
|
||||
|
||||
if (frameworkData.communication !== undefined) {
|
||||
if (frameworkData.communication.large_image === undefined && frameworkData.communication.large_image === null || frameworkData.communication.large_image === "") {
|
||||
frameworkData.communication = {}
|
||||
}
|
||||
|
||||
parsedFrameworkData.Comms = frameworkData.communication
|
||||
} else {
|
||||
parsedFrameworkData.Comms = {}
|
||||
}
|
||||
|
||||
if (frameworkData.network !== undefined) {
|
||||
if (frameworkData.network.large_image === undefined && frameworkData.network.large_image === null || frameworkData.network.large_image === "") {
|
||||
frameworkData.network = {}
|
||||
}
|
||||
|
||||
parsedFrameworkData.Network = frameworkData.network
|
||||
} else {
|
||||
parsedFrameworkData.Network = {}
|
||||
}
|
||||
|
||||
if (frameworkData.iam !== undefined) {
|
||||
if (frameworkData.iam.large_image === undefined && frameworkData.iam.large_image === null || frameworkData.iam.large_image === "") {
|
||||
frameworkData.iam = {}
|
||||
}
|
||||
|
||||
parsedFrameworkData.IAM = frameworkData.iam
|
||||
} else {
|
||||
parsedFrameworkData.IAM = {}
|
||||
}
|
||||
|
||||
if (frameworkData.edr !== undefined) {
|
||||
if (frameworkData.edr.large_image === undefined && frameworkData.edr.large_image === null || frameworkData.edr.large_image === "") {
|
||||
frameworkData.edr = {}
|
||||
}
|
||||
|
||||
parsedFrameworkData["EDR & AV"] = frameworkData.edr
|
||||
} else {
|
||||
parsedFrameworkData["EDR & AV"] = {}
|
||||
}
|
||||
|
||||
} else {
|
||||
//console.log("No frameworkdata for org! Setting default")
|
||||
parsedFrameworkData["Cases"] = {}
|
||||
parsedFrameworkData["SIEM"] = {}
|
||||
parsedFrameworkData["Assets"] = {}
|
||||
parsedFrameworkData["IAM"] = {}
|
||||
parsedFrameworkData["Intel"] = {}
|
||||
parsedFrameworkData["Comms"] = {}
|
||||
parsedFrameworkData["Network"] = {}
|
||||
parsedFrameworkData["EDR & AV"] = {}
|
||||
}
|
||||
|
||||
//console.log("Framework - update? ", parsedFrameworkData)
|
||||
|
||||
// 0 = automated, 1 = manual
|
||||
const [usecaseType, setUsecaseType] = React.useState(0)
|
||||
const [selectedUsecase, setSelectedUsecase] = React.useState(selectedOption !== undefined ? selectedOption : "Phishing")
|
||||
|
||||
const elements = []
|
||||
const surfaceColor = "#27292D"
|
||||
@@ -865,14 +1170,61 @@ const Framework = (props) => {
|
||||
)
|
||||
}
|
||||
|
||||
const onNodeUnselect = (event) => {
|
||||
var data = event.target.data();
|
||||
console.log("UNSELECT: ", data)
|
||||
|
||||
var parsedStyle = {
|
||||
"border-width": "10px",
|
||||
"border-opacity": ".7",
|
||||
"border-color": "#7fe57f",
|
||||
}
|
||||
|
||||
// Some error here?
|
||||
if (event.target !== undefined && event.target !== null) {
|
||||
event.target.animate(
|
||||
{
|
||||
style: parsedStyle,
|
||||
},
|
||||
{
|
||||
duration: animationDuration,
|
||||
}
|
||||
)
|
||||
|
||||
setTimeout(() => {
|
||||
event.target.animate(
|
||||
{
|
||||
style: {
|
||||
"border-width": "3px",
|
||||
},
|
||||
},
|
||||
{
|
||||
duration: animationDuration,
|
||||
}
|
||||
)
|
||||
}, 2500)
|
||||
}
|
||||
|
||||
//setDiscoveryData({})
|
||||
setDiscoveryWrapper({})
|
||||
setSelectionOpen(false)
|
||||
setDefaultSearch("")
|
||||
setPaperTitle("")
|
||||
|
||||
//setDiscoveryData({})
|
||||
}
|
||||
|
||||
const onNodeSelect = (event) => {
|
||||
const data = event.target.data();
|
||||
var data = event.target.data();
|
||||
console.log("Node: ", data)
|
||||
if (data.id === "SHUFFLE") {
|
||||
event.target.unselect()
|
||||
return
|
||||
}
|
||||
|
||||
if (data.label === "EDR & AV") {
|
||||
data.label = "ERADICATION"
|
||||
}
|
||||
|
||||
setDiscoveryData(data)
|
||||
setSelectionOpen(true)
|
||||
@@ -908,6 +1260,9 @@ const Framework = (props) => {
|
||||
cy.on("select", "node", (e) => {
|
||||
onNodeSelect(e)
|
||||
})
|
||||
cy.on("unselect", "node", (e) => {
|
||||
onNodeUnselect(e)
|
||||
})
|
||||
|
||||
cy.on("mouseover", "node", (e) => {onNodeHover(e)})
|
||||
cy.on("mouseout", "node", (e) => onNodeHoverOut(e));
|
||||
@@ -924,10 +1279,7 @@ const Framework = (props) => {
|
||||
const shiftmodifier = 3*scale
|
||||
//const svgSize = `${40*scale}px`
|
||||
const svgSize = `${40}px`
|
||||
console.log("Size: ", svgSize)
|
||||
|
||||
|
||||
console.log("Framework: ", parsedFrameworkData)
|
||||
const fontSize = `${12*scale}px`
|
||||
const defaultSize = `${85*scale}px`
|
||||
const iconSize = `${45*scale}px`
|
||||
@@ -1121,10 +1473,10 @@ const Framework = (props) => {
|
||||
description: parsedFrameworkData.Network.description === undefined ? "" : parsedFrameworkData.Network.description,
|
||||
app_id: parsedFrameworkData.Network.id === undefined ? "" : parsedFrameworkData.Network.id,
|
||||
text_margin_y: parsedFrameworkData.Network.large_image === undefined ? textMarginDefault : textMarginImage,
|
||||
margin_x: parsedFrameworkData.Network.large_image === undefined ? `${32*scale}px` : "0px",
|
||||
margin_y: parsedFrameworkData.Network.large_image === undefined ? `${19*scale}px` : `0px`,
|
||||
width: parsedFrameworkData.Network.large_image === undefined ? iconSize : defaultSize,
|
||||
height: parsedFrameworkData.Network.large_image === undefined ? iconSize : defaultSize,
|
||||
margin_x: parsedFrameworkData.Network.large_image === undefined ? `${32*scale}px` : "0px",
|
||||
margin_y: parsedFrameworkData.Network.large_image === undefined ? `${19*scale}px` : `0px`,
|
||||
width: parsedFrameworkData.Network.large_image === undefined ? iconSize : defaultSize,
|
||||
height: parsedFrameworkData.Network.large_image === undefined ? iconSize : defaultSize,
|
||||
large_image: parsedFrameworkData.Network.large_image === undefined ? encodeURI(`data:image/svg+xml;utf-8,<svg fill="rgb(248,90,62)" width="${svgSize}" height="${svgSize}" viewBox="0 0 ${svgSize} ${svgSize}" version="1.1" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M0.251953 10.6011H3.8391L9.38052 -4.92572e-08L10.8977 11.5696L15.0377 6.28838L19.3191 10.6011H23.3948V13.1836H18.252L15.2562 10.175L9.1491 18L7.88909 8.41894L5.39481 13.1836H0.251953V10.6011Z" />,
|
||||
</svg>`) : parsedFrameworkData.Network.large_image,
|
||||
@@ -1265,14 +1617,11 @@ const Framework = (props) => {
|
||||
changeUsecase(selectedUsecase, usecaseType)
|
||||
|
||||
if (inputUsecase !== undefined && inputUsecase !== null) {
|
||||
console.log("Got usecase: ", inputUsecase)
|
||||
|
||||
for (var key in inputUsecase.process) {
|
||||
if (inputUsecase.process[key].source === "" || inputUsecase.process[key].target === "") {
|
||||
continue
|
||||
}
|
||||
|
||||
console.log("Edge: ", inputUsecase.process[key])
|
||||
inputUsecase.process[key].label = parseInt(key)+1
|
||||
inputUsecase.process[key].id = uuidv4();
|
||||
|
||||
@@ -1377,8 +1726,11 @@ const Framework = (props) => {
|
||||
//}
|
||||
}
|
||||
|
||||
const bgColor = color === undefined || color === null || color.length === 0 ? theme.palette.surfaceColor : color
|
||||
console.log("BGCOLOR: ", bgColor)
|
||||
|
||||
return (
|
||||
<Paper style={{marginBottom: 15, width: 250, maxHeight: 400, overflow: "hidden", zIndex: 12500, padding: 15, backgroundColor: theme.palette.surfaceColor, border: "1px solid rgba(255,255,255,0.2)", }} onMouseOver={handleHover} onMouseOut={handleHoverOut}>
|
||||
<Paper style={{marginBottom: 15, width: 250, maxHeight: 400, overflow: "hidden", zIndex: 12500, padding: 15, backgroundColor: bgColor, border: "1px solid rgba(255,255,255,0.2)", }} onMouseOver={handleHover} onMouseOut={handleHoverOut}>
|
||||
<Typography style={{textAlign: "center"}}>
|
||||
{data.name}
|
||||
</Typography>
|
||||
@@ -1447,8 +1799,23 @@ const Framework = (props) => {
|
||||
|
||||
//autounselectify={true}
|
||||
var usecasediff = -100
|
||||
return (
|
||||
<div style={{margin: "auto", backgroundColor: theme.palette.surfaceColor, position: "relative", }}>
|
||||
const bgColor = color === undefined || color === null || color.length === 0 ? theme.palette.surfaceColor : color
|
||||
|
||||
return (
|
||||
<div style={{margin: "auto", backgroundColor: bgColor, position: "relative", }}>
|
||||
<div style={{position: "absolute"}}>
|
||||
|
||||
<SuggestedWorkflows
|
||||
globalUrl={globalUrl}
|
||||
userdata={userdata}
|
||||
frameworkData={frameworkData}
|
||||
usecaseSuggestions={frameworkSuggestions}
|
||||
setUsecaseSuggestions={setFrameworkSuggestions}
|
||||
inputSearch={changedApp}
|
||||
apps={apps}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{showOptions === false ? null :
|
||||
<div style={{textAlign: "center",}}>
|
||||
{Object.keys(usecases).map((data, index) => {
|
||||
@@ -1480,7 +1847,15 @@ const Framework = (props) => {
|
||||
|
||||
{
|
||||
Object.getOwnPropertyNames(discoveryData).length > 0 ?
|
||||
<Paper style={{width: 250, 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, }}>
|
||||
<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, }}>
|
||||
{paperTitle.length > 0 ?
|
||||
<span>
|
||||
<Typography variant="h6" style={{textAlign: "center"}}>
|
||||
{paperTitle}
|
||||
</Typography>
|
||||
<Divider style={{marginTop: 5, marginBottom: 5 }} />
|
||||
</span>
|
||||
: null}
|
||||
<Tooltip
|
||||
title="Close window"
|
||||
placement="top"
|
||||
@@ -1495,12 +1870,12 @@ const Framework = (props) => {
|
||||
e.preventDefault();
|
||||
setDiscoveryData({})
|
||||
setDefaultSearch("")
|
||||
setPaperTitle("")
|
||||
}}
|
||||
>
|
||||
<CloseIcon style={{ color: "white", height: 15, width: 15, }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{/* {/*Causes errors in Cytoscape. Removing for now.}
|
||||
<Tooltip
|
||||
title="Unselect app"
|
||||
placement="top"
|
||||
@@ -1515,12 +1890,29 @@ const Framework = (props) => {
|
||||
"label": discoveryData.label,
|
||||
"name": ""
|
||||
})
|
||||
|
||||
setNewSelectedApp({
|
||||
"image_url": "",
|
||||
"name": "",
|
||||
"animate": false,
|
||||
"app_id": "",
|
||||
"boxheight": "66.3px",
|
||||
"boxwidth": "66.3px",
|
||||
"description": "",
|
||||
"errors": [],
|
||||
"font_size": "9.36px",
|
||||
"height": "66.3px",
|
||||
"id": "",
|
||||
"isValid": true,
|
||||
"is_valid": true,
|
||||
"label": "SIEM",
|
||||
"large_image": "asd",
|
||||
"margin_x": "0px",
|
||||
"margin_y": "0px",
|
||||
"name": "",
|
||||
"text_margin_y": "46.800000000000004px",
|
||||
"width": "66.3px",
|
||||
"objectID": "remove",
|
||||
})
|
||||
|
||||
setSelectionOpen(true)
|
||||
setDefaultSearch("")
|
||||
|
||||
@@ -1544,7 +1936,6 @@ const Framework = (props) => {
|
||||
<DeleteIcon style={{ color: "white", height: 15, width: 15, }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
*/}
|
||||
<div style={{display: "flex"}}>
|
||||
{discoveryData.name !== undefined && discoveryData.name !== null && discoveryData.name.length > 0 ?
|
||||
<div style={{border: "1px solid rgba(255,255,255,0.2)", borderRadius: 25, height: 40, width: 40, textAlign: "center", overflow: "hidden",}}>
|
||||
@@ -1561,18 +1952,18 @@ const Framework = (props) => {
|
||||
newSelectedApp.name !== undefined && newSelectedApp.name !== null && newSelectedApp.name.length > 0 ?
|
||||
newSelectedApp.name
|
||||
:
|
||||
`No ${discoveryData.label} app chosen`
|
||||
`Find your ${discoveryData.label} app!`
|
||||
}
|
||||
</Typography>
|
||||
</div>
|
||||
<div>
|
||||
{discoveryData !== undefined && discoveryData.name !== undefined && discoveryData.name !== null && discoveryData.name.length > 0 ?
|
||||
<span>
|
||||
<Typography variant="body2" color="textSecondary" style={{marginTop: 10, marginBottom: 10, }}>
|
||||
<Typography variant="body2" color="textSecondary" style={{marginTop: 10, marginBottom: 10, maxHeight: 75, overflowY: "auto", overflowX: "hidden", }}>
|
||||
{discoveryData.description}
|
||||
</Typography>
|
||||
{/*isCloud && defaultSearch !== undefined && defaultSearch.length > 0 ?
|
||||
{<WorkflowSearch
|
||||
{<
|
||||
newSelectedApp={newSelectedApp}
|
||||
setNewSelectedApp={setNewSelectedApp}
|
||||
defaultSearch={defaultSearch}
|
||||
@@ -1604,19 +1995,16 @@ const Framework = (props) => {
|
||||
}
|
||||
</div>
|
||||
<div style={{marginTop: 10}}>
|
||||
{selectionOpen ?
|
||||
isCloud && defaultSearch !== undefined && defaultSearch.length > 0 ?
|
||||
<WorkflowSearch
|
||||
{selectionOpen ?
|
||||
<AppSearch
|
||||
defaultSearch={defaultSearch}
|
||||
newSelectedApp={newSelectedApp}
|
||||
setNewSelectedApp={setNewSelectedApp}
|
||||
userdata={userdata}
|
||||
cy={cy}
|
||||
/>
|
||||
:
|
||||
<div>
|
||||
Coming in 1.0.0. <a style={{ textDecoration: "none", color: "#f85a3e" }} href="https://shuffler.io/register" target="_blank">Register for Shuffle cloud</a> to try an early version now.
|
||||
</div>
|
||||
: null}
|
||||
</div>
|
||||
: null}
|
||||
</div>
|
||||
</Paper>
|
||||
: null
|
||||
}
|
||||
@@ -1649,4 +2037,4 @@ const Framework = (props) => {
|
||||
)
|
||||
}
|
||||
|
||||
export default Framework;
|
||||
export default AppFramework;
|
||||
@@ -0,0 +1,378 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
|
||||
import ReactGA from 'react-ga';
|
||||
import { useTheme } from '@material-ui/core/styles';
|
||||
import {Link} from 'react-router-dom';
|
||||
|
||||
import { Search as SearchIcon, CloudQueue as CloudQueueIcon, Code as CodeIcon } from '@material-ui/icons';
|
||||
|
||||
import algoliasearch from 'algoliasearch/lite';
|
||||
import { InstantSearch, Configure, connectSearchBox, connectHits, connectHitInsights } from 'react-instantsearch-dom';
|
||||
|
||||
import aa from 'search-insights'
|
||||
|
||||
import {
|
||||
Zoom,
|
||||
Grid,
|
||||
Paper,
|
||||
TextField,
|
||||
ButtonBase,
|
||||
InputAdornment,
|
||||
Typography,
|
||||
Button,
|
||||
Tooltip
|
||||
} from '@material-ui/core';
|
||||
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240")
|
||||
//const searchClient = algoliasearch("L55H18ZINA", "a19be455e7e75ee8f20a93d26b9fc6d6")
|
||||
const AppGrid = props => {
|
||||
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, userdata } = props
|
||||
|
||||
const isCloud =
|
||||
window.location.host === "localhost:3002" ||
|
||||
window.location.host === "shuffler.io";
|
||||
|
||||
const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows
|
||||
const xs = parsedXs === undefined || parsedXs === null ? isMobile ? 6 : 2 : 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("");
|
||||
|
||||
const buttonStyle = {borderRadius: 30, height: 50, width: 220, margin: isMobile ? "15px auto 15px auto" : 20, fontSize: 18,}
|
||||
|
||||
const innerColor = "rgba(255,255,255,0.65)"
|
||||
const borderRadius = 3
|
||||
window.title = "Shuffle | Apps | Find and integrate any app"
|
||||
|
||||
const submitContact = (email, message) => {
|
||||
const data = {
|
||||
"firstname": "",
|
||||
"lastname": "",
|
||||
"title": "",
|
||||
"companyname": "",
|
||||
"email": email,
|
||||
"phone": "",
|
||||
"message": message,
|
||||
}
|
||||
|
||||
const errorMessage = "Something went wrong. Please contact frikky@shuffler.io directly."
|
||||
|
||||
fetch(globalUrl+"/api/v1/contact", {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(response => {
|
||||
if (response.success === true) {
|
||||
setFormMessage(response.reason)
|
||||
//alert.info("Thanks for submitting!")
|
||||
} else {
|
||||
setFormMessage(errorMessage)
|
||||
}
|
||||
|
||||
setFormMail("")
|
||||
setMessage("")
|
||||
})
|
||||
.catch(error => {
|
||||
setFormMessage(errorMessage)
|
||||
console.log(error)
|
||||
});
|
||||
}
|
||||
|
||||
const SearchBox = ({currentRefinement, refine, isSearchStalled} ) => {
|
||||
useEffect(() => {
|
||||
if (window !== undefined && window.location !== undefined && window.location.search !== undefined && window.location.search !== null) {
|
||||
const urlSearchParams = new URLSearchParams(window.location.search)
|
||||
const params = Object.fromEntries(urlSearchParams.entries())
|
||||
const foundQuery = params["q"]
|
||||
if (foundQuery !== null && foundQuery !== undefined) {
|
||||
console.log("Got query: ", foundQuery)
|
||||
refine(foundQuery)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<form noValidate action="" role="search">
|
||||
<TextField
|
||||
fullWidth
|
||||
style={{backgroundColor: theme.palette.inputColor, borderRadius: borderRadius, margin: 10, width: "100%",}}
|
||||
InputProps={{
|
||||
style:{
|
||||
color: "white",
|
||||
fontSize: "1em",
|
||||
height: 50,
|
||||
},
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon style={{marginLeft: 5}}/>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
autoComplete='off'
|
||||
type="search"
|
||||
color="primary"
|
||||
defaultValue={currentRefinement}
|
||||
placeholder="Find Apps..."
|
||||
id="shuffle_search_field"
|
||||
onChange={(event) => {
|
||||
refine(event.currentTarget.value)
|
||||
}}
|
||||
limit={5}
|
||||
/>
|
||||
{/*isSearchStalled ? 'My search is stalled' : ''*/}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
var workflowDelay = -50
|
||||
const Hits = ({ hits, insights }) => {
|
||||
const [mouseHoverIndex, setMouseHoverIndex] = useState(-1)
|
||||
var counted = 0
|
||||
|
||||
//console.log(hits)
|
||||
//var curhits = hits
|
||||
//if (hits.length > 0 && defaultApps.length === 0) {
|
||||
// setDefaultApps(hits)
|
||||
//}
|
||||
|
||||
//const [defaultApps, setDefaultApps] = React.useState([])
|
||||
//console.log(hits)
|
||||
//if (hits.length > 0 && hits.length !== innerHits.length) {
|
||||
// setInnerHits(hits)
|
||||
//}
|
||||
|
||||
console.log("In appgrid")
|
||||
|
||||
return (
|
||||
<Grid container spacing={2}>
|
||||
{hits.map((data, index) => {
|
||||
|
||||
workflowDelay += 50
|
||||
|
||||
const paperStyle = {
|
||||
backgroundColor: index === mouseHoverIndex ? "rgba(255,255,255,0.8)" : theme.palette.inputColor,
|
||||
color: index === mouseHoverIndex ? theme.palette.inputColor : "rgba(255,255,255,0.8)",
|
||||
border: `1px solid ${innerColor}`,
|
||||
padding: 15,
|
||||
cursor: "pointer",
|
||||
position: "relative",
|
||||
minHeight: 116,
|
||||
}
|
||||
|
||||
if (counted === 12/xs*rowHandler) {
|
||||
return null
|
||||
}
|
||||
|
||||
counted += 1
|
||||
var parsedname = ""
|
||||
for (var key = 0; key < data.name.length; key++) {
|
||||
var character = data.name.charAt(key)
|
||||
if (character === character.toUpperCase()) {
|
||||
//console.log(data.name[key], data.name[key+1])
|
||||
if (data.name.charAt(key+1) !== undefined && data.name.charAt(key+1) === data.name.charAt(key+1).toUpperCase()) {
|
||||
} else {
|
||||
parsedname += " "
|
||||
}
|
||||
}
|
||||
|
||||
parsedname += character
|
||||
}
|
||||
|
||||
parsedname = (parsedname.charAt(0).toUpperCase()+parsedname.substring(1)).replaceAll("_", " ")
|
||||
const appUrl = isCloud ? `/apps/${data.objectID}?queryID=${data.__queryID}` : `https://shuffler.io/apps/${data.objectID}?queryID=${data.__queryID}`
|
||||
return (
|
||||
<Zoom key={index} in={true} style={{ transitionDelay: `${workflowDelay}ms` }}>
|
||||
<Grid item xs={xs} key={index}>
|
||||
<a href={appUrl} rel="noopener noreferrer" target="_blank" style={{textDecoration: "none", color: "#f85a3e"}}>
|
||||
<Paper elevation={0} style={paperStyle} onMouseOver={() => {
|
||||
setMouseHoverIndex(index)
|
||||
/*
|
||||
ReactGA.event({
|
||||
category: "app_grid_view",
|
||||
action: `search_bar_click`,
|
||||
label: "",
|
||||
})
|
||||
*/
|
||||
}} onMouseOut={() => {
|
||||
setMouseHoverIndex(-1)
|
||||
}} onClick={() => {
|
||||
if (isCloud) {
|
||||
ReactGA.event({
|
||||
category: "app_grid_view",
|
||||
action: `app_${parsedname}_${data.id}_click`,
|
||||
label: "",
|
||||
})
|
||||
}
|
||||
|
||||
//const searchClient = algoliasearch("L55H18ZINA", "a19be455e7e75ee8f20a93d26b9fc6d6")
|
||||
console.log(searchClient)
|
||||
aa('init', {
|
||||
appId: searchClient.appId,
|
||||
apiKey: searchClient.transporter.queryParameters["x-algolia-api-key"]
|
||||
})
|
||||
|
||||
const timestamp = new Date().getTime()
|
||||
aa('sendEvents', [
|
||||
{
|
||||
eventType: 'click',
|
||||
eventName: 'Product Clicked',
|
||||
index: 'appsearch',
|
||||
objectIDs: [data.objectID],
|
||||
timestamp: timestamp,
|
||||
queryID: data.__queryID,
|
||||
positions: [data.__position],
|
||||
userToken: userdata === undefined || userdata === null || userdata.id === undefined ? "unauthenticated" : userdata.id,
|
||||
}
|
||||
])
|
||||
|
||||
}}>
|
||||
<ButtonBase style={{padding: 5, borderRadius: 3, minHeight: 100, minWidth: 100,}}>
|
||||
<img alt={data.name} src={data.image_url} style={{width: "100%", maxWidth: 100, minWidth: 100, minHeight: 100, maxHeight: 100, display: "block", margin: "0 auto"}} />
|
||||
</ButtonBase>
|
||||
<div/>
|
||||
{index === mouseHoverIndex || showName === true ?
|
||||
parsedname
|
||||
:
|
||||
null
|
||||
}
|
||||
{data.generated ?
|
||||
<Tooltip title={"Created with App editor"} style={{marginTop: "28px", width: "100%"}} aria-label={data.name}>
|
||||
{data.invalid ?
|
||||
<CloudQueueIcon style={{position: "absolute", top: 1, left: 3, height: 16, width: 16, color: theme.palette.primary.main }}/>
|
||||
:
|
||||
<CloudQueueIcon style={{position: "absolute", top: 1, left: 3, height: 16, width: 16, color: "rgba(255,255,255,0.95)",}}/>
|
||||
}
|
||||
</Tooltip>
|
||||
:
|
||||
<Tooltip title={"Created with python (custom app)"} style={{marginTop: "28px", width: "100%"}} aria-label={data.name}>
|
||||
<CodeIcon style={{position: "absolute", top: 1, left: 3, height: 16, width: 16, color: "rgba(255,255,255,0.95)",}}/>
|
||||
</Tooltip>
|
||||
}
|
||||
</Paper>
|
||||
</a>
|
||||
</Grid>
|
||||
</Zoom>
|
||||
)
|
||||
})}
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
const CustomSearchBox = connectSearchBox(SearchBox)
|
||||
const CustomHits = connectHits(Hits)
|
||||
//const CustomHits = connectHitInsights(aa)(Hits)
|
||||
const selectButtonStyle = {
|
||||
minWidth: 150,
|
||||
maxWidth: 150,
|
||||
minHeight: 50,
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{width: "100%", textAlign: "center", position: "relative", height: "100%", display: "flex"}}>
|
||||
{/*
|
||||
<div style={{padding: 10, }}>
|
||||
<Button
|
||||
style={selectButtonStyle}
|
||||
variant="outlined"
|
||||
onClick={() => {
|
||||
const searchField = document.createElement("shuffle_search_field")
|
||||
console.log("Field: ", searchField)
|
||||
if (searchField !== null & searchField !== undefined) {
|
||||
console.log("Set field.")
|
||||
searchField.value = "WHAT WABALABA"
|
||||
searchField.setAttribute("value", "WHAT WABALABA")
|
||||
}
|
||||
}}
|
||||
>
|
||||
Cases
|
||||
</Button>
|
||||
</div>
|
||||
*/}
|
||||
<div style={{width: "100%", position: "relative", height: "100%",}}>
|
||||
<InstantSearch searchClient={searchClient} indexName="appsearch">
|
||||
<div style={{maxWidth: 450, margin: "auto", marginTop: 15, marginBottom: 15, }}>
|
||||
<CustomSearchBox />
|
||||
</div>
|
||||
<CustomHits hitsPerPage={5}/>
|
||||
<Configure clickAnalytics />
|
||||
</InstantSearch>
|
||||
{showSuggestion === true ?
|
||||
<div style={{paddingTop: 0, maxWidth: isMobile ? "100%" : "60%", margin: "auto"}}>
|
||||
<Typography variant="h6" style={{color: "white", marginTop: 50,}}>
|
||||
Can't find what you're looking for?
|
||||
</Typography>
|
||||
<div style={{flex: "1", display: "flex", flexDirection: "row", textAlign: "center",}}>
|
||||
<TextField
|
||||
required
|
||||
style={{flex: "1", marginRight: "15px", backgroundColor: theme.palette.inputColor}}
|
||||
InputProps={{
|
||||
style:{
|
||||
color: "#ffffff",
|
||||
},
|
||||
}}
|
||||
color="primary"
|
||||
fullWidth={true}
|
||||
placeholder="Email (optional)"
|
||||
type="email"
|
||||
id="email-handler"
|
||||
autoComplete="email"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
onChange={e => setFormMail(e.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
required
|
||||
style={{flex: "1", backgroundColor: theme.palette.inputColor}}
|
||||
InputProps={{
|
||||
style:{
|
||||
color: "#ffffff",
|
||||
},
|
||||
}}
|
||||
color="primary"
|
||||
fullWidth={true}
|
||||
placeholder="What apps do you want to see?"
|
||||
type=""
|
||||
id="standard-required"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
autoComplete="off"
|
||||
onChange={e => setMessage(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
style={buttonStyle}
|
||||
disabled={message.length === 0}
|
||||
onClick={() => {
|
||||
submitContact(formMail, message)
|
||||
}}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
<Typography style={{color: "white"}} variant="body2">{formMessage}</Typography>
|
||||
</div>
|
||||
: null
|
||||
}
|
||||
|
||||
<span style={{position: "absolute", display: "flex", textAlign: "right", float: "right", right: 0, bottom: 120, }}>
|
||||
<Typography variant="body2" color="textSecondary" style={{}}>
|
||||
Search by
|
||||
</Typography>
|
||||
<a rel="noopener noreferrer" href="https://www.algolia.com/" target="_blank" style={{textDecoration: "none", color: "white"}}>
|
||||
<img src={"/images/logo-algolia-nebula-blue-full.svg"} alt="Algolia logo" style={{height: 17, marginLeft: 5, marginTop: 3,}} />
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AppGrid;
|
||||
@@ -0,0 +1,245 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
import ReactGA from 'react-ga';
|
||||
import { useTheme } from '@material-ui/core/styles';
|
||||
import {Link} from 'react-router-dom';
|
||||
|
||||
import { Search as SearchIcon, CloudQueue as CloudQueueIcon, Code as CodeIcon } from '@material-ui/icons';
|
||||
|
||||
//import algoliasearch from 'algoliasearch/lite';
|
||||
import algoliasearch from 'algoliasearch';
|
||||
import { InstantSearch, connectSearchBox, connectHits } from 'react-instantsearch-dom';
|
||||
import { Grid, Paper, TextField, ButtonBase, InputAdornment, Typography, Button, Tooltip} from '@material-ui/core';
|
||||
import aa from 'search-insights'
|
||||
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240")
|
||||
const Appsearch = props => {
|
||||
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, newSelectedApp, setNewSelectedApp, defaultSearch, showSearch, ConfiguredHits, userdata, cy, } = props
|
||||
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
|
||||
|
||||
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("");
|
||||
const [selectedApp, setSelectedApp] = React.useState({});
|
||||
|
||||
const buttonStyle = {borderRadius: 30, height: 50, width: 220, margin: isMobile ? "15px auto 15px auto" : 20, fontSize: 18,}
|
||||
|
||||
const innerColor = "rgba(255,255,255,0.65)"
|
||||
const borderRadius = 3
|
||||
window.title = "Shuffle | Apps | Find and integration any app"
|
||||
|
||||
const submitContact = (email, message) => {
|
||||
const data = {
|
||||
"firstname": "",
|
||||
"lastname": "",
|
||||
"title": "",
|
||||
"companyname": "",
|
||||
"email": email,
|
||||
"phone": "",
|
||||
"message": message,
|
||||
}
|
||||
|
||||
const errorMessage = "Something went wrong. Please contact frikky@shuffler.io directly."
|
||||
|
||||
fetch(globalUrl+"/api/v1/contact", {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(response => {
|
||||
if (response.success === true) {
|
||||
setFormMessage(response.reason)
|
||||
//alert.info("Thanks for submitting!")
|
||||
} else {
|
||||
setFormMessage(errorMessage)
|
||||
}
|
||||
|
||||
setFormMail("")
|
||||
setMessage("")
|
||||
})
|
||||
.catch(error => {
|
||||
setFormMessage(errorMessage)
|
||||
console.log(error)
|
||||
});
|
||||
}
|
||||
|
||||
// value={currentRefinement}
|
||||
const SearchBox = ({currentRefinement, refine, isSearchStalled} ) => {
|
||||
useEffect(() => {
|
||||
//console.log("FIRST LOAD ONLY? RUN REFINEMENT: !", currentRefinement)
|
||||
if (defaultSearch !== undefined && defaultSearch !== null) {
|
||||
refine(defaultSearch)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<form noValidate action="" role="search">
|
||||
<TextField
|
||||
fullWidth
|
||||
style={{backgroundColor: theme.palette.inputColor, borderRadius: borderRadius, width: "100%",}}
|
||||
InputProps={{
|
||||
style:{
|
||||
color: "white",
|
||||
fontSize: "1em",
|
||||
height: 50,
|
||||
},
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon style={{marginLeft: 5}}/>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
autoComplete='on'
|
||||
type="search"
|
||||
color="primary"
|
||||
defaultValue={defaultSearch}
|
||||
placeholder={`Find ${defaultSearch} Apps...`}
|
||||
id="shuffle_workflow_search_field"
|
||||
onChange={(event) => {
|
||||
refine(event.currentTarget.value)
|
||||
}}
|
||||
limit={5}
|
||||
/>
|
||||
{/*isSearchStalled ? 'My search is stalled' : ''*/}
|
||||
</form>
|
||||
)
|
||||
//value={currentRefinement}
|
||||
}
|
||||
|
||||
const Hits = ({ hits }) => {
|
||||
const [mouseHoverIndex, setMouseHoverIndex] = useState(-1)
|
||||
var counted = 0
|
||||
|
||||
return (
|
||||
<Grid container spacing={0} style={{border: "1px solid rgba(255,255,255,0.2)", maxHeight: 250, minHeight: 250, overflowY: "auto", overflowX: "hidden", }}>
|
||||
{hits.map((data, index) => {
|
||||
const paperStyle = {
|
||||
backgroundColor: index === mouseHoverIndex ? "rgba(255,255,255,0.8)" : theme.palette.inputColor,
|
||||
color: index === mouseHoverIndex ? theme.palette.inputColor : "rgba(255,255,255,0.8)",
|
||||
border: newSelectedApp.objectID !== data.objectID ? `1px solid rgba(255,255,255,0.2)` : "2px solid #f86a3e",
|
||||
textAlign: "left",
|
||||
padding: 10,
|
||||
cursor: "pointer",
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
width: "100%",
|
||||
minHeight: 37,
|
||||
maxHeight: 52,
|
||||
}
|
||||
|
||||
if (counted === 12/xs*rowHandler) {
|
||||
return null
|
||||
}
|
||||
|
||||
counted += 1
|
||||
var parsedname = data.name.valueOf()
|
||||
//for (var key = 0; key < data.name.length; key++) {
|
||||
// var character = data.name.charAt(key)
|
||||
// if (character === character.toUpperCase()) {
|
||||
// //console.log(data.name[key], data.name[key+1])
|
||||
// if (data.name.charAt(key+1) !== undefined && data.name.charAt(key+1) === data.name.charAt(key+1).toUpperCase()) {
|
||||
// } else {
|
||||
// parsedname += " "
|
||||
// }
|
||||
// }
|
||||
|
||||
// parsedname += character
|
||||
//}
|
||||
|
||||
parsedname = (parsedname.charAt(0).toUpperCase()+parsedname.substring(1)).replaceAll("_", " ")
|
||||
|
||||
return (
|
||||
<Paper key={index} elevation={0} style={paperStyle} onMouseOver={() => {
|
||||
setMouseHoverIndex(index)
|
||||
/*
|
||||
ReactGA.event({
|
||||
category: "app_grid_view",
|
||||
action: `search_bar_click`,
|
||||
label: "",
|
||||
})
|
||||
*/
|
||||
}} onMouseOut={() => {
|
||||
setMouseHoverIndex(-1)
|
||||
}} onClick={() => {
|
||||
if (setNewSelectedApp !== undefined) {
|
||||
setNewSelectedApp(data)
|
||||
}
|
||||
|
||||
if (isCloud) {
|
||||
ReactGA.event({
|
||||
category: "app_search",
|
||||
action: `app_${parsedname}_${data.id}_personalize_click`,
|
||||
label: "",
|
||||
})
|
||||
}
|
||||
|
||||
const queryID = ""
|
||||
|
||||
if (queryID !== undefined && queryID !== null) {
|
||||
try {
|
||||
aa('init', {
|
||||
appId: searchClient.appId,
|
||||
apiKey: searchClient.transporter.headers["x-algolia-api-key"]
|
||||
})
|
||||
|
||||
const timestamp = new Date().getTime()
|
||||
aa('sendEvents', [
|
||||
{
|
||||
eventType: 'conversion',
|
||||
eventName: 'App Framework Activation',
|
||||
index: 'appsearch',
|
||||
objectIDs: [data.objectID],
|
||||
timestamp: timestamp,
|
||||
queryID: queryID,
|
||||
userToken: userdata === undefined || userdata === null || userdata.id === undefined ? "unauthenticated" : userdata.id,
|
||||
}
|
||||
])
|
||||
} catch (e) {
|
||||
console.log("Failed algolia search update: ", e)
|
||||
}
|
||||
}
|
||||
}}>
|
||||
<div style={{display: "flex"}}>
|
||||
<img alt={data.name} src={data.image_url} style={{width: "100%", maxWidth: 30, minWidth: 30, minHeight: 30, maxHeight: 30, display: "block", }} />
|
||||
<Typography variant="body1" style={{marginTop: 2, marginLeft: 10, }}>
|
||||
{parsedname}
|
||||
</Typography>
|
||||
</div>
|
||||
</Paper>
|
||||
)
|
||||
})}
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
const InputHits = ConfiguredHits === undefined ? Hits : ConfiguredHits
|
||||
const CustomSearchBox = connectSearchBox(SearchBox)
|
||||
const CustomHits = connectHits(InputHits)
|
||||
|
||||
return (
|
||||
<div style={{width: "100%", textAlign: "center", position: "relative", height: "100%",}}>
|
||||
<InstantSearch searchClient={searchClient} indexName="appsearch">
|
||||
{/* showSearch === false ? null :
|
||||
<div style={{maxWidth: 450, margin: "auto", }}>
|
||||
<CustomSearchBox />
|
||||
</div>
|
||||
*/}
|
||||
<div style={{maxWidth: 450, margin: "auto", }}>
|
||||
<CustomSearchBox />
|
||||
</div>
|
||||
<CustomHits hitsPerPage={5}/>
|
||||
</InstantSearch>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Appsearch;
|
||||
@@ -0,0 +1,189 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
import theme from '../theme';
|
||||
import AppSearch from './Appsearch.jsx';
|
||||
|
||||
import {
|
||||
Paper,
|
||||
Typography,
|
||||
Divider,
|
||||
IconButton,
|
||||
Badge,
|
||||
CircularProgress,
|
||||
Tooltip,
|
||||
Button,
|
||||
} from "@material-ui/core";
|
||||
|
||||
import {
|
||||
Close as CloseIcon,
|
||||
Delete as DeleteIcon,
|
||||
} from "@material-ui/icons";
|
||||
|
||||
const AppSearchPopout = (props) => {
|
||||
const {
|
||||
cy,
|
||||
paperTitle,
|
||||
setPaperTitle,
|
||||
newSelectedApp,
|
||||
setNewSelectedApp,
|
||||
selectionOpen,
|
||||
setSelectionOpen,
|
||||
discoveryData,
|
||||
setDiscoveryData,
|
||||
userdata,
|
||||
} = props;
|
||||
|
||||
const [defaultSearch, setDefaultSearch] = React.useState(paperTitle !== undefined ? paperTitle : "")
|
||||
|
||||
if (selectionOpen !== true) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Paper style={{width: 275, maxHeight: 400, zIndex: 12500, padding: 25, paddingRight: 35, backgroundColor: theme.palette.surfaceColor, border: "1px solid rgba(255,255,255,0.2)", position: "absolute", top: -15, left: 50, overflow: "hidden", }}>
|
||||
{paperTitle !== undefined && paperTitle.length > 0 ?
|
||||
<span>
|
||||
<Typography variant="h6" style={{textAlign: "center"}}>
|
||||
{paperTitle}
|
||||
</Typography>
|
||||
<Divider style={{marginTop: 5, marginBottom: 5 }} />
|
||||
</span>
|
||||
: null}
|
||||
<Tooltip
|
||||
title="Close window"
|
||||
placement="top"
|
||||
style={{ zIndex: 10011 }}
|
||||
>
|
||||
<IconButton
|
||||
style={{ zIndex: 12501, position: "absolute", top: 10, right: 10}}
|
||||
onClick={(e) => {
|
||||
//cy.elements().unselectify();
|
||||
if (cy !== undefined) {
|
||||
cy.elements().unselect()
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
setSelectionOpen(false)
|
||||
}}
|
||||
>
|
||||
<CloseIcon style={{ color: "white", height: 15, width: 15, }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{/* {/*Causes errors in Cytoscape. Removing for now.}
|
||||
<Tooltip
|
||||
title="Unselect app"
|
||||
placement="top"
|
||||
style={{ zIndex: 10011 }}
|
||||
>
|
||||
<IconButton
|
||||
style={{ zIndex: 12501, position: "absolute", top: 32, right: 10}}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setDiscoveryData({
|
||||
"id": discoveryData.id,
|
||||
"label": discoveryData.label,
|
||||
"name": ""
|
||||
})
|
||||
setNewSelectedApp({
|
||||
"image_url": "",
|
||||
"name": "",
|
||||
"id": "",
|
||||
"objectID": "remove",
|
||||
})
|
||||
setSelectionOpen(true)
|
||||
setDefaultSearch("")
|
||||
|
||||
const foundelement = cy.getElementById(discoveryData.id)
|
||||
if (foundelement !== undefined && foundelement !== null) {
|
||||
console.log("element: ", foundelement)
|
||||
foundelement.data("large_image", discoveryData.large_image)
|
||||
foundelement.data("text_margin_y", "14px")
|
||||
foundelement.data("margin_x", "32px")
|
||||
foundelement.data("margin_y", "19x")
|
||||
foundelement.data("width", "45px")
|
||||
foundelement.data("height", "45px")
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
setDiscoveryData({})
|
||||
setNewSelectedApp({})
|
||||
}, 1000)
|
||||
}}
|
||||
>
|
||||
<DeleteIcon style={{ color: "white", height: 15, width: 15, }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
*/}
|
||||
<div style={{display: "flex"}}>
|
||||
{discoveryData.name !== undefined && discoveryData.name !== null && discoveryData.name.length > 0 ?
|
||||
<div style={{border: "1px solid rgba(255,255,255,0.2)", borderRadius: 25, height: 40, width: 40, textAlign: "center", overflow: "hidden",}}>
|
||||
|
||||
<img alt={discoveryData.id} src={newSelectedApp.image_url !== undefined && newSelectedApp.image_url !== null && newSelectedApp.image_url.length > 0 ? newSelectedApp.image_url : discoveryData.large_image} style={{height: 40, width: 40, margin: "auto",}}/>
|
||||
</div>
|
||||
:
|
||||
<img alt={discoveryData.id} src={discoveryData.large_image} style={{height: 40,}}/>
|
||||
}
|
||||
<Typography variant="body1" style={{marginLeft: 10, marginTop: 6}}>
|
||||
{discoveryData.name !== undefined && discoveryData.name !== null && discoveryData.name.length > 0 ?
|
||||
discoveryData.name
|
||||
:
|
||||
newSelectedApp.name !== undefined && newSelectedApp.name !== null && newSelectedApp.name.length > 0 ?
|
||||
newSelectedApp.name
|
||||
:
|
||||
`No ${discoveryData.label} app chosen`
|
||||
}
|
||||
</Typography>
|
||||
</div>
|
||||
<div>
|
||||
{discoveryData !== undefined && discoveryData.name !== undefined && discoveryData.name !== null && discoveryData.name.length > 0 ?
|
||||
<span>
|
||||
<Typography variant="body2" color="textSecondary" style={{marginTop: 10, marginBottom: 10, maxHeight: 75, overflowY: "auto", overflowX: "hidden", }}>
|
||||
{discoveryData.description}
|
||||
</Typography>
|
||||
{/*isCloud && defaultSearch !== undefined && defaultSearch.length > 0 ?
|
||||
{<
|
||||
newSelectedApp={newSelectedApp}
|
||||
setNewSelectedApp={setNewSelectedApp}
|
||||
defaultSearch={defaultSearch}
|
||||
/>}
|
||||
:
|
||||
null
|
||||
*/}
|
||||
</span>
|
||||
:
|
||||
selectionOpen
|
||||
?
|
||||
<span>
|
||||
<Typography variant="body2" color="textSecondary" style={{marginTop: 10}}>
|
||||
Click an app below to select it
|
||||
</Typography>
|
||||
</span>
|
||||
:
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
style={{marginTop: 10, }}
|
||||
onClick={() => {
|
||||
setSelectionOpen(true)
|
||||
setDefaultSearch(discoveryData.label)
|
||||
}}
|
||||
>
|
||||
Choose {discoveryData.label} app
|
||||
</Button>
|
||||
}
|
||||
</div>
|
||||
<div style={{marginTop: 10}}>
|
||||
{selectionOpen ?
|
||||
<AppSearch
|
||||
defaultSearch={defaultSearch}
|
||||
newSelectedApp={newSelectedApp}
|
||||
setNewSelectedApp={setNewSelectedApp}
|
||||
userdata={userdata}
|
||||
/>
|
||||
: null}
|
||||
</div>
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
|
||||
export default AppSearchPopout;
|
||||
@@ -0,0 +1,278 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
import theme from '../theme';
|
||||
import { useAlert } from "react-alert";
|
||||
import {
|
||||
Tooltip,
|
||||
IconButton,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
FormGroup,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
FormLabel,
|
||||
FormControlLabel,
|
||||
Select,
|
||||
MenuItem,
|
||||
Grid,
|
||||
Paper,
|
||||
Typography,
|
||||
TextField,
|
||||
Zoom,
|
||||
} from "@material-ui/core";
|
||||
|
||||
import {
|
||||
Edit as EditIcon,
|
||||
Delete as DeleteIcon,
|
||||
SelectAll as SelectAllIcon,
|
||||
} from "@material-ui/icons";
|
||||
|
||||
const AuthenticationItem = (props) => {
|
||||
const { data, index, globalUrl, getAppAuthentication } = props
|
||||
|
||||
const [selectedAuthentication, setSelectedAuthentication] = React.useState({})
|
||||
const [selectedAuthenticationModalOpen, setSelectedAuthenticationModalOpen] = React.useState(false);
|
||||
const [authenticationFields, setAuthenticationFields] = React.useState([]);
|
||||
|
||||
const alert = useAlert();
|
||||
var bgColor = "#27292d";
|
||||
if (index % 2 === 0) {
|
||||
bgColor = "#1f2023";
|
||||
}
|
||||
|
||||
//console.log("Auth data: ", data)
|
||||
if (data.type === "oauth2") {
|
||||
data.fields = [
|
||||
{
|
||||
key: "url",
|
||||
value: "Secret. Replaced during app execution!",
|
||||
},
|
||||
{
|
||||
key: "client_id",
|
||||
value: "Secret. Replaced during app execution!",
|
||||
},
|
||||
{
|
||||
key: "client_secret",
|
||||
value: "Secret. Replaced during app execution!",
|
||||
},
|
||||
{
|
||||
key: "scope",
|
||||
value: "Secret. Replaced during app execution!",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const deleteAuthentication = (data) => {
|
||||
alert.info("Deleting auth " + data.label);
|
||||
|
||||
// Just use this one?
|
||||
const url = globalUrl + "/api/v1/apps/authentication/" + data.id;
|
||||
console.log("URL: ", url);
|
||||
fetch(url, {
|
||||
method: "DELETE",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((response) =>
|
||||
response.json().then((responseJson) => {
|
||||
console.log("RESP: ", responseJson);
|
||||
if (responseJson["success"] === false) {
|
||||
alert.error("Failed deleting auth");
|
||||
} else {
|
||||
// Need to wait because query in ES is too fast
|
||||
setTimeout(() => {
|
||||
getAppAuthentication();
|
||||
}, 1000);
|
||||
//alert.success("Successfully deleted authentication!")
|
||||
}
|
||||
})
|
||||
)
|
||||
.catch((error) => {
|
||||
console.log("Error in userdata: ", error);
|
||||
});
|
||||
}
|
||||
|
||||
const editAuthenticationConfig = (id) => {
|
||||
const data = {
|
||||
id: id,
|
||||
action: "assign_everywhere",
|
||||
};
|
||||
const url = globalUrl + "/api/v1/apps/authentication/" + id + "/config";
|
||||
|
||||
fetch(url, {
|
||||
mode: "cors",
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
credentials: "include",
|
||||
crossDomain: true,
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
})
|
||||
.then((response) =>
|
||||
response.json().then((responseJson) => {
|
||||
if (responseJson["success"] === false) {
|
||||
alert.error("Failed overwriting appauth in workflows");
|
||||
} else {
|
||||
alert.success("Successfully updated auth everywhere!");
|
||||
//setSelectedUserModalOpen(false);
|
||||
setTimeout(() => {
|
||||
getAppAuthentication();
|
||||
}, 1000);
|
||||
}
|
||||
})
|
||||
)
|
||||
.catch((error) => {
|
||||
alert.error("Err: " + error.toString());
|
||||
});
|
||||
};
|
||||
|
||||
const updateAppAuthentication = (field) => {
|
||||
setSelectedAuthenticationModalOpen(true);
|
||||
setSelectedAuthentication(field);
|
||||
//{selectedAuthentication.fields.map((data, index) => {
|
||||
var newfields = [];
|
||||
for (var key in field.fields) {
|
||||
newfields.push({
|
||||
key: field.fields[key].key,
|
||||
value: "",
|
||||
});
|
||||
}
|
||||
setAuthenticationFields(newfields);
|
||||
}
|
||||
|
||||
return (
|
||||
<ListItem key={index} style={{ backgroundColor: bgColor }}>
|
||||
<ListItemText
|
||||
primary=<img
|
||||
alt=""
|
||||
src={data.app.large_image}
|
||||
style={{
|
||||
maxWidth: 50,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
}}
|
||||
/>
|
||||
style={{ minWidth: 75, maxWidth: 75 }}
|
||||
/>
|
||||
<ListItemText
|
||||
primary={data.label}
|
||||
style={{
|
||||
minWidth: 225,
|
||||
maxWidth: 225,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
/>
|
||||
<ListItemText
|
||||
primary={data.app.name}
|
||||
style={{ minWidth: 175, maxWidth: 175, marginLeft: 10 }}
|
||||
/>
|
||||
{/*
|
||||
<ListItemText
|
||||
primary={data.defined === false ? "No" : "Yes"}
|
||||
style={{ minWidth: 100, maxWidth: 100, }}
|
||||
/>
|
||||
*/}
|
||||
<ListItemText
|
||||
primary={
|
||||
data.workflow_count === null ? 0 : data.workflow_count
|
||||
}
|
||||
style={{
|
||||
minWidth: 100,
|
||||
maxWidth: 100,
|
||||
textAlign: "center",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
/>
|
||||
{/*
|
||||
<ListItemText
|
||||
primary={data.node_count}
|
||||
style={{
|
||||
minWidth: 110,
|
||||
maxWidth: 110,
|
||||
textAlign: "center",
|
||||
overflow: "hidden",
|
||||
}}
|
||||
/>
|
||||
*/}
|
||||
<ListItemText
|
||||
primary={
|
||||
data.fields === null || data.fields === undefined
|
||||
? ""
|
||||
: data.fields
|
||||
.map((data) => {
|
||||
return data.key;
|
||||
})
|
||||
.join(", ")
|
||||
}
|
||||
style={{
|
||||
minWidth: 125,
|
||||
maxWidth: 125,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
/>
|
||||
<ListItemText
|
||||
style={{
|
||||
maxWidth: 230,
|
||||
minWidth: 230,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
primary={new Date(data.created * 1000).toISOString()}
|
||||
/>
|
||||
<ListItemText>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
updateAppAuthentication(data);
|
||||
}}
|
||||
>
|
||||
<EditIcon color="primary" />
|
||||
</IconButton>
|
||||
{data.defined ? (
|
||||
<Tooltip
|
||||
color="primary"
|
||||
title="Set in EVERY workflow"
|
||||
placement="top"
|
||||
>
|
||||
<IconButton
|
||||
style={{ marginRight: 10 }}
|
||||
disabled={data.defined === false}
|
||||
onClick={() => {
|
||||
editAuthenticationConfig(data.id);
|
||||
}}
|
||||
>
|
||||
<SelectAllIcon
|
||||
color={data.defined ? "primary" : "secondary"}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<Tooltip
|
||||
color="primary"
|
||||
title="Must edit before you can set in all workflows"
|
||||
placement="top"
|
||||
>
|
||||
<IconButton
|
||||
style={{ marginRight: 10 }}
|
||||
onClick={() => {}}
|
||||
>
|
||||
<SelectAllIcon
|
||||
color={data.defined ? "primary" : "secondary"}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
deleteAuthentication(data);
|
||||
}}
|
||||
>
|
||||
<DeleteIcon color="primary" />
|
||||
</IconButton>
|
||||
</ListItemText>
|
||||
</ListItem>
|
||||
)
|
||||
}
|
||||
|
||||
export default AuthenticationItem
|
||||
@@ -0,0 +1,366 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import theme from '../theme';
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
|
||||
import {
|
||||
Button,
|
||||
Divider,
|
||||
Select,
|
||||
MenuItem,
|
||||
TextField,
|
||||
DialogActions,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
Typography,
|
||||
} from "@material-ui/core";
|
||||
|
||||
import {
|
||||
LockOpen as LockOpenIcon,
|
||||
} from "@material-ui/icons";
|
||||
|
||||
const AuthenticationData = (props) => {
|
||||
const {
|
||||
globalUrl,
|
||||
saveWorkflow,
|
||||
selectedApp,
|
||||
workflow,
|
||||
selectedAction,
|
||||
authenticationType,
|
||||
getAppAuthentication,
|
||||
appAuthentication,
|
||||
setSelectedAction,
|
||||
setAuthenticationModalOpen,
|
||||
isCloud,
|
||||
} = props;
|
||||
|
||||
const setNewAppAuth = (appAuthData) => {
|
||||
console.log("DAta: ", appAuthData);
|
||||
fetch(globalUrl + "/api/v1/apps/authentication", {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: JSON.stringify(appAuthData),
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status !== 200) {
|
||||
console.log("Status not 200 for setting app auth :O!");
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (!responseJson.success) {
|
||||
alert.error("Failed to set app auth: " + responseJson.reason);
|
||||
} else {
|
||||
if (getAppAuthentication !== undefined) {
|
||||
getAppAuthentication()
|
||||
}
|
||||
|
||||
if (setAuthenticationModalOpen !== undefined) {
|
||||
setAuthenticationModalOpen(false)
|
||||
}
|
||||
|
||||
// Needs a refresh with the new authentication..
|
||||
//alert.success("Successfully saved new app auth")
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
//alert.error(error.toString());
|
||||
console.log("New auth error: ", error.toString());
|
||||
});
|
||||
}
|
||||
|
||||
const [authenticationOption, setAuthenticationOptions] = React.useState({
|
||||
app: JSON.parse(JSON.stringify(selectedApp)),
|
||||
fields: {},
|
||||
label: "",
|
||||
usage: [
|
||||
{
|
||||
workflow_id: workflow.id,
|
||||
},
|
||||
],
|
||||
id: uuidv4(),
|
||||
active: true,
|
||||
});
|
||||
|
||||
if (
|
||||
selectedApp.authentication === undefined ||
|
||||
selectedApp.authentication.parameters === null ||
|
||||
selectedApp.authentication.parameters === undefined ||
|
||||
selectedApp.authentication.parameters.length === 0
|
||||
) {
|
||||
return (
|
||||
<DialogContent style={{ textAlign: "center", marginTop: 50 }}>
|
||||
<Typography variant="h4" id="draggable-dialog-title" style={{cursor: "move",}}>
|
||||
{selectedApp.name} does not require authentication
|
||||
</Typography>
|
||||
</DialogContent>
|
||||
);
|
||||
}
|
||||
|
||||
authenticationOption.app.actions = [];
|
||||
|
||||
for (var key in selectedApp.authentication.parameters) {
|
||||
if (
|
||||
authenticationOption.fields[
|
||||
selectedApp.authentication.parameters[key].name
|
||||
] === undefined
|
||||
) {
|
||||
authenticationOption.fields[
|
||||
selectedApp.authentication.parameters[key].name
|
||||
] = "";
|
||||
}
|
||||
}
|
||||
|
||||
const handleSubmitCheck = () => {
|
||||
console.log("NEW AUTH: ", authenticationOption);
|
||||
if (authenticationOption.label.length === 0) {
|
||||
authenticationOption.label = `Auth for ${selectedApp.name}`;
|
||||
}
|
||||
|
||||
// Automatically mapping fields that already exist (predefined).
|
||||
// Warning if fields are NOT filled
|
||||
for (var key in selectedApp.authentication.parameters) {
|
||||
if (
|
||||
authenticationOption.fields[
|
||||
selectedApp.authentication.parameters[key].name
|
||||
].length === 0
|
||||
) {
|
||||
if (
|
||||
selectedApp.authentication.parameters[key].value !== undefined &&
|
||||
selectedApp.authentication.parameters[key].value !== null &&
|
||||
selectedApp.authentication.parameters[key].value.length > 0
|
||||
) {
|
||||
authenticationOption.fields[
|
||||
selectedApp.authentication.parameters[key].name
|
||||
] = selectedApp.authentication.parameters[key].value;
|
||||
} else {
|
||||
if (
|
||||
selectedApp.authentication.parameters[key].schema.type === "bool"
|
||||
) {
|
||||
authenticationOption.fields[
|
||||
selectedApp.authentication.parameters[key].name
|
||||
] = "false";
|
||||
} else {
|
||||
alert.info(
|
||||
"Field " +
|
||||
selectedApp.authentication.parameters[key].name +
|
||||
" can't be empty"
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Action: ", selectedAction);
|
||||
selectedAction.authentication_id = authenticationOption.id;
|
||||
selectedAction.selectedAuthentication = authenticationOption;
|
||||
if (
|
||||
selectedAction.authentication === undefined ||
|
||||
selectedAction.authentication === null
|
||||
) {
|
||||
selectedAction.authentication = [authenticationOption];
|
||||
} else {
|
||||
selectedAction.authentication.push(authenticationOption);
|
||||
}
|
||||
|
||||
setSelectedAction(selectedAction);
|
||||
|
||||
var newAuthOption = JSON.parse(JSON.stringify(authenticationOption));
|
||||
var newFields = [];
|
||||
for (const key in newAuthOption.fields) {
|
||||
const value = newAuthOption.fields[key];
|
||||
newFields.push({
|
||||
key: key,
|
||||
value: value,
|
||||
});
|
||||
}
|
||||
|
||||
console.log("FIELDS: ", newFields);
|
||||
newAuthOption.fields = newFields;
|
||||
setNewAppAuth(newAuthOption);
|
||||
|
||||
//if (configureWorkflowModalOpen) {
|
||||
// setSelectedAction({});
|
||||
//}
|
||||
|
||||
//setUpdate(authenticationOption.id);
|
||||
};
|
||||
|
||||
if (
|
||||
authenticationOption.label === null ||
|
||||
authenticationOption.label === undefined
|
||||
) {
|
||||
authenticationOption.label = selectedApp.name + " authentication";
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<DialogTitle id="draggable-dialog-title" style={{cursor: "move",}}>
|
||||
<div style={{ color: "white" }}>
|
||||
Authentication for {selectedApp.name}
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://shuffler.io/docs/apps#authentication"
|
||||
style={{ textDecoration: "none", color: "#f85a3e" }}
|
||||
>
|
||||
What is app authentication?
|
||||
</a>
|
||||
<div />
|
||||
These are required fields for authenticating with {selectedApp.name}
|
||||
<div style={{ marginTop: 15 }} />
|
||||
<b>Name - what is this used for?</b>
|
||||
<TextField
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
color: "white",
|
||||
marginLeft: "5px",
|
||||
maxWidth: "95%",
|
||||
height: 50,
|
||||
fontSize: "1em",
|
||||
},
|
||||
}}
|
||||
fullWidth
|
||||
color="primary"
|
||||
placeholder={"Auth july 2020"}
|
||||
defaultValue={`Auth for ${selectedApp.name}`}
|
||||
onChange={(event) => {
|
||||
authenticationOption.label = event.target.value;
|
||||
}}
|
||||
/>
|
||||
<Divider
|
||||
style={{
|
||||
marginTop: 15,
|
||||
marginBottom: 15,
|
||||
backgroundColor: "rgb(91, 96, 100)",
|
||||
}}
|
||||
/>
|
||||
<div />
|
||||
{selectedApp.authentication.parameters.map((data, index) => {
|
||||
return (
|
||||
<div key={index} style={{ marginTop: 10 }}>
|
||||
<LockOpenIcon style={{ marginRight: 10 }} />
|
||||
<b>{data.name}</b>
|
||||
|
||||
{data.schema !== undefined &&
|
||||
data.schema !== null &&
|
||||
data.schema.type === "bool" ? (
|
||||
<Select
|
||||
MenuProps={{
|
||||
disableScrollLock: true,
|
||||
}}
|
||||
SelectDisplayProps={{
|
||||
style: {
|
||||
marginLeft: 10,
|
||||
},
|
||||
}}
|
||||
defaultValue={"false"}
|
||||
fullWidth
|
||||
onChange={(e) => {
|
||||
console.log("Value: ", e.target.value);
|
||||
authenticationOption.fields[data.name] = e.target.value;
|
||||
}}
|
||||
style={{
|
||||
backgroundColor: theme.palette.surfaceColor,
|
||||
color: "white",
|
||||
height: 50,
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
key={"false"}
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
color: "white",
|
||||
}}
|
||||
value={"false"}
|
||||
>
|
||||
false
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
key={"true"}
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
color: "white",
|
||||
}}
|
||||
value={"true"}
|
||||
>
|
||||
true
|
||||
</MenuItem>
|
||||
</Select>
|
||||
) : (
|
||||
<TextField
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
color: "white",
|
||||
marginLeft: "5px",
|
||||
maxWidth: "95%",
|
||||
height: 50,
|
||||
fontSize: "1em",
|
||||
},
|
||||
}}
|
||||
fullWidth
|
||||
type={
|
||||
data.example !== undefined && data.example.includes("***")
|
||||
? "password"
|
||||
: "text"
|
||||
}
|
||||
color="primary"
|
||||
defaultValue={
|
||||
data.value !== undefined && data.value !== null
|
||||
? data.value
|
||||
: ""
|
||||
}
|
||||
placeholder={data.example}
|
||||
onChange={(event) => {
|
||||
authenticationOption.fields[data.name] =
|
||||
event.target.value;
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
style={{ borderRadius: "0px" }}
|
||||
onClick={() => {
|
||||
setAuthenticationModalOpen(false);
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
style={{ borderRadius: "0px" }}
|
||||
onClick={() => {
|
||||
setAuthenticationOptions(authenticationOption);
|
||||
handleSubmitCheck();
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthenticationData
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useInterval } from "react-powerhooks";
|
||||
|
||||
import {
|
||||
InputAdornment,
|
||||
@@ -13,9 +14,15 @@ import {
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Fade,
|
||||
} from "@material-ui/core";
|
||||
import { FavoriteBorder as FavoriteBorderIcon } from "@material-ui/icons";
|
||||
import {
|
||||
FavoriteBorder as FavoriteBorderIcon,
|
||||
Error as ErrorIcon,
|
||||
CheckCircleRounded as CheckCircleRoundedIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { FixName } from "../views/Apps.jsx";
|
||||
import aa from 'search-insights'
|
||||
|
||||
// Handles workflow updates on first open to highlight the issues of the workflow
|
||||
// Variables
|
||||
@@ -26,6 +33,7 @@ import { FixName } from "../views/Apps.jsx";
|
||||
// Specifically used for UNSAVED workflows only?
|
||||
const ConfigureWorkflow = (props) => {
|
||||
const {
|
||||
userdata,
|
||||
globalUrl,
|
||||
theme,
|
||||
workflow,
|
||||
@@ -43,15 +51,34 @@ const ConfigureWorkflow = (props) => {
|
||||
isCloud,
|
||||
setAuthenticationType,
|
||||
alert,
|
||||
showTriggers,
|
||||
workflowExecutions,
|
||||
getWorkflowExecution,
|
||||
} = props;
|
||||
const [requiredActions, setRequiredActions] = React.useState([]);
|
||||
const [requiredVariables, setRequiredVariables] = React.useState([]);
|
||||
const [requiredTriggers, setRequiredTriggers] = React.useState([]);
|
||||
const [previousAuth, setPreviousAuth] = React.useState(appAuthentication);
|
||||
const [firstLoad, setFirstLoad] = React.useState("");
|
||||
const [itemChanged, setItemChanged] = React.useState(false);
|
||||
var finished = false;
|
||||
const [firstLoad, setFirstLoad] = React.useState("");
|
||||
const [showFinalizeAnimation, setShowFinalizeAnimation] = React.useState(false);
|
||||
|
||||
const [checkStarted, setCheckStarted] = React.useState(false);
|
||||
|
||||
const { start, stop } = useInterval({
|
||||
duration: 3000,
|
||||
startImmediate: false,
|
||||
callback: () => {
|
||||
if (getWorkflowExecution !== undefined && workflowExecutions !== undefined) {
|
||||
const paramkey = workflow.id
|
||||
getWorkflowExecution(paramkey)
|
||||
} else {
|
||||
console.log("Executions or getWorkflowExecutions not defined")
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
// Where is this from?
|
||||
if (workflow === undefined || workflow === null) {
|
||||
return null;
|
||||
}
|
||||
@@ -94,18 +121,13 @@ const ConfigureWorkflow = (props) => {
|
||||
};
|
||||
|
||||
if (firstLoad.length === 0 || firstLoad !== workflow.id) {
|
||||
if (finished) {
|
||||
setConfigureWorkflowModalOpen(false);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (apps === undefined || apps === null || apps.length === 0) {
|
||||
console.log("No apps loaded: ", apps);
|
||||
setConfigureWorkflowModalOpen(false);
|
||||
return null;
|
||||
}
|
||||
|
||||
setFirstLoad(workflow.id);
|
||||
setFirstLoad(workflow.id)
|
||||
const newactions = [];
|
||||
for (var key in workflow.actions) {
|
||||
const action = workflow.actions[key];
|
||||
@@ -121,6 +143,8 @@ const ConfigureWorkflow = (props) => {
|
||||
action: action,
|
||||
update_version: action.app_version,
|
||||
app: {},
|
||||
steps: [],
|
||||
show_steps: false,
|
||||
};
|
||||
|
||||
const app = apps.find(
|
||||
@@ -129,17 +153,21 @@ const ConfigureWorkflow = (props) => {
|
||||
(app.app_version === action.app_version ||
|
||||
(app.loop_versions !== null &&
|
||||
app.loop_versions.includes(action.app_version)))
|
||||
);
|
||||
)
|
||||
|
||||
//newaction.steps = wazuhSteps
|
||||
if (app === undefined || app === null) {
|
||||
//console.log("App not found: ", action.app_name);
|
||||
|
||||
const subapp = apps.find(app => app.name === action.app_name)
|
||||
if (subapp !== undefined && subapp !== null) {
|
||||
newaction.update_version = "1.1.0"
|
||||
}
|
||||
|
||||
|
||||
newaction.must_activate = true;
|
||||
newaction.steps.push({
|
||||
"title": "Activate app",
|
||||
"type": "activate",
|
||||
"required": true,
|
||||
})
|
||||
} else {
|
||||
if (
|
||||
action.authentication_id === "" &&
|
||||
@@ -160,6 +188,12 @@ const ConfigureWorkflow = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
newaction.steps.push({
|
||||
"title": "Authenticate app",
|
||||
"type": "authenticate",
|
||||
"required": true,
|
||||
})
|
||||
|
||||
if (!filled) {
|
||||
newaction.must_authenticate = true;
|
||||
newaction.action_ids.push(action.id);
|
||||
@@ -247,6 +281,38 @@ const ConfigureWorkflow = (props) => {
|
||||
var trigger = workflow.triggers[key];
|
||||
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)
|
||||
|
||||
// Adding webhook to start of it
|
||||
if (foundindex >= 0) {
|
||||
const tmpsteps = newactions[foundindex].steps
|
||||
newactions[foundindex].steps = [
|
||||
{
|
||||
"title": "Configure Webhook",
|
||||
"type": "webhook",
|
||||
"required": true,
|
||||
}
|
||||
]
|
||||
|
||||
for (var subkey in tmpsteps) {
|
||||
newactions[foundindex].steps.push(tmpsteps[subkey])
|
||||
}
|
||||
|
||||
newactions[foundindex].show_steps = true
|
||||
|
||||
console.log("CHANGED ACTION: ", newactions[foundindex])
|
||||
//console.log("Index: ", newactions[foundindex])
|
||||
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (trigger.status === "running") {
|
||||
continue;
|
||||
}
|
||||
@@ -272,17 +338,19 @@ const ConfigureWorkflow = (props) => {
|
||||
setRequiredTriggers(requiredTriggers);
|
||||
setRequiredVariables(requiredVariables);
|
||||
setRequiredActions(newactions);
|
||||
}
|
||||
}
|
||||
|
||||
if (appAuthentication.length !== previousAuth.length) {
|
||||
var newactions = [];
|
||||
var newactions = []
|
||||
for (var actionkey in requiredActions) {
|
||||
var newaction = requiredActions[actionkey];
|
||||
const app = newaction.app;
|
||||
|
||||
for (var key in appAuthentication) {
|
||||
const auth = appAuthentication[key];
|
||||
if (auth.app.name === app.name && auth.active) {
|
||||
|
||||
// Does this account for all the different ones of the same?
|
||||
if (auth.app.name === app.name && auth.active === true) {
|
||||
newaction.auth_done = true;
|
||||
break;
|
||||
}
|
||||
@@ -298,7 +366,7 @@ const ConfigureWorkflow = (props) => {
|
||||
}
|
||||
|
||||
const TriggerSection = (props) => {
|
||||
const { trigger } = props;
|
||||
const { trigger } = props
|
||||
|
||||
return (
|
||||
<ListItem>
|
||||
@@ -460,6 +528,26 @@ const ConfigureWorkflow = (props) => {
|
||||
};
|
||||
|
||||
const activateApp = (app_id, app_name, app_version) => {
|
||||
|
||||
if (aa !== undefined) {
|
||||
aa('init', {
|
||||
appId: "JNSS5CFDZZ",
|
||||
apiKey: "db08e40265e2941b9a7d8f644b6e5240",
|
||||
})
|
||||
|
||||
const timestamp = new Date().getTime()
|
||||
aa('sendEvents', [
|
||||
{
|
||||
eventType: 'conversion',
|
||||
eventName: 'Public App Activated',
|
||||
index: 'appsearch',
|
||||
objectIDs: [app_id],
|
||||
timestamp: timestamp,
|
||||
userToken: userdata === undefined || userdata === null || userdata.id === undefined ? "unauthenticated" : userdata.id,
|
||||
}
|
||||
])
|
||||
}
|
||||
|
||||
fetch(
|
||||
`${globalUrl}/api/v1/apps/${app_id}/activate?app_name=${app_name}&app_version=${app_version}`,
|
||||
{
|
||||
@@ -502,6 +590,7 @@ const ConfigureWorkflow = (props) => {
|
||||
|
||||
return (
|
||||
<ListItem>
|
||||
{/*
|
||||
<ListItemAvatar>
|
||||
<Avatar variant="rounded">
|
||||
<img
|
||||
@@ -516,19 +605,28 @@ const ConfigureWorkflow = (props) => {
|
||||
secondary={action.app_version}
|
||||
style={{}}
|
||||
/>
|
||||
{action.must_authenticate ? (
|
||||
action.auth_done ? (
|
||||
<Button color="primary" variant="outlined" onClick={() => {}}>
|
||||
Authenticated
|
||||
</Button>
|
||||
) : selectedAction.app_name === action.app_name ? (
|
||||
<CircularProgress />
|
||||
) : (
|
||||
<Button
|
||||
color="primary"
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
setAuthenticationType(
|
||||
*/}
|
||||
{action.must_authenticate ?
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
disabled={action.auth_done}
|
||||
style={{
|
||||
flex: 1,
|
||||
textTransform: "none",
|
||||
textAlign: "left",
|
||||
justifyContent: "flex-start",
|
||||
backgroundColor: action.auth_done ? theme.palette.surfaceColor : theme.palette.inputColor,
|
||||
color: action.auth_done ? "#686a6c" : "#ffffff",
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
minWidth: 350,
|
||||
maxHeight: 50,
|
||||
overflow: "hidden",
|
||||
border: `1px solid ${theme.palette.inputColor}`,
|
||||
}}
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
setAuthenticationType(
|
||||
action.app.authentication.type === "oauth2" &&
|
||||
action.app.authentication.redirect_uri !== undefined &&
|
||||
action.app.authentication.redirect_uri !== null
|
||||
@@ -541,37 +639,52 @@ const ConfigureWorkflow = (props) => {
|
||||
: {
|
||||
type: "",
|
||||
}
|
||||
);
|
||||
)
|
||||
|
||||
setItemChanged(true);
|
||||
setSelectedAction(action.action);
|
||||
setSelectedApp(action.app);
|
||||
|
||||
if (setSelectedAction !== undefined) {
|
||||
setSelectedAction(action.action);
|
||||
}
|
||||
|
||||
if (setSelectedApp !== undefined) {
|
||||
setSelectedApp(action.app);
|
||||
}
|
||||
|
||||
setAuthenticationModalOpen(true);
|
||||
}}
|
||||
}}
|
||||
>
|
||||
Authenticate
|
||||
</Button>
|
||||
)
|
||||
) : null}
|
||||
{action.must_activate ? (
|
||||
<img
|
||||
alt={action.app_name}
|
||||
style={{ margin: 4, minHeight: 30, maxHeight: 30, borderRadius: theme.palette.borderRadius, }}
|
||||
src={action.large_image}
|
||||
/>
|
||||
<Typography style={{ margin: 0, marginLeft: 10 }} variant="body1">
|
||||
{action.auth_done ? "Authenticated" : `Authenticate ${action.app_name.replaceAll("_", " ")}`}
|
||||
</Typography>
|
||||
</Button>
|
||||
: null}
|
||||
{action.update_version !== action.app_version ?
|
||||
<Button
|
||||
color="primary"
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
console.log("ACTION: ", action)
|
||||
activateApp(action.action.app_id, action.app_name, action.app_version);
|
||||
setItemChanged(true);
|
||||
}}
|
||||
>
|
||||
Activate
|
||||
</Button>
|
||||
) : null}
|
||||
{action.update_version !== action.app_version ? (
|
||||
<Button
|
||||
color="primary"
|
||||
variant="contained"
|
||||
style={{marginLeft: 5}}
|
||||
onClick={() => {
|
||||
fullWidth
|
||||
variant="contained"
|
||||
disabled={action.auth_done}
|
||||
style={{
|
||||
flex: 1,
|
||||
textTransform: "none",
|
||||
textAlign: "left",
|
||||
justifyContent: "flex-start",
|
||||
backgroundColor: action.auth_done ? theme.palette.surfaceColor : theme.palette.inputColor,
|
||||
color: action.auth_done ? "#686a6c" : "#ffffff",
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
minWidth: 350,
|
||||
maxHeight: 50,
|
||||
overflow: "hidden",
|
||||
border: `1px solid ${theme.palette.inputColor}`,
|
||||
}}
|
||||
color="primary"
|
||||
onClick={(event) => {
|
||||
event.preventDefault()
|
||||
console.log("Set version to: ", action.update_version)
|
||||
|
||||
if (workflow.actions !== null) {
|
||||
@@ -592,84 +705,398 @@ const ConfigureWorkflow = (props) => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
{action.update_version}
|
||||
<img
|
||||
alt={action.app_name}
|
||||
style={{ margin: 4, minHeight: 30, maxHeight: 30, borderRadius: theme.palette.borderRadius, }}
|
||||
src={action.large_image}
|
||||
/>
|
||||
<Typography style={{ margin: 0, marginLeft: 10 }} variant="body1">
|
||||
Update to version {action.update_version}
|
||||
</Typography>
|
||||
</Button>
|
||||
) : null}
|
||||
:
|
||||
action.must_activate ?
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
disabled={action.auth_done}
|
||||
style={{
|
||||
flex: 1,
|
||||
textTransform: "none",
|
||||
textAlign: "left",
|
||||
justifyContent: "flex-start",
|
||||
backgroundColor: action.auth_done ? theme.palette.surfaceColor : theme.palette.inputColor,
|
||||
color: action.auth_done ? "#686a6c" : "#ffffff",
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
minWidth: 350,
|
||||
maxHeight: 50,
|
||||
overflow: "hidden",
|
||||
border: `1px solid ${theme.palette.inputColor}`,
|
||||
}}
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
console.log("ACTION: ", action)
|
||||
activateApp(action.action.app_id, action.app_name, action.app_version);
|
||||
setItemChanged(true);
|
||||
}}
|
||||
>
|
||||
<img
|
||||
alt={action.app_name}
|
||||
style={{ margin: 4, minHeight: 30, maxHeight: 30, borderRadius: theme.palette.borderRadius, }}
|
||||
src={action.large_image}
|
||||
/>
|
||||
<Typography style={{ margin: 0, marginLeft: 10 }} variant="body1">
|
||||
Activate
|
||||
</Typography>
|
||||
</Button>
|
||||
:
|
||||
null
|
||||
}
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
// Based on the color here. Default: #f86a3e
|
||||
//backgroundColor: selectedUsecaseCategory === usecase.name ? usecase.color : theme.palette.surfaceColor,
|
||||
|
||||
const BoxHighlight = (props) => {
|
||||
const {data, appname, appinfo, index, activeStep, setActiveStep, finished, } = props
|
||||
|
||||
const [hovered, setHovered] = useState(false)
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
// This kind of just works for new workflows..
|
||||
// What if we try many times?
|
||||
|
||||
var webhook = {
|
||||
"name": "Testhook",
|
||||
"description": `A Webhook Trigger has been started and is ready to receive events from ${appname}. Click to copy the URL to send events to.`,
|
||||
"url": "",
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (data.type === "webhook" && !finished) {
|
||||
if (!checkStarted) {
|
||||
setCheckStarted(true)
|
||||
start()
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Load webhook docs from the app itself (Wazuh)
|
||||
// Add a "sample" for what the event is supposed to look like
|
||||
// Have a listener for when ACTUALLY is received
|
||||
// INJECT the URL into the documentation when loading it in
|
||||
// How can we load it in? Should we just use the app name & get docs -> parse?
|
||||
if (data.type === "webhook" && workflow.triggers !== undefined && workflow.triggers !== null) {
|
||||
//console.log("Find webhook in the workflow!")
|
||||
for (var key in workflow.triggers) {
|
||||
if (workflow.triggers[key].trigger_type !== "WEBHOOK") {
|
||||
continue
|
||||
}
|
||||
|
||||
for (var subkey in workflow.triggers[key].parameters) {
|
||||
const param = workflow.triggers[key].parameters[subkey]
|
||||
if (param.name === "url") {
|
||||
webhook.url = param.value
|
||||
|
||||
if (isLoading === false) {
|
||||
setIsLoading(true)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (data.type == "authenticate") {
|
||||
//console.log("Handle app authentication in the workflow!")
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{backgroundColor: hovered ? theme.palette.inputColor : "inherit", padding: "10px 15px 10px 15px", borderTop: "1px solid rgba(255,255,255,0.15)",}}
|
||||
onClick={() => {
|
||||
setIsOpen(!isOpen)
|
||||
setActiveStep(index)
|
||||
}}
|
||||
onMouseOver={() => {
|
||||
setHovered(true);
|
||||
}}
|
||||
onMouseOut={() => {
|
||||
setHovered(false);
|
||||
}}
|
||||
>
|
||||
|
||||
<div style={{display: "flex"}}>
|
||||
<Typography variant="h6" style={{flex: 10, }}>{data.title}</Typography>
|
||||
{finished ?
|
||||
<CheckCircleRoundedIcon style={{color: "#0f9d58", flex: 1, }} />
|
||||
:
|
||||
<ErrorIcon style={{color: "#ffd300", flex: 1, }} />
|
||||
}
|
||||
</div>
|
||||
|
||||
{activeStep === index ?
|
||||
<div>
|
||||
{data.type === "webhook" ?
|
||||
<div onClick={(event) => {
|
||||
event.preventDefault()
|
||||
console.log("Clicked Webhook")
|
||||
|
||||
var copyText = document.getElementById("copy_element_shuffle")
|
||||
if (copyText !== undefined && copyText !== null) {
|
||||
console.log("NAVIGATOR: ", navigator);
|
||||
const clipboard = navigator.clipboard;
|
||||
if (clipboard === undefined) {
|
||||
alert.error("Can only copy over HTTPS (port 3443)");
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(webhook.url);
|
||||
copyText.select();
|
||||
copyText.setSelectionRange(
|
||||
0,
|
||||
99999
|
||||
); /* For mobile devices */
|
||||
|
||||
/* Copy the text inside the text field */
|
||||
document.execCommand("copy");
|
||||
alert.success("Copied Webhook URL");
|
||||
}
|
||||
}}>
|
||||
<Typography variant="body2" color="textSecondary">{webhook.description}</Typography>
|
||||
{/*<Typography variant="body2" color="textSecondary">{webhook.url}</Typography>*/}
|
||||
{isLoading && finished === false ?
|
||||
<div style={{margin: "auto", width: 60, height: 60, marginTop: 5, }}>
|
||||
<CircularProgress />
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
</div>
|
||||
:
|
||||
<AppSection key={index} action={appinfo} />
|
||||
}
|
||||
</div>
|
||||
: null}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const AppWrapper = (props) => {
|
||||
const {data, parentindex} = props
|
||||
const [clicked, setClicked] = useState(true)
|
||||
const [hovered, setHovered] = useState(false)
|
||||
const [activeStep, setActiveStep] = useState(0)
|
||||
const [firstRun, setFirstRun] = useState(true)
|
||||
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={{display: "flex", marginLeft: 15, marginTop: 15, marginBottom: 15, }}
|
||||
onClick={() => {
|
||||
//setClicked(!clicked)
|
||||
}}
|
||||
onMouseOver={() => {
|
||||
setHovered(true);
|
||||
}}
|
||||
onMouseOut={() => {
|
||||
setHovered(false);
|
||||
}}
|
||||
>
|
||||
<Avatar variant="rounded" style={{}}>
|
||||
<img
|
||||
alt={data.label}
|
||||
src={data.large_image}
|
||||
style={{ width: 50, }}
|
||||
/>
|
||||
</Avatar>
|
||||
<Typography variant="h6" style={{marginLeft: 15, }}>
|
||||
Configure {data.app_name.replaceAll("_", " ")}
|
||||
</Typography>
|
||||
</div>
|
||||
{clicked === true ?
|
||||
data.steps.map((step, index) => {
|
||||
var finished = false
|
||||
if (step.type === "activate") {
|
||||
if (data.activation_done === true) {
|
||||
finished = true
|
||||
|
||||
if (index === activeStep && firstRun === true) {
|
||||
setActiveStep(activeStep+1)
|
||||
}
|
||||
|
||||
if (firstRun) {
|
||||
setFinishCount(finishCount+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (step.type === "authenticate") {
|
||||
console.log("AUTH STEP: ", step)
|
||||
if (data.must_authenticate === true ) {
|
||||
finished = false
|
||||
} else {
|
||||
if (data.activation_done === true && data.auth_done === true) {
|
||||
finished = true
|
||||
|
||||
if (firstRun) {
|
||||
setFinishCount(finishCount+1)
|
||||
}
|
||||
|
||||
if (index === activeStep && firstRun === true) {
|
||||
setActiveStep(activeStep+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (step.type === "webhook") {
|
||||
for (var key in workflowExecutions) {
|
||||
const exec = workflowExecutions[key]
|
||||
if (exec.execution_argument !== undefined && exec.execution_argument !== null && exec.execution_argument.length > 0 && exec.execution_source === "webhook") {
|
||||
//console.log("Done: ", exec)
|
||||
|
||||
finished = true
|
||||
if (index === activeStep && firstRun === true) {
|
||||
setActiveStep(activeStep+1)
|
||||
|
||||
}
|
||||
|
||||
if (firstRun) {
|
||||
setFinishCount(finishCount+1)
|
||||
}
|
||||
|
||||
// Finished + source = webhook
|
||||
|
||||
stop()
|
||||
//if (isLoading === true) {
|
||||
// setIsLoading(false)
|
||||
//}
|
||||
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (firstRun === true && index === data.steps.length-1) {
|
||||
setFirstRun(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<BoxHighlight appinfo={data} appname={"Wazuh"} key={index} data={step} index={index} activeStep={activeStep} setActiveStep={setActiveStep} finished={finished} />
|
||||
)
|
||||
})
|
||||
: null}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const topColor = "#f86a3e, #fc3922"
|
||||
return (
|
||||
<div>
|
||||
<Typography variant="h6">{workflow.name}</Typography>
|
||||
<Typography variant="body1" color="textSecondary">
|
||||
The following configuration makes the workflow ready immediately.
|
||||
</Typography>
|
||||
{requiredActions.length > 0 ? (
|
||||
<span>
|
||||
<Typography variant="body1" style={{ marginTop: 10 }}>
|
||||
Actions
|
||||
</Typography>
|
||||
<List>
|
||||
{requiredActions.map((data, index) => {
|
||||
return <AppSection key={index} action={data} />;
|
||||
})}
|
||||
</List>
|
||||
</span>
|
||||
) : null}
|
||||
<div style={{height: 75, width: "100%", background: `linear-gradient(to right, ${topColor}`, position: "relative",}}>
|
||||
</div>
|
||||
<div style={{margin: "25px 50px 50px 50px", maxHeight: 475, }}>
|
||||
<Typography variant="h6">{workflow.name}</Typography>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
The following configuration makes the workflow ready immediately.
|
||||
</Typography>
|
||||
{requiredActions.length > 0 ? (
|
||||
<span>
|
||||
<Typography variant="body1" style={{ marginTop: 10, }}>
|
||||
Required Actions
|
||||
</Typography>
|
||||
|
||||
{requiredVariables.length > 0 ? (
|
||||
<span>
|
||||
<Typography variant="body1" style={{ marginTop: 10 }}>
|
||||
Variables
|
||||
</Typography>
|
||||
<List>
|
||||
{requiredVariables.map((data, index) => {
|
||||
return <VariableSection key={index} variable={data} />;
|
||||
})}
|
||||
</List>
|
||||
</span>
|
||||
) : null}
|
||||
<List>
|
||||
{requiredActions.map((data, index) => {
|
||||
return (
|
||||
<div>
|
||||
{data.steps !== undefined && data.steps !== null && data.show_steps === true ?
|
||||
<AppWrapper data={data} parentindex={index} />
|
||||
:
|
||||
<AppSection key={index} action={data} />
|
||||
}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</List>
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
{requiredTriggers.length > 0 ? (
|
||||
<span>
|
||||
<Typography variant="body1" style={{ marginTop: 10 }}>
|
||||
Triggers
|
||||
</Typography>
|
||||
<List>
|
||||
{requiredTriggers.map((data, index) => {
|
||||
return <TriggerSection key={index} trigger={data} />;
|
||||
})}
|
||||
</List>
|
||||
</span>
|
||||
) : null}
|
||||
<div style={{ textAlign: "center", display: "flex", marginTop: 20 }}>
|
||||
<ButtonGroup style={{ margin: "auto" }}>
|
||||
{/*
|
||||
<Button color="primary" variant={"outlined"} style={{
|
||||
}} onClick={() => {
|
||||
setConfigureWorkflowModalOpen(false)
|
||||
}}>
|
||||
Skip
|
||||
</Button>
|
||||
*/}
|
||||
<Button
|
||||
color="primary"
|
||||
variant={itemChanged ? "contained" : "outlined"}
|
||||
style={{}}
|
||||
onClick={() => {
|
||||
if (itemChanged) {
|
||||
saveWorkflow(workflow);
|
||||
window.location.reload();
|
||||
} else {
|
||||
setConfigureWorkflowModalOpen(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Close window
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
{requiredVariables.length > 0 ? (
|
||||
<span>
|
||||
<Typography variant="body1" style={{ marginTop: 10 }}>
|
||||
Variables
|
||||
</Typography>
|
||||
<List>
|
||||
{requiredVariables.map((data, index) => {
|
||||
return <VariableSection key={index} variable={data} />;
|
||||
})}
|
||||
</List>
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
{requiredTriggers.length > 0 && showTriggers !== false ? (
|
||||
<span>
|
||||
<Typography variant="body1" style={{ marginTop: 10 }}>
|
||||
Triggers
|
||||
</Typography>
|
||||
<List>
|
||||
{requiredTriggers.map((data, index) => {
|
||||
return <TriggerSection key={index} trigger={data} />;
|
||||
})}
|
||||
</List>
|
||||
</span>
|
||||
) : null}
|
||||
|
||||
<div style={{ textAlign: "center", display: "flex", marginTop: 20 }}>
|
||||
{showFinalizeAnimation ?
|
||||
<img id="finalize_gif" src="/images/finalize.gif" alt="finalize workflow animation" style={{width: 150, margin: "auto",}} onLoad={() => {
|
||||
console.log("Img loaded.")
|
||||
setTimeout(() => {
|
||||
console.log("Img closing.")
|
||||
setConfigureWorkflowModalOpen(false);
|
||||
}, 1250)
|
||||
|
||||
}}/>
|
||||
:
|
||||
<ButtonGroup style={{ margin: "auto" }}>
|
||||
{/*
|
||||
<Button color="primary" variant={"outlined"} style={{
|
||||
}} onClick={() => {
|
||||
setConfigureWorkflowModalOpen(false)
|
||||
}}>
|
||||
Skip
|
||||
</Button>
|
||||
*/}
|
||||
<Button
|
||||
color="textSecondary"
|
||||
variant={"outlined"}
|
||||
style={{}}
|
||||
onClick={() => {
|
||||
stop()
|
||||
setShowFinalizeAnimation(true)
|
||||
setTimeout(() => {
|
||||
if (itemChanged) {
|
||||
if (saveWorkflow !== undefined) {
|
||||
saveWorkflow(workflow);
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
} else {
|
||||
}
|
||||
}, 1000)
|
||||
}}
|
||||
>
|
||||
Finalize
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,434 @@
|
||||
const countries = [
|
||||
{ code: 'GB', label: 'United Kingdom', phone: '44' },
|
||||
{
|
||||
code: 'US',
|
||||
label: 'United States',
|
||||
phone: '1',
|
||||
suggested: true,
|
||||
},
|
||||
{ code: 'IN', label: 'India', phone: '91' },
|
||||
{ code: 'AD', label: 'Andorra', phone: '376' },
|
||||
{
|
||||
code: 'AE',
|
||||
label: 'United Arab Emirates',
|
||||
phone: '971',
|
||||
},
|
||||
{ code: 'AF', label: 'Afghanistan', phone: '93' },
|
||||
{
|
||||
code: 'AG',
|
||||
label: 'Antigua and Barbuda',
|
||||
phone: '1-268',
|
||||
},
|
||||
{ code: 'AI', label: 'Anguilla', phone: '1-264' },
|
||||
{ code: 'AL', label: 'Albania', phone: '355' },
|
||||
{ code: 'AM', label: 'Armenia', phone: '374' },
|
||||
{ code: 'AO', label: 'Angola', phone: '244' },
|
||||
{ code: 'AQ', label: 'Antarctica', phone: '672' },
|
||||
{ code: 'AR', label: 'Argentina', phone: '54' },
|
||||
{ code: 'AS', label: 'American Samoa', phone: '1-684' },
|
||||
{ code: 'AT', label: 'Austria', phone: '43' },
|
||||
{
|
||||
code: 'AU',
|
||||
label: 'Australia',
|
||||
phone: '61',
|
||||
suggested: true,
|
||||
},
|
||||
{ code: 'AW', label: 'Aruba', phone: '297' },
|
||||
{ code: 'AX', label: 'Alland Islands', phone: '358' },
|
||||
{ code: 'AZ', label: 'Azerbaijan', phone: '994' },
|
||||
{
|
||||
code: 'BA',
|
||||
label: 'Bosnia and Herzegovina',
|
||||
phone: '387',
|
||||
},
|
||||
{ code: 'BB', label: 'Barbados', phone: '1-246' },
|
||||
{ code: 'BD', label: 'Bangladesh', phone: '880' },
|
||||
{ code: 'BE', label: 'Belgium', phone: '32' },
|
||||
{ code: 'BF', label: 'Burkina Faso', phone: '226' },
|
||||
{ code: 'BG', label: 'Bulgaria', phone: '359' },
|
||||
{ code: 'BH', label: 'Bahrain', phone: '973' },
|
||||
{ code: 'BI', label: 'Burundi', phone: '257' },
|
||||
{ code: 'BJ', label: 'Benin', phone: '229' },
|
||||
{ code: 'BL', label: 'Saint Barthelemy', phone: '590' },
|
||||
{ code: 'BM', label: 'Bermuda', phone: '1-441' },
|
||||
{ code: 'BN', label: 'Brunei Darussalam', phone: '673' },
|
||||
{ code: 'BO', label: 'Bolivia', phone: '591' },
|
||||
{ code: 'BR', label: 'Brazil', phone: '55' },
|
||||
{ code: 'BS', label: 'Bahamas', phone: '1-242' },
|
||||
{ code: 'BT', label: 'Bhutan', phone: '975' },
|
||||
{ code: 'BV', label: 'Bouvet Island', phone: '47' },
|
||||
{ code: 'BW', label: 'Botswana', phone: '267' },
|
||||
{ code: 'BY', label: 'Belarus', phone: '375' },
|
||||
{ code: 'BZ', label: 'Belize', phone: '501' },
|
||||
{
|
||||
code: 'CA',
|
||||
label: 'Canada',
|
||||
phone: '1',
|
||||
suggested: true,
|
||||
},
|
||||
{
|
||||
code: 'CC',
|
||||
label: 'Cocos (Keeling) Islands',
|
||||
phone: '61',
|
||||
},
|
||||
{
|
||||
code: 'CD',
|
||||
label: 'Congo, Democratic Republic of the',
|
||||
phone: '243',
|
||||
},
|
||||
{
|
||||
code: 'CF',
|
||||
label: 'Central African Republic',
|
||||
phone: '236',
|
||||
},
|
||||
{
|
||||
code: 'CG',
|
||||
label: 'Congo, Republic of the',
|
||||
phone: '242',
|
||||
},
|
||||
{ code: 'CH', label: 'Switzerland', phone: '41' },
|
||||
{ code: 'CI', label: "Cote d'Ivoire", phone: '225' },
|
||||
{ code: 'CK', label: 'Cook Islands', phone: '682' },
|
||||
{ code: 'CL', label: 'Chile', phone: '56' },
|
||||
{ code: 'CM', label: 'Cameroon', phone: '237' },
|
||||
{ code: 'CN', label: 'China', phone: '86' },
|
||||
{ code: 'CO', label: 'Colombia', phone: '57' },
|
||||
{ code: 'CR', label: 'Costa Rica', phone: '506' },
|
||||
{ code: 'CU', label: 'Cuba', phone: '53' },
|
||||
{ code: 'CV', label: 'Cape Verde', phone: '238' },
|
||||
{ code: 'CW', label: 'Curacao', phone: '599' },
|
||||
{ code: 'CX', label: 'Christmas Island', phone: '61' },
|
||||
{ code: 'CY', label: 'Cyprus', phone: '357' },
|
||||
{ code: 'CZ', label: 'Czech Republic', phone: '420' },
|
||||
{
|
||||
code: 'DE',
|
||||
label: 'Germany',
|
||||
phone: '49',
|
||||
suggested: true,
|
||||
},
|
||||
{ code: 'DJ', label: 'Djibouti', phone: '253' },
|
||||
{ code: 'DK', label: 'Denmark', phone: '45' },
|
||||
{ code: 'DM', label: 'Dominica', phone: '1-767' },
|
||||
{
|
||||
code: 'DO',
|
||||
label: 'Dominican Republic',
|
||||
phone: '1-809',
|
||||
},
|
||||
{ code: 'DZ', label: 'Algeria', phone: '213' },
|
||||
{ code: 'EC', label: 'Ecuador', phone: '593' },
|
||||
{ code: 'EE', label: 'Estonia', phone: '372' },
|
||||
{ code: 'EG', label: 'Egypt', phone: '20' },
|
||||
{ code: 'EH', label: 'Western Sahara', phone: '212' },
|
||||
{ code: 'ER', label: 'Eritrea', phone: '291' },
|
||||
{ code: 'ES', label: 'Spain', phone: '34' },
|
||||
{ code: 'ET', label: 'Ethiopia', phone: '251' },
|
||||
{ code: 'FI', label: 'Finland', phone: '358' },
|
||||
{ code: 'FJ', label: 'Fiji', phone: '679' },
|
||||
{
|
||||
code: 'FK',
|
||||
label: 'Falkland Islands (Malvinas)',
|
||||
phone: '500',
|
||||
},
|
||||
{
|
||||
code: 'FM',
|
||||
label: 'Micronesia, Federated States of',
|
||||
phone: '691',
|
||||
},
|
||||
{ code: 'FO', label: 'Faroe Islands', phone: '298' },
|
||||
{
|
||||
code: 'FR',
|
||||
label: 'France',
|
||||
phone: '33',
|
||||
suggested: true,
|
||||
},
|
||||
{ code: 'GA', label: 'Gabon', phone: '241' },
|
||||
{ code: 'GB', label: 'United Kingdom', phone: '44' },
|
||||
{ code: 'GD', label: 'Grenada', phone: '1-473' },
|
||||
{ code: 'GE', label: 'Georgia', phone: '995' },
|
||||
{ code: 'GF', label: 'French Guiana', phone: '594' },
|
||||
{ code: 'GG', label: 'Guernsey', phone: '44' },
|
||||
{ code: 'GH', label: 'Ghana', phone: '233' },
|
||||
{ code: 'GI', label: 'Gibraltar', phone: '350' },
|
||||
{ code: 'GL', label: 'Greenland', phone: '299' },
|
||||
{ code: 'GM', label: 'Gambia', phone: '220' },
|
||||
{ code: 'GN', label: 'Guinea', phone: '224' },
|
||||
{ code: 'GP', label: 'Guadeloupe', phone: '590' },
|
||||
{ code: 'GQ', label: 'Equatorial Guinea', phone: '240' },
|
||||
{ code: 'GR', label: 'Greece', phone: '30' },
|
||||
{
|
||||
code: 'GS',
|
||||
label: 'South Georgia and the South Sandwich Islands',
|
||||
phone: '500',
|
||||
},
|
||||
{ code: 'GT', label: 'Guatemala', phone: '502' },
|
||||
{ code: 'GU', label: 'Guam', phone: '1-671' },
|
||||
{ code: 'GW', label: 'Guinea-Bissau', phone: '245' },
|
||||
{ code: 'GY', label: 'Guyana', phone: '592' },
|
||||
{ code: 'HK', label: 'Hong Kong', phone: '852' },
|
||||
{
|
||||
code: 'HM',
|
||||
label: 'Heard Island and McDonald Islands',
|
||||
phone: '672',
|
||||
},
|
||||
{ code: 'HN', label: 'Honduras', phone: '504' },
|
||||
{ code: 'HR', label: 'Croatia', phone: '385' },
|
||||
{ code: 'HT', label: 'Haiti', phone: '509' },
|
||||
{ code: 'HU', label: 'Hungary', phone: '36' },
|
||||
{ code: 'ID', label: 'Indonesia', phone: '62' },
|
||||
{ code: 'IE', label: 'Ireland', phone: '353' },
|
||||
{ code: 'IL', label: 'Israel', phone: '972' },
|
||||
{ code: 'IM', label: 'Isle of Man', phone: '44' },
|
||||
{ code: 'IN', label: 'India', phone: '91' },
|
||||
{
|
||||
code: 'IO',
|
||||
label: 'British Indian Ocean Territory',
|
||||
phone: '246',
|
||||
},
|
||||
{ code: 'IQ', label: 'Iraq', phone: '964' },
|
||||
{
|
||||
code: 'IR',
|
||||
label: 'Iran, Islamic Republic of',
|
||||
phone: '98',
|
||||
},
|
||||
{ code: 'IS', label: 'Iceland', phone: '354' },
|
||||
{ code: 'IT', label: 'Italy', phone: '39' },
|
||||
{ code: 'JE', label: 'Jersey', phone: '44' },
|
||||
{ code: 'JM', label: 'Jamaica', phone: '1-876' },
|
||||
{ code: 'JO', label: 'Jordan', phone: '962' },
|
||||
{
|
||||
code: 'JP',
|
||||
label: 'Japan',
|
||||
phone: '81',
|
||||
suggested: true,
|
||||
},
|
||||
{ code: 'KE', label: 'Kenya', phone: '254' },
|
||||
{ code: 'KG', label: 'Kyrgyzstan', phone: '996' },
|
||||
{ code: 'KH', label: 'Cambodia', phone: '855' },
|
||||
{ code: 'KI', label: 'Kiribati', phone: '686' },
|
||||
{ code: 'KM', label: 'Comoros', phone: '269' },
|
||||
{
|
||||
code: 'KN',
|
||||
label: 'Saint Kitts and Nevis',
|
||||
phone: '1-869',
|
||||
},
|
||||
{
|
||||
code: 'KP',
|
||||
label: "Korea, Democratic People's Republic of",
|
||||
phone: '850',
|
||||
},
|
||||
{ code: 'KR', label: 'Korea, Republic of', phone: '82' },
|
||||
{ code: 'KW', label: 'Kuwait', phone: '965' },
|
||||
{ code: 'KY', label: 'Cayman Islands', phone: '1-345' },
|
||||
{ code: 'KZ', label: 'Kazakhstan', phone: '7' },
|
||||
{
|
||||
code: 'LA',
|
||||
label: "Lao People's Democratic Republic",
|
||||
phone: '856',
|
||||
},
|
||||
{ code: 'LB', label: 'Lebanon', phone: '961' },
|
||||
{ code: 'LC', label: 'Saint Lucia', phone: '1-758' },
|
||||
{ code: 'LI', label: 'Liechtenstein', phone: '423' },
|
||||
{ code: 'LK', label: 'Sri Lanka', phone: '94' },
|
||||
{ code: 'LR', label: 'Liberia', phone: '231' },
|
||||
{ code: 'LS', label: 'Lesotho', phone: '266' },
|
||||
{ code: 'LT', label: 'Lithuania', phone: '370' },
|
||||
{ code: 'LU', label: 'Luxembourg', phone: '352' },
|
||||
{ code: 'LV', label: 'Latvia', phone: '371' },
|
||||
{ code: 'LY', label: 'Libya', phone: '218' },
|
||||
{ code: 'MA', label: 'Morocco', phone: '212' },
|
||||
{ code: 'MC', label: 'Monaco', phone: '377' },
|
||||
{
|
||||
code: 'MD',
|
||||
label: 'Moldova, Republic of',
|
||||
phone: '373',
|
||||
},
|
||||
{ code: 'ME', label: 'Montenegro', phone: '382' },
|
||||
{
|
||||
code: 'MF',
|
||||
label: 'Saint Martin (French part)',
|
||||
phone: '590',
|
||||
},
|
||||
{ code: 'MG', label: 'Madagascar', phone: '261' },
|
||||
{ code: 'MH', label: 'Marshall Islands', phone: '692' },
|
||||
{
|
||||
code: 'MK',
|
||||
label: 'Macedonia, the Former Yugoslav Republic of',
|
||||
phone: '389',
|
||||
},
|
||||
{ code: 'ML', label: 'Mali', phone: '223' },
|
||||
{ code: 'MM', label: 'Myanmar', phone: '95' },
|
||||
{ code: 'MN', label: 'Mongolia', phone: '976' },
|
||||
{ code: 'MO', label: 'Macao', phone: '853' },
|
||||
{
|
||||
code: 'MP',
|
||||
label: 'Northern Mariana Islands',
|
||||
phone: '1-670',
|
||||
},
|
||||
{ code: 'MQ', label: 'Martinique', phone: '596' },
|
||||
{ code: 'MR', label: 'Mauritania', phone: '222' },
|
||||
{ code: 'MS', label: 'Montserrat', phone: '1-664' },
|
||||
{ code: 'MT', label: 'Malta', phone: '356' },
|
||||
{ code: 'MU', label: 'Mauritius', phone: '230' },
|
||||
{ code: 'MV', label: 'Maldives', phone: '960' },
|
||||
{ code: 'MW', label: 'Malawi', phone: '265' },
|
||||
{ code: 'MX', label: 'Mexico', phone: '52' },
|
||||
{ code: 'MY', label: 'Malaysia', phone: '60' },
|
||||
{ code: 'MZ', label: 'Mozambique', phone: '258' },
|
||||
{ code: 'NA', label: 'Namibia', phone: '264' },
|
||||
{ code: 'NC', label: 'New Caledonia', phone: '687' },
|
||||
{ code: 'NE', label: 'Niger', phone: '227' },
|
||||
{ code: 'NF', label: 'Norfolk Island', phone: '672' },
|
||||
{ code: 'NG', label: 'Nigeria', phone: '234' },
|
||||
{ code: 'NI', label: 'Nicaragua', phone: '505' },
|
||||
{ code: 'NL', label: 'Netherlands', phone: '31' },
|
||||
{ code: 'NO', label: 'Norway', phone: '47' },
|
||||
{ code: 'NP', label: 'Nepal', phone: '977' },
|
||||
{ code: 'NR', label: 'Nauru', phone: '674' },
|
||||
{ code: 'NU', label: 'Niue', phone: '683' },
|
||||
{ code: 'NZ', label: 'New Zealand', phone: '64' },
|
||||
{ code: 'OM', label: 'Oman', phone: '968' },
|
||||
{ code: 'PA', label: 'Panama', phone: '507' },
|
||||
{ code: 'PE', label: 'Peru', phone: '51' },
|
||||
{ code: 'PF', label: 'French Polynesia', phone: '689' },
|
||||
{ code: 'PG', label: 'Papua New Guinea', phone: '675' },
|
||||
{ code: 'PH', label: 'Philippines', phone: '63' },
|
||||
{ code: 'PK', label: 'Pakistan', phone: '92' },
|
||||
{ code: 'PL', label: 'Poland', phone: '48' },
|
||||
{
|
||||
code: 'PM',
|
||||
label: 'Saint Pierre and Miquelon',
|
||||
phone: '508',
|
||||
},
|
||||
{ code: 'PN', label: 'Pitcairn', phone: '870' },
|
||||
{ code: 'PR', label: 'Puerto Rico', phone: '1' },
|
||||
{
|
||||
code: 'PS',
|
||||
label: 'Palestine, State of',
|
||||
phone: '970',
|
||||
},
|
||||
{ code: 'PT', label: 'Portugal', phone: '351' },
|
||||
{ code: 'PW', label: 'Palau', phone: '680' },
|
||||
{ code: 'PY', label: 'Paraguay', phone: '595' },
|
||||
{ code: 'QA', label: 'Qatar', phone: '974' },
|
||||
{ code: 'RE', label: 'Reunion', phone: '262' },
|
||||
{ code: 'RO', label: 'Romania', phone: '40' },
|
||||
{ code: 'RS', label: 'Serbia', phone: '381' },
|
||||
{ code: 'RU', label: 'Russian Federation', phone: '7' },
|
||||
{ code: 'RW', label: 'Rwanda', phone: '250' },
|
||||
{ code: 'SA', label: 'Saudi Arabia', phone: '966' },
|
||||
{ code: 'SB', label: 'Solomon Islands', phone: '677' },
|
||||
{ code: 'SC', label: 'Seychelles', phone: '248' },
|
||||
{ code: 'SD', label: 'Sudan', phone: '249' },
|
||||
{ code: 'SE', label: 'Sweden', phone: '46' },
|
||||
{ code: 'SG', label: 'Singapore', phone: '65' },
|
||||
{ code: 'SH', label: 'Saint Helena', phone: '290' },
|
||||
{ code: 'SI', label: 'Slovenia', phone: '386' },
|
||||
{
|
||||
code: 'SJ',
|
||||
label: 'Svalbard and Jan Mayen',
|
||||
phone: '47',
|
||||
},
|
||||
{ code: 'SK', label: 'Slovakia', phone: '421' },
|
||||
{ code: 'SL', label: 'Sierra Leone', phone: '232' },
|
||||
{ code: 'SM', label: 'San Marino', phone: '378' },
|
||||
{ code: 'SN', label: 'Senegal', phone: '221' },
|
||||
{ code: 'SO', label: 'Somalia', phone: '252' },
|
||||
{ code: 'SR', label: 'Suriname', phone: '597' },
|
||||
{ code: 'SS', label: 'South Sudan', phone: '211' },
|
||||
{
|
||||
code: 'ST',
|
||||
label: 'Sao Tome and Principe',
|
||||
phone: '239',
|
||||
},
|
||||
{ code: 'SV', label: 'El Salvador', phone: '503' },
|
||||
{
|
||||
code: 'SX',
|
||||
label: 'Sint Maarten (Dutch part)',
|
||||
phone: '1-721',
|
||||
},
|
||||
{
|
||||
code: 'SY',
|
||||
label: 'Syrian Arab Republic',
|
||||
phone: '963',
|
||||
},
|
||||
{ code: 'SZ', label: 'Swaziland', phone: '268' },
|
||||
{
|
||||
code: 'TC',
|
||||
label: 'Turks and Caicos Islands',
|
||||
phone: '1-649',
|
||||
},
|
||||
{ code: 'TD', label: 'Chad', phone: '235' },
|
||||
{
|
||||
code: 'TF',
|
||||
label: 'French Southern Territories',
|
||||
phone: '262',
|
||||
},
|
||||
{ code: 'TG', label: 'Togo', phone: '228' },
|
||||
{ code: 'TH', label: 'Thailand', phone: '66' },
|
||||
{ code: 'TJ', label: 'Tajikistan', phone: '992' },
|
||||
{ code: 'TK', label: 'Tokelau', phone: '690' },
|
||||
{ code: 'TL', label: 'Timor-Leste', phone: '670' },
|
||||
{ code: 'TM', label: 'Turkmenistan', phone: '993' },
|
||||
{ code: 'TN', label: 'Tunisia', phone: '216' },
|
||||
{ code: 'TO', label: 'Tonga', phone: '676' },
|
||||
{ code: 'TR', label: 'Turkey', phone: '90' },
|
||||
{
|
||||
code: 'TT',
|
||||
label: 'Trinidad and Tobago',
|
||||
phone: '1-868',
|
||||
},
|
||||
{ code: 'TV', label: 'Tuvalu', phone: '688' },
|
||||
{
|
||||
code: 'TW',
|
||||
label: 'Taiwan, Province of China',
|
||||
phone: '886',
|
||||
},
|
||||
{
|
||||
code: 'TZ',
|
||||
label: 'United Republic of Tanzania',
|
||||
phone: '255',
|
||||
},
|
||||
{ code: 'UA', label: 'Ukraine', phone: '380' },
|
||||
{ code: 'UG', label: 'Uganda', phone: '256' },
|
||||
{
|
||||
code: 'US',
|
||||
label: 'United States',
|
||||
phone: '1',
|
||||
suggested: true,
|
||||
},
|
||||
{ code: 'UY', label: 'Uruguay', phone: '598' },
|
||||
{ code: 'UZ', label: 'Uzbekistan', phone: '998' },
|
||||
{
|
||||
code: 'VA',
|
||||
label: 'Holy See (Vatican City State)',
|
||||
phone: '379',
|
||||
},
|
||||
{
|
||||
code: 'VC',
|
||||
label: 'Saint Vincent and the Grenadines',
|
||||
phone: '1-784',
|
||||
},
|
||||
{ code: 'VE', label: 'Venezuela', phone: '58' },
|
||||
{
|
||||
code: 'VG',
|
||||
label: 'British Virgin Islands',
|
||||
phone: '1-284',
|
||||
},
|
||||
{
|
||||
code: 'VI',
|
||||
label: 'US Virgin Islands',
|
||||
phone: '1-340',
|
||||
},
|
||||
{ code: 'VN', label: 'Vietnam', phone: '84' },
|
||||
{ code: 'VU', label: 'Vanuatu', phone: '678' },
|
||||
{ code: 'WF', label: 'Wallis and Futuna', phone: '681' },
|
||||
{ code: 'WS', label: 'Samoa', phone: '685' },
|
||||
{ code: 'XK', label: 'Kosovo', phone: '383' },
|
||||
{ code: 'YE', label: 'Yemen', phone: '967' },
|
||||
{ code: 'YT', label: 'Mayotte', phone: '262' },
|
||||
{ code: 'ZA', label: 'South Africa', phone: '27' },
|
||||
{ code: 'ZM', label: 'Zambia', phone: '260' },
|
||||
{ code: 'ZW', label: 'Zimbabwe', phone: '263' },
|
||||
];
|
||||
|
||||
export default countries
|
||||
@@ -0,0 +1,306 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import ReactGA from 'react-ga';
|
||||
import { useTheme } from '@material-ui/core/styles';
|
||||
import {Link} from 'react-router-dom';
|
||||
|
||||
import { Search as SearchIcon, CloudQueue as CloudQueueIcon, Code as CodeIcon } from '@material-ui/icons';
|
||||
|
||||
import algoliasearch from 'algoliasearch/lite';
|
||||
import { InstantSearch, Configure, connectSearchBox, connectHits } from 'react-instantsearch-dom';
|
||||
import {
|
||||
Grid,
|
||||
Paper,
|
||||
TextField,
|
||||
ButtonBase,
|
||||
InputAdornment,
|
||||
Typography,
|
||||
Button,
|
||||
Tooltip,
|
||||
Card,
|
||||
Box,
|
||||
CardContent,
|
||||
IconButton,
|
||||
Zoom,
|
||||
CardMedia,
|
||||
CardActionArea,
|
||||
} from '@material-ui/core';
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarGroup,
|
||||
} from "@mui/material"
|
||||
|
||||
import {
|
||||
SkipNext as SkipNextIcon,
|
||||
SkipPrevious as SkipPreviousIcon,
|
||||
PlayArrow as PlayArrowIcon,
|
||||
VerifiedUser as VerifiedUserIcon,
|
||||
} from "@material-ui/icons";
|
||||
|
||||
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240")
|
||||
const CreatorGrid = props => {
|
||||
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs } = props
|
||||
const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows
|
||||
const xs = parsedXs === undefined || parsedXs === null ? isMobile ? 6 : 4 : 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("");
|
||||
|
||||
const buttonStyle = {borderRadius: 30, height: 50, width: 220, margin: isMobile ? "15px auto 15px auto" : 20, fontSize: 18,}
|
||||
|
||||
const isCloud =
|
||||
window.location.host === "localhost:3002" ||
|
||||
window.location.host === "shuffler.io";
|
||||
|
||||
const innerColor = "rgba(255,255,255,0.65)"
|
||||
const borderRadius = 3
|
||||
window.title = "Shuffle | Workflows | Discover your use-case"
|
||||
|
||||
const submitContact = (email, message) => {
|
||||
const data = {
|
||||
"firstname": "",
|
||||
"lastname": "",
|
||||
"title": "",
|
||||
"companyname": "",
|
||||
"email": email,
|
||||
"phone": "",
|
||||
"message": message,
|
||||
}
|
||||
|
||||
const errorMessage = "Something went wrong. Please contact frikky@shuffler.io directly."
|
||||
|
||||
fetch(globalUrl+"/api/v1/contact", {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(response => {
|
||||
if (response.success === true) {
|
||||
setFormMessage(response.reason)
|
||||
//alert.info("Thanks for submitting!")
|
||||
} else {
|
||||
setFormMessage(errorMessage)
|
||||
}
|
||||
|
||||
setFormMail("")
|
||||
setMessage("")
|
||||
})
|
||||
.catch(error => {
|
||||
setFormMessage(errorMessage)
|
||||
console.log(error)
|
||||
});
|
||||
}
|
||||
|
||||
// value={currentRefinement}
|
||||
const SearchBox = ({currentRefinement, refine, isSearchStalled} ) => {
|
||||
useEffect(() => {
|
||||
if (window !== undefined && window.location !== undefined && window.location.search !== undefined && window.location.search !== null) {
|
||||
const urlSearchParams = new URLSearchParams(window.location.search)
|
||||
const params = Object.fromEntries(urlSearchParams.entries())
|
||||
const foundQuery = params["q"]
|
||||
if (foundQuery !== null && foundQuery !== undefined) {
|
||||
refine(foundQuery)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<form noValidate action="" role="search">
|
||||
<TextField
|
||||
fullWidth
|
||||
style={{backgroundColor: theme.palette.inputColor, borderRadius: borderRadius, margin: 10, width: "100%",}}
|
||||
InputProps={{
|
||||
style:{
|
||||
color: "white",
|
||||
fontSize: "1em",
|
||||
height: 50,
|
||||
},
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon style={{marginLeft: 5}}/>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
autoComplete='off'
|
||||
type="search"
|
||||
color="primary"
|
||||
value={currentRefinement}
|
||||
placeholder="Find Creators..."
|
||||
id="shuffle_search_field"
|
||||
onChange={(event) => {
|
||||
refine(event.currentTarget.value)
|
||||
}}
|
||||
/>
|
||||
{/*isSearchStalled ? 'My search is stalled' : ''*/}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
const paperAppContainer = {
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
alignContent: "space-between",
|
||||
marginTop: 5,
|
||||
}
|
||||
|
||||
const Hits = ({ hits }) => {
|
||||
const [mouseHoverIndex, setMouseHoverIndex] = useState(-1)
|
||||
var counted = 0
|
||||
|
||||
return (
|
||||
<Grid container spacing={4} style={paperAppContainer}>
|
||||
{hits.map((data, index) => {
|
||||
if (counted === 12/xs*rowHandler) {
|
||||
return null
|
||||
}
|
||||
|
||||
counted += 1
|
||||
const creatorUrl = !isCloud ? `https://shuffler.io/creators/${data.username}` : `/creators/${data.username}`
|
||||
|
||||
return (
|
||||
<Zoom key={index} in={true} style={{}}>
|
||||
<Grid item xs={xs} style={{ padding: "12px 10px 12px 10px", }}>
|
||||
<Card style={{border: "1px solid rgba(255,255,255,0.3)", minHeight: 177, maxHeight: 177,}}>
|
||||
<a href={creatorUrl} rel="noopener noreferrer" target="_blank" style={{textDecoration: "none", color: "inherit",}}>
|
||||
<CardActionArea style={{padding: "5px 10px 5px 10px", minHeight: 177, maxHeight: 177,}}>
|
||||
<CardContent sx={{ flex: '1 0 auto', minWidth: 160, maxWidth: 160, overflow: "hidden", padding: 0, }}>
|
||||
<div style={{display: "flex"}}>
|
||||
<img style={{height: 74, width: 74, borderRadius: 100, }} alt={"Creator profile of "+data.username} src={data.image} />
|
||||
<Typography component="div" variant="body1" style={{marginTop: 20, marginLeft: 15, }}>
|
||||
@{data.username}
|
||||
</Typography>
|
||||
<span style={{marginTop: "auto", marginBottom: "auto", marginLeft: 10, }}>
|
||||
{data.verified === true ?
|
||||
<Tooltip title="Verified and earning from Shuffle contributions" placement="top">
|
||||
<VerifiedUserIcon style={{}}/>
|
||||
</Tooltip>
|
||||
:
|
||||
null
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<Typography variant="body1" color="textSecondary" style={{marginTop: 10, }}>
|
||||
<b>{data.apps === undefined || data.apps === null ? 0 : data.apps}</b> apps <span style={{marginLeft: 15, }}/><b>{data.workflows === null || data.workflows === undefined ? 0 : data.workflows}</b> workflows
|
||||
</Typography>
|
||||
{data.specialized_apps !== undefined && data.specialized_apps !== null && data.specialized_apps.length > 0 ?
|
||||
<AvatarGroup max={10} style={{flexDirection: "row", padding: 0, margin: 0, itemAlign: "left", textAlign: "left", marginTop: 3,}}>
|
||||
{data.specialized_apps.map((app, index) => {
|
||||
// Putting all this in secondary of ListItemText looked weird.
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
height: 24,
|
||||
width: 24,
|
||||
filter: "brightness(0.6)",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => {
|
||||
console.log("Click")
|
||||
//navigate("/apps/"+app.id)
|
||||
}}
|
||||
>
|
||||
<Tooltip color="primary" title={app.name} placement="bottom">
|
||||
<Avatar alt={app.name} src={app.image} style={{width: 24, height: 24}}/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</AvatarGroup>
|
||||
:
|
||||
null}
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
</a>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Zoom>
|
||||
)
|
||||
})}
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
const CustomSearchBox = connectSearchBox(SearchBox)
|
||||
const CustomHits = connectHits(Hits)
|
||||
|
||||
return (
|
||||
<div style={{width: "100%", position: "relative", height: "100%",}}>
|
||||
<InstantSearch searchClient={searchClient} indexName="creators">
|
||||
<Configure clickAnalytics />
|
||||
<div style={{maxWidth: 450, margin: "auto", marginTop: 15, marginBottom: 15, }}>
|
||||
<CustomSearchBox />
|
||||
</div>
|
||||
<CustomHits hitsPerPage={20}/>
|
||||
</InstantSearch>
|
||||
{showSuggestion === true ?
|
||||
<div style={{maxWidth: isMobile ? "100%" : "60%", margin: "auto", paddingTop: 50, textAlign: "center",}}>
|
||||
<Typography variant="h6" style={{color: "white", marginTop: 50,}}>
|
||||
Can't find what you're looking for?
|
||||
</Typography>
|
||||
<div style={{flex: "1", display: "flex", flexDirection: "row"}}>
|
||||
<TextField
|
||||
required
|
||||
style={{flex: "1", marginRight: "15px", backgroundColor: theme.palette.inputColor}}
|
||||
InputProps={{
|
||||
style:{
|
||||
color: "#ffffff",
|
||||
},
|
||||
}}
|
||||
color="primary"
|
||||
fullWidth={true}
|
||||
placeholder="Email (optional)"
|
||||
type="email"
|
||||
id="email-handler"
|
||||
autoComplete="email"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
onChange={e => setFormMail(e.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
required
|
||||
style={{flex: "1", backgroundColor: theme.palette.inputColor}}
|
||||
InputProps={{
|
||||
style:{
|
||||
color: "#ffffff",
|
||||
},
|
||||
}}
|
||||
color="primary"
|
||||
fullWidth={true}
|
||||
placeholder="What are we missing?"
|
||||
type=""
|
||||
id="standard-required"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
autoComplete="off"
|
||||
onChange={e => setMessage(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
style={buttonStyle}
|
||||
disabled={message.length === 0}
|
||||
onClick={() => {
|
||||
submitContact(formMail, message)
|
||||
}}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
<Typography style={{color: "white"}} variant="body2">{formMessage}</Typography>
|
||||
</div>
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default CreatorGrid;
|
||||
@@ -0,0 +1,356 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
|
||||
import ReactGA from 'react-ga';
|
||||
import { useTheme } from '@material-ui/core/styles';
|
||||
import {Link} from 'react-router-dom';
|
||||
|
||||
import { Search as SearchIcon, CloudQueue as CloudQueueIcon, Code as CodeIcon } from '@material-ui/icons';
|
||||
import aa from 'search-insights'
|
||||
|
||||
import algoliasearch from 'algoliasearch/lite';
|
||||
import { InstantSearch, Configure, connectSearchBox, connectHits } from 'react-instantsearch-dom';
|
||||
import {
|
||||
Zoom,
|
||||
Grid,
|
||||
Paper,
|
||||
TextField,
|
||||
Avatar,
|
||||
ButtonBase,
|
||||
InputAdornment,
|
||||
Typography,
|
||||
Button,
|
||||
Tooltip,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
} from '@material-ui/core';
|
||||
|
||||
import {Close as CloseIcon, Folder as FolderIcon, Polymer as PolymerIcon, LibraryBooks as LibraryBooksIcon} from '@material-ui/icons'
|
||||
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240")
|
||||
const DocsGrid = props => {
|
||||
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, userdata, } = props
|
||||
const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows
|
||||
const xs = parsedXs === undefined || parsedXs === null ? isMobile ? 6 : 2 : 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("");
|
||||
|
||||
const buttonStyle = {borderRadius: 30, height: 50, width: 220, margin: isMobile ? "15px auto 15px auto" : 20, fontSize: 18,}
|
||||
|
||||
const innerColor = "rgba(255,255,255,0.65)"
|
||||
const borderRadius = 3
|
||||
window.title = "Shuffle | Apps | Find and integrate any app"
|
||||
|
||||
const submitContact = (email, message) => {
|
||||
const data = {
|
||||
"firstname": "",
|
||||
"lastname": "",
|
||||
"title": "",
|
||||
"companyname": "",
|
||||
"email": email,
|
||||
"phone": "",
|
||||
"message": message,
|
||||
}
|
||||
|
||||
const errorMessage = "Something went wrong. Please contact frikky@shuffler.io directly."
|
||||
|
||||
fetch(globalUrl+"/api/v1/contact", {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(response => {
|
||||
if (response.success === true) {
|
||||
setFormMessage(response.reason)
|
||||
//alert.info("Thanks for submitting!")
|
||||
} else {
|
||||
setFormMessage(errorMessage)
|
||||
}
|
||||
|
||||
setFormMail("")
|
||||
setMessage("")
|
||||
})
|
||||
.catch(error => {
|
||||
setFormMessage(errorMessage)
|
||||
console.log(error)
|
||||
});
|
||||
}
|
||||
|
||||
const SearchBox = ({currentRefinement, refine, isSearchStalled} ) => {
|
||||
useEffect(() => {
|
||||
if (window !== undefined && window.location !== undefined && window.location.search !== undefined && window.location.search !== null) {
|
||||
const urlSearchParams = new URLSearchParams(window.location.search)
|
||||
const params = Object.fromEntries(urlSearchParams.entries())
|
||||
const foundQuery = params["q"]
|
||||
if (foundQuery !== null && foundQuery !== undefined) {
|
||||
console.log("Got query: ", foundQuery)
|
||||
refine(foundQuery)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<form noValidate action="" role="search">
|
||||
<TextField
|
||||
fullWidth
|
||||
style={{backgroundColor: theme.palette.inputColor, borderRadius: borderRadius, margin: 10, width: "100%",}}
|
||||
InputProps={{
|
||||
style:{
|
||||
color: "white",
|
||||
fontSize: "1em",
|
||||
height: 50,
|
||||
},
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon style={{marginLeft: 5}}/>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
autoComplete='off'
|
||||
type="search"
|
||||
color="primary"
|
||||
defaultValue={currentRefinement}
|
||||
placeholder="Search our Documentation..."
|
||||
id="shuffle_search_field"
|
||||
onChange={(event) => {
|
||||
refine(event.currentTarget.value)
|
||||
}}
|
||||
limit={5}
|
||||
/>
|
||||
{/*isSearchStalled ? 'My search is stalled' : ''*/}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
var workflowDelay = -50
|
||||
const Hits = ({ hits }) => {
|
||||
const [mouseHoverIndex, setMouseHoverIndex] = useState(-1)
|
||||
//console.log(hits)
|
||||
//var curhits = hits
|
||||
//if (hits.length > 0 && defaultApps.length === 0) {
|
||||
// setDefaultApps(hits)
|
||||
//}
|
||||
|
||||
//const [defaultApps, setDefaultApps] = React.useState([])
|
||||
//console.log(hits)
|
||||
//if (hits.length > 0 && hits.length !== innerHits.length) {
|
||||
// setInnerHits(hits)
|
||||
//}
|
||||
|
||||
var counted = 0
|
||||
return (
|
||||
<List>
|
||||
{hits.map((data, index) => {
|
||||
workflowDelay += 50
|
||||
|
||||
const innerlistitemStyle = {
|
||||
width: "100%",
|
||||
overflowX: "hidden",
|
||||
overflowY: "hidden",
|
||||
borderBottom: "1px solid rgba(255,255,255,0.4)",
|
||||
backgroundColor: mouseHoverIndex === index ? "#1f2023" : "inherit",
|
||||
cursor: "pointer",
|
||||
marginLeft: 5,
|
||||
marginRight: 5,
|
||||
maxHeight: 75,
|
||||
minHeight: 75,
|
||||
maxWidth: 420,
|
||||
minWidth: "100%",
|
||||
}
|
||||
|
||||
if (counted >= 12/xs*rowHandler) {
|
||||
return null
|
||||
}
|
||||
|
||||
counted += 1
|
||||
|
||||
var name = data.name === undefined ?
|
||||
data.filename.charAt(0).toUpperCase() + data.filename.slice(1).replaceAll("_", " ") + " - " + data.title :
|
||||
(data.name.charAt(0).toUpperCase()+data.name.slice(1)).replaceAll("_", " ")
|
||||
|
||||
if (name.length > 96) {
|
||||
name = name.slice(0, 96)+"..."
|
||||
}
|
||||
|
||||
//const secondaryText = data.data !== undefined ? data.data.slice(0, 100)+"..." : ""
|
||||
const secondaryText = data.data !== undefined ? data.data.slice(0, 100)+"..." : ""
|
||||
const baseImage = <PolymerIcon />
|
||||
const avatar = data.image_url === undefined ?
|
||||
baseImage
|
||||
:
|
||||
<Avatar
|
||||
src={data.image_url}
|
||||
variant="rounded"
|
||||
/>
|
||||
|
||||
var parsedUrl = data.urlpath !== undefined ? data.urlpath : ""
|
||||
parsedUrl += `?queryID=${data.__queryID}`
|
||||
|
||||
return (
|
||||
<Zoom key={index} in={true} style={{ transitionDelay: `${workflowDelay}ms` }}>
|
||||
<Link key={data.objectID} to={parsedUrl} style={{textDecoration: "none", color: "white",}} onClick={(event) => {
|
||||
aa('init', {
|
||||
appId: searchClient.appId,
|
||||
apiKey: searchClient.transporter.queryParameters["x-algolia-api-key"]
|
||||
})
|
||||
|
||||
const timestamp = new Date().getTime()
|
||||
aa('sendEvents', [
|
||||
{
|
||||
eventType: 'click',
|
||||
eventName: 'Product Clicked Appgrid',
|
||||
index: 'documentation',
|
||||
objectIDs: [data.objectID],
|
||||
timestamp: timestamp,
|
||||
queryID: data.__queryID,
|
||||
positions: [data.__position],
|
||||
userToken: userdata === undefined || userdata === null || userdata.id === undefined ? "unauthenticated" : userdata.id,
|
||||
}
|
||||
])
|
||||
|
||||
console.log("CLICK")
|
||||
}}>
|
||||
<ListItem key={data.objectID} style={innerlistitemStyle} onMouseOver={() => {
|
||||
setMouseHoverIndex(index)
|
||||
}}>
|
||||
<ListItemAvatar>
|
||||
{avatar}
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={name}
|
||||
secondary={secondaryText}
|
||||
/>
|
||||
{/*
|
||||
<ListItemSecondaryAction>
|
||||
<IconButton edge="end" aria-label="delete">
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</ListItemSecondaryAction>
|
||||
*/}
|
||||
</ListItem>
|
||||
</Link>
|
||||
</Zoom>
|
||||
)
|
||||
})}
|
||||
</List>
|
||||
)
|
||||
}
|
||||
|
||||
const CustomSearchBox = connectSearchBox(SearchBox)
|
||||
const CustomHits = connectHits(Hits)
|
||||
const selectButtonStyle = {
|
||||
minWidth: 150,
|
||||
maxWidth: 150,
|
||||
minHeight: 50,
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{width: "100%", textAlign: "center", position: "relative", height: "100%", display: "flex"}}>
|
||||
{/*
|
||||
<div style={{padding: 10, }}>
|
||||
<Button
|
||||
style={selectButtonStyle}
|
||||
variant="outlined"
|
||||
onClick={() => {
|
||||
const searchField = document.createElement("shuffle_search_field")
|
||||
console.log("Field: ", searchField)
|
||||
if (searchField !== null & searchField !== undefined) {
|
||||
console.log("Set field.")
|
||||
searchField.value = "WHAT WABALABA"
|
||||
searchField.setAttribute("value", "WHAT WABALABA")
|
||||
}
|
||||
}}
|
||||
>
|
||||
Cases
|
||||
</Button>
|
||||
</div>
|
||||
*/}
|
||||
<div style={{width: "100%", position: "relative", height: "100%",}}>
|
||||
<InstantSearch searchClient={searchClient} indexName="documentation">
|
||||
<div style={{maxWidth: 450, margin: "auto", marginTop: 15, marginBottom: 15, }}>
|
||||
<CustomSearchBox />
|
||||
</div>
|
||||
<Configure clickAnalytics />
|
||||
<CustomHits hitsPerPage={5}/>
|
||||
</InstantSearch>
|
||||
{showSuggestion === true ?
|
||||
<div style={{paddingTop: 0, maxWidth: isMobile ? "100%" : "60%", margin: "auto"}}>
|
||||
<Typography variant="h6" style={{color: "white", marginTop: 50,}}>
|
||||
Can't find what you're looking for?
|
||||
</Typography>
|
||||
<div style={{flex: "1", display: "flex", flexDirection: "row", textAlign: "center",}}>
|
||||
<TextField
|
||||
required
|
||||
style={{flex: "1", marginRight: "15px", backgroundColor: theme.palette.inputColor}}
|
||||
InputProps={{
|
||||
style:{
|
||||
color: "#ffffff",
|
||||
},
|
||||
}}
|
||||
color="primary"
|
||||
fullWidth={true}
|
||||
placeholder="Email (optional)"
|
||||
type="email"
|
||||
id="email-handler"
|
||||
autoComplete="email"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
onChange={e => setFormMail(e.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
required
|
||||
style={{flex: "1", backgroundColor: theme.palette.inputColor}}
|
||||
InputProps={{
|
||||
style:{
|
||||
color: "#ffffff",
|
||||
},
|
||||
}}
|
||||
color="primary"
|
||||
fullWidth={true}
|
||||
placeholder="What are we missing?"
|
||||
type=""
|
||||
id="standard-required"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
autoComplete="off"
|
||||
onChange={e => setMessage(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
style={buttonStyle}
|
||||
disabled={message.length === 0}
|
||||
onClick={() => {
|
||||
submitContact(formMail, message)
|
||||
}}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
<Typography style={{color: "white"}} variant="body2">{formMessage}</Typography>
|
||||
</div>
|
||||
: null
|
||||
}
|
||||
|
||||
<span style={{position: "absolute", display: "flex", textAlign: "right", float: "right", right: 0, bottom: 120, }}>
|
||||
<Typography variant="body2" color="textSecondary" style={{}}>
|
||||
Search by
|
||||
</Typography>
|
||||
<a rel="noopener noreferrer" href="https://www.algolia.com/" target="_blank" style={{textDecoration: "none", color: "white"}}>
|
||||
<img src={"/images/logo-algolia-nebula-blue-full.svg"} alt="Algolia logo" style={{height: 17, marginLeft: 5, marginTop: 3,}} />
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default DocsGrid;
|
||||
@@ -0,0 +1,490 @@
|
||||
import React, { useEffect, useContext } from "react";
|
||||
import theme from '../theme';
|
||||
import { isMobile } from "react-device-detect"
|
||||
import ChipInput from "material-ui-chip-input";
|
||||
import UsecaseSearch from "../components/UsecaseSearch.jsx"
|
||||
|
||||
import {
|
||||
Badge,
|
||||
Avatar,
|
||||
Grid,
|
||||
InputLabel,
|
||||
Select,
|
||||
ListSubheader,
|
||||
Paper,
|
||||
Tooltip,
|
||||
Divider,
|
||||
Button,
|
||||
TextField,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem,
|
||||
FormControlLabel,
|
||||
Chip,
|
||||
Switch,
|
||||
Typography,
|
||||
Zoom,
|
||||
CircularProgress,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
OutlinedInput,
|
||||
Checkbox,
|
||||
ListItemText,
|
||||
Radio,
|
||||
RadioGroup,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
|
||||
} from "@material-ui/core";
|
||||
|
||||
import {
|
||||
ExpandLess as ExpandLessIcon,
|
||||
ExpandMore as ExpandMoreIcon,
|
||||
Publish as PublishIcon,
|
||||
} from "@material-ui/icons";
|
||||
|
||||
const EditWorkflow = (props) => {
|
||||
const { globalUrl, workflow, setWorkflow, modalOpen, setModalOpen, showUpload, usecases, setNewWorkflow, appFramework, isEditing, userdata, } = props
|
||||
|
||||
const [submitLoading, setSubmitLoading] = React.useState(false);
|
||||
const [showMoreClicked, setShowMoreClicked] = React.useState(false);
|
||||
const [innerWorkflow, setInnerWorkflow] = React.useState(workflow)
|
||||
const [_, setUpdate] = React.useState(""); // Used for rendering, don't remove
|
||||
const [newWorkflowTags, setNewWorkflowTags] = React.useState(workflow.tags !== undefined && workflow.tags !== null ? JSON.parse(JSON.stringify(workflow.tags)) : [])
|
||||
const [selectedUsecases, setSelectedUsecases] = React.useState(workflow.usecase_ids !== undefined && workflow.usecase_ids !== null ? JSON.parse(JSON.stringify(workflow.usecase_ids)) : []);
|
||||
const [foundWorkflowId, setFoundWorkflowId] = React.useState("")
|
||||
const [name, setName] = React.useState(workflow.name !== undefined ? workflow.name : "")
|
||||
const [description, setDescription] = React.useState(workflow.description !== undefined ? workflow.description : "")
|
||||
|
||||
|
||||
// Gets the generated workflow
|
||||
const getGeneratedWorkflow = (workflow_id) => {
|
||||
fetch(globalUrl + "/api/v1/workflows/" + workflow_id, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status !== 200) {
|
||||
console.log("Status not 200 when getting workflow");
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson.id === workflow_id) {
|
||||
console.log("GOT WORKFLOW: ", responseJson)
|
||||
if (name === "") {
|
||||
innerWorkflow.name = responseJson.name
|
||||
setName(responseJson.name)
|
||||
}
|
||||
|
||||
if (description === "") {
|
||||
innerWorkflow.description = responseJson.description
|
||||
setDescription(description)
|
||||
}
|
||||
|
||||
if (newWorkflowTags === []) {
|
||||
innerWorkflow.tags = responseJson.tags
|
||||
setNewWorkflowTags(responseJson.tags)
|
||||
}
|
||||
|
||||
if (selectedUsecases === []) {
|
||||
selectedUsecases = responseJson.usecase_ids
|
||||
}
|
||||
|
||||
innerWorkflow.id = responseJson.id
|
||||
innerWorkflow.blogpost = responseJson.blogpost
|
||||
innerWorkflow.actions = responseJson.actions
|
||||
innerWorkflow.triggers = responseJson.triggers
|
||||
innerWorkflow.branches = responseJson.branches
|
||||
innerWorkflow.comments = responseJson.comments
|
||||
innerWorkflow.workflow_variables = responseJson.workflow_variables
|
||||
innerWorkflow.execution_variables = responseJson.execution_variables
|
||||
|
||||
|
||||
setInnerWorkflow(innerWorkflow)
|
||||
setUpdate(Math.random())
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
//alert.error(error.toString());
|
||||
console.log("Get workflow error: ", error.toString());
|
||||
})
|
||||
}
|
||||
|
||||
if (foundWorkflowId.length > 0) {
|
||||
getGeneratedWorkflow(foundWorkflowId)
|
||||
|
||||
setFoundWorkflowId("")
|
||||
} else {
|
||||
}
|
||||
|
||||
if (modalOpen !== true) {
|
||||
return null
|
||||
}
|
||||
|
||||
const newWorkflow = isEditing === true ? false : true
|
||||
|
||||
var upload = "";
|
||||
var total_count = 0
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={modalOpen}
|
||||
onClose={() => {
|
||||
setModalOpen(false);
|
||||
}}
|
||||
PaperProps={{
|
||||
style: {
|
||||
backgroundColor: theme.palette.surfaceColor,
|
||||
color: "white",
|
||||
minWidth: isMobile ? "90%" : newWorkflow === true ? 1000 : 550,
|
||||
maxWidth: isMobile ? "90%" : newWorkflow === true ? 1000 : 550,
|
||||
minHeight: 400,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DialogTitle style={{padding: 30, paddingBottom: 0, zIndex: 1000,}}>
|
||||
<div style={{display: "flex"}}>
|
||||
<div style={{flex: 1, color: "rgba(255,255,255,0.9)" }}>
|
||||
<Typography variant="h6">
|
||||
{newWorkflow ? "New" : "Editing"} workflow
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" style={{maxWidth: 440,}}>
|
||||
Workflows can be built from scratch, or from templates. <a href="/usecases" rel="noopener noreferrer" target="_blank" style={{ textDecoration: "none", color: "#f86a3e" }}>Usecases</a> can help you discover next steps, and you can <a href="/search?tab=workflows" rel="noopener noreferrer" target="_blank" style={{ textDecoration: "none", color: "#f86a3e" }}>search</a> for them directly. <a href="/docs/workflows" rel="noopener noreferrer" target="_blank" style={{ textDecoration: "none", color: "#f86a3e" }}>Learn more</a>
|
||||
</Typography>
|
||||
{showUpload === true ?
|
||||
<div style={{ float: "right" }}>
|
||||
<Tooltip color="primary" title={"Import manually"} placement="top">
|
||||
<Button
|
||||
color="primary"
|
||||
style={{}}
|
||||
variant="text"
|
||||
onClick={() => upload.click()}
|
||||
>
|
||||
<PublishIcon />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
: null}
|
||||
</div>
|
||||
{newWorkflow === true ?
|
||||
<div style={{flex: 1, marginLeft: 45, }}>
|
||||
<Typography variant="h6">
|
||||
Use a Template
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" style={{maxWidth: 440,}}>
|
||||
Start your workflow from our templating system. This uses publied workflows from our <a href="/creators" rel="noopener noreferrer" target="_blank" style={{ textDecoration: "none", color: "#f86a3e"}}>Creators</a> to generate full Usecases or parts of your Workflow.
|
||||
</Typography>
|
||||
</div>
|
||||
: null}
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<FormControl>
|
||||
<DialogContent style={{paddingTop: 10, display: "flex", minHeight: 350, zIndex: 1001, }}>
|
||||
<div style={{minWidth: newWorkflow ? 450 : 500, maxWidth: newWorkflow ? 450 : 500, }}>
|
||||
<TextField
|
||||
onBlur={(event) => {
|
||||
setName(event.target.value)
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
color="primary"
|
||||
placeholder="Name"
|
||||
required
|
||||
margin="dense"
|
||||
defaultValue={innerWorkflow.name}
|
||||
label="Name"
|
||||
autoFocus
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
onBlur={(event) => {
|
||||
setDescription(event.target.value)
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
maxRows={4}
|
||||
color="primary"
|
||||
defaultValue={innerWorkflow.description}
|
||||
placeholder="Description"
|
||||
multiline
|
||||
label="Description"
|
||||
margin="dense"
|
||||
fullWidth
|
||||
/>
|
||||
<div style={{display: "flex", marginTop: 10, }}>
|
||||
<ChipInput
|
||||
style={{ flex: 1, maxHeight: 40, marginTop: 12, overflow: "auto", }}
|
||||
InputProps={{
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
placeholder="Tags"
|
||||
color="primary"
|
||||
fullWidth
|
||||
value={newWorkflowTags}
|
||||
onAdd={(chip) => {
|
||||
newWorkflowTags.push(chip);
|
||||
setNewWorkflowTags(newWorkflowTags);
|
||||
}}
|
||||
onDelete={(chip, index) => {
|
||||
newWorkflowTags.splice(index, 1);
|
||||
setNewWorkflowTags(newWorkflowTags);
|
||||
}}
|
||||
/>
|
||||
{usecases !== null && usecases !== undefined && usecases.length > 0 ?
|
||||
<FormControl style={{flex: 1, marginLeft: 5, }}>
|
||||
<InputLabel htmlFor="grouped-select-usecase">Usecases</InputLabel>
|
||||
<Select
|
||||
defaultValue=""
|
||||
id="grouped-select"
|
||||
label="Matching Usecase"
|
||||
multiple
|
||||
value={selectedUsecases}
|
||||
renderValue={(selected) => selected.join(', ')}
|
||||
onChange={(event) => {
|
||||
console.log("Changed: ", event)
|
||||
}}
|
||||
>
|
||||
<MenuItem value="">
|
||||
<em>None</em>
|
||||
</MenuItem>
|
||||
{usecases.map((usecase, index) => {
|
||||
//console.log(usecase)
|
||||
return (
|
||||
<span key={index}>
|
||||
<ListSubheader
|
||||
style={{color: usecase.color}}
|
||||
>
|
||||
{usecase.name}
|
||||
</ListSubheader>
|
||||
{usecase.list.map((subcase, subindex) => {
|
||||
//console.log(subcase)
|
||||
total_count += 1
|
||||
return (
|
||||
<MenuItem key={subindex} value={total_count} onClick={(event) => {
|
||||
if (selectedUsecases.includes(subcase.name)) {
|
||||
const itemIndex = selectedUsecases.indexOf(subcase.name)
|
||||
if (itemIndex > -1) {
|
||||
selectedUsecases.splice(itemIndex, 1)
|
||||
}
|
||||
} else {
|
||||
selectedUsecases.push(subcase.name)
|
||||
}
|
||||
|
||||
setUpdate(Math.random());
|
||||
setSelectedUsecases(selectedUsecases)
|
||||
}}>
|
||||
<Checkbox style={{color: selectedUsecases.includes(subcase.name) ? usecase.color : theme.palette.inputColor}} checked={selectedUsecases.includes(subcase.name)} />
|
||||
<ListItemText primary={subcase.name} />
|
||||
</MenuItem>
|
||||
)
|
||||
})}
|
||||
</span>
|
||||
)
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
: null}
|
||||
</div>
|
||||
|
||||
{showMoreClicked === true ?
|
||||
<span style={{marginTop: 25, }}>
|
||||
|
||||
<FormControl style={{marginTop: 15, }}>
|
||||
<FormLabel id="demo-row-radio-buttons-group-label">Status</FormLabel>
|
||||
<RadioGroup
|
||||
row
|
||||
aria-labelledby="demo-row-radio-buttons-group-label"
|
||||
name="row-radio-buttons-group"
|
||||
defaultValue={innerWorkflow.status}
|
||||
onChange={(e) => {
|
||||
console.log("Data: ", e.target.value)
|
||||
|
||||
innerWorkflow.workflow_type = e.target.value
|
||||
setInnerWorkflow(innerWorkflow)
|
||||
}}
|
||||
>
|
||||
<FormControlLabel value="test" control={<Radio />} label="Test" />
|
||||
<FormControlLabel value="production" control={<Radio />} label="Production" />
|
||||
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
<div />
|
||||
|
||||
<FormControl style={{marginTop: 15, }}>
|
||||
<FormLabel id="demo-row-radio-buttons-group-label">Type</FormLabel>
|
||||
<RadioGroup
|
||||
row
|
||||
aria-labelledby="demo-row-radio-buttons-group-label"
|
||||
name="row-radio-buttons-group"
|
||||
defaultValue={innerWorkflow.workflow_type}
|
||||
onChange={(e) => {
|
||||
console.log("Data: ", e.target.value)
|
||||
|
||||
innerWorkflow.workflow_type = e.target.value
|
||||
setInnerWorkflow(innerWorkflow)
|
||||
}}
|
||||
>
|
||||
<FormControlLabel value="trigger" control={<Radio />} label="Trigger" />
|
||||
<FormControlLabel value="subflow" control={<Radio />} label="Subflow" />
|
||||
<FormControlLabel value="standalone" control={<Radio />} label="Standalone" />
|
||||
|
||||
</RadioGroup>
|
||||
</FormControl>
|
||||
|
||||
|
||||
<TextField
|
||||
onBlur={(event) => {
|
||||
innerWorkflow.blogpost = event.target.value
|
||||
setInnerWorkflow(innerWorkflow)
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
color="primary"
|
||||
defaultValue={innerWorkflow.blogpost}
|
||||
placeholder="A blogpost or other reference for how this work workflow was built, and what it's for."
|
||||
rows="1"
|
||||
label="blogpost"
|
||||
margin="dense"
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
onBlur={(event) => {
|
||||
innerWorkflow.video = event.target.value
|
||||
setInnerWorkflow(innerWorkflow)
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
color="primary"
|
||||
defaultValue={innerWorkflow.video}
|
||||
placeholder="A youtube or loom link to the video"
|
||||
rows="1"
|
||||
label="Video"
|
||||
margin="dense"
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
onBlur={(event) => {
|
||||
innerWorkflow.default_return_value = event.target.value
|
||||
setInnerWorkflow(innerWorkflow)
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
color="primary"
|
||||
defaultValue={innerWorkflow.default_return_value}
|
||||
placeholder="Default return value (used for Subflows if the subflow fails)"
|
||||
rows="3"
|
||||
multiline
|
||||
label="Default return value"
|
||||
margin="dense"
|
||||
fullWidth
|
||||
/>
|
||||
</span>
|
||||
: null}
|
||||
<Tooltip color="primary" title={"Add more details"} placement="top">
|
||||
<IconButton
|
||||
style={{ color: "white", margin: "auto", marginTop: 10, textAlign: "center", width: 50,}}
|
||||
onClick={() => {
|
||||
setShowMoreClicked(!showMoreClicked);
|
||||
}}
|
||||
>
|
||||
{showMoreClicked ? <ExpandLessIcon /> : <ExpandMoreIcon/>}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{newWorkflow === true ?
|
||||
<div style={{marginLeft: 50, maxWidth: 400, minWidth: 400, position: "relative",}}>
|
||||
<UsecaseSearch
|
||||
globalUrl={globalUrl}
|
||||
appFramework={appFramework}
|
||||
defaultSearch={undefined}
|
||||
apps={undefined}
|
||||
setFoundWorkflowId={setFoundWorkflowId}
|
||||
userdata={userdata}
|
||||
/>
|
||||
</div>
|
||||
: null}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
style={{}}
|
||||
onClick={() => {
|
||||
if (setNewWorkflow !== undefined) {
|
||||
setWorkflow({})
|
||||
}
|
||||
|
||||
setModalOpen(false)
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
style={{}}
|
||||
disabled={name.length === 0}
|
||||
onClick={() => {
|
||||
innerWorkflow.name = name
|
||||
innerWorkflow.description = description
|
||||
if (newWorkflowTags.length > 0) {
|
||||
innerWorkflow.tags = newWorkflowTags
|
||||
}
|
||||
|
||||
if (selectedUsecases.length > 0) {
|
||||
innerWorkflow.usecase_ids = selectedUsecases
|
||||
}
|
||||
|
||||
|
||||
if (setNewWorkflow !== undefined) {
|
||||
setNewWorkflow(
|
||||
innerWorkflow.name,
|
||||
innerWorkflow.description,
|
||||
innerWorkflow.tags,
|
||||
innerWorkflow.default_return_value,
|
||||
innerWorkflow,
|
||||
newWorkflow,
|
||||
innerWorkflow.usecase_ids,
|
||||
innerWorkflow.blogpost,
|
||||
innerWorkflow.status,
|
||||
)
|
||||
setWorkflow({})
|
||||
} else {
|
||||
setWorkflow(innerWorkflow)
|
||||
}
|
||||
setModalOpen(false)
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
{submitLoading ? <CircularProgress color="secondary" /> : "Submit"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</FormControl>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
export default EditWorkflow;
|
||||
@@ -0,0 +1,27 @@
|
||||
// Move this to the backend to be loaded in?
|
||||
const extraApps = [{
|
||||
"name": "Cases",
|
||||
"description": "Allows use of other Case Management apps without knowing how to use them.",
|
||||
"app_version": "1.0.0",
|
||||
"app_name": "Cases",
|
||||
"type": "ACTION",
|
||||
"large_image": encodeURI('data:image/svg+xml;utf-8,<svg fill="rgb(248,90,62)" width="${svgSize}" height="${svgSize}" viewBox="0 0 ${svgSize} ${svgSize}" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M15.6408 8.39233H18.0922V10.0287H15.6408V8.39233ZM0.115234 8.39233H2.56663V10.0287H0.115234V8.39233ZM9.92083 0.21051V2.66506H8.28656V0.21051H9.92083ZM3.31839 2.25596L5.05889 4.00687L3.89856 5.16051L2.15807 3.42596L3.31839 2.25596ZM13.1485 3.99869L14.8808 2.25596L16.0493 3.42596L14.3088 5.16051L13.1485 3.99869ZM9.10369 4.30142C10.404 4.30142 11.651 4.81863 12.5705 5.73926C13.4899 6.65989 14.0065 7.90854 14.0065 9.21051C14.0065 11.0269 13.0178 12.6141 11.5551 13.4651V14.9378C11.5551 15.1548 11.469 15.3629 11.3158 15.5163C11.1625 15.6698 10.9547 15.756 10.738 15.756H7.46943C7.25271 15.756 7.04487 15.6698 6.89163 15.5163C6.73839 15.3629 6.6523 15.1548 6.6523 14.9378V13.4651C5.18963 12.6141 4.2009 11.0269 4.2009 9.21051C4.2009 7.90854 4.71744 6.65989 5.63689 5.73926C6.55635 4.81863 7.80339 4.30142 9.10369 4.30142ZM10.738 16.5741V17.3923C10.738 17.6093 10.6519 17.8174 10.4986 17.9709C10.3454 18.1243 10.1375 18.2105 9.92083 18.2105H8.28656C8.06984 18.2105 7.862 18.1243 7.70876 17.9709C7.55552 17.8174 7.46943 17.6093 7.46943 17.3923V16.5741H10.738ZM8.28656 14.1196H9.92083V12.3769C11.3345 12.0169 12.3722 10.7323 12.3722 9.21051C12.3722 8.34253 12.0279 7.5101 11.4149 6.89634C10.8019 6.28259 9.97056 5.93778 9.10369 5.93778C8.23683 5.93778 7.40546 6.28259 6.79249 6.89634C6.17953 7.5101 5.83516 8.34253 5.83516 9.21051C5.83516 10.7323 6.87292 12.0169 8.28656 12.3769V14.1196Z" /></svg>'),
|
||||
"template": true,
|
||||
"actions": [{
|
||||
"name": "Create Alert",
|
||||
"description": "Create a ticket",
|
||||
"parameters": [{
|
||||
"name": "id",
|
||||
},
|
||||
{
|
||||
"name": "name",
|
||||
},
|
||||
{
|
||||
"name": "description",
|
||||
"multiline": true,
|
||||
},
|
||||
],
|
||||
}],
|
||||
}]
|
||||
|
||||
export default extraApps
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
} from "@mui/icons-material";
|
||||
//import LogoutIcon from '@mui/icons-material/Logout';
|
||||
import { useAlert } from "react-alert";
|
||||
import SearchField from '../components/Searchfield'
|
||||
|
||||
const hoverColor = "#f85a3e";
|
||||
const hoverOutColor = "#e8eaf6";
|
||||
@@ -179,6 +180,8 @@ const Header = (props) => {
|
||||
org_id: orgId,
|
||||
};
|
||||
|
||||
localStorage.setItem("getting_started_sidebar", "open");
|
||||
|
||||
fetch(`${globalUrl}/api/v1/orgs/${orgId}/change`, {
|
||||
mode: "cors",
|
||||
method: "POST",
|
||||
@@ -506,7 +509,7 @@ const Header = (props) => {
|
||||
//<div style={{position: "fixed", top: 0, left: 0, display: "flex"}}>
|
||||
const loginTextBrowser = !isLoggedIn ? (
|
||||
<div style={{ display: "flex" }}>
|
||||
<List style={{ display: "flex", flexDirect: "row" }} component="nav">
|
||||
<List style={{ flex: 1, display: "flex", flexDirect: "row" }} component="nav">
|
||||
<ListItem style={{ textAlign: "center", marginLeft: "0px" }}>
|
||||
<Link to="/docs" style={hrefStyle}>
|
||||
<div
|
||||
@@ -519,7 +522,13 @@ const Header = (props) => {
|
||||
</Link>
|
||||
</ListItem>
|
||||
</List>
|
||||
<div style={{ flex: "7", display: "flex", flexDirection: "row-reverse" }}>
|
||||
{!isLoaded ? null :
|
||||
userdata.chat_disabled === true ? null :
|
||||
<div style={{flex: 1, }}>
|
||||
<SearchField serverside={false} userdata={userdata} />
|
||||
</div>
|
||||
}
|
||||
<div style={{ flex: 1, display: "flex", flexDirection: "row-reverse" }}>
|
||||
<List
|
||||
style={{ display: "flex", flexDirection: "row-reverse" }}
|
||||
component="nav"
|
||||
@@ -540,12 +549,12 @@ const Header = (props) => {
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: "flex" }}>
|
||||
<div style={{ flex: "1", flexDirection: "row" }}>
|
||||
<div style={{ flex: 1, flexDirection: "row" }}>
|
||||
<List
|
||||
style={{ display: "flex", flexDirect: "row", flex: "1" }}
|
||||
component="nav"
|
||||
>
|
||||
<ListItem style={{ textAlign: "center" }}>
|
||||
<ListItem style={{ textAlign: "center", maxWidth: 140, }}>
|
||||
<Link to="/workflows" style={hrefStyle}>
|
||||
<div
|
||||
onMouseOver={handleSoarHover}
|
||||
@@ -561,7 +570,7 @@ const Header = (props) => {
|
||||
</div>
|
||||
</Link>
|
||||
</ListItem>
|
||||
<ListItem style={{ textAlign: "center" }}>
|
||||
<ListItem style={{ textAlign: "center", maxWidth: 100, }}>
|
||||
<Link to="/apps" style={hrefStyle}>
|
||||
<div
|
||||
onMouseOver={handleHelpHover}
|
||||
@@ -584,7 +593,7 @@ const Header = (props) => {
|
||||
</Link>
|
||||
</ListItem>
|
||||
*/}
|
||||
<ListItem style={{ textAlign: "center" }}>
|
||||
<ListItem style={{ textAlign: "center", maxWidth: 120, }}>
|
||||
<Link to="/docs" style={hrefStyle}>
|
||||
<div
|
||||
onMouseOver={handleDocsHover}
|
||||
@@ -619,8 +628,14 @@ const Header = (props) => {
|
||||
*/}
|
||||
</List>
|
||||
</div>
|
||||
{!isLoaded ? null :
|
||||
userdata.chat_disabled === true ? null :
|
||||
<div style={{flex: 1, }}>
|
||||
<SearchField serverside={false} userdata={userdata} />
|
||||
</div>
|
||||
}
|
||||
<div
|
||||
style={{ flex: "10", display: "flex", flexDirection: "row-reverse" }}
|
||||
style={{ flex: 1, display: "flex", flexDirection: "row-reverse" }}
|
||||
>
|
||||
{avatarMenu}
|
||||
{notificationMenu}
|
||||
@@ -710,7 +725,6 @@ const Header = (props) => {
|
||||
userdata.orgs.splice(foundIndex+1, 1)
|
||||
} else {
|
||||
console.log("ORG NOT FOUND IN LIST: ", childorg)
|
||||
|
||||
}
|
||||
|
||||
// This is stupid :)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {isMobile} from "react-device-detect";
|
||||
import DetectionFramework, { usecases } from "../components/DetectionFramework.jsx";
|
||||
import AppFramework, { usecases } from "../components/AppFramework.jsx";
|
||||
import {Link} from 'react-router-dom';
|
||||
import ReactGA from 'react-ga';
|
||||
|
||||
@@ -169,7 +169,7 @@ const LandingpageUsecases = (props) => {
|
||||
</div>
|
||||
{isMobile ? null :
|
||||
<div style={{marginLeft: 200, marginTop: 125, zIndex: 1000}}>
|
||||
<DetectionFramework showOptions={false} selectedOption={selectedUsecase} rolling={true} />
|
||||
<AppFramework showOptions={false} selectedOption={selectedUsecase} rolling={true} />
|
||||
</div>
|
||||
}
|
||||
{isMobile ? null :
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useRef, useState, useEffect, useLayoutEffect } from "react";
|
||||
import { useTheme } from "@material-ui/core/styles";
|
||||
import theme from '../theme';
|
||||
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import {
|
||||
@@ -36,7 +37,10 @@ import {
|
||||
Switch,
|
||||
Fade,
|
||||
} from "@material-ui/core";
|
||||
import { LockOpen as LockOpenIcon } from "@material-ui/icons";
|
||||
import {
|
||||
LockOpen as LockOpenIcon,
|
||||
SupervisorAccount as SupervisorAccountIcon,
|
||||
} from "@mui/icons-material";
|
||||
|
||||
const ITEM_HEIGHT = 55;
|
||||
const ITEM_PADDING_TOP = 8;
|
||||
@@ -53,6 +57,20 @@ const MenuProps = {
|
||||
getContentAnchorEl: null,
|
||||
};
|
||||
|
||||
const registeredApps = [
|
||||
"gmail",
|
||||
"slack",
|
||||
"webex",
|
||||
"zoho_desk",
|
||||
"outlook_graph",
|
||||
"outlook_office365",
|
||||
"microsoft_teams",
|
||||
"microsoft_teams_user_access",
|
||||
"todoist",
|
||||
"microsoft_sentinel",
|
||||
"microsoft_365_defender",
|
||||
]
|
||||
|
||||
const AuthenticationOauth2 = (props) => {
|
||||
const {
|
||||
saveWorkflow,
|
||||
@@ -65,8 +83,9 @@ const AuthenticationOauth2 = (props) => {
|
||||
setSelectedAction,
|
||||
setNewAppAuth,
|
||||
setAuthenticationModalOpen,
|
||||
isCloud,
|
||||
autoAuth,
|
||||
} = props;
|
||||
const theme = useTheme();
|
||||
|
||||
//const [update, setUpdate] = React.useState("|")
|
||||
const [defaultConfigSet, setDefaultConfigSet] = React.useState(
|
||||
@@ -76,7 +95,8 @@ const AuthenticationOauth2 = (props) => {
|
||||
authenticationType.client_secret !== undefined &&
|
||||
authenticationType.client_secret !== null &&
|
||||
authenticationType.client_secret.length > 0
|
||||
);
|
||||
);
|
||||
|
||||
const [clientId, setClientId] = React.useState(
|
||||
defaultConfigSet ? authenticationType.client_id : ""
|
||||
);
|
||||
@@ -85,11 +105,13 @@ const AuthenticationOauth2 = (props) => {
|
||||
);
|
||||
const [oauthUrl, setOauthUrl] = React.useState("");
|
||||
const [buttonClicked, setButtonClicked] = React.useState(false);
|
||||
const [selectedScopes, setSelectedScopes] = React.useState([]);
|
||||
|
||||
const [offlineAccess, setOfflineAccess] = React.useState(true);
|
||||
const allscopes =
|
||||
authenticationType.scope !== undefined ? authenticationType.scope : [];
|
||||
|
||||
console.log("ALLSCOPES: ", allscopes)
|
||||
const [selectedScopes, setSelectedScopes] = React.useState(allscopes.length === 1 ? [allscopes[0]] : [])
|
||||
const [manuallyConfigure, setManuallyConfigure] = React.useState(
|
||||
defaultConfigSet ? false : true
|
||||
);
|
||||
@@ -106,11 +128,103 @@ const AuthenticationOauth2 = (props) => {
|
||||
active: true,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
console.log("Should automatically click the auto-auth button?")
|
||||
if (autoAuth === true && selectedApp !== undefined) {
|
||||
startOauth2Request()
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (selectedApp.authentication === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleOauth2Request = (client_id, client_secret, oauth_url, scopes) => {
|
||||
const startOauth2Request = (admin_consent) => {
|
||||
console.log("APP: ", selectedApp)
|
||||
if (selectedApp.name.toLowerCase() == "outlook_graph" || selectedApp.name.toLowerCase() == "outlook_office365") {
|
||||
handleOauth2Request(
|
||||
|
||||
"efe4c3fe-84a1-4821-a84f-23a6cfe8e72d",
|
||||
"",
|
||||
"https://graph.microsoft.com",
|
||||
["Mail.ReadWrite"],
|
||||
admin_consent,
|
||||
);
|
||||
} else if (selectedApp.name.toLowerCase() == "gmail") {
|
||||
handleOauth2Request(
|
||||
"253565968129-c0a35knic7q1pdk6i6qk9gdkvr07ci49.apps.googleusercontent.com",
|
||||
"",
|
||||
"https://gmail.googleapis.com",
|
||||
["https://www.googleapis.com/auth/gmail.modify",
|
||||
"https://www.googleapis.com/auth/gmail.send",
|
||||
"https://www.googleapis.com/auth/gmail.insert",
|
||||
"https://www.googleapis.com/auth/gmail.compose"],
|
||||
admin_consent,
|
||||
)
|
||||
} else if (selectedApp.name.toLowerCase() == "zoho_desk") {
|
||||
handleOauth2Request(
|
||||
"1000.ZR5MHUW6B0L6W1VUENFGIATFS0TOJT",
|
||||
"",
|
||||
"https://desk.zoho.com",
|
||||
["Desk.tickets.READ",
|
||||
"Desk.tickets.UPDATE",
|
||||
"Desk.tickets.DELETE",
|
||||
"Desk.tickets.CREATE"],
|
||||
admin_consent,
|
||||
)
|
||||
} else if (selectedApp.name.toLowerCase() == "slack") {
|
||||
handleOauth2Request(
|
||||
"151779186901.2448678750935",
|
||||
"",
|
||||
"https://slack.com",
|
||||
["admin", "chat:write", "im:read", "im:write", "search:read", "usergroups:read", "usergroups:write"],
|
||||
admin_consent,
|
||||
)
|
||||
} else if (selectedApp.name.toLowerCase() == "webex") {
|
||||
handleOauth2Request(
|
||||
"Cab184f3d7271f540443c79b5b79845e3387abbbdb3db4233a87ea3a5432fb3d5",
|
||||
"",
|
||||
"https://webexapis.com",
|
||||
["spark:all"],
|
||||
admin_consent,
|
||||
)
|
||||
} else if (selectedApp.name.toLowerCase().includes("microsoft_teams")) {
|
||||
handleOauth2Request(
|
||||
"31cb4c84-658e-43d5-ae84-22c9142e967a",
|
||||
"",
|
||||
"https://graph.microsoft.com",
|
||||
["ChannelMessage.Edit", "ChannelMessage.Read.All", "ChannelMessage.Send", "Chat.Create", "Chat.ReadWrite", "Chat.Read"],
|
||||
admin_consent,
|
||||
)
|
||||
} else if (selectedApp.name.toLowerCase().includes("todoist")) {
|
||||
handleOauth2Request(
|
||||
"35fa3a384040470db0c8527e90a3c2eb",
|
||||
"",
|
||||
"https://api.todoist.com",
|
||||
["task:add"],
|
||||
admin_consent,
|
||||
)
|
||||
} else if (selectedApp.name.toLowerCase().includes("microsoft_sentinel")) {
|
||||
handleOauth2Request(
|
||||
"4c16e8c4-3d34-4aa1-ac94-262ea170b7f7",
|
||||
"",
|
||||
"https://management.azure.com",
|
||||
["https://management.azure.com/user_impersonation"],
|
||||
admin_consent,
|
||||
)
|
||||
} else if (selectedApp.name.toLowerCase().includes("microsoft_365_defender")) {
|
||||
handleOauth2Request(
|
||||
"4c16e8c4-3d34-4aa1-ac94-262ea170b7f7",
|
||||
"",
|
||||
"https://graph.microsoft.com",
|
||||
["SecurityEvents.ReadWrite.All"],
|
||||
admin_consent,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const handleOauth2Request = (client_id, client_secret, oauth_url, scopes, admin_consent) => {
|
||||
setButtonClicked(true);
|
||||
console.log("SCOPES: ", scopes);
|
||||
|
||||
@@ -151,12 +265,21 @@ const AuthenticationOauth2 = (props) => {
|
||||
state += `%26refresh_uri%3d${authentication_url}`;
|
||||
}
|
||||
|
||||
const url = `${authenticationType.redirect_uri}?client_id=${client_id}&redirect_uri=${redirectUri}&response_type=code&scope=${resources}&prompt=consent&state=${state}&access_type=offline`;
|
||||
// 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 (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
|
||||
//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
|
||||
//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`
|
||||
//console.log("Full URI: ", url)
|
||||
//console.log("Redirect Uri: ", redirectUri)
|
||||
// &resource=https%3A%2F%2Fgraph.microsoft.com&
|
||||
|
||||
// &resource=https%3A%2F%2Fgraph.microsoft.com&
|
||||
|
||||
// FIXME: Awful, but works for prototyping
|
||||
// How can we get a callback properly realtime?
|
||||
@@ -168,12 +291,20 @@ const AuthenticationOauth2 = (props) => {
|
||||
var open = true;
|
||||
const timer = setInterval(() => {
|
||||
if (newwin.closed) {
|
||||
console.log("Closing?")
|
||||
|
||||
if (setAuthenticationModalOpen !== undefined) {
|
||||
setAuthenticationModalOpen(false)
|
||||
}
|
||||
|
||||
setButtonClicked(false);
|
||||
clearInterval(timer);
|
||||
//alert('"Secure Payment" window closed!');
|
||||
|
||||
getAppAuthentication(true, true);
|
||||
}
|
||||
} else {
|
||||
console.log("Not closed")
|
||||
}
|
||||
}, 1000);
|
||||
//do {
|
||||
// setTimeout(() => {
|
||||
@@ -337,6 +468,95 @@ const AuthenticationOauth2 = (props) => {
|
||||
</a>
|
||||
<div />
|
||||
</span>
|
||||
|
||||
{isCloud && registeredApps.includes(selectedApp.name.toLowerCase()) ?
|
||||
<span>
|
||||
<span style={{display: "flex"}}>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="contained"
|
||||
style={{
|
||||
marginBottom: 20,
|
||||
marginTop: 20,
|
||||
flex: 1,
|
||||
textTransform: "none",
|
||||
textAlign: "left",
|
||||
justifyContent: "flex-start",
|
||||
backgroundColor: "#ffffff",
|
||||
color: "#2f2f2f",
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
minWidth: 300,
|
||||
maxWidth: 300,
|
||||
maxHeight: 50,
|
||||
overflow: "hidden",
|
||||
border: `1px solid ${theme.palette.inputColor}`,
|
||||
}}
|
||||
color="primary"
|
||||
disabled={
|
||||
clientSecret.length > 0 || clientId.length > 0
|
||||
}
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
// Hardcode some stuff?
|
||||
// This could prolly be added to the app itself with a "default" client ID
|
||||
startOauth2Request()
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
{buttonClicked ? (
|
||||
<CircularProgress style={{ color: "#f86a3e", width: 45, height: 45, margin: "auto", }} />
|
||||
) : (
|
||||
<span style={{display: "flex"}}>
|
||||
<img
|
||||
alt={selectedAction.app_name}
|
||||
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">
|
||||
Auto-Authenticate
|
||||
</Typography>
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
{buttonClicked ?
|
||||
null
|
||||
:
|
||||
<Tooltip
|
||||
color="primary"
|
||||
title={"Force Admin Consent"}
|
||||
placement="top"
|
||||
>
|
||||
<Button
|
||||
fullWidth
|
||||
variant="outlined"
|
||||
style={{
|
||||
maxWidth: 50,
|
||||
marginBottom: 20,
|
||||
marginTop: 20,
|
||||
maxHeight: 50,
|
||||
}}
|
||||
color="primary"
|
||||
disabled={
|
||||
clientSecret.length > 0 || clientId.length > 0
|
||||
}
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
// Hardcode some stuff?
|
||||
// This could prolly be added to the app itself with a "default" client ID
|
||||
startOauth2Request(true)
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
<SupervisorAccountIcon />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
}
|
||||
</span>
|
||||
<Typography style={{textAlign: "center", marginTop: 0, marginBottom: 10, }}>
|
||||
OR
|
||||
</Typography>
|
||||
</span>
|
||||
: null}
|
||||
{/*<TextField
|
||||
style={{backgroundColor: theme.palette.inputColor, borderRadius: theme.palette.borderRadius,}}
|
||||
InputProps={{
|
||||
@@ -580,10 +800,12 @@ const AuthenticationOauth2 = (props) => {
|
||||
{buttonClicked ? (
|
||||
<CircularProgress style={{ color: "white" }} />
|
||||
) : (
|
||||
"Oauth2 request"
|
||||
"Manually Authenticate"
|
||||
)}
|
||||
</Button>
|
||||
|
||||
|
||||
|
||||
{defaultConfigSet ? (
|
||||
<span style={{}}>
|
||||
... or
|
||||
|
||||
@@ -97,6 +97,15 @@ const OrgHeader = (props) => {
|
||||
? ""
|
||||
: selectedOrganization.defaults.notification_workflow
|
||||
);
|
||||
|
||||
const [documentationReference, setDocumentationReference] = React.useState(
|
||||
selectedOrganization.defaults === undefined
|
||||
? ""
|
||||
: selectedOrganization.defaults.documentation_reference === undefined ||
|
||||
selectedOrganization.defaults.documentation_reference.length === 0
|
||||
? ""
|
||||
: selectedOrganization.defaults.documentation_reference
|
||||
);
|
||||
const [openidClientId, setOpenidClientId] = React.useState(
|
||||
selectedOrganization.sso_config === undefined
|
||||
? ""
|
||||
@@ -105,6 +114,14 @@ const OrgHeader = (props) => {
|
||||
? ""
|
||||
: selectedOrganization.sso_config.client_id
|
||||
);
|
||||
const [openidClientSecret, setOpenidClientSecret] = React.useState(
|
||||
selectedOrganization.sso_config === undefined
|
||||
? ""
|
||||
: selectedOrganization.sso_config.client_secret === undefined ||
|
||||
selectedOrganization.sso_config.client_secret.length === 0
|
||||
? ""
|
||||
: selectedOrganization.sso_config.client_secret
|
||||
);
|
||||
const [openidAuthorization, setOpenidAuthorization] = React.useState(
|
||||
selectedOrganization.sso_config === undefined
|
||||
? ""
|
||||
@@ -237,11 +254,13 @@ const OrgHeader = (props) => {
|
||||
workflow_download_repo: workflowDownloadUrl,
|
||||
workflow_download_branch: workflowDownloadBranch,
|
||||
notification_workflow: notificationWorkflow,
|
||||
documentation_reference: documentationReference,
|
||||
},
|
||||
{
|
||||
sso_entrypoint: ssoEntrypoint,
|
||||
sso_certificate: ssoCertificate,
|
||||
client_id: openidClientId,
|
||||
client_secret: openidClientSecret,
|
||||
openid_authorization: openidAuthorization,
|
||||
openid_token: openidToken,
|
||||
}
|
||||
@@ -439,8 +458,279 @@ const OrgHeader = (props) => {
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</Grid>
|
||||
<Grid item xs={12} style={{}}>
|
||||
<span>
|
||||
<Typography>Org Documentation reference</Typography>
|
||||
<TextField
|
||||
required
|
||||
style={{
|
||||
flex: "1",
|
||||
marginTop: "5px",
|
||||
marginRight: "15px",
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
}}
|
||||
fullWidth={true}
|
||||
type="name"
|
||||
id="outlined-with-placeholder"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
placeholder="URL to an external reference for this implementation"
|
||||
value={documentationReference}
|
||||
onChange={(e) => {
|
||||
setDocumentationReference(e.target.value);
|
||||
}}
|
||||
InputProps={{
|
||||
classes: {
|
||||
notchedOutline: classes.notchedOutline,
|
||||
},
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</Grid>
|
||||
{isCloud ? null : (
|
||||
{isCloud ? null :
|
||||
<Grid item xs={12} style={{marginTop: 50 }}>
|
||||
<Typography variant="h4" style={{textAlign: "center",}}>OpenID connect</Typography>
|
||||
<Grid container style={{marginTop: 10, }}>
|
||||
<Grid item xs={6} style={{}}>
|
||||
<span>
|
||||
<Typography>Client ID</Typography>
|
||||
<TextField
|
||||
required
|
||||
style={{
|
||||
flex: "1",
|
||||
marginTop: "5px",
|
||||
marginRight: "15px",
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
}}
|
||||
fullWidth={true}
|
||||
type="name"
|
||||
multiline={true}
|
||||
rows={2}
|
||||
disabled={
|
||||
selectedOrganization.manager_orgs !== undefined &&
|
||||
selectedOrganization.manager_orgs !== null &&
|
||||
selectedOrganization.manager_orgs.length > 0
|
||||
}
|
||||
id="outlined-with-placeholder"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
placeholder="The OpenID client ID from the identity provider"
|
||||
value={openidClientId}
|
||||
onChange={(e) => {
|
||||
setOpenidClientId(e.target.value);
|
||||
}}
|
||||
InputProps={{
|
||||
classes: {
|
||||
notchedOutline: classes.notchedOutline,
|
||||
},
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</Grid>
|
||||
<Grid item xs={6} style={{}}>
|
||||
<span>
|
||||
<Typography>Client Secret (optional)</Typography>
|
||||
<TextField
|
||||
required
|
||||
style={{
|
||||
flex: "1",
|
||||
marginTop: "5px",
|
||||
marginRight: "15px",
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
}}
|
||||
fullWidth={true}
|
||||
type="name"
|
||||
multiline={true}
|
||||
rows={2}
|
||||
disabled={
|
||||
selectedOrganization.manager_orgs !== undefined &&
|
||||
selectedOrganization.manager_orgs !== null &&
|
||||
selectedOrganization.manager_orgs.length > 0
|
||||
}
|
||||
id="outlined-with-placeholder"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
placeholder="The OpenID client secret - DONT use this if dealing with implicit auth / PKCE"
|
||||
value={openidClientSecret}
|
||||
onChange={(e) => {
|
||||
setOpenidClientSecret(e.target.value);
|
||||
}}
|
||||
InputProps={{
|
||||
classes: {
|
||||
notchedOutline: classes.notchedOutline,
|
||||
},
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid container style={{marginTop: 10, }}>
|
||||
<Grid item xs={6} style={{}}>
|
||||
<span>
|
||||
<Typography>Authorization URL</Typography>
|
||||
<TextField
|
||||
required
|
||||
style={{
|
||||
flex: "1",
|
||||
marginTop: "5px",
|
||||
marginRight: "15px",
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
}}
|
||||
fullWidth={true}
|
||||
type="name"
|
||||
id="outlined-with-placeholder"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
multiline={true}
|
||||
rows={2}
|
||||
placeholder="The OpenID authorization URL (usually ends with /authorize)"
|
||||
value={openidAuthorization}
|
||||
onChange={(e) => {
|
||||
setOpenidAuthorization(e.target.value)
|
||||
}}
|
||||
InputProps={{
|
||||
classes: {
|
||||
notchedOutline: classes.notchedOutline,
|
||||
},
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</Grid>
|
||||
<Grid item xs={6} style={{}}>
|
||||
<span>
|
||||
<Typography>Token URL</Typography>
|
||||
<TextField
|
||||
required
|
||||
style={{
|
||||
flex: "1",
|
||||
marginTop: "5px",
|
||||
marginRight: "15px",
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
}}
|
||||
fullWidth={true}
|
||||
type="name"
|
||||
id="outlined-with-placeholder"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
multiline={true}
|
||||
rows={2}
|
||||
placeholder="The OpenID token URL (usually ends with /token)"
|
||||
value={openidToken}
|
||||
onChange={(e) => {
|
||||
setOpenidToken(e.target.value)
|
||||
}}
|
||||
InputProps={{
|
||||
classes: {
|
||||
notchedOutline: classes.notchedOutline,
|
||||
},
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
}
|
||||
{/*isCloud ? null : */}
|
||||
<Grid item xs={12} style={{marginTop: 50,}}>
|
||||
<Typography variant="h4" style={{textAlign: "center",}}>SAML SSO (v1.1)</Typography>
|
||||
<Grid container style={{marginTop: 20, }}>
|
||||
<Grid item xs={6} style={{}}>
|
||||
<span>
|
||||
<Typography>SSO Entrypoint (IdP)</Typography>
|
||||
<TextField
|
||||
required
|
||||
style={{
|
||||
flex: "1",
|
||||
marginTop: "5px",
|
||||
marginRight: "15px",
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
}}
|
||||
fullWidth={true}
|
||||
type="name"
|
||||
multiline={true}
|
||||
rows={2}
|
||||
disabled={
|
||||
selectedOrganization.manager_orgs !== undefined &&
|
||||
selectedOrganization.manager_orgs !== null &&
|
||||
selectedOrganization.manager_orgs.length > 0
|
||||
}
|
||||
id="outlined-with-placeholder"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
placeholder="The entrypoint URL from your provider"
|
||||
value={ssoEntrypoint}
|
||||
onChange={(e) => {
|
||||
setSsoEntrypoint(e.target.value);
|
||||
}}
|
||||
InputProps={{
|
||||
classes: {
|
||||
notchedOutline: classes.notchedOutline,
|
||||
},
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</Grid>
|
||||
<Grid item xs={6} style={{}}>
|
||||
<span>
|
||||
<Typography>SSO Certificate (X509)</Typography>
|
||||
<TextField
|
||||
required
|
||||
style={{
|
||||
flex: "1",
|
||||
marginTop: "5px",
|
||||
marginRight: "15px",
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
}}
|
||||
fullWidth={true}
|
||||
type="name"
|
||||
id="outlined-with-placeholder"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
multiline={true}
|
||||
rows={2}
|
||||
placeholder="The X509 certificate to use"
|
||||
value={ssoCertificate}
|
||||
onChange={(e) => {
|
||||
setSsoCertificate(e.target.value);
|
||||
}}
|
||||
InputProps={{
|
||||
classes: {
|
||||
notchedOutline: classes.notchedOutline,
|
||||
},
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</Grid>
|
||||
</Grid>
|
||||
{isCloud ?
|
||||
<Typography variant="body2" style={{textAlign: "left",}} color="textSecondary">
|
||||
IdP URL for Shuffle: https://shuffler.io/api/v1/login_sso
|
||||
</Typography>
|
||||
: null}
|
||||
</Grid>
|
||||
{isCloud ? null : (
|
||||
<Grid item xs={6} style={{}}>
|
||||
<span>
|
||||
<Typography>App Download URL</Typography>
|
||||
@@ -576,200 +866,10 @@ const OrgHeader = (props) => {
|
||||
</span>
|
||||
</Grid>
|
||||
)}
|
||||
{isCloud ? null :
|
||||
<Grid item xs={12} style={{marginTop: 50 }}>
|
||||
<Typography variant="h4" style={{textAlign: "center",}}>OpenID connect</Typography>
|
||||
<Grid container style={{marginTop: 10, }}>
|
||||
<Grid item xs={4} style={{}}>
|
||||
<span>
|
||||
<Typography>Client ID</Typography>
|
||||
<TextField
|
||||
required
|
||||
style={{
|
||||
flex: "1",
|
||||
marginTop: "5px",
|
||||
marginRight: "15px",
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
}}
|
||||
fullWidth={true}
|
||||
type="name"
|
||||
multiline={true}
|
||||
rows={2}
|
||||
disabled={
|
||||
selectedOrganization.manager_orgs !== undefined &&
|
||||
selectedOrganization.manager_orgs !== null &&
|
||||
selectedOrganization.manager_orgs.length > 0
|
||||
}
|
||||
id="outlined-with-placeholder"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
placeholder="The OpenID client ID from the identity provider"
|
||||
value={openidClientId}
|
||||
onChange={(e) => {
|
||||
setOpenidClientId(e.target.value);
|
||||
}}
|
||||
InputProps={{
|
||||
classes: {
|
||||
notchedOutline: classes.notchedOutline,
|
||||
},
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</Grid>
|
||||
<Grid item xs={4} style={{}}>
|
||||
<span>
|
||||
<Typography>Authorization URL</Typography>
|
||||
<TextField
|
||||
required
|
||||
style={{
|
||||
flex: "1",
|
||||
marginTop: "5px",
|
||||
marginRight: "15px",
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
}}
|
||||
fullWidth={true}
|
||||
type="name"
|
||||
id="outlined-with-placeholder"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
multiline={true}
|
||||
rows={2}
|
||||
placeholder="The OpenID authorization URL (usually ends with /authorize)"
|
||||
value={openidAuthorization}
|
||||
onChange={(e) => {
|
||||
setOpenidAuthorization(e.target.value)
|
||||
}}
|
||||
InputProps={{
|
||||
classes: {
|
||||
notchedOutline: classes.notchedOutline,
|
||||
},
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</Grid>
|
||||
<Grid item xs={4} style={{}}>
|
||||
<span>
|
||||
<Typography>Token URL</Typography>
|
||||
<TextField
|
||||
required
|
||||
style={{
|
||||
flex: "1",
|
||||
marginTop: "5px",
|
||||
marginRight: "15px",
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
}}
|
||||
fullWidth={true}
|
||||
type="name"
|
||||
id="outlined-with-placeholder"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
multiline={true}
|
||||
rows={2}
|
||||
placeholder="The OpenID token URL (usually ends with /token)"
|
||||
value={openidToken}
|
||||
onChange={(e) => {
|
||||
setOpenidToken(e.target.value)
|
||||
}}
|
||||
InputProps={{
|
||||
classes: {
|
||||
notchedOutline: classes.notchedOutline,
|
||||
},
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
}
|
||||
{isCloud ? null :
|
||||
<Grid item xs={12} style={{marginTop: 50,}}>
|
||||
<Typography variant="h4" style={{textAlign: "center",}}>SAML SSO (v1.1)</Typography>
|
||||
<Grid container style={{marginTop: 10, }}>
|
||||
<Grid item xs={6} style={{}}>
|
||||
<span>
|
||||
<Typography>SSO Entrypoint (IdP)</Typography>
|
||||
<TextField
|
||||
required
|
||||
style={{
|
||||
flex: "1",
|
||||
marginTop: "5px",
|
||||
marginRight: "15px",
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
}}
|
||||
fullWidth={true}
|
||||
type="name"
|
||||
multiline={true}
|
||||
rows={2}
|
||||
disabled={
|
||||
selectedOrganization.manager_orgs !== undefined &&
|
||||
selectedOrganization.manager_orgs !== null &&
|
||||
selectedOrganization.manager_orgs.length > 0
|
||||
}
|
||||
id="outlined-with-placeholder"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
placeholder="The entrypoint URL from your provider"
|
||||
value={ssoEntrypoint}
|
||||
onChange={(e) => {
|
||||
setSsoEntrypoint(e.target.value);
|
||||
}}
|
||||
InputProps={{
|
||||
classes: {
|
||||
notchedOutline: classes.notchedOutline,
|
||||
},
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</Grid>
|
||||
<Grid item xs={6} style={{}}>
|
||||
<span>
|
||||
<Typography>SSO Certificate (X509)</Typography>
|
||||
<TextField
|
||||
required
|
||||
style={{
|
||||
flex: "1",
|
||||
marginTop: "5px",
|
||||
marginRight: "15px",
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
}}
|
||||
fullWidth={true}
|
||||
type="name"
|
||||
id="outlined-with-placeholder"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
multiline={true}
|
||||
rows={2}
|
||||
placeholder="The X509 certificate to use"
|
||||
value={ssoCertificate}
|
||||
onChange={(e) => {
|
||||
setSsoCertificate(e.target.value);
|
||||
}}
|
||||
InputProps={{
|
||||
classes: {
|
||||
notchedOutline: classes.notchedOutline,
|
||||
},
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Grid>
|
||||
}
|
||||
|
||||
<div style={{ margin: "auto", textalign: "center", marginTop: 15, marginBottom: 15, }}>
|
||||
{orgSaveButton}
|
||||
</div>
|
||||
{/*
|
||||
<span style={{textAlign: "center"}}>
|
||||
{expanded ?
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,23 +2,26 @@ import { useEffect } from "react";
|
||||
//import { withRouter } from "react-router-dom";
|
||||
import { useLocation } from "react-router-dom";
|
||||
|
||||
function ScrollToTop({ getUserNotifications, setCurpath, history }) {
|
||||
// ensures scrolling happens in the right way on different pages and when changing
|
||||
function ScrollToTop({ getUserNotifications, curpath, setCurpath, history }) {
|
||||
let location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
//const unlisten = history.listen(() => {
|
||||
window.scroll({
|
||||
top: 0,
|
||||
left: 0,
|
||||
behavior: "smooth",
|
||||
});
|
||||
// Custom handler for certain scroll mechanics
|
||||
//
|
||||
console.log("OLD: ", curpath, "NeW: ", window.location.pathname)
|
||||
if (curpath === window.location.pathname && curpath === "/usecases") {
|
||||
} else {
|
||||
|
||||
setCurpath(window.location.pathname);
|
||||
getUserNotifications();
|
||||
//});
|
||||
//return () => {
|
||||
// unlisten();
|
||||
//};
|
||||
window.scroll({
|
||||
top: 0,
|
||||
left: 0,
|
||||
behavior: "smooth",
|
||||
});
|
||||
|
||||
setCurpath(window.location.pathname);
|
||||
getUserNotifications();
|
||||
}
|
||||
}, [location]);
|
||||
|
||||
return null;
|
||||
|
||||
@@ -0,0 +1,648 @@
|
||||
import React, {useState, useEffect, useRef} from 'react';
|
||||
|
||||
import { useNavigate, Link, useParams } from "react-router-dom";
|
||||
import { useTheme } from '@material-ui/core/styles';
|
||||
import SearchIcon from '@material-ui/icons/Search';
|
||||
|
||||
import {
|
||||
Chip,
|
||||
IconButton,
|
||||
TextField,
|
||||
InputAdornment,
|
||||
List,
|
||||
Card,
|
||||
ListItem,
|
||||
ListItemAvatar,
|
||||
ListItemText,
|
||||
Avatar,
|
||||
Typography,
|
||||
Tooltip,
|
||||
} from '@material-ui/core';
|
||||
|
||||
import {
|
||||
AvatarGroup,
|
||||
} from "@mui/material"
|
||||
|
||||
import {Close as CloseIcon, Folder as FolderIcon, Polymer as PolymerIcon, LibraryBooks as LibraryBooksIcon} from '@material-ui/icons'
|
||||
|
||||
import algoliasearch from 'algoliasearch/lite';
|
||||
import aa from 'search-insights'
|
||||
import { InstantSearch, Configure, connectSearchBox, connectHits, Index } from 'react-instantsearch-dom';
|
||||
//import { InstantSearch, SearchBox, Hits, connectSearchBox, connectHits, Index } from 'react-instantsearch-dom';
|
||||
|
||||
// https://www.algolia.com/doc/api-reference/widgets/search-box/react/
|
||||
const chipStyle = {
|
||||
backgroundColor: "#3d3f43", height: 30, marginRight: 5, paddingLeft: 5, paddingRight: 5, height: 28, cursor: "pointer", borderColor: "#3d3f43", color: "white",
|
||||
}
|
||||
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240")
|
||||
const SearchField = props => {
|
||||
const { serverside, userdata } = props
|
||||
|
||||
const theme = useTheme();
|
||||
let navigate = useNavigate();
|
||||
const borderRadius = 3
|
||||
const node = useRef()
|
||||
const [searchOpen, setSearchOpen] = useState(false)
|
||||
const [oldPath, setOldPath] = useState("")
|
||||
|
||||
if (serverside === true) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (window !== undefined && window.location !== undefined && window.location.pathname === "/search") {
|
||||
return null
|
||||
}
|
||||
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
|
||||
|
||||
if (window.location.pathname !== oldPath) {
|
||||
setSearchOpen(false)
|
||||
setOldPath(window.location.pathname)
|
||||
}
|
||||
|
||||
//useEffect(() => {
|
||||
// if (searchOpen) {
|
||||
// var tarfield = document.getElementById("shuffle_search_field")
|
||||
// tarfield.focus()
|
||||
// }
|
||||
//}, searchOpen)
|
||||
|
||||
const SearchBox = ({currentRefinement, refine, isSearchStalled, } ) => {
|
||||
|
||||
/*
|
||||
endAdornment: (
|
||||
<InputAdornment position="end" style={{textAlign: "right", zIndex: 5001, cursor: "pointer", width: 100, }} onMouseOver={(event) => {
|
||||
event.preventDefault()
|
||||
}}>
|
||||
<CloseIcon style={{marginRight: 5,}} onClick={() => {
|
||||
setSearchOpen(false)
|
||||
}} />
|
||||
</InputAdornment>
|
||||
),
|
||||
*/
|
||||
|
||||
return (
|
||||
<form id="search_form" noValidate type="searchbox" action="" role="search" style={{margin: 0, }} onClick={() => {
|
||||
}}>
|
||||
<TextField
|
||||
fullWidth
|
||||
style={{backgroundColor: theme.palette.inputColor, borderRadius: borderRadius, margin: 10, width: "100%",}}
|
||||
InputProps={{
|
||||
style:{
|
||||
color: "white",
|
||||
fontSize: "1em",
|
||||
height: 50,
|
||||
margin: 0,
|
||||
},
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon style={{marginLeft: 5}}/>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
autoComplete='off'
|
||||
type="search"
|
||||
color="primary"
|
||||
placeholder="Find Public Apps, Workflows, Documentation and more"
|
||||
value={currentRefinement}
|
||||
id="shuffle_search_field"
|
||||
onClick={(event) => {
|
||||
if (!searchOpen) {
|
||||
setSearchOpen(true)
|
||||
setTimeout(() => {
|
||||
var tarfield = document.getElementById("shuffle_search_field")
|
||||
//console.log("TARFIELD: ", tarfield)
|
||||
tarfield.focus()
|
||||
}, 100)
|
||||
}
|
||||
}}
|
||||
onBlur={(event) => {
|
||||
setTimeout(() => {
|
||||
setSearchOpen(false)
|
||||
}, 500)
|
||||
}}
|
||||
onChange={(event) => {
|
||||
//if (event.currentTarget.value.length > 0 && !searchOpen) {
|
||||
// setSearchOpen(true)
|
||||
//}
|
||||
|
||||
refine(event.currentTarget.value)
|
||||
}}
|
||||
limit={5}
|
||||
/>
|
||||
{/*isSearchStalled ? 'My search is stalled' : ''*/}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
const WorkflowHits = ({ hits }) => {
|
||||
const [mouseHoverIndex, setMouseHoverIndex] = useState(0)
|
||||
|
||||
var tmp = searchOpen
|
||||
if (!searchOpen) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
const positionInfo = document.activeElement.getBoundingClientRect()
|
||||
const outerlistitemStyle = {
|
||||
width: "100%",
|
||||
overflowX: "hidden",
|
||||
overflowY: "hidden",
|
||||
borderBottom: "1px solid rgba(255,255,255,0.4)",
|
||||
}
|
||||
|
||||
if (hits.length > 4) {
|
||||
hits = hits.slice(0, 4)
|
||||
}
|
||||
|
||||
var type = "workflows"
|
||||
const baseImage = <PolymerIcon />
|
||||
|
||||
return (
|
||||
<Card elevation={0} style={{position: "relative", marginLeft: 10, marginRight: 10, position: "absolute", color: "white", zIndex: 1002, backgroundColor: theme.palette.inputColor, width: 405, height: 408, left: 75, boxShadows: "none",}}>
|
||||
<Typography variant="h6" style={{margin: "10px 10px 0px 20px", }}>
|
||||
Workflows
|
||||
</Typography>
|
||||
|
||||
<List style={{backgroundColor: theme.palette.inputColor, }}>
|
||||
{hits.length === 0 ?
|
||||
<ListItem style={outerlistitemStyle}>
|
||||
<ListItemAvatar onClick={() => console.log(hits)}>
|
||||
<Avatar>
|
||||
<FolderIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={"No workflows found."}
|
||||
secondary={"Try a broader search term"}
|
||||
/>
|
||||
</ListItem>
|
||||
:
|
||||
hits.map((hit, index) => {
|
||||
const innerlistitemStyle = {
|
||||
width: positionInfo.width+35,
|
||||
overflowX: "hidden",
|
||||
overflowY: "hidden",
|
||||
borderBottom: "1px solid rgba(255,255,255,0.4)",
|
||||
backgroundColor: mouseHoverIndex === index ? "#1f2023" : "inherit",
|
||||
cursor: "pointer",
|
||||
marginLeft: 5,
|
||||
marginRight: 5,
|
||||
maxHeight: 75,
|
||||
minHeight: 75,
|
||||
maxWidth: 420,
|
||||
minWidth: "100%",
|
||||
}
|
||||
|
||||
const name = hit.name === undefined ?
|
||||
hit.filename.charAt(0).toUpperCase() + hit.filename.slice(1).replaceAll("_", " ") + " - " + hit.title :
|
||||
(hit.name.charAt(0).toUpperCase()+hit.name.slice(1)).replaceAll("_", " ")
|
||||
const secondaryText = hit.description !== undefined && hit.description !== null && hit.description.length > 3 ? hit.description.slice(0, 40)+"..." : ""
|
||||
const appGroup = hit.action_references === undefined || hit.action_references === null ? [] : hit.action_references
|
||||
const avatar = baseImage
|
||||
|
||||
var parsedUrl = isCloud ? `/workflows/${hit.objectID}` : `https://shuffler.io/workflows/${hit.objectID}`
|
||||
parsedUrl += `?queryID=${hit.__queryID}`
|
||||
|
||||
// <a rel="noopener noreferrer" href="https://www.algolia.com/" target="_blank" style={{textDecoration: "none", color: "white"}}>
|
||||
return (
|
||||
<Link key={hit.objectID} to={{ pathname: parsedUrl }} rel="noopener noreferrer" style={{textDecoration: "none", color: "white",}} onClick={(event) => {
|
||||
//console.log("CLICK")
|
||||
setSearchOpen(true)
|
||||
|
||||
aa('init', {
|
||||
appId: searchClient.appId,
|
||||
apiKey: searchClient.transporter.queryParameters["x-algolia-api-key"]
|
||||
})
|
||||
|
||||
const timestamp = new Date().getTime()
|
||||
aa('sendEvents', [
|
||||
{
|
||||
eventType: 'click',
|
||||
eventName: 'Workflow Clicked',
|
||||
index: 'workflows',
|
||||
objectIDs: [hit.objectID],
|
||||
timestamp: timestamp,
|
||||
queryID: hit.__queryID,
|
||||
positions: [hit.__position],
|
||||
userToken: userdata === undefined || userdata === null || userdata.id === undefined ? "unauthenticated" : userdata.id,
|
||||
}
|
||||
])
|
||||
|
||||
if (!isCloud) {
|
||||
event.preventDefault()
|
||||
window.open(parsedUrl, '_blank');
|
||||
}
|
||||
}}>
|
||||
<ListItem key={hit.objectID} style={innerlistitemStyle} onMouseOver={() => {
|
||||
setMouseHoverIndex(index)
|
||||
}}>
|
||||
<ListItemAvatar>
|
||||
{avatar}
|
||||
</ListItemAvatar>
|
||||
<div style={{}}>
|
||||
<ListItemText
|
||||
primary={name}
|
||||
/>
|
||||
<AvatarGroup max={10} style={{flexDirection: "row", padding: 0, margin: 0, itemAlign: "left", textAlign: "left",}}>
|
||||
{appGroup.map((app, index) => {
|
||||
// Putting all this in secondary of ListItemText looked weird.
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
height: 24,
|
||||
width: 24,
|
||||
filter: "brightness(0.6)",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => {
|
||||
navigate("/apps/"+app.id)
|
||||
}}
|
||||
>
|
||||
<Tooltip color="primary" title={app.name} placement="bottom">
|
||||
<Avatar alt={app.name} src={app.image_url} style={{width: 24, height: 24}}/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</AvatarGroup>
|
||||
</div>
|
||||
{/*
|
||||
<ListItemSecondaryAction>
|
||||
<IconButton edge="end" aria-label="delete">
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</ListItemSecondaryAction>
|
||||
*/}
|
||||
</ListItem>
|
||||
</Link>
|
||||
)})
|
||||
}
|
||||
</List>
|
||||
{/*
|
||||
<span style={{display: "flex", textAlign: "left", float: "left", position: "absolute", left: 15, bottom: 10, }}>
|
||||
<Link to="/search" style={{textDecoration: "none", color: "#f85a3e"}}>
|
||||
<Typography variant="body2" style={{}}>
|
||||
See all workflows
|
||||
</Typography>
|
||||
</Link>
|
||||
</span>
|
||||
*/}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const AppHits = ({ hits }) => {
|
||||
const [mouseHoverIndex, setMouseHoverIndex] = useState(0)
|
||||
|
||||
var tmp = searchOpen
|
||||
if (!searchOpen) {
|
||||
return null
|
||||
}
|
||||
|
||||
const positionInfo = document.activeElement.getBoundingClientRect()
|
||||
const outerlistitemStyle = {
|
||||
width: "100%",
|
||||
overflowX: "hidden",
|
||||
overflowY: "hidden",
|
||||
borderBottom: "1px solid rgba(255,255,255,0.4)",
|
||||
}
|
||||
|
||||
if (hits.length > 4) {
|
||||
hits = hits.slice(0, 4)
|
||||
}
|
||||
|
||||
var type = "app"
|
||||
const baseImage = <LibraryBooksIcon />
|
||||
|
||||
return (
|
||||
<Card elevation={0} style={{position: "relative", marginLeft: 10, marginRight: 10, position: "absolute", color: "white", zIndex: 1001, backgroundColor: theme.palette.inputColor, width: 1155, height: 408, left: -305, boxShadows: "none",}}>
|
||||
<IconButton style={{zIndex: 5000, position: "absolute", right: 14, color: "grey"}} onClick={() => {
|
||||
setSearchOpen(false)
|
||||
}}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
<Typography variant="h6" style={{margin: "10px 10px 0px 20px", }}>
|
||||
Apps
|
||||
</Typography>
|
||||
|
||||
<List style={{backgroundColor: theme.palette.inputColor, }}>
|
||||
{hits.length === 0 ?
|
||||
<ListItem style={outerlistitemStyle}>
|
||||
<ListItemAvatar onClick={() => console.log(hits)}>
|
||||
<Avatar>
|
||||
<FolderIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={"No apps found."}
|
||||
secondary={"Try a broader search term"}
|
||||
/>
|
||||
</ListItem>
|
||||
:
|
||||
hits.map((hit, index) => {
|
||||
const innerlistitemStyle = {
|
||||
width: positionInfo.width+35,
|
||||
overflowX: "hidden",
|
||||
overflowY: "hidden",
|
||||
borderBottom: "1px solid rgba(255,255,255,0.4)",
|
||||
backgroundColor: mouseHoverIndex === index ? "#1f2023" : "inherit",
|
||||
cursor: "pointer",
|
||||
marginLeft: 5,
|
||||
marginRight: 5,
|
||||
maxHeight: 75,
|
||||
minHeight: 75,
|
||||
maxWidth: 420,
|
||||
minWidth: "100%",
|
||||
}
|
||||
|
||||
const name = hit.name === undefined ?
|
||||
hit.filename.charAt(0).toUpperCase() + hit.filename.slice(1).replaceAll("_", " ") + " - " + hit.title :
|
||||
(hit.name.charAt(0).toUpperCase()+hit.name.slice(1)).replaceAll("_", " ")
|
||||
|
||||
var secondaryText = hit.data !== undefined ? hit.data.slice(0, 40)+"..." : ""
|
||||
const avatar = hit.image_url === undefined ?
|
||||
baseImage
|
||||
:
|
||||
<Avatar
|
||||
src={hit.image_url}
|
||||
variant="rounded"
|
||||
/>
|
||||
|
||||
//console.log(hit)
|
||||
if (hit.categories !== undefined && hit.categories !== null && hit.categories.length > 0) {
|
||||
secondaryText = hit.categories.slice(0,3).map((data, index) => {
|
||||
if (index === 0) {
|
||||
return data
|
||||
}
|
||||
|
||||
return ", "+data
|
||||
|
||||
/*
|
||||
<Chip
|
||||
key={index}
|
||||
style={chipStyle}
|
||||
label={data}
|
||||
onClick={() => {
|
||||
//handleChipClick
|
||||
}}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
/>
|
||||
*/
|
||||
})
|
||||
}
|
||||
|
||||
var parsedUrl = isCloud ? `/apps/${hit.objectID}` : `https://shuffler.io/apps/${hit.objectID}`
|
||||
parsedUrl += `?queryID=${hit.__queryID}`
|
||||
|
||||
return (
|
||||
<Link key={hit.objectID} to={{ pathname: parsedUrl }} style={{textDecoration: "none", color: "white",}} onClick={(event) => {
|
||||
console.log("CLICK")
|
||||
setSearchOpen(true)
|
||||
|
||||
aa('init', {
|
||||
appId: searchClient.appId,
|
||||
apiKey: searchClient.transporter.queryParameters["x-algolia-api-key"]
|
||||
})
|
||||
|
||||
const timestamp = new Date().getTime()
|
||||
aa('sendEvents', [
|
||||
{
|
||||
eventType: 'click',
|
||||
eventName: 'App Clicked',
|
||||
index: 'appsearch',
|
||||
objectIDs: [hit.objectID],
|
||||
timestamp: timestamp,
|
||||
queryID: hit.__queryID,
|
||||
positions: [hit.__position],
|
||||
userToken: userdata === undefined || userdata === null || userdata.id === undefined ? "unauthenticated" : userdata.id,
|
||||
}
|
||||
])
|
||||
|
||||
if (!isCloud) {
|
||||
event.preventDefault()
|
||||
window.open(parsedUrl, '_blank');
|
||||
}
|
||||
}}>
|
||||
<ListItem key={hit.objectID} style={innerlistitemStyle} onMouseOver={() => {
|
||||
setMouseHoverIndex(index)
|
||||
}}>
|
||||
<ListItemAvatar>
|
||||
{avatar}
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={name}
|
||||
secondary={secondaryText}
|
||||
/>
|
||||
{/*
|
||||
<ListItemSecondaryAction>
|
||||
<IconButton edge="end" aria-label="delete">
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</ListItemSecondaryAction>
|
||||
*/}
|
||||
</ListItem>
|
||||
</Link>
|
||||
)})
|
||||
}
|
||||
</List>
|
||||
<span style={{display: "flex", textAlign: "left", float: "left", position: "absolute", left: 15, bottom: 10, }}>
|
||||
<Link to="/search" style={{textDecoration: "none", color: "#f85a3e"}}>
|
||||
<Typography variant="body1" style={{}}>
|
||||
See more
|
||||
</Typography>
|
||||
</Link>
|
||||
</span>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const DocHits = ({ hits }) => {
|
||||
const [mouseHoverIndex, setMouseHoverIndex] = useState(0)
|
||||
|
||||
var tmp = searchOpen
|
||||
if (!searchOpen) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
const positionInfo = document.activeElement.getBoundingClientRect()
|
||||
const outerlistitemStyle = {
|
||||
width: "100%",
|
||||
overflowX: "hidden",
|
||||
overflowY: "hidden",
|
||||
borderBottom: "1px solid rgba(255,255,255,0.4)",
|
||||
}
|
||||
|
||||
if (hits.length > 4) {
|
||||
hits = hits.slice(0, 4)
|
||||
}
|
||||
|
||||
const type = "documentation"
|
||||
const baseImage = <LibraryBooksIcon />
|
||||
|
||||
//console.log(type, hits.length, hits)
|
||||
|
||||
return (
|
||||
<Card elevation={0} style={{position: "relative", marginLeft: 10, marginRight: 10, position: "absolute", color: "white", zIndex: 1002, backgroundColor: theme.palette.inputColor, width: 405, height: 408, left: 470, boxShadows: "none",}}>
|
||||
<IconButton style={{zIndex: 5000, position: "absolute", right: 14, color: "grey"}} onClick={() => {
|
||||
setSearchOpen(false)
|
||||
}}>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
<Typography variant="h6" style={{margin: "10px 10px 0px 20px", }}>
|
||||
Documentation
|
||||
</Typography>
|
||||
{/*
|
||||
<IconButton edge="end" aria-label="delete" style={{position: "absolute", top: 5, right: 15,}} onClick={() => {
|
||||
setSearchOpen(false)
|
||||
}}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
*/}
|
||||
<List style={{backgroundColor: theme.palette.inputColor, }}>
|
||||
{hits.length === 0 ?
|
||||
<ListItem style={outerlistitemStyle}>
|
||||
<ListItemAvatar onClick={() => console.log(hits)}>
|
||||
<Avatar>
|
||||
<FolderIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={"No documentation."}
|
||||
secondary={"Try a broader search term"}
|
||||
/>
|
||||
</ListItem>
|
||||
:
|
||||
hits.map((hit, index) => {
|
||||
const innerlistitemStyle = {
|
||||
width: positionInfo.width+35,
|
||||
overflowX: "hidden",
|
||||
overflowY: "hidden",
|
||||
borderBottom: "1px solid rgba(255,255,255,0.4)",
|
||||
backgroundColor: mouseHoverIndex === index ? "#1f2023" : "inherit",
|
||||
cursor: "pointer",
|
||||
marginLeft: 5,
|
||||
marginRight: 5,
|
||||
maxHeight: 75,
|
||||
minHeight: 75,
|
||||
maxWidth: 420,
|
||||
minWidth: "100%",
|
||||
}
|
||||
|
||||
var name = hit.name === undefined ?
|
||||
hit.filename.charAt(0).toUpperCase() + hit.filename.slice(1).replaceAll("_", " ") + " - " + hit.title
|
||||
:
|
||||
(hit.name.charAt(0).toUpperCase()+hit.name.slice(1)).replaceAll("_", " ")
|
||||
|
||||
if (name.length > 30) {
|
||||
name = name.slice(0, 30)+"..."
|
||||
}
|
||||
const secondaryText = hit.data !== undefined ? hit.data.slice(0, 40)+"..." : ""
|
||||
const avatar = hit.image_url === undefined ?
|
||||
baseImage
|
||||
:
|
||||
<Avatar
|
||||
src={hit.image_url}
|
||||
variant="rounded"
|
||||
/>
|
||||
|
||||
var parsedUrl = hit.urlpath !== undefined ? hit.urlpath : ""
|
||||
parsedUrl += `?queryID=${hit.__queryID}`
|
||||
if (parsedUrl.includes("/apps/")) {
|
||||
const extraHash = hit.url_hash === undefined ? "" : `#${hit.url_hash}`
|
||||
|
||||
parsedUrl = `/apps/${hit.filename}?tab=docs&queryID=${hit.__queryID}${extraHash}`
|
||||
}
|
||||
|
||||
return (
|
||||
<Link key={hit.objectID} to={parsedUrl} style={{textDecoration: "none", color: "white",}} onClick={(event) => {
|
||||
aa('init', {
|
||||
appId: searchClient.appId,
|
||||
apiKey: searchClient.transporter.queryParameters["x-algolia-api-key"]
|
||||
})
|
||||
|
||||
const timestamp = new Date().getTime()
|
||||
aa('sendEvents', [
|
||||
{
|
||||
eventType: 'click',
|
||||
eventName: 'Document Clicked',
|
||||
index: 'documentation',
|
||||
objectIDs: [hit.objectID],
|
||||
timestamp: timestamp,
|
||||
queryID: hit.__queryID,
|
||||
positions: [hit.__position],
|
||||
userToken: userdata === undefined || userdata === null || userdata.id === undefined ? "unauthenticated" : userdata.id,
|
||||
}
|
||||
])
|
||||
|
||||
console.log("CLICK")
|
||||
setSearchOpen(true)
|
||||
}}>
|
||||
<ListItem key={hit.objectID} style={innerlistitemStyle} onMouseOver={() => {
|
||||
setMouseHoverIndex(index)
|
||||
}}>
|
||||
<ListItemAvatar>
|
||||
{avatar}
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary={name}
|
||||
secondary={secondaryText}
|
||||
/>
|
||||
{/*
|
||||
<ListItemSecondaryAction>
|
||||
<IconButton edge="end" aria-label="delete">
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</ListItemSecondaryAction>
|
||||
*/}
|
||||
</ListItem>
|
||||
</Link>
|
||||
)})
|
||||
}
|
||||
</List>
|
||||
{type === "documentation" ?
|
||||
<span style={{display: "flex", textAlign: "right", position: "absolute", right: 15, bottom: 10,}}>
|
||||
<Typography variant="body2" style={{}}>
|
||||
Search by
|
||||
</Typography>
|
||||
<a rel="noopener noreferrer" href="https://www.algolia.com/" target="_blank" style={{textDecoration: "none", color: "white"}}>
|
||||
<img src={"/images/logo-algolia-nebula-blue-full.svg"} alt="Algolia logo" style={{height: 17, marginLeft: 5, marginTop: 3,}} />
|
||||
</a>
|
||||
</span>
|
||||
: null}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
const CustomSearchBox = connectSearchBox(SearchBox)
|
||||
const CustomAppHits = connectHits(AppHits)
|
||||
const CustomWorkflowHits = connectHits(WorkflowHits)
|
||||
const CustomDocHits = connectHits(DocHits)
|
||||
|
||||
return (
|
||||
<div ref={node} style={{width: "100%", maxWidth: 425, margin: "auto", position: "relative",}}>
|
||||
<InstantSearch searchClient={searchClient} indexName="appsearch" onClick={() => {
|
||||
console.log("CLICKED")
|
||||
}}>
|
||||
<Configure clickAnalytics />
|
||||
<CustomSearchBox />
|
||||
<Index indexName="appsearch">
|
||||
<CustomAppHits />
|
||||
</Index>
|
||||
<Index indexName="documentation">
|
||||
<CustomDocHits />
|
||||
</Index>
|
||||
<Index indexName="workflows">
|
||||
<CustomWorkflowHits />
|
||||
</Index>
|
||||
</InstantSearch>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default SearchField;
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, {useState } from 'react';
|
||||
import {isMobile} from "react-device-detect";
|
||||
import DetectionFramework, { usecases } from "../components/DetectionFramework.jsx";
|
||||
import AppFramework, { usecases } from "../components/AppFramework.jsx";
|
||||
import {Link} from 'react-router-dom';
|
||||
import ReactGA from 'react-ga';
|
||||
|
||||
@@ -56,6 +56,8 @@ export const securityFramework = [
|
||||
]
|
||||
|
||||
const LandingpageUsecases = (props) => {
|
||||
const { userdata } = props
|
||||
|
||||
const [selectedUsecase, setSelectedUsecase] = useState("Phishing")
|
||||
const usecasekeys = usecases === undefined || usecases === null ? [] : Object.keys(usecases)
|
||||
const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)"
|
||||
@@ -169,7 +171,12 @@ const LandingpageUsecases = (props) => {
|
||||
</div>
|
||||
{isMobile ? null :
|
||||
<div style={{marginLeft: 200, marginTop: 125, zIndex: 1000}}>
|
||||
<DetectionFramework showOptions={false} selectedOption={selectedUsecase} rolling={true} />
|
||||
<AppFramework
|
||||
userdata={userdata}
|
||||
showOptions={false}
|
||||
selectedOption={selectedUsecase}
|
||||
rolling={true}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
{isMobile ? null :
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,230 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import ReactGA from 'react-ga';
|
||||
import theme from '../theme';
|
||||
import PaperComponent from "../components/PaperComponent.jsx"
|
||||
import UsecaseSearch, { usecaseTypes } from "../components/UsecaseSearch.jsx"
|
||||
|
||||
import {
|
||||
Paper,
|
||||
Typography,
|
||||
Divider,
|
||||
IconButton,
|
||||
Badge,
|
||||
CircularProgress,
|
||||
Tooltip,
|
||||
Dialog,
|
||||
} from "@material-ui/core";
|
||||
|
||||
import {
|
||||
Close as CloseIcon,
|
||||
Delete as DeleteIcon,
|
||||
AutoFixHigh as AutoFixHighIcon,
|
||||
Done as DoneIcon,
|
||||
} from "@mui/icons-material";
|
||||
|
||||
const SuggestedWorkflows = (props) => {
|
||||
const { globalUrl, userdata, usecaseSuggestions, frameworkData, setUsecaseSuggestions, inputSearch, apps, } = props
|
||||
|
||||
const [usecaseSearch, setUsecaseSearch] = React.useState("")
|
||||
const [usecaseSearchType, setUsecaseSearchType] = React.useState("")
|
||||
const [finishedUsecases, setFinishedUsecases] = React.useState([])
|
||||
const [previousUsecase, setPreviousUsecase] = React.useState("")
|
||||
const [closeWindow, setCloseWindow] = React.useState(false)
|
||||
|
||||
const isCloud =
|
||||
window.location.host === "localhost:3002" ||
|
||||
window.location.host === "shuffler.io";
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (closeWindow === true) {
|
||||
console.log("WINDOW CLOSED")
|
||||
finishedUsecases.push(usecaseSearch)
|
||||
setFinishedUsecases(finishedUsecases)
|
||||
|
||||
setCloseWindow(false)
|
||||
}
|
||||
}, [closeWindow])
|
||||
|
||||
if (usecaseSuggestions === undefined || usecaseSuggestions.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (inputSearch !== previousUsecase) {
|
||||
setPreviousUsecase(inputSearch)
|
||||
setFinishedUsecases([])
|
||||
}
|
||||
|
||||
if (finishedUsecases.length === usecaseSuggestions.length) {
|
||||
console.log("Closing finished usecases 2")
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
//useEffect(() => {
|
||||
// //if (defaultSearch ===
|
||||
// //setFinishedUsecases(finishedUsecases)
|
||||
// console.log("Finished default usecase?", usecaseSearch)
|
||||
//}, [usecaseSearch])
|
||||
|
||||
const foundZindex = usecaseSearch.length > 0 && usecaseSearchType.length > 0 ? -1 : 12500
|
||||
|
||||
const IndividualUsecase = (props) => {
|
||||
const { usecase, index } = props
|
||||
const [hovering, setHovering] = React.useState(false)
|
||||
|
||||
const usecasename = usecase.name
|
||||
const bordercolor = usecase.color !== undefined ? usecase.color : "rgba(255,255,255,0.3)"
|
||||
|
||||
|
||||
const srcimage = usecase.items[0].app
|
||||
var dstimage = usecase.items[1].app
|
||||
if (usecase.items.length > 2) {
|
||||
dstimage = usecase.items[2].app
|
||||
}
|
||||
|
||||
const finished = finishedUsecases.includes(usecasename)
|
||||
const selectedIcon = finished ? <DoneIcon /> : <AutoFixHighIcon />
|
||||
|
||||
if (finished) {
|
||||
return null
|
||||
}
|
||||
|
||||
// Simple visual of the usecase
|
||||
return (
|
||||
<Tooltip
|
||||
title={`Try usecase "${usecasename}"`}
|
||||
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={() => {
|
||||
setHovering(true)
|
||||
}} onMouseOut={() => {
|
||||
setHovering(false)
|
||||
}} onClick={() => {
|
||||
if (isCloud) {
|
||||
ReactGA.event({
|
||||
category: "welcome",
|
||||
action: "click_suggested_workflow",
|
||||
label: usecasename,
|
||||
})
|
||||
}
|
||||
|
||||
console.log("Try usecase ", usecasename)
|
||||
setUsecaseSearchType(usecase.type)
|
||||
setUsecaseSearch(usecasename)
|
||||
|
||||
}}>
|
||||
<div style={{flex: 10}}>
|
||||
<Typography variant="body2">
|
||||
{usecasename}
|
||||
</Typography>
|
||||
<div style={{display: "flex", marginTop: 5, }}>
|
||||
<img alt={srcimage.large_image} src={srcimage.large_image} style={{borderRadius: 20, height: 30, width: 30, marginRight: 15, }}/>
|
||||
<img alt={dstimage.large_image} src={dstimage.large_image} style={{borderRadius: 20, height: 30, width: 30, }}/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div style={{flex: 1}}>
|
||||
{selectedIcon}
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
//<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,}}>
|
||||
<Dialog
|
||||
open={usecaseSearch.length > 0 && usecaseSearchType.length > 0}
|
||||
onClose={() => {
|
||||
finishedUsecases.push(usecaseSearch)
|
||||
setFinishedUsecases(finishedUsecases)
|
||||
|
||||
|
||||
|
||||
setUsecaseSearch("")
|
||||
setUsecaseSearchType("")
|
||||
}}
|
||||
PaperProps={{
|
||||
style: {
|
||||
pointerEvents: "auto",
|
||||
backgroundColor: theme.palette.surfaceColor,
|
||||
color: "white",
|
||||
minWidth: 450,
|
||||
padding: 50,
|
||||
overflow: "hidden",
|
||||
zIndex: 10012,
|
||||
border: theme.palette.defaultBorder,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<IconButton
|
||||
style={{
|
||||
zIndex: 5000,
|
||||
position: "absolute",
|
||||
top: 14,
|
||||
right: 18,
|
||||
color: "grey",
|
||||
}}
|
||||
onClick={() => {
|
||||
finishedUsecases.push(usecaseSearch)
|
||||
setFinishedUsecases(finishedUsecases)
|
||||
|
||||
setUsecaseSearch("")
|
||||
setUsecaseSearchType("")
|
||||
}}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
<UsecaseSearch
|
||||
globalUrl={globalUrl}
|
||||
defaultSearch={usecaseSearchType}
|
||||
usecaseSearch={usecaseSearch}
|
||||
appFramework={frameworkData}
|
||||
userdata={userdata}
|
||||
autotry={true}
|
||||
setCloseWindow={setCloseWindow}
|
||||
setUsecaseSearch={setUsecaseSearch}
|
||||
apps={apps}
|
||||
/>
|
||||
</Dialog>
|
||||
<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>
|
||||
<IconButton
|
||||
style={{
|
||||
zIndex: 5000,
|
||||
position: "absolute",
|
||||
top: 8,
|
||||
right: 8,
|
||||
color: "grey",
|
||||
padding: 2,
|
||||
}}
|
||||
onClick={() => {
|
||||
if (setUsecaseSuggestions !== undefined) {
|
||||
setUsecaseSuggestions([])
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CloseIcon style={{height: 18, width: 18, }} />
|
||||
</IconButton>
|
||||
{usecaseSuggestions.map((usecase, index) => {
|
||||
|
||||
return (
|
||||
<IndividualUsecase
|
||||
key={index}
|
||||
usecase={usecase}
|
||||
index={index}
|
||||
/>
|
||||
)
|
||||
|
||||
})}
|
||||
</div>
|
||||
</Paper>
|
||||
)
|
||||
}
|
||||
|
||||
export default SuggestedWorkflows;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,830 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import ReactGA from 'react-ga';
|
||||
import Button from "@material-ui/core/Button";
|
||||
import Checkbox from '@mui/material/Checkbox';
|
||||
|
||||
import AliceCarousel from 'react-alice-carousel';
|
||||
import 'react-alice-carousel/lib/alice-carousel.css';
|
||||
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import EmailIcon from '@mui/icons-material/Email';
|
||||
import NewReleasesIcon from '@mui/icons-material/NewReleases';
|
||||
import ExtensionIcon from '@mui/icons-material/Extension';
|
||||
import LightbulbIcon from '@mui/icons-material/Lightbulb';
|
||||
import ArrowBackIosNewIcon from '@mui/icons-material/ArrowBackIosNew';
|
||||
import ArrowForwardIosIcon from '@mui/icons-material/ArrowForwardIos';
|
||||
|
||||
import theme from '../theme';
|
||||
import {
|
||||
Fade,
|
||||
IconButton,
|
||||
FormGroup,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
FormLabel,
|
||||
FormControlLabel,
|
||||
Select,
|
||||
MenuItem,
|
||||
Grid,
|
||||
Paper,
|
||||
Typography,
|
||||
TextField,
|
||||
Zoom,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Divider,
|
||||
Tooltip,
|
||||
Chip,
|
||||
} from "@material-ui/core";
|
||||
import { useAlert } from "react-alert";
|
||||
|
||||
import { useNavigate, Link } from "react-router-dom";
|
||||
import WorkflowSearch from '../components/Workflowsearch.jsx';
|
||||
import AuthenticationItem from '../components/AuthenticationItem.jsx';
|
||||
import WorkflowPaper from "../components/WorkflowPaper.jsx"
|
||||
import UsecaseSearch from "../components/UsecaseSearch.jsx"
|
||||
|
||||
|
||||
const responsive = {
|
||||
0: { items: 1 },
|
||||
};
|
||||
|
||||
const WelcomeForm = (props) => {
|
||||
const { userdata, globalUrl, discoveryWrapper, setDiscoveryWrapper, appFramework, getFramework, activeStep, setActiveStep, steps, skipped, setSkipped, getApps, apps, handleSetSearch, usecaseButtons, defaultSearch, setDefaultSearch, selectionOpen, setSelectionOpen, } = props
|
||||
|
||||
const usecaseItems = [
|
||||
<div style={{minWidth: "95%", maxWidth: "95%", marginLeft: 5, marginRight: 5, }}>
|
||||
<UsecaseSearch
|
||||
globalUrl={globalUrl}
|
||||
defaultSearch={"Phishing"}
|
||||
appFramework={appFramework}
|
||||
apps={apps}
|
||||
getFramework={getFramework}
|
||||
userdata={userdata}
|
||||
/>
|
||||
</div>
|
||||
,
|
||||
<div style={{minWidth: "95%", maxWidth: "95%", marginLeft: 5, marginRight: 5, }}>
|
||||
<UsecaseSearch
|
||||
globalUrl={globalUrl}
|
||||
defaultSearch={"Enrichment"}
|
||||
appFramework={appFramework}
|
||||
apps={apps}
|
||||
getFramework={getFramework}
|
||||
userdata={userdata}
|
||||
/>
|
||||
</div>
|
||||
,
|
||||
<div style={{minWidth: "95%", maxWidth: "95%", marginLeft: 5, marginRight: 5, }}>
|
||||
<UsecaseSearch
|
||||
globalUrl={globalUrl}
|
||||
defaultSearch={"Enrichment"}
|
||||
usecaseSearch={"SIEM alert enrichment"}
|
||||
appFramework={appFramework}
|
||||
apps={apps}
|
||||
getFramework={getFramework}
|
||||
userdata={userdata}
|
||||
/>
|
||||
</div>
|
||||
,
|
||||
<div style={{minWidth: "95%", maxWidth: "95%", marginLeft: 5, marginRight: 5, }}>
|
||||
<UsecaseSearch
|
||||
globalUrl={globalUrl}
|
||||
defaultSearch={"Build your own"}
|
||||
appFramework={appFramework}
|
||||
apps={apps}
|
||||
getFramework={getFramework}
|
||||
userdata={userdata}
|
||||
/>
|
||||
</div>
|
||||
]
|
||||
|
||||
const [discoveryData, setDiscoveryData] = React.useState({})
|
||||
const [name, setName] = React.useState("")
|
||||
const [orgName, setOrgName] = React.useState("")
|
||||
const [role, setRole] = React.useState("")
|
||||
const [orgType, setOrgType] = React.useState("")
|
||||
const [finishedApps, setFinishedApps] = React.useState([])
|
||||
const [authentication, setAuthentication] = React.useState([]);
|
||||
const [newSelectedApp, setNewSelectedApp] = React.useState({})
|
||||
const [thumbIndex, setThumbIndex] = useState(0);
|
||||
const [thumbAnimation, setThumbAnimation] = useState(false);
|
||||
const [clickdiff, setclickdiff] = useState(0);
|
||||
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
|
||||
|
||||
const alert = useAlert();
|
||||
let navigate = useNavigate();
|
||||
|
||||
const onNodeSelect = (label) => {
|
||||
if (setDiscoveryWrapper !== undefined) {
|
||||
setDiscoveryWrapper(
|
||||
{"id": label}
|
||||
)
|
||||
}
|
||||
|
||||
setSelectionOpen(true)
|
||||
setDefaultSearch(label)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (userdata.id === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
if (userdata.name !== undefined && userdata.name !== null && userdata.name.length > 0) {
|
||||
setName(userdata.name)
|
||||
}
|
||||
|
||||
if (userdata.active_org !== undefined && userdata.active_org.name !== undefined && userdata.active_org.name !== null && userdata.active_org.name.length > 0) {
|
||||
setOrgName(userdata.active_org.name)
|
||||
}
|
||||
}, [userdata])
|
||||
|
||||
useEffect(() => {
|
||||
if (discoveryWrapper === undefined || discoveryWrapper.id === undefined) {
|
||||
setDefaultSearch("")
|
||||
var newfinishedApps = finishedApps
|
||||
newfinishedApps.push(defaultSearch)
|
||||
setFinishedApps(finishedApps)
|
||||
}
|
||||
}, [discoveryWrapper])
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
window.location.search !== undefined &&
|
||||
window.location.search !== null
|
||||
) {
|
||||
const urlSearchParams = new URLSearchParams(window.location.search);
|
||||
const params = Object.fromEntries(urlSearchParams.entries());
|
||||
const foundTab = params["tab"];
|
||||
if (foundTab !== null && foundTab !== undefined && !isNaN(foundTab)) {
|
||||
if (foundTab === 3 || foundTab === "3") {
|
||||
//console.log("Set search!")
|
||||
}
|
||||
} else {
|
||||
//navigate(`/welcome?tab=1`)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const isStepOptional = step => {
|
||||
return step === 1
|
||||
}
|
||||
|
||||
const sendUserUpdate = (name, role, userId) => {
|
||||
const data = {
|
||||
"tutorial": "welcome",
|
||||
"firstname": name,
|
||||
"company_role": role,
|
||||
"user_id": userId,
|
||||
}
|
||||
|
||||
const url = `${globalUrl}/api/v1/users/updateuser`
|
||||
fetch(url, {
|
||||
mode: "cors",
|
||||
method: "PUT",
|
||||
body: JSON.stringify(data),
|
||||
credentials: "include",
|
||||
crossDomain: true,
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
})
|
||||
.then((response) =>
|
||||
response.json().then((responseJson) => {
|
||||
if (responseJson["success"] === false) {
|
||||
console.log("Update user success")
|
||||
//alert.error("Failed updating org: ", responseJson.reason);
|
||||
} else {
|
||||
console.log("Update success!")
|
||||
//alert.success("Successfully edited org!");
|
||||
}
|
||||
})
|
||||
)
|
||||
.catch((error) => {
|
||||
console.log("Update err: ", error.toString())
|
||||
//alert.error("Err: " + error.toString());
|
||||
});
|
||||
}
|
||||
|
||||
const sendOrgUpdate = (orgname, company_type, orgId, priority) => {
|
||||
var data = {
|
||||
org_id: orgId,
|
||||
};
|
||||
|
||||
if (orgname.length > 0) {
|
||||
data.name = orgname
|
||||
}
|
||||
|
||||
if (company_type.length > 0) {
|
||||
data.company_type = company_type
|
||||
}
|
||||
|
||||
if (priority.length > 0) {
|
||||
data.priority = priority
|
||||
}
|
||||
|
||||
const url = globalUrl + `/api/v1/orgs/${orgId}`;
|
||||
fetch(url, {
|
||||
mode: "cors",
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
credentials: "include",
|
||||
crossDomain: true,
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
"Content-Type": "application/json; charset=utf-8",
|
||||
},
|
||||
})
|
||||
.then((response) =>
|
||||
response.json().then((responseJson) => {
|
||||
if (responseJson["success"] === false) {
|
||||
console.log("Update of org failed")
|
||||
//alert.error("Failed updating org: ", responseJson.reason);
|
||||
} else {
|
||||
//alert.success("Successfully edited org!");
|
||||
}
|
||||
})
|
||||
)
|
||||
.catch((error) => {
|
||||
console.log("Update err: ", error.toString())
|
||||
//alert.error("Err: " + error.toString());
|
||||
});
|
||||
}
|
||||
|
||||
var workflowDelay = -50
|
||||
const NewHits = ({ hits }) => {
|
||||
const [mouseHoverIndex, setMouseHoverIndex] = useState(-1)
|
||||
var counted = 0
|
||||
|
||||
const paperAppContainer = {
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
alignContent: "space-between",
|
||||
marginTop: 5,
|
||||
}
|
||||
|
||||
return (
|
||||
<Grid container spacing={4} style={paperAppContainer}>
|
||||
{hits.map((data, index) => {
|
||||
workflowDelay += 50
|
||||
|
||||
if (index > 3) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<Zoom key={index} in={true} style={{ transitionDelay: `${workflowDelay}ms` }}>
|
||||
<Grid item xs={6} style={{ padding: "12px 10px 12px 10px" }}>
|
||||
<WorkflowPaper key={index} data={data} />
|
||||
</Grid>
|
||||
</Zoom>
|
||||
)
|
||||
})}
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
const isStepSkipped = step => {
|
||||
return skipped.has(step)
|
||||
}
|
||||
|
||||
const handleNext = () => {
|
||||
setDefaultSearch("")
|
||||
|
||||
if (activeStep === 0) {
|
||||
console.log("Should send basic information about org (fetch)")
|
||||
setclickdiff(240)
|
||||
navigate(`/welcome?tab=2`)
|
||||
|
||||
if (isCloud) {
|
||||
ReactGA.event({
|
||||
category: "welcome",
|
||||
action: "click_page_one_next",
|
||||
label: "",
|
||||
})
|
||||
}
|
||||
|
||||
if (userdata.active_org !== undefined && userdata.active_org.id !== undefined && userdata.active_org.id !== null && userdata.active_org.id.length > 0) {
|
||||
sendOrgUpdate(orgName, orgType, userdata.active_org.id, "")
|
||||
}
|
||||
|
||||
if (userdata.id !== undefined && userdata.id !== null && userdata.id.length > 0) {
|
||||
sendUserUpdate(name, role, userdata.id)
|
||||
}
|
||||
|
||||
} else if (activeStep === 1) {
|
||||
console.log("Should send secondary info about apps and other things")
|
||||
setDiscoveryWrapper({})
|
||||
|
||||
navigate(`/welcome?tab=3`)
|
||||
//handleSetSearch("Enrichment", "2. Enrich")
|
||||
handleSetSearch(usecaseButtons[0].name, usecaseButtons[0].usecase)
|
||||
getApps()
|
||||
|
||||
// Make sure it's up to date
|
||||
if (getFramework !== undefined) {
|
||||
getFramework()
|
||||
}
|
||||
} else if (activeStep === 2) {
|
||||
console.log("Should send third page with workflows activated and the like")
|
||||
}
|
||||
|
||||
|
||||
let newSkipped = skipped;
|
||||
if (isStepSkipped(activeStep)) {
|
||||
newSkipped = new Set(newSkipped.values());
|
||||
newSkipped.delete(activeStep);
|
||||
}
|
||||
|
||||
setActiveStep(prevActiveStep => prevActiveStep + 1);
|
||||
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 handleSkip = () => {
|
||||
setclickdiff(240)
|
||||
if (!isStepOptional(activeStep)) {
|
||||
throw new Error("You can't skip a step that isn't optional.");
|
||||
}
|
||||
setActiveStep(prevActiveStep => prevActiveStep + 1);
|
||||
setSkipped(prevSkipped => {
|
||||
const newSkipped = new Set(prevSkipped.values());
|
||||
newSkipped.add(activeStep);
|
||||
return newSkipped;
|
||||
});
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setActiveStep(0);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
console.log("Selected app changed (effect)")
|
||||
}, [newSelectedApp])
|
||||
|
||||
//const buttonWidth = 145
|
||||
const buttonWidth = 450
|
||||
const buttonMargin = 10
|
||||
const sizing = 475
|
||||
const buttonStyle = {
|
||||
flex: 1,
|
||||
width: "100%",
|
||||
padding: 25,
|
||||
margin: buttonMargin,
|
||||
fontSize: 18,
|
||||
}
|
||||
|
||||
const slideNext = () => {
|
||||
if (!thumbAnimation && thumbIndex < usecaseItems.length - 1) {
|
||||
//handleSetSearch(usecaseButtons[0].name, usecaseButtons[0].usecase)
|
||||
setThumbIndex(thumbIndex + 1);
|
||||
} else if (!thumbAnimation && thumbIndex === usecaseItems.length - 1) {
|
||||
setThumbIndex(0)
|
||||
}
|
||||
};
|
||||
|
||||
const slidePrev = () => {
|
||||
if (!thumbAnimation && thumbIndex > 0) {
|
||||
setThumbIndex(thumbIndex - 1);
|
||||
} else if (!thumbAnimation && thumbIndex === 0) {
|
||||
setThumbIndex(usecaseItems.length-1)
|
||||
}
|
||||
};
|
||||
|
||||
const newButtonStyle = {
|
||||
padding: 22,
|
||||
flex: 1,
|
||||
margin: buttonMargin,
|
||||
minWidth: buttonWidth,
|
||||
maxWidth: buttonWidth,
|
||||
}
|
||||
|
||||
const getStepContent = (step) => {
|
||||
switch (step) {
|
||||
case 0:
|
||||
return (
|
||||
<Fade in={true}>
|
||||
<Grid container spacing={1} style={{margin: "auto", maxWidth: 500, minWidth: 500, minHeight: sizing, maxHeight: sizing, }}>
|
||||
{/*isCloud ? null :
|
||||
<Typography variant="body1" style={{marginLeft: 8, marginTop: 10, marginRight: 30, }} color="textSecondary">
|
||||
This data will be used within the product and NOT be shared unless <a href="https://shuffler.io/docs/organizations#cloud_synchronization" target="_blank" rel="norefferer" style={{color: "#f86a3e", textDecoration: "none"}}>cloud synchronization</a> is configured.
|
||||
</Typography>
|
||||
*/}
|
||||
<Typography variant="body1" style={{marginLeft: 8, marginTop: 10, marginRight: 30, }} color="textSecondary">
|
||||
In order to understand how we best can help you find relevant Usecases, please provide the information below. This is optional, but highly encouraged.
|
||||
</Typography>
|
||||
<Grid item xs={11} style={{marginTop: 16, padding: 0,}}>
|
||||
<TextField
|
||||
required
|
||||
style={{width: "100%", marginTop: 0,}}
|
||||
placeholder="Name"
|
||||
autoFocus
|
||||
label="Name"
|
||||
type="name"
|
||||
id="standard-required"
|
||||
autoComplete="name"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
value={name}
|
||||
onChange={(e) => {
|
||||
setName(e.target.value)
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={11} style={{marginTop: 10, padding: 0,}}>
|
||||
<TextField
|
||||
required
|
||||
style={{width: "100%", marginTop: 0,}}
|
||||
placeholder="Company / Institution"
|
||||
label="Company Name"
|
||||
type="companyname"
|
||||
id="standard-required"
|
||||
autoComplete="CompanyName"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
value={orgName}
|
||||
onChange={(e) => {
|
||||
setOrgName(e.target.value)
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={11} style={{marginTop: 10}}>
|
||||
<FormControl fullWidth={true}>
|
||||
<InputLabel style={{marginLeft: 10, color: "#B9B9BA" }}>Your Role</InputLabel>
|
||||
<Select
|
||||
variant="outlined"
|
||||
required
|
||||
onChange={(e) => {
|
||||
setRole(e.target.value)
|
||||
}}
|
||||
>
|
||||
<MenuItem value={"Student"}>Student</MenuItem>
|
||||
<MenuItem value={"Security Analyst/Engineer"}>Security Analyst/Engineer</MenuItem>
|
||||
<MenuItem value={"SOC Manager"}>SOC Manager</MenuItem>
|
||||
<MenuItem value={"C-Level"}>C-Level</MenuItem>
|
||||
<MenuItem value={"Other"}>Other</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
<Grid item xs={11} style={{marginTop: 16}}>
|
||||
<FormControl fullWidth={true}>
|
||||
<InputLabel style={{ marginLeft: 10, color: "#B9B9BA" }}>Company Type</InputLabel>
|
||||
<Select
|
||||
required
|
||||
variant="outlined"
|
||||
onChange={(e) => {
|
||||
setOrgType(e.target.value)
|
||||
}}
|
||||
>
|
||||
<MenuItem value={"Education"}>Education</MenuItem>
|
||||
<MenuItem value={"MSSP"}>MSSP</MenuItem>
|
||||
<MenuItem value={"Security Product Company"}>Security Product Company</MenuItem>
|
||||
<MenuItem value={"Other"}>Other</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Fade>
|
||||
)
|
||||
case 1:
|
||||
return (
|
||||
<Fade in={true}>
|
||||
<div style={{minHeight: sizing, maxHeight: sizing, marginTop: 20, maxWidth: 500, }}>
|
||||
<Typography variant="body1" style={{marginLeft: 8, marginTop: 25, marginRight: 30, marginBottom: 0, }} color="textSecondary">
|
||||
Clicks the buttons below to find your apps, then we will help you find relevant workflows. Can't find your app? <span style={{color: "#f86a3e", cursor: "pointer"}} onClick={() => {
|
||||
if (window.drift !== undefined) {
|
||||
window.drift.api.startInteraction({ interactionId: 340043 })
|
||||
} else {
|
||||
console.log("Couldn't find drift in window.drift and not .drift-open-chat with querySelector: ", window.drift)
|
||||
}
|
||||
}}>Contact our App Developers!</span>
|
||||
</Typography>
|
||||
{/*The app framework helps us access and authenticate the most important APIs for you. */}
|
||||
|
||||
{/*
|
||||
<Grid item xs={10}>
|
||||
<FormControl fullWidth={true}>
|
||||
<InputLabel style={{ color: "#B9B9BA" }}>What is your development experience?</InputLabel>
|
||||
<Select
|
||||
required
|
||||
>
|
||||
<MenuItem value={10}>Beginner</MenuItem>
|
||||
<MenuItem value={20}>Intermediate</MenuItem>
|
||||
<MenuItem value={30}>Automation Ninja</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
*/}
|
||||
<Grid item xs={11} style={{marginTop: 25, }}>
|
||||
{/*<FormLabel style={{ color: "#B9B9BA" }}>Find your integrations!</FormLabel>*/}
|
||||
<div style={{display: "flex"}}>
|
||||
<Button disabled={finishedApps.includes("CASES")} variant={defaultSearch === "CASES" ? "contained" : "outlined"} style={buttonStyle} startIcon={<LightbulbIcon />} onClick={(event) => { onNodeSelect("CASES") }} >
|
||||
Case Management
|
||||
</Button>
|
||||
</div>
|
||||
<div style={{display: "flex"}}>
|
||||
<Button disabled={finishedApps.includes("SIEM")} variant={defaultSearch === "SIEM" ? "contained" : "outlined"} style={buttonStyle} startIcon={<SearchIcon />} onClick={(event) => { onNodeSelect("SIEM") }} >
|
||||
SIEM
|
||||
</Button>
|
||||
<Button disabled={finishedApps.includes("EDR & AV") || finishedApps.includes("ERADICATION")} variant={defaultSearch === "Eradication" ? "contained" : "outlined"} style={buttonStyle} startIcon={<NewReleasesIcon />} onClick={(event) => { onNodeSelect("ERADICATION") }} >
|
||||
Endpoint
|
||||
</Button>
|
||||
</div>
|
||||
<div style={{display: "flex"}}>
|
||||
<Button disabled={finishedApps.includes("INTEL")} variant={defaultSearch === "INTEL" ? "contained" : "outlined"} style={buttonStyle} startIcon={<ExtensionIcon />} onClick={(event) => { onNodeSelect("INTEL") }} >
|
||||
|
||||
|
||||
Intel
|
||||
</Button>
|
||||
<Button disabled={finishedApps.includes("COMMS") || finishedApps.includes("EMAIL")} variant={defaultSearch === "EMAIL" ? "contained" : "outlined"} style={buttonStyle} startIcon={<EmailIcon />} onClick={(event) => { onNodeSelect("EMAIL") }} >
|
||||
Email
|
||||
</Button>
|
||||
</div>
|
||||
{/* <FormControl>
|
||||
<FormLabel style={{ color: "#B9B9BA" }}>What do you want to automate first ?</FormLabel>
|
||||
<FormGroup>
|
||||
<FormControlLabel
|
||||
value="Email"
|
||||
control={<Checkbox style={{ color: "#F85A3E" }} onChange={(event) => { onNodeSelect("Email") }} />}
|
||||
label="Email"
|
||||
labelPlacement="Email"
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="SIEM"
|
||||
control={<Checkbox style={{ color: "#F85A3E" }} onChange={(event) => { onNodeSelect("SIEM") }} />}
|
||||
label="SIEM"
|
||||
labelPlacement="SIEM"
|
||||
/>
|
||||
<FormControlLabel
|
||||
value="EDR"
|
||||
control={<Checkbox style={{ color: "#F85A3E" }} onChange={(event) => { onNodeSelect("EDR") }} />}
|
||||
label="EDR"
|
||||
labelPlacement="EDR"
|
||||
/>
|
||||
</FormGroup>
|
||||
</FormControl> */}
|
||||
</Grid>
|
||||
{/*
|
||||
<Grid item xs={10} paddingBottom="20px">
|
||||
<FormControl fullWidth={true}>
|
||||
<InputLabel style={{ color: "#B9B9BA" }}>What tools do you use?</InputLabel>
|
||||
<Select
|
||||
required
|
||||
>
|
||||
<MenuItem value={10}>Email</MenuItem>
|
||||
<MenuItem value={20}>SIEM</MenuItem>
|
||||
<MenuItem value={30}>EDR</MenuItem>
|
||||
<MenuItem value={30}>Chat System</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Grid>
|
||||
*/}
|
||||
</div>
|
||||
</Fade>
|
||||
)
|
||||
case 2:
|
||||
return (
|
||||
<Fade in={true}>
|
||||
<div style={{marginTop: 0, maxWidth: 700, minWidth: 700, margin: "auto", minHeight: sizing, maxHeight: sizing, }}>
|
||||
<Typography variant="body1" style={{marginTop: 15, marginBottom: 0, maxWidth: 500, margin: "auto", marginBottom: 15, }} color="textSecondary">
|
||||
These are some of our Workflow templates, used to start new Workflows. Use the right and left buttons to find <a href="/usecases" target="_blank" rel="norefferer" style={{color: "#f86a3e", textDecoration: "none", }}>new Usecases</a>, and click the orange button to build it.
|
||||
</Typography>
|
||||
{/*<Divider />*/}
|
||||
{/*
|
||||
<div style={{width: 475, margin: "auto",}}>
|
||||
{usecaseButtons.map((usecase, index) => {
|
||||
|
||||
return (
|
||||
<Chip
|
||||
key={usecase.name}
|
||||
style={{
|
||||
backgroundColor: defaultSearch === usecase.name ? usecase.color : theme.palette.surfaceColor,
|
||||
marginRight: 10,
|
||||
paddingLeft: 5,
|
||||
paddingRight: 5,
|
||||
height: 28,
|
||||
cursor: "pointer",
|
||||
border: `1px solid ${usecase.color}`,
|
||||
color: "white",
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
}}
|
||||
label={`${index+1}. ${usecase.name}`}
|
||||
onClick={() => {
|
||||
console.log("Clicked: ", usecase.name)
|
||||
if (defaultSearch === usecase.name) {
|
||||
//setSelectedUsecaseCategory("")
|
||||
} else {
|
||||
handleSetSearch(usecase.name, usecase.usecase)
|
||||
}
|
||||
//addFilter(usecase.name.slice(3,usecase.name.length))
|
||||
}}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
*/}
|
||||
<div style={{marginTop: 0, }}>
|
||||
{/*
|
||||
<UsecaseSearch
|
||||
globalUrl={globalUrl}
|
||||
defaultSearch={defaultSearch}
|
||||
appFramework={appFramework}
|
||||
apps={apps}
|
||||
/>
|
||||
*/}
|
||||
|
||||
<div className="thumbs" style={{display: "flex"}}>
|
||||
<Tooltip title={"Previous usecase"}>
|
||||
<IconButton
|
||||
style={{
|
||||
backgroundColor: thumbIndex === 0 ? "inherit" : "white",
|
||||
zIndex: 5000,
|
||||
minHeight: 50,
|
||||
maxHeight: 50,
|
||||
color: "grey",
|
||||
marginTop: 150,
|
||||
borderRadius: 50,
|
||||
border: "1px solid rgba(255,255,255,0.3)",
|
||||
}}
|
||||
onClick={() => {
|
||||
slidePrev()
|
||||
}}
|
||||
>
|
||||
<ArrowBackIosNewIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<div style={{minWidth: 554, maxWidth: 554, borderRadius: theme.palette.borderRadius, padding: 25, }}>
|
||||
<AliceCarousel
|
||||
style={{ backgroundColor: theme.palette.surfaceColor, minHeight: 750, maxHeight: 750, }}
|
||||
items={usecaseItems}
|
||||
activeIndex={thumbIndex}
|
||||
infiniteLoop
|
||||
mouseTracking
|
||||
responsive={responsive}
|
||||
// activeIndex={activeIndex}
|
||||
controlsStrategy="responsive"
|
||||
autoPlay={false}
|
||||
infinite={true}
|
||||
animationType="fadeout"
|
||||
animationDuration={800}
|
||||
disableButtonsControls
|
||||
disableDotsControls
|
||||
|
||||
/>
|
||||
</div>
|
||||
<Tooltip title={"Next usecase"}>
|
||||
<IconButton
|
||||
style={{
|
||||
backgroundColor: thumbIndex === usecaseButtons.length-1 ? "inherit" : "white",
|
||||
zIndex: 5000,
|
||||
minHeight: 50,
|
||||
maxHeight: 50,
|
||||
color: "grey",
|
||||
marginTop: 150,
|
||||
borderRadius: 50,
|
||||
border: "1px solid rgba(255,255,255,0.3)",
|
||||
}}
|
||||
onClick={() => {
|
||||
slideNext()
|
||||
}}
|
||||
>
|
||||
<ArrowForwardIosIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Fade>
|
||||
)
|
||||
default:
|
||||
return "unknown step"
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{}}>
|
||||
{/*selectionOpen ?
|
||||
<WorkflowSearch
|
||||
defaultSearch={defaultSearch}
|
||||
newSelectedApp={newSelectedApp}
|
||||
setNewSelectedApp={setNewSelectedApp}
|
||||
/>
|
||||
: null*/}
|
||||
<div>
|
||||
{activeStep === steps.length ? (
|
||||
<div paddingTop="20px">
|
||||
You Will be Redirected to getting Start Page Wait for 5-sec.
|
||||
<Button onClick={handleReset}>Reset</Button>
|
||||
<script>
|
||||
setTimeout(function() {
|
||||
navigate("/workflows")
|
||||
}, 5000);
|
||||
</script>
|
||||
<Button>
|
||||
<Link style={{color: "#f86a3e", }} to="/workflows" className="btn btn-primary">
|
||||
Getting Started
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{getStepContent(activeStep)}
|
||||
<div style={{marginBottom: 20, }}/>
|
||||
{activeStep === 2 || activeStep === 1 ?
|
||||
<div style={{margin: "auto", minWidth: 500, maxWidth: 500, position: "relative", }}>
|
||||
<Button
|
||||
disabled={activeStep === 0}
|
||||
onClick={handleBack}
|
||||
variant={"outlined"}
|
||||
style={{marginLeft: 10, height: 64, width: 100, position: "absolute", top: activeStep === 1 ? -600 : -577, left: activeStep === 1 ? 105 : -145+clickdiff, }}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
variant={"outlined"}
|
||||
color="primary"
|
||||
onClick={handleNext}
|
||||
style={{marginLeft: 10, height: 64, width: 100, position: "absolute", top: activeStep === 1 ? -600: -577, left: activeStep === 1 ? 748 : 510+clickdiff, }}
|
||||
disabled={activeStep === 0 ? orgName.length === 0 || name.length === 0 : false}
|
||||
>
|
||||
{activeStep === steps.length - 1 ? "Finish" : "Next"}
|
||||
</Button>
|
||||
</div>
|
||||
:
|
||||
<div style={{margin: "auto", minWidth: 500, maxWidth: 500, marginLeft: activeStep === 1 ? 250 : "auto", marginTop: activeStep === 0 ? 25 : 0, }}>
|
||||
<Button disabled={activeStep === 0} onClick={handleBack}>
|
||||
Back
|
||||
</Button>
|
||||
{/*isStepOptional(activeStep) && (
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleSkip}
|
||||
>
|
||||
Skip
|
||||
</Button>
|
||||
)*/}
|
||||
<Button
|
||||
variant={activeStep === 1 ? finishedApps.length >= 4 ? "contained" : "outlined" : "outlined"}
|
||||
color="primary"
|
||||
onClick={handleNext}
|
||||
style={{marginLeft: 10, }}
|
||||
disabled={activeStep === 0 ? orgName.length === 0 || name.length === 0 : false}
|
||||
>
|
||||
{activeStep === steps.length - 1 ? "Finish" : "Next"}
|
||||
</Button>
|
||||
{activeStep === 0 ?
|
||||
<Button
|
||||
variant={"outlined"}
|
||||
color="secondary"
|
||||
onClick={() => {
|
||||
console.log("Skip!")
|
||||
|
||||
setclickdiff(240)
|
||||
if (isCloud) {
|
||||
ReactGA.event({
|
||||
category: "welcome",
|
||||
action: "click_page_one_skip",
|
||||
label: "",
|
||||
})
|
||||
}
|
||||
|
||||
setActiveStep(1)
|
||||
navigate(`/welcome?tab=2`)
|
||||
}}
|
||||
style={{marginLeft: 240, }}
|
||||
disabled={activeStep !== 0}
|
||||
>
|
||||
Skip
|
||||
</Button>
|
||||
: null}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default WelcomeForm
|
||||
@@ -0,0 +1,370 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
|
||||
import { useTheme } from '@material-ui/core/styles';
|
||||
import {Link} from 'react-router-dom';
|
||||
|
||||
import { Search as SearchIcon, CloudQueue as CloudQueueIcon, Code as CodeIcon } from '@material-ui/icons';
|
||||
|
||||
import algoliasearch from 'algoliasearch/lite';
|
||||
import { InstantSearch, Configure, connectSearchBox, connectHits } from 'react-instantsearch-dom';
|
||||
import {
|
||||
Grid,
|
||||
Paper,
|
||||
TextField,
|
||||
ButtonBase,
|
||||
InputAdornment,
|
||||
Typography,
|
||||
Button,
|
||||
Tooltip,
|
||||
Zoom,
|
||||
Chip,
|
||||
} from '@material-ui/core';
|
||||
|
||||
import WorkflowPaper from "../components/WorkflowPaper.jsx"
|
||||
import WorkflowPaperNew from "../components/WorkflowPaperNew.jsx"
|
||||
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240")
|
||||
const AppGrid = props => {
|
||||
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, alternativeView, } = props
|
||||
|
||||
const isCloud =
|
||||
window.location.host === "localhost:3002" ||
|
||||
window.location.host === "shuffler.io";
|
||||
|
||||
const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows
|
||||
const xs = parsedXs === undefined || parsedXs === null ? isMobile ? 6 : 4 : 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("");
|
||||
const [usecases, setUsecases] = React.useState([]);
|
||||
|
||||
const buttonStyle = {borderRadius: 30, height: 50, width: 220, margin: isMobile ? "15px auto 15px auto" : 20, fontSize: 18,}
|
||||
|
||||
const innerColor = "rgba(255,255,255,0.65)"
|
||||
const borderRadius = 3
|
||||
window.title = "Shuffle | Workflows | Discover your use-case"
|
||||
|
||||
const submitContact = (email, message) => {
|
||||
const data = {
|
||||
"firstname": "",
|
||||
"lastname": "",
|
||||
"title": "",
|
||||
"companyname": "",
|
||||
"email": email,
|
||||
"phone": "",
|
||||
"message": message,
|
||||
}
|
||||
|
||||
const errorMessage = "Something went wrong. Please contact frikky@shuffler.io directly."
|
||||
|
||||
fetch(globalUrl+"/api/v1/contact", {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(response => {
|
||||
if (response.success === true) {
|
||||
setFormMessage(response.reason)
|
||||
//alert.info("Thanks for submitting!")
|
||||
} else {
|
||||
setFormMessage(errorMessage)
|
||||
}
|
||||
|
||||
setFormMail("")
|
||||
setMessage("")
|
||||
})
|
||||
.catch(error => {
|
||||
setFormMessage(errorMessage)
|
||||
console.log(error)
|
||||
});
|
||||
}
|
||||
|
||||
const handleKeysetting = (categorydata, workflows) => {
|
||||
console.log("Workflows: ", workflows)
|
||||
//workflows[0].category = ["detect"]
|
||||
//workflows[0].usecase_ids = ["Correlate tickets"]
|
||||
|
||||
if (workflows !== undefined && workflows !== null) {
|
||||
const 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()) {
|
||||
console.log("Got match: ", workflow.usecase_ids[usecasekey])
|
||||
|
||||
category.matches.push({
|
||||
"workflow": workflow.id,
|
||||
"category": subcategory.name,
|
||||
})
|
||||
subcategory.matches.push(workflow.id)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (subcategory.matches.length > 0) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newcategories.push(category)
|
||||
}
|
||||
|
||||
console.log("Categories: ", newcategories)
|
||||
setUsecases(newcategories)
|
||||
} else {
|
||||
for (var key in categorydata) {
|
||||
categorydata[key].matches = []
|
||||
}
|
||||
setUsecases(categorydata)
|
||||
}
|
||||
}
|
||||
|
||||
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) => {
|
||||
if (responseJson.success !== false) {
|
||||
console.log("Usecases: ", responseJson)
|
||||
//handleKeysetting(responseJson, workflows)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
//alert.error("ERROR: " + error.toString());
|
||||
console.log("ERROR: " + error.toString());
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsecases()
|
||||
}, [])
|
||||
|
||||
// value={currentRefinement}
|
||||
const SearchBox = ({currentRefinement, refine, isSearchStalled} ) => {
|
||||
useEffect(() => {
|
||||
if (window !== undefined && window.location !== undefined && window.location.search !== undefined && window.location.search !== null) {
|
||||
const urlSearchParams = new URLSearchParams(window.location.search)
|
||||
const params = Object.fromEntries(urlSearchParams.entries())
|
||||
const foundQuery = params["q"]
|
||||
if (foundQuery !== null && foundQuery !== undefined) {
|
||||
console.log("Got query: ", foundQuery)
|
||||
refine(foundQuery)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<form noValidate action="" role="search">
|
||||
<TextField
|
||||
fullWidth
|
||||
style={{backgroundColor: theme.palette.inputColor, borderRadius: borderRadius, margin: 10, width: "100%",}}
|
||||
InputProps={{
|
||||
style:{
|
||||
color: "white",
|
||||
fontSize: "1em",
|
||||
height: 50,
|
||||
},
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon style={{marginLeft: 5}}/>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
autoComplete='off'
|
||||
type="search"
|
||||
color="primary"
|
||||
value={currentRefinement}
|
||||
placeholder="Find Workflows..."
|
||||
id="shuffle_search_field"
|
||||
onChange={(event) => {
|
||||
refine(event.currentTarget.value)
|
||||
}}
|
||||
limit={5}
|
||||
/>
|
||||
{/*isSearchStalled ? 'My search is stalled' : ''*/}
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
const paperAppContainer = {
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
alignContent: "space-between",
|
||||
marginTop: 5,
|
||||
}
|
||||
|
||||
var workflowDelay = -50
|
||||
const Hits = ({ hits }) => {
|
||||
const [mouseHoverIndex, setMouseHoverIndex] = useState(-1)
|
||||
var counted = 0
|
||||
|
||||
return (
|
||||
<Grid container spacing={4} style={paperAppContainer}>
|
||||
{hits.map((data, index) => {
|
||||
workflowDelay += 50
|
||||
|
||||
if (counted === 12/xs*rowHandler) {
|
||||
return null
|
||||
}
|
||||
|
||||
counted += 1
|
||||
|
||||
return (
|
||||
<Zoom key={index} in={true} style={{ transitionDelay: `${workflowDelay}ms` }}>
|
||||
<Grid item xs={xs} style={{ padding: "12px 10px 12px 10px" }}>
|
||||
{alternativeView === true ?
|
||||
<WorkflowPaperNew key={index} data={data} />
|
||||
:
|
||||
<WorkflowPaper key={index} data={data} />
|
||||
}
|
||||
</Grid>
|
||||
</Zoom>
|
||||
)
|
||||
})}
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
const CustomSearchBox = connectSearchBox(SearchBox)
|
||||
const CustomHits = connectHits(Hits)
|
||||
|
||||
return (
|
||||
<div style={{width: "100%", position: "relative", height: "100%",}}>
|
||||
<InstantSearch searchClient={searchClient} indexName="workflows">
|
||||
<Configure clickAnalytics />
|
||||
<div style={{maxWidth: 450, margin: "auto", marginTop: 15, marginBottom: 15, }}>
|
||||
<CustomSearchBox />
|
||||
</div>
|
||||
{usecases !== null && usecases !== undefined && usecases.length > 0 ?
|
||||
<div style={{ display: "flex", margin: "auto", width: 875,}}>
|
||||
{usecases.map((usecase, index) => {
|
||||
console.log(usecase)
|
||||
return (
|
||||
<Chip
|
||||
key={usecase.name}
|
||||
style={{
|
||||
backgroundColor: theme.palette.surfaceColor,
|
||||
marginRight: 10,
|
||||
paddingLeft: 5,
|
||||
paddingRight: 5,
|
||||
height: 28,
|
||||
cursor: "pointer",
|
||||
border: `1px solid ${usecase.color}`,
|
||||
color: "white",
|
||||
}}
|
||||
label={`${usecase.name} (${usecase.matches.length}/${usecase.list.length})`}
|
||||
onClick={() => {
|
||||
console.log("Clicked!")
|
||||
//addFilter(usecase.name.slice(3,usecase.name.length))
|
||||
}}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
: null}
|
||||
<CustomHits hitsPerPage={5}/>
|
||||
</InstantSearch>
|
||||
{showSuggestion === true ?
|
||||
<div style={{maxWidth: isMobile ? "100%" : "60%", margin: "auto", paddingTop: 0, textAlign: "center",}}>
|
||||
<Typography variant="h6" style={{color: "white", marginTop: 50,}}>
|
||||
Can't find what you're looking for?
|
||||
</Typography>
|
||||
<div style={{flex: "1", display: "flex", flexDirection: "row"}}>
|
||||
<TextField
|
||||
required
|
||||
style={{flex: "1", marginRight: "15px", backgroundColor: theme.palette.inputColor}}
|
||||
InputProps={{
|
||||
style:{
|
||||
color: "#ffffff",
|
||||
},
|
||||
}}
|
||||
color="primary"
|
||||
fullWidth={true}
|
||||
placeholder="Email (optional)"
|
||||
type="email"
|
||||
id="email-handler"
|
||||
autoComplete="email"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
onChange={e => setFormMail(e.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
required
|
||||
style={{flex: "1", backgroundColor: theme.palette.inputColor}}
|
||||
InputProps={{
|
||||
style:{
|
||||
color: "#ffffff",
|
||||
},
|
||||
}}
|
||||
color="primary"
|
||||
fullWidth={true}
|
||||
placeholder="What apps do you want to see?"
|
||||
type=""
|
||||
id="standard-required"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
autoComplete="off"
|
||||
onChange={e => setMessage(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
style={buttonStyle}
|
||||
disabled={message.length === 0}
|
||||
onClick={() => {
|
||||
submitContact(formMail, message)
|
||||
}}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
<Typography style={{color: "white"}} variant="body2">{formMessage}</Typography>
|
||||
</div>
|
||||
: null
|
||||
}
|
||||
|
||||
<span style={{position: "absolute", display: "flex", textAlign: "right", float: "right", right: 0, bottom: 120, }}>
|
||||
<Typography variant="body2" color="textSecondary" style={{}}>
|
||||
Search by
|
||||
</Typography>
|
||||
<a rel="noopener noreferrer" href="https://www.algolia.com/" target="_blank" style={{textDecoration: "none", color: "white"}}>
|
||||
<img src={"/images/logo-algolia-nebula-blue-full.svg"} alt="Algolia logo" style={{height: 17, marginLeft: 5, marginTop: 3,}} />
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AppGrid;
|
||||
@@ -0,0 +1,291 @@
|
||||
import React, { useState, useEffect, useLayoutEffect } from "react";
|
||||
import theme from '../theme';
|
||||
|
||||
import {
|
||||
Chip,
|
||||
Typography,
|
||||
Paper,
|
||||
Avatar,
|
||||
Grid,
|
||||
Tooltip,
|
||||
} from "@material-ui/core";
|
||||
|
||||
import {
|
||||
AvatarGroup,
|
||||
} from "@mui/material"
|
||||
|
||||
import {
|
||||
Restore as RestoreIcon,
|
||||
Edit as EditIcon,
|
||||
BubbleChart as BubbleChartIcon,
|
||||
MoreVert as MoreVertIcon,
|
||||
} from '@material-ui/icons';
|
||||
|
||||
import { useNavigate, Link, useParams } from "react-router-dom";
|
||||
|
||||
const workflowActionStyle = {
|
||||
display: "flex",
|
||||
width: 160,
|
||||
height: 44,
|
||||
justifyContent: "space-between",
|
||||
}
|
||||
|
||||
const paperAppStyle = {
|
||||
minHeight: 130,
|
||||
maxHeight: 130,
|
||||
overflow: "hidden",
|
||||
width: "100%",
|
||||
color: "white",
|
||||
backgroundColor: theme.palette.surfaceColor,
|
||||
padding: "12px 12px 0px 15px",
|
||||
borderRadius: 5,
|
||||
display: "flex",
|
||||
boxSizing: "border-box",
|
||||
position: "relative",
|
||||
}
|
||||
|
||||
const chipStyle = {
|
||||
backgroundColor: "#3d3f43",
|
||||
marginRight: 5,
|
||||
paddingLeft: 5,
|
||||
paddingRight: 5,
|
||||
height: 28,
|
||||
cursor: "pointer",
|
||||
borderColor: "#3d3f43",
|
||||
color: "white",
|
||||
}
|
||||
|
||||
const WorkflowPaper = (props) => {
|
||||
const { data } = props;
|
||||
let navigate = useNavigate();
|
||||
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [anchorEl, setAnchorEl] = React.useState(null);
|
||||
const appGroup = data.action_references === undefined || data.action_references === null ? [] : data.action_references
|
||||
|
||||
//console.log("Workflow: ", data)
|
||||
var boxColor = "#86c142";
|
||||
|
||||
var parsedName = data.name;
|
||||
if (
|
||||
parsedName !== undefined &&
|
||||
parsedName !== null &&
|
||||
parsedName.length > 20
|
||||
) {
|
||||
parsedName = parsedName.slice(0, 21) + "..";
|
||||
}
|
||||
|
||||
|
||||
const imageStyle = {
|
||||
width: 24,
|
||||
height: 24,
|
||||
marginRight: 10,
|
||||
border: "1px solid rgba(255,255,255,0.3)",
|
||||
}
|
||||
var image = data.creator_info !== undefined && data.creator_info !== null && data.creator_info.image !== undefined && data.creator_info.image !== null && data.creator_info.image.length > 0 ? <Avatar alt={data.creator} src={data.creator_info.image} style={imageStyle}/> : <Avatar alt={"shuffle_image"} src={theme.palette.defaultImage} style={imageStyle}/>
|
||||
const creatorname = data.creator_info !== undefined && data.creator_info !== null && data.creator_info.username !== undefined && data.creator_info.username !== null && data.creator_info.username.length > 0 ? data.creator_info.username : ""
|
||||
var orgName = "";
|
||||
var orgId = "";
|
||||
if ((data.objectID === undefined || data.objectID === null) && data.id !== undefined && data.id !== null) {
|
||||
data.objectID = data.id
|
||||
}
|
||||
|
||||
//console.log("IMG: ", data)
|
||||
var parsedUrl = `/workflows/${data.objectID}`
|
||||
if (data.__queryID !== undefined && data.__queryID !== null) {
|
||||
parsedUrl += `?queryID=${data.__queryID}`
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{width: "100%", position: "relative",}}>
|
||||
<Paper square style={paperAppStyle}>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 1,
|
||||
left: 1,
|
||||
height: 12,
|
||||
width: 12,
|
||||
backgroundColor: boxColor,
|
||||
borderRadius: "0 100px 0 0",
|
||||
}}
|
||||
/>
|
||||
<Grid
|
||||
item
|
||||
style={{ display: "flex", flexDirection: "column", width: "100%" }}
|
||||
>
|
||||
<Grid item style={{ display: "flex", maxHeight: 34 }}>
|
||||
<Tooltip title={`${creatorname}`} placement="bottom">
|
||||
<div
|
||||
style={{ cursor: data.creator_info !== undefined ? "pointer" : "inherit" }}
|
||||
onClick={() => {
|
||||
if (data.creator_info !== undefined) {
|
||||
navigate("/creators/"+data.creator_info.username)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{image}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip title={`Edit ${data.name}`} placement="bottom">
|
||||
<Typography
|
||||
variant="body1"
|
||||
style={{
|
||||
marginBottom: 0,
|
||||
paddingBottom: 0,
|
||||
maxHeight: 30,
|
||||
flex: 10,
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
to={parsedUrl}
|
||||
style={{ textDecoration: "none", color: "inherit" }}
|
||||
>
|
||||
{parsedName}
|
||||
</Link>
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
<Grid item style={workflowActionStyle}>
|
||||
{appGroup.length > 0 ?
|
||||
<div style={{display: "flex", marginTop: 8, }}>
|
||||
<AvatarGroup max={4} style={{marginLeft: 5, maxHeight: 24,}}>
|
||||
{appGroup.map((app, index) => {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
height: 24,
|
||||
width: 24,
|
||||
filter: "brightness(0.6)",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => {
|
||||
navigate("/apps/"+app.id)
|
||||
}}
|
||||
>
|
||||
<Tooltip color="primary" title={app.name} placement="bottom">
|
||||
<Avatar alt={app.name} src={app.image_url} style={{width: 24, height: 24}}/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</AvatarGroup>
|
||||
</div>
|
||||
:
|
||||
<Tooltip color="primary" title="Action amount" placement="bottom">
|
||||
<span style={{ color: "#979797", display: "flex" }}>
|
||||
<BubbleChartIcon
|
||||
style={{ marginTop: "auto", marginBottom: "auto" }}
|
||||
/>
|
||||
<Typography
|
||||
style={{
|
||||
marginLeft: 5,
|
||||
marginTop: "auto",
|
||||
marginBottom: "auto",
|
||||
}}
|
||||
>
|
||||
{data.actions === undefined || data.actions === null ? 1 : data.actions.length}
|
||||
</Typography>
|
||||
</span>
|
||||
</Tooltip>
|
||||
}
|
||||
<Tooltip
|
||||
color="primary"
|
||||
title="Trigger amount"
|
||||
placement="bottom"
|
||||
>
|
||||
<span
|
||||
style={{ marginLeft: 15, color: "#979797", display: "flex" }}
|
||||
>
|
||||
<RestoreIcon
|
||||
style={{
|
||||
color: "#979797",
|
||||
marginTop: "auto",
|
||||
marginBottom: "auto",
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
style={{
|
||||
marginLeft: 5,
|
||||
marginTop: "auto",
|
||||
marginBottom: "auto",
|
||||
}}
|
||||
>
|
||||
{data.triggers === undefined || data.triggers === null ? 1 : data.triggers.length}
|
||||
</Typography>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip color="primary" title="Subflows used" placement="bottom">
|
||||
<span
|
||||
style={{
|
||||
marginLeft: 15,
|
||||
display: "flex",
|
||||
color: "#979797",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => {
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style={{
|
||||
color: "#979797",
|
||||
marginTop: "auto",
|
||||
marginBottom: "auto",
|
||||
}}
|
||||
>
|
||||
<path
|
||||
d="M0 0H15V15H0V0ZM16 16H18V18H16V16ZM16 13H18V15H16V13ZM16 10H18V12H16V10ZM16 7H18V9H16V7ZM16 4H18V6H16V4ZM13 16H15V18H13V16ZM10 16H12V18H10V16ZM7 16H9V18H7V16ZM4 16H6V18H4V16Z"
|
||||
fill="#979797"
|
||||
/>
|
||||
</svg>
|
||||
<Typography
|
||||
style={{
|
||||
marginLeft: 5,
|
||||
marginTop: "auto",
|
||||
marginBottom: "auto",
|
||||
}}
|
||||
>
|
||||
{0}
|
||||
</Typography>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
<Grid
|
||||
item
|
||||
style={{
|
||||
justifyContent: "left",
|
||||
overflow: "hidden",
|
||||
marginTop: 5,
|
||||
}}
|
||||
>
|
||||
{data.tags !== undefined && data.tags !== null
|
||||
? data.tags.map((tag, index) => {
|
||||
if (index >= 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Chip
|
||||
key={index}
|
||||
style={chipStyle}
|
||||
label={tag}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
/>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Paper>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default WorkflowPaper
|
||||
@@ -63,6 +63,10 @@ const WorkflowPaper = (props) => {
|
||||
const [anchorEl, setAnchorEl] = React.useState(null);
|
||||
const appGroup = data.action_references === undefined || data.action_references === null ? [] : data.action_references
|
||||
|
||||
const isCloud =
|
||||
window.location.host === "localhost:3002" ||
|
||||
window.location.host === "shuffler.io";
|
||||
|
||||
//console.log("Workflow: ", data)
|
||||
var boxColor = "#86c142";
|
||||
|
||||
@@ -91,6 +95,14 @@ const WorkflowPaper = (props) => {
|
||||
}
|
||||
|
||||
//console.log("IMG: ", data)
|
||||
var parsedUrl = `/workflows/${data.objectID}`
|
||||
if (data.__queryID !== undefined && data.__queryID !== null) {
|
||||
parsedUrl += `?queryID=${data.__queryID}`
|
||||
}
|
||||
|
||||
if (!isCloud) {
|
||||
parsedUrl = `https://shuffler.io${parsedUrl}`
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{width: "100%", position: "relative",}}>
|
||||
@@ -133,12 +145,14 @@ const WorkflowPaper = (props) => {
|
||||
flex: 10,
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
to={"/workflows/" + data.objectID}
|
||||
<a
|
||||
href={parsedUrl}
|
||||
rel="norefferer"
|
||||
target="_blank"
|
||||
style={{ textDecoration: "none", color: "inherit" }}
|
||||
>
|
||||
{parsedName}
|
||||
</Link>
|
||||
</a>
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
|
||||
@@ -0,0 +1,332 @@
|
||||
import React, { useState, useEffect, useLayoutEffect } from "react";
|
||||
import theme from '../theme';
|
||||
|
||||
import {
|
||||
Chip,
|
||||
Typography,
|
||||
Paper,
|
||||
Avatar,
|
||||
Grid,
|
||||
Tooltip,
|
||||
Button,
|
||||
} from "@material-ui/core";
|
||||
|
||||
import {
|
||||
AvatarGroup,
|
||||
} from "@mui/material"
|
||||
|
||||
import {
|
||||
Restore as RestoreIcon,
|
||||
Edit as EditIcon,
|
||||
BubbleChart as BubbleChartIcon,
|
||||
MoreVert as MoreVertIcon,
|
||||
} from '@material-ui/icons';
|
||||
|
||||
import { useNavigate, Link, useParams } from "react-router-dom";
|
||||
|
||||
const workflowActionStyle = {
|
||||
display: "flex",
|
||||
width: 160,
|
||||
height: 44,
|
||||
justifyContent: "space-between",
|
||||
}
|
||||
|
||||
const paperAppStyle = {
|
||||
minHeight: 130,
|
||||
maxHeight: 130,
|
||||
overflow: "hidden",
|
||||
width: "100%",
|
||||
color: "white",
|
||||
backgroundColor: theme.palette.surfaceColor,
|
||||
padding: "12px 12px 0px 15px",
|
||||
borderRadius: 5,
|
||||
display: "flex",
|
||||
boxSizing: "border-box",
|
||||
position: "relative",
|
||||
}
|
||||
|
||||
const chipStyle = {
|
||||
backgroundColor: "#3d3f43",
|
||||
marginRight: 5,
|
||||
paddingLeft: 5,
|
||||
paddingRight: 5,
|
||||
height: 28,
|
||||
cursor: "pointer",
|
||||
borderColor: "#3d3f43",
|
||||
color: "white",
|
||||
}
|
||||
|
||||
const WorkflowPaper = (props) => {
|
||||
const { data } = props;
|
||||
let navigate = useNavigate();
|
||||
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [anchorEl, setAnchorEl] = React.useState(null);
|
||||
const appGroup = data.action_references === undefined || data.action_references === null ? [] : data.action_references
|
||||
|
||||
const activateWorkflow = (workflow) => {
|
||||
console.log("Should activate: ", workflow)
|
||||
|
||||
}
|
||||
|
||||
//console.log("Workflow: ", data)
|
||||
var boxColor = "#86c142";
|
||||
|
||||
var parsedName = data.name;
|
||||
if (
|
||||
parsedName !== undefined &&
|
||||
parsedName !== null &&
|
||||
parsedName.length > 35
|
||||
) {
|
||||
parsedName = parsedName.slice(0, 36) + "..";
|
||||
}
|
||||
|
||||
const imageStyle = {
|
||||
width: 28,
|
||||
height: 28,
|
||||
marginRight: 10,
|
||||
border: "1px solid rgba(255,255,255,0.3)",
|
||||
}
|
||||
|
||||
var image = data.creator_info !== undefined && data.creator_info !== null && data.creator_info.image !== undefined && data.creator_info.image !== null && data.creator_info.image.length > 0 ? <Avatar alt={data.creator} src={data.creator_info.image} style={imageStyle}/> : <Avatar alt={"shuffle_image"} src={theme.palette.defaultImage} style={imageStyle}/>
|
||||
const creatorname = data.creator_info !== undefined && data.creator_info !== null && data.creator_info.username !== undefined && data.creator_info.username !== null && data.creator_info.username.length > 0 ? data.creator_info.username : "Shuffle"
|
||||
var orgName = "";
|
||||
var orgId = "";
|
||||
if ((data.objectID === undefined || data.objectID === null) && data.id !== undefined && data.id !== null) {
|
||||
data.objectID = data.id
|
||||
}
|
||||
|
||||
//console.log("IMG: ", data)
|
||||
var parsedUrl = `/workflows/${data.objectID}`
|
||||
if (data.__queryID !== undefined && data.__queryID !== null) {
|
||||
parsedUrl += `?queryID=${data.__queryID}`
|
||||
}
|
||||
|
||||
const paperImgStyle = {
|
||||
height: 150,
|
||||
width: "100%",
|
||||
backgroundImage: "linear-gradient(to right, #f86a3e, #f34079)",
|
||||
color: "white",
|
||||
position: "relative",
|
||||
borderRadius: "10px 10px 0% 0%",
|
||||
}
|
||||
|
||||
const bgImage1 = "https://avatars.githubusercontent.com/u/5719530?v=4"
|
||||
const bgImage2 = "https://avatars.githubusercontent.com/u/5719530?v=4"
|
||||
const itemSize = 70
|
||||
|
||||
return (
|
||||
<div style={{width: "100%", position: "relative",}}>
|
||||
<div style={paperImgStyle}>
|
||||
<div style={{position: "absolute", left: 55, top: 42, height: itemSize, width: itemSize, }}>
|
||||
<img src={bgImage1} alt="Image alt" style={{overflow: "hidden", width: itemSize, height: itemSize, borderRadius: 50, border: "1px solid rgba(255,255,255,0.3)"}} />
|
||||
</div>
|
||||
<div style={{position: "absolute", left: 160, top: 42, height: itemSize, width: itemSize, }}>
|
||||
<img src={bgImage2} alt="Image alt" style={{overflow: "hidden", width: itemSize, height: itemSize, borderRadius: 50, border: "1px solid rgba(255,255,255,0.3)"}} />
|
||||
</div>
|
||||
</div>
|
||||
<Paper square style={paperAppStyle}>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 1,
|
||||
left: 1,
|
||||
height: 12,
|
||||
width: 12,
|
||||
backgroundColor: boxColor,
|
||||
borderRadius: "0 100px 0 0",
|
||||
}}
|
||||
/>
|
||||
<Grid
|
||||
item
|
||||
style={{ display: "flex", flexDirection: "column", width: "100%" }}
|
||||
>
|
||||
<Grid item style={{ display: "flex", maxHeight: 34 }}>
|
||||
<Tooltip title={`Released by ${creatorname}`} placement="bottom">
|
||||
<div
|
||||
style={{
|
||||
cursor: data.creator_info !== undefined ? "pointer" : "inherit",
|
||||
}}
|
||||
onClick={() => {
|
||||
if (data.creator_info !== undefined) {
|
||||
navigate("/creators/"+data.creator_info.username)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{image}
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip title={`See ${data.name}`} placement="bottom">
|
||||
<Typography
|
||||
variant="h6"
|
||||
style={{
|
||||
marginBottom: 0,
|
||||
paddingBottom: 0,
|
||||
maxHeight: 30,
|
||||
flex: 10,
|
||||
}}
|
||||
>
|
||||
<Link
|
||||
to={parsedUrl}
|
||||
style={{ textDecoration: "none", color: "inherit" }}
|
||||
>
|
||||
{parsedName}
|
||||
</Link>
|
||||
</Typography>
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
<Grid item style={workflowActionStyle}>
|
||||
{/*
|
||||
{appGroup.length > 0 ?
|
||||
<div style={{display: "flex", marginTop: 8, }}>
|
||||
<AvatarGroup max={4} style={{marginLeft: 5, maxHeight: 24,}}>
|
||||
{appGroup.map((app, index) => {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
height: 24,
|
||||
width: 24,
|
||||
filter: "brightness(0.6)",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => {
|
||||
navigate("/apps/"+app.id)
|
||||
}}
|
||||
>
|
||||
<Tooltip color="primary" title={app.name} placement="bottom">
|
||||
<Avatar alt={app.name} src={app.image_url} style={{width: 24, height: 24}}/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</AvatarGroup>
|
||||
</div>
|
||||
:
|
||||
<Tooltip color="primary" title="Action amount" placement="bottom">
|
||||
<span style={{ color: "#979797", display: "flex" }}>
|
||||
<BubbleChartIcon
|
||||
style={{ marginTop: "auto", marginBottom: "auto" }}
|
||||
/>
|
||||
<Typography
|
||||
style={{
|
||||
marginLeft: 5,
|
||||
marginTop: "auto",
|
||||
marginBottom: "auto",
|
||||
}}
|
||||
>
|
||||
{data.actions === undefined || data.actions === null ? 1 : data.actions.length}
|
||||
</Typography>
|
||||
</span>
|
||||
</Tooltip>
|
||||
}
|
||||
*/}
|
||||
{/*
|
||||
<Tooltip
|
||||
color="primary"
|
||||
title="Trigger amount"
|
||||
placement="bottom"
|
||||
>
|
||||
<span
|
||||
style={{ marginLeft: 15, color: "#979797", display: "flex" }}
|
||||
>
|
||||
<RestoreIcon
|
||||
style={{
|
||||
color: "#979797",
|
||||
marginTop: "auto",
|
||||
marginBottom: "auto",
|
||||
}}
|
||||
/>
|
||||
<Typography
|
||||
style={{
|
||||
marginLeft: 5,
|
||||
marginTop: "auto",
|
||||
marginBottom: "auto",
|
||||
}}
|
||||
>
|
||||
{data.triggers === undefined || data.triggers === null ? 1 : data.triggers.length}
|
||||
</Typography>
|
||||
</span>
|
||||
</Tooltip>
|
||||
<Tooltip color="primary" title="Subflows used" placement="bottom">
|
||||
<span
|
||||
style={{
|
||||
marginLeft: 15,
|
||||
display: "flex",
|
||||
color: "#979797",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => {
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 18 18"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
style={{
|
||||
color: "#979797",
|
||||
marginTop: "auto",
|
||||
marginBottom: "auto",
|
||||
}}
|
||||
>
|
||||
<path
|
||||
d="M0 0H15V15H0V0ZM16 16H18V18H16V16ZM16 13H18V15H16V13ZM16 10H18V12H16V10ZM16 7H18V9H16V7ZM16 4H18V6H16V4ZM13 16H15V18H13V16ZM10 16H12V18H10V16ZM7 16H9V18H7V16ZM4 16H6V18H4V16Z"
|
||||
fill="#979797"
|
||||
/>
|
||||
</svg>
|
||||
<Typography
|
||||
style={{
|
||||
marginLeft: 5,
|
||||
marginTop: "auto",
|
||||
marginBottom: "auto",
|
||||
}}
|
||||
>
|
||||
{0}
|
||||
</Typography>
|
||||
</span>
|
||||
</Tooltip>
|
||||
*/}
|
||||
</Grid>
|
||||
{/*
|
||||
<Grid
|
||||
item
|
||||
style={{
|
||||
justifyContent: "left",
|
||||
overflow: "hidden",
|
||||
marginTop: 5,
|
||||
}}
|
||||
>
|
||||
{data.tags !== undefined && data.tags !== null
|
||||
? data.tags.map((tag, index) => {
|
||||
if (index >= 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Chip
|
||||
key={index}
|
||||
style={chipStyle}
|
||||
label={tag}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
/>
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</Grid>
|
||||
*/}
|
||||
|
||||
<Button variant="outlined" style={{textDecoration: "none", borderRadius: 25,}} onClick={() => {
|
||||
activateWorkflow(data)
|
||||
}}>
|
||||
Try this workflow
|
||||
</Button>
|
||||
</Grid>
|
||||
</Paper>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default WorkflowPaper
|
||||
@@ -0,0 +1,216 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
import ReactGA from 'react-ga';
|
||||
import { useTheme } from '@material-ui/core/styles';
|
||||
import {Link} from 'react-router-dom';
|
||||
|
||||
import { Search as SearchIcon, CloudQueue as CloudQueueIcon, Code as CodeIcon } from '@material-ui/icons';
|
||||
|
||||
//import algoliasearch from 'algoliasearch/lite';
|
||||
import algoliasearch from 'algoliasearch';
|
||||
import { InstantSearch, connectSearchBox, connectHits } from 'react-instantsearch-dom';
|
||||
import { Grid, Paper, TextField, ButtonBase, InputAdornment, Typography, Button, Tooltip} from '@material-ui/core';
|
||||
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240")
|
||||
const WorkflowSearch = props => {
|
||||
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, newSelectedApp, setNewSelectedApp, defaultSearch, showSearch, ConfiguredHits, selectAble, } = props
|
||||
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("");
|
||||
const [selectedApp, setSelectedApp] = React.useState({});
|
||||
|
||||
const buttonStyle = {borderRadius: 30, height: 50, width: 220, margin: isMobile ? "15px auto 15px auto" : 20, fontSize: 18,}
|
||||
|
||||
const innerColor = "rgba(255,255,255,0.65)"
|
||||
const borderRadius = 3
|
||||
window.title = "Shuffle | Apps | Find and integration any app"
|
||||
|
||||
const submitContact = (email, message) => {
|
||||
const data = {
|
||||
"firstname": "",
|
||||
"lastname": "",
|
||||
"title": "",
|
||||
"companyname": "",
|
||||
"email": email,
|
||||
"phone": "",
|
||||
"message": message,
|
||||
}
|
||||
|
||||
const errorMessage = "Something went wrong. Please contact frikky@shuffler.io directly."
|
||||
|
||||
fetch(globalUrl+"/api/v1/contact", {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(response => {
|
||||
if (response.success === true) {
|
||||
setFormMessage(response.reason)
|
||||
//alert.info("Thanks for submitting!")
|
||||
} else {
|
||||
setFormMessage(errorMessage)
|
||||
}
|
||||
|
||||
setFormMail("")
|
||||
setMessage("")
|
||||
})
|
||||
.catch(error => {
|
||||
setFormMessage(errorMessage)
|
||||
console.log(error)
|
||||
});
|
||||
}
|
||||
|
||||
// value={currentRefinement}
|
||||
const SearchBox = ({currentRefinement, refine, isSearchStalled} ) => {
|
||||
useEffect(() => {
|
||||
//console.log("FIRST LOAD ONLY? RUN REFINEMENT: !", currentRefinement)
|
||||
if (defaultSearch !== undefined && defaultSearch !== null) {
|
||||
refine(defaultSearch)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<form noValidate action="" role="search">
|
||||
<TextField
|
||||
fullWidth
|
||||
style={{backgroundColor: theme.palette.inputColor, borderRadius: borderRadius, width: "100%",}}
|
||||
InputProps={{
|
||||
style:{
|
||||
color: "white",
|
||||
fontSize: "1em",
|
||||
height: 50,
|
||||
},
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon style={{marginLeft: 5}}/>
|
||||
</InputAdornment>
|
||||
),
|
||||
}}
|
||||
autoComplete='on'
|
||||
type="search"
|
||||
color="primary"
|
||||
defaultValue={defaultSearch}
|
||||
placeholder={`Find ${defaultSearch} Workflows...`}
|
||||
id="shuffle_workflow_search_field"
|
||||
onChange={(event) => {
|
||||
refine(event.currentTarget.value)
|
||||
}}
|
||||
limit={5}
|
||||
/>
|
||||
{/*isSearchStalled ? 'My search is stalled' : ''*/}
|
||||
</form>
|
||||
)
|
||||
//value={currentRefinement}
|
||||
}
|
||||
|
||||
if (selectAble === true) {
|
||||
console.log("Make it possible to select a Workflow!!")
|
||||
}
|
||||
|
||||
const Hits = ({ hits }) => {
|
||||
const [mouseHoverIndex, setMouseHoverIndex] = useState(-1)
|
||||
var counted = 0
|
||||
|
||||
return (
|
||||
<Grid container spacing={0} style={{border: "1px solid rgba(255,255,255,0.2)", maxHeight: 250, minHeight: 250, overflowY: "auto", overflowX: "hidden",}}>
|
||||
{hits.map((data, index) => {
|
||||
const paperStyle = {
|
||||
backgroundColor: index === mouseHoverIndex ? "rgba(255,255,255,0.8)" : theme.palette.inputColor,
|
||||
color: index === mouseHoverIndex ? theme.palette.inputColor : "rgba(255,255,255,0.8)",
|
||||
border: newSelectedApp.objectID !== data.objectID ? `1px solid rgba(255,255,255,0.2)` : "2px solid #f86a3e",
|
||||
textAlign: "left",
|
||||
padding: 10,
|
||||
cursor: "pointer",
|
||||
position: "relative",
|
||||
overflow: "hidden",
|
||||
width: "100%",
|
||||
}
|
||||
|
||||
if (counted === 12/xs*rowHandler) {
|
||||
return null
|
||||
}
|
||||
|
||||
counted += 1
|
||||
var parsedname = ""
|
||||
for (var key = 0; key < data.name.length; key++) {
|
||||
var character = data.name.charAt(key)
|
||||
if (character === character.toUpperCase()) {
|
||||
//console.log(data.name[key], data.name[key+1])
|
||||
if (data.name.charAt(key+1) !== undefined && data.name.charAt(key+1) === data.name.charAt(key+1).toUpperCase()) {
|
||||
} else {
|
||||
parsedname += " "
|
||||
}
|
||||
}
|
||||
|
||||
parsedname += character
|
||||
}
|
||||
|
||||
parsedname = (parsedname.charAt(0).toUpperCase()+parsedname.substring(1)).replaceAll("_", " ")
|
||||
|
||||
return (
|
||||
<Paper key={index} elevation={0} style={paperStyle} onMouseOver={() => {
|
||||
setMouseHoverIndex(index)
|
||||
/*
|
||||
ReactGA.event({
|
||||
category: "app_grid_view",
|
||||
action: `search_bar_click`,
|
||||
label: "",
|
||||
})
|
||||
*/
|
||||
}} onMouseOut={() => {
|
||||
setMouseHoverIndex(-1)
|
||||
}} onClick={() => {
|
||||
setNewSelectedApp(data)
|
||||
//if (data.objectID !== data.objectID) {
|
||||
//}
|
||||
|
||||
//ReactGA.event({
|
||||
// category: "app_search",
|
||||
// action: `app_${parsedname}_${data.id}_click`,
|
||||
// label: "",
|
||||
//})
|
||||
}}>
|
||||
<div style={{display: "flex"}}>
|
||||
{/*<img alt={data.name} src={data.image_url} style={{width: "100%", maxWidth: 30, minWidth: 30, minHeight: 30, maxHeight: 30, display: "block", }} />*/}
|
||||
<Typography variant="body1" style={{marginTop: 2, marginLeft: 10, }}>
|
||||
{parsedname}
|
||||
</Typography>
|
||||
</div>
|
||||
</Paper>
|
||||
)
|
||||
})}
|
||||
</Grid>
|
||||
)
|
||||
}
|
||||
|
||||
const InputHits = ConfiguredHits === undefined ? Hits : ConfiguredHits
|
||||
const CustomSearchBox = connectSearchBox(SearchBox)
|
||||
const CustomHits = connectHits(InputHits)
|
||||
|
||||
return (
|
||||
<div style={{width: "100%", textAlign: "center", position: "relative", height: "100%",}}>
|
||||
<InstantSearch searchClient={searchClient} indexName="workflows">
|
||||
{/* showSearch === false ? null :
|
||||
<div style={{maxWidth: 450, margin: "auto", }}>
|
||||
<CustomSearchBox />
|
||||
</div>
|
||||
*/}
|
||||
<div style={{maxWidth: 450, margin: "auto", }}>
|
||||
<CustomSearchBox />
|
||||
</div>
|
||||
<CustomHits hitsPerPage={5}/>
|
||||
</InstantSearch>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default WorkflowSearch;
|
||||
|
||||
@@ -57,6 +57,7 @@ const data = [
|
||||
padding: "0px",
|
||||
margin: "0px",
|
||||
"background-color": "data(backgroundcolor)",
|
||||
"background-image": "data(backgroundimage)",
|
||||
"border-color": "#ffffff",
|
||||
"text-margin-x": "0px",
|
||||
"z-index": 4999,
|
||||
@@ -163,8 +164,8 @@ const data = [
|
||||
selector: "node[?isSuggestion]",
|
||||
css: {
|
||||
shape: "ellipse",
|
||||
width: "30px",
|
||||
height: "30px",
|
||||
width: "50px",
|
||||
height: "50px",
|
||||
"z-index": "5002",
|
||||
"font-size": "0px",
|
||||
border: "1px solid rgba(255,255,255,0.9)",
|
||||
@@ -173,6 +174,15 @@ const data = [
|
||||
label: "data(label)",
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: "node[?canConnect]",
|
||||
css: {
|
||||
"border-color": "#f86a3e",
|
||||
"border-width": "10px",
|
||||
"z-index": "5002",
|
||||
"background-color": "#f86a3e",
|
||||
},
|
||||
},
|
||||
{
|
||||
selector: "node[?isDescriptor]",
|
||||
css: {
|
||||
|
||||
@@ -15,6 +15,7 @@ const theme = createMuiTheme({
|
||||
type: "dark",
|
||||
surfaceColor: "#27292d",
|
||||
inputColor: "#383B40",
|
||||
platformColor: "#1F2023",
|
||||
borderRadius: 5,
|
||||
defaultBorder: "1px solid rgba(255,255,255,0.3)",
|
||||
jsonTheme: "brewer",
|
||||
|
||||
+1247
-327
File diff suppressed because it is too large
Load Diff
+1904
-553
File diff suppressed because it is too large
Load Diff
+256
-130
@@ -34,8 +34,17 @@ import {
|
||||
AttachFile as AttachFileIcon,
|
||||
Apps as AppsIcon,
|
||||
ErrorOutline as ErrorOutlineIcon,
|
||||
AddAPhoto as AddAPhotoIcon,
|
||||
AddAPhotoOutlined as AddAPhotoOutlinedIcon,
|
||||
ZoomInOutlined as ZoomInOutlinedIcon,
|
||||
ZoomOutOutlined as ZoomOutOutlinedIcon,
|
||||
Loop as LoopIcon,
|
||||
} from "@material-ui/icons";
|
||||
|
||||
import {
|
||||
AddPhotoAlternate as AddPhotoAlternateIcon,
|
||||
} from '@mui/icons-material';
|
||||
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import YAML from "yaml";
|
||||
@@ -44,12 +53,6 @@ import { useAlert } from "react-alert";
|
||||
import words from "shellwords";
|
||||
|
||||
import AvatarEditor from "react-avatar-editor";
|
||||
import AddAPhotoIcon from "@material-ui/icons/AddAPhoto";
|
||||
import AddAPhotoOutlinedIcon from "@material-ui/icons/AddAPhotoOutlined";
|
||||
import ZoomInOutlinedIcon from "@material-ui/icons/ZoomInOutlined";
|
||||
import ZoomOutOutlinedIcon from "@material-ui/icons/ZoomOutOutlined";
|
||||
import LoopIcon from "@material-ui/icons/Loop";
|
||||
import AddPhotoAlternateIcon from "@material-ui/icons/AddPhotoAlternate";
|
||||
|
||||
const surfaceColor = "#27292D";
|
||||
const inputColor = "#383B40";
|
||||
@@ -289,8 +292,8 @@ const AppCreator = (defaultprops) => {
|
||||
type: "header",
|
||||
example: "",
|
||||
};
|
||||
const [extraAuth, setExtraAuth] = useState([]);
|
||||
|
||||
const [extraAuth, setExtraAuth] = useState([]);
|
||||
const [app, setApp] = useState({});
|
||||
const [appAuthentication, setAppAuthentication] = React.useState([]);
|
||||
const [selectedAction, setSelectedAction] = useState({});
|
||||
@@ -344,14 +347,14 @@ const AppCreator = (defaultprops) => {
|
||||
useEffect(() => {
|
||||
if (window.location.pathname.includes("apps/edit")) {
|
||||
setIsEditing(true);
|
||||
handleEditApp();
|
||||
handleEditApp(props.match.params.appid);
|
||||
} else {
|
||||
checkQuery();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleEditApp = () => {
|
||||
fetch(globalUrl + "/api/v1/apps/" + props.match.params.appid + "/config", {
|
||||
const handleEditApp = (appid) => {
|
||||
fetch(globalUrl + "/api/v1/apps/" + appid + "/config", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -389,7 +392,10 @@ const AppCreator = (defaultprops) => {
|
||||
setIsAppLoaded(true);
|
||||
return;
|
||||
}
|
||||
|
||||
//handleEditApp(urlParams.get("id"))
|
||||
|
||||
// THIS has to stay due to ID may not exist as normal app yet
|
||||
fetch(globalUrl + "/api/v1/get_openapi/" + urlParams.get("id"), {
|
||||
method: "GET",
|
||||
headers: {
|
||||
@@ -408,7 +414,7 @@ const AppCreator = (defaultprops) => {
|
||||
.then((responseJson) => {
|
||||
setIsAppLoaded(true);
|
||||
if (!responseJson.success) {
|
||||
alert.error("Failed to verify");
|
||||
alert.error("Failed to get app config. Do you have access?");
|
||||
} else {
|
||||
parseIncomingOpenapiData(responseJson);
|
||||
}
|
||||
@@ -670,7 +676,8 @@ const AppCreator = (defaultprops) => {
|
||||
if (newaction.url !== undefined && newaction.url !== null && newaction.url.includes("_shuffle_replace_")) {
|
||||
const regex = /_shuffle_replace_\d/i;
|
||||
//console.log("NEW: ",
|
||||
newaction.url = newaction.url.replace(regex, "")
|
||||
newaction.url = newaction.url.replaceAll(new RegExp(regex, 'g'), "")
|
||||
console.log("Replaced: ", newaction.url)
|
||||
}
|
||||
|
||||
// Finding category
|
||||
@@ -679,6 +686,12 @@ const AppCreator = (defaultprops) => {
|
||||
var categoryindex = -1;
|
||||
// Stupid way of finding a category/grouping
|
||||
for (var key in pathsplit) {
|
||||
if (pathsplit[key].includes("_shuffle_replace_")) {
|
||||
const regex = /_shuffle_replace_\d/i;
|
||||
//console.log("NEW: ",
|
||||
pathsplit[key] = pathsplit[key].replaceAll(new RegExp(regex, 'g'), "")
|
||||
}
|
||||
|
||||
if (
|
||||
pathsplit[key].length > 0 &&
|
||||
pathsplit[key] !== "v1" &&
|
||||
@@ -1420,13 +1433,16 @@ const AppCreator = (defaultprops) => {
|
||||
|
||||
//console.log("SECURITYSCHEMES: ", securitySchemes)
|
||||
if (securitySchemes !== undefined) {
|
||||
console.log("NEWAUTH: ", securitySchemes)
|
||||
// FIXME: Should add Oauth2 (Microsoft) and JWT (Wazuh)
|
||||
//console.log("SECURITY: ", securitySchemes)
|
||||
//if (Object.entries(securitySchemes) > 1 &&
|
||||
var newauth = [];
|
||||
try {
|
||||
var optionset = false
|
||||
for (const [key, value] of Object.entries(securitySchemes)) {
|
||||
console.log(key, value);
|
||||
|
||||
if (key === "jwt") {
|
||||
setAuthenticationOption("JWT");
|
||||
setAuthenticationRequired(true);
|
||||
@@ -1437,25 +1453,48 @@ const AppCreator = (defaultprops) => {
|
||||
value.in.length > 0
|
||||
) {
|
||||
setParameterName(value.in);
|
||||
optionset = true
|
||||
}
|
||||
|
||||
} else if (value.scheme === "bearer") {
|
||||
setAuthenticationOption("Bearer auth");
|
||||
setAuthenticationRequired(true);
|
||||
optionset = true
|
||||
|
||||
} else if (key === "ApiKeyAuth" || key === "Token" || ((value.in === "header" || value.in === "query") && value.name !== undefined)) {
|
||||
setAuthenticationOption("API key");
|
||||
//if (optionset === false) {
|
||||
// optionset = true
|
||||
//}
|
||||
|
||||
value.in = value.in.charAt(0).toUpperCase() + value.in.slice(1);
|
||||
setParameterLocation(value.in);
|
||||
if (!apikeySelection.includes(value.in)) {
|
||||
console.log("APIKEY SELECT: ", apikeySelection);
|
||||
alert.error("Might be error in setting up API key authentication");
|
||||
}
|
||||
if (optionset === false) {
|
||||
optionset = true
|
||||
value.in = value.in.charAt(0).toUpperCase() + value.in.slice(1)
|
||||
|
||||
console.log("PARAM NAME: ", value.name);
|
||||
setParameterName(value.name);
|
||||
setAuthenticationRequired(true);
|
||||
setParameterLocation(value.in);
|
||||
if (!apikeySelection.includes(value.in)) {
|
||||
console.log("APIKEY SELECT: ", apikeySelection);
|
||||
alert.error("Might be error in setting up API key authentication");
|
||||
}
|
||||
|
||||
console.log("PARAM NAME: ", value.name);
|
||||
setAuthenticationOption("API key");
|
||||
setParameterName(value.name);
|
||||
setAuthenticationRequired(true);
|
||||
|
||||
newauth.push({
|
||||
"name": key,
|
||||
"type": value.in.toLowerCase(),
|
||||
"in": value.in.toLowerCase(),
|
||||
"example": "",
|
||||
})
|
||||
} else {
|
||||
newauth.push({
|
||||
"name": key,
|
||||
"type": value.in.toLowerCase(),
|
||||
"in": value.in.toLowerCase(),
|
||||
"example": "",
|
||||
})
|
||||
}
|
||||
|
||||
if (value.description !== undefined && value.description !== null && value.description.length > 0) {
|
||||
// Don't want a real description - just the ones we're replacing with
|
||||
@@ -1467,15 +1506,18 @@ const AppCreator = (defaultprops) => {
|
||||
} else if (value.scheme === "basic") {
|
||||
setAuthenticationOption("Basic auth");
|
||||
setAuthenticationRequired(true);
|
||||
optionset = true
|
||||
|
||||
} else if (value.scheme === "oauth2") {
|
||||
setAuthenticationOption("Oauth2");
|
||||
setAuthenticationRequired(true);
|
||||
optionset = true
|
||||
|
||||
} else if (value.type === "oauth2" || key === "Oauth2" || key === "Oauth2c" || (key !== undefined && key !== null && key.toLowerCase().includes("oauth2"))) {
|
||||
//alert.info("Can't handle Oauth2 auth yet.")
|
||||
setAuthenticationOption("Oauth2");
|
||||
setAuthenticationRequired(true);
|
||||
optionset = true
|
||||
|
||||
//console.log("FLOW-1: ", value)
|
||||
const flowkey = value.flow === undefined ? "flows" : "flow";
|
||||
@@ -1558,6 +1600,7 @@ const AppCreator = (defaultprops) => {
|
||||
}
|
||||
|
||||
if (newauth.length > 0) {
|
||||
newauth = newauth.filter(data => data.name != "ApiKeyAuth")
|
||||
setExtraAuth(newauth);
|
||||
}
|
||||
}
|
||||
@@ -1630,9 +1673,9 @@ const AppCreator = (defaultprops) => {
|
||||
data.info["contact"] = basedata.info.contact;
|
||||
} else if (contact === "") {
|
||||
data.info["contact"] = {
|
||||
name: "@frikkylikeme",
|
||||
url: "https://twitter.com/frikkylikeme",
|
||||
email: "frikky@shuffler.io",
|
||||
name: "@Anonymous Shuffle User",
|
||||
url: "https://twitter.com/shuffleio",
|
||||
email: "support@shuffler.io",
|
||||
};
|
||||
} else {
|
||||
data.info["contact"] = contact;
|
||||
@@ -1667,7 +1710,6 @@ const AppCreator = (defaultprops) => {
|
||||
// Basic way to allow multiple of the same path
|
||||
var pathjoin = item.url+"_"+item.method.toLowerCase()
|
||||
if (handledPaths.includes(pathjoin)) {
|
||||
console.log("ALREADY INCLUDED: ", pathjoin)
|
||||
|
||||
// Max 100 of same lol
|
||||
for (var i = 0; i < 100; i++) {
|
||||
@@ -1678,7 +1720,6 @@ const AppCreator = (defaultprops) => {
|
||||
continue
|
||||
}
|
||||
|
||||
console.log("FOUND NEW: ", item.url)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -1784,13 +1825,16 @@ const AppCreator = (defaultprops) => {
|
||||
for (var querykey in item.queries) {
|
||||
const queryitem = item.queries[querykey];
|
||||
|
||||
if (queryitem === undefined || queryitem === null || queryitem.name === undefined || queryitem.name === null || queryitem.name === "") {
|
||||
continue
|
||||
}
|
||||
|
||||
// A fix for duplicate items
|
||||
if (querynames.includes(queryitem.name.toLowerCase())) {
|
||||
continue
|
||||
}
|
||||
|
||||
querynames.push(queryitem.name.toLowerCase())
|
||||
|
||||
if (queryitem.name.toLowerCase() == "url") {
|
||||
console.log(item.name + " uses a bad query: url");
|
||||
continue;
|
||||
@@ -1937,70 +1981,110 @@ const AppCreator = (defaultprops) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
item.body !== undefined &&
|
||||
item.body !== null &&
|
||||
item.body.length > 0
|
||||
) {
|
||||
const required = false;
|
||||
newitem = {
|
||||
in: "body",
|
||||
name: "body",
|
||||
multiline: true,
|
||||
description: "Generated by shuffler.io OpenAPI",
|
||||
required: required,
|
||||
example: item.body,
|
||||
schema: {
|
||||
type: "string",
|
||||
},
|
||||
};
|
||||
const methodname = item.method.toLowerCase()
|
||||
if (methodname === "post" || methodname === "put" || methodname === "patch") {
|
||||
if (
|
||||
item.body !== undefined &&
|
||||
item.body !== null &&
|
||||
item.body.length > 0
|
||||
) {
|
||||
console.log("GOT BODY: ", item.url, item.method, item.body)
|
||||
|
||||
// FIXME - add application/json if JSON example?
|
||||
data.paths[item.url][item.method.toLowerCase()]["requestBody"] = {
|
||||
description: "Generated by Shuffler.io",
|
||||
required: required,
|
||||
content: {
|
||||
example: {
|
||||
example: item.body,
|
||||
},
|
||||
},
|
||||
};
|
||||
// Replacing dollarsign insertions that aren't escaped
|
||||
// This is to stop it from messing with systems in Shuffle.
|
||||
// This MAY cause it to be a little weird in other systems however,
|
||||
// but it's the only way we can properly support e.g. GraphQL
|
||||
// with good examples
|
||||
var newbody = ""
|
||||
for (var key in item.body) {
|
||||
if (item.body[key] === "$") {
|
||||
if (key > 0) {
|
||||
//console.log("Found: ", item.body[key-1])
|
||||
const newkey = parseInt(key, 10)
|
||||
if (item.body[newkey-1] !== "\\") {
|
||||
if (item.body[newkey+1] !== "\{") {
|
||||
newbody += "\\"
|
||||
}
|
||||
}
|
||||
|
||||
data.paths[item.url][item.method.toLowerCase()].parameters.push(
|
||||
newitem
|
||||
);
|
||||
} else if (actionBodyRequest.includes(item.method.toUpperCase())) {
|
||||
// Appending an empty field
|
||||
const required = false;
|
||||
newitem = {
|
||||
in: "body",
|
||||
name: "body",
|
||||
multiline: true,
|
||||
description: "Generated by shuffler.io OpenAPI",
|
||||
required: required,
|
||||
example: "",
|
||||
schema: {
|
||||
type: "string",
|
||||
},
|
||||
};
|
||||
newbody += item.body[key]
|
||||
} else {
|
||||
newbody += "\\"
|
||||
newbody += item.body[key]
|
||||
}
|
||||
//newbody += item.body[key]
|
||||
} else {
|
||||
newbody += item.body[key]
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME - add application/json if JSON example?
|
||||
data.paths[item.url][item.method.toLowerCase()]["requestBody"] = {
|
||||
description: "Generated by Shuffler.io",
|
||||
required: required,
|
||||
content: {
|
||||
example: {
|
||||
example: "",
|
||||
},
|
||||
},
|
||||
};
|
||||
console.log("New body: ", newbody)
|
||||
if (newbody !== item.body) {
|
||||
item.body = newbody
|
||||
}
|
||||
|
||||
data.paths[item.url][item.method.toLowerCase()].parameters.push(
|
||||
newitem
|
||||
);
|
||||
} else {
|
||||
//console.log("Nothing to append?")
|
||||
}
|
||||
//var pathjoin = item.url+"_"+item.method.toLowerCase()
|
||||
|
||||
const required = false;
|
||||
newitem = {
|
||||
in: "body",
|
||||
name: "body",
|
||||
multiline: true,
|
||||
description: "Generated by shuffler.io OpenAPI",
|
||||
required: required,
|
||||
example: item.body,
|
||||
schema: {
|
||||
type: "string",
|
||||
},
|
||||
};
|
||||
|
||||
// FIXME - add application/json if JSON example?
|
||||
data.paths[item.url][item.method.toLowerCase()]["requestBody"] = {
|
||||
description: "Generated by Shuffler.io",
|
||||
required: required,
|
||||
content: {
|
||||
example: {
|
||||
example: item.body,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
data.paths[item.url][item.method.toLowerCase()].parameters.push(
|
||||
newitem
|
||||
);
|
||||
} else if (actionBodyRequest.includes(item.method.toUpperCase())) {
|
||||
// Appending an empty field
|
||||
const required = false;
|
||||
newitem = {
|
||||
in: "body",
|
||||
name: "body",
|
||||
multiline: true,
|
||||
description: "Generated by shuffler.io OpenAPI",
|
||||
required: required,
|
||||
example: "",
|
||||
schema: {
|
||||
type: "string",
|
||||
},
|
||||
};
|
||||
|
||||
// FIXME - add application/json if JSON example?
|
||||
data.paths[item.url][item.method.toLowerCase()]["requestBody"] = {
|
||||
description: "Generated by Shuffler.io",
|
||||
required: required,
|
||||
content: {
|
||||
example: {
|
||||
example: "",
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
data.paths[item.url][item.method.toLowerCase()].parameters.push(
|
||||
newitem
|
||||
);
|
||||
} else {
|
||||
//console.log("Nothing to append?")
|
||||
}
|
||||
}
|
||||
|
||||
// https://swagger.io/docs/specification/describing-request-body/file-upload/
|
||||
if (
|
||||
@@ -2365,6 +2449,7 @@ const AppCreator = (defaultprops) => {
|
||||
<span style={{ width: 50 }} />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{extraAuth.map((value, index) => {
|
||||
return (
|
||||
<span
|
||||
@@ -2978,6 +3063,23 @@ const AppCreator = (defaultprops) => {
|
||||
data["file_field"] !== null &&
|
||||
data["file_field"].length > 0) || data["example_response"] === "shuffle_file_download"
|
||||
|
||||
// In case of extremely long summaries/names from OpenAPI def
|
||||
//const maxlen = 35
|
||||
//if (data.description === undefined || data.description === null || data.description.length === 0) {
|
||||
// if (data.name !== undefined && data.name !== null && data.name.length > maxlen ) {
|
||||
// var newname = []
|
||||
// for (var key in data.name.split(" ")) {
|
||||
// console.log("Name: ", data.name[key])
|
||||
// if (newname.join(" ").length < maxlen) {
|
||||
// newname.push(data.name[key])
|
||||
// }
|
||||
// }
|
||||
|
||||
// data.description = data.name.valueOf()
|
||||
// data.name = newname.join(" ")
|
||||
// }
|
||||
//}
|
||||
|
||||
return (
|
||||
<Paper key={index} style={actionListStyle}>
|
||||
{error}
|
||||
@@ -2991,6 +3093,16 @@ const AppCreator = (defaultprops) => {
|
||||
overflowX: "hidden",
|
||||
}}
|
||||
onClick={() => {
|
||||
console.log("Data: ", data)
|
||||
if (hasFile) {
|
||||
setFileUploadEnabled(true);
|
||||
//setActionField("headers", "")
|
||||
console.log("It has a file: ", data["file_field"])
|
||||
data.headers = ""
|
||||
} else {
|
||||
console.log("No file")
|
||||
}
|
||||
|
||||
setCurrentAction(data);
|
||||
setCurrentActionMethod(data.method);
|
||||
setUrlPathQueries(data.queries);
|
||||
@@ -3003,11 +3115,10 @@ const AppCreator = (defaultprops) => {
|
||||
data["body"].length > 0
|
||||
) {
|
||||
findBodyParams(data["body"]);
|
||||
}
|
||||
} else {
|
||||
console.log("No body param")
|
||||
}
|
||||
|
||||
if (hasFile) {
|
||||
setFileUploadEnabled(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div style={{ display: "flex" }}>
|
||||
@@ -3170,7 +3281,7 @@ const AppCreator = (defaultprops) => {
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
multiline
|
||||
rows="5"
|
||||
minRows="5"
|
||||
defaultValue={currentAction["body"]}
|
||||
onChange={(e) => {
|
||||
setActionField("body", e.target.value);
|
||||
@@ -3208,7 +3319,7 @@ const AppCreator = (defaultprops) => {
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
multiline
|
||||
rows="2"
|
||||
minRows="2"
|
||||
defaultValue={currentAction["example_response"]}
|
||||
onChange={(e) => setActionField("example_response", e.target.value)}
|
||||
helperText={
|
||||
@@ -3670,7 +3781,11 @@ const AppCreator = (defaultprops) => {
|
||||
headers += key + "=" + value + "\n";
|
||||
}
|
||||
|
||||
setActionField("headers", headers.trim());
|
||||
try {
|
||||
setActionField("headers", headers.trim());
|
||||
} catch (e) {
|
||||
console.log("Failed to parse header: ", e)
|
||||
}
|
||||
}
|
||||
|
||||
if (request.body !== undefined && request.body !== null) {
|
||||
@@ -3751,8 +3866,8 @@ const AppCreator = (defaultprops) => {
|
||||
}
|
||||
|
||||
// Found that dashes in the URL doesn't work
|
||||
parsedurl = parsedurl.replace("-", "_")
|
||||
console.log("Actions: ", actions)
|
||||
//parsedurl = parsedurl.replace("-", "_")
|
||||
//console.log("Actions: ", actions)
|
||||
|
||||
if (baseUrl.length === 0 && parsedurl.includes("http")) {
|
||||
const newurl = new URL(encodeURI(parsedurl))
|
||||
@@ -3809,7 +3924,7 @@ const AppCreator = (defaultprops) => {
|
||||
Enable Fileupload
|
||||
</Button>
|
||||
) : null}
|
||||
{currentActionMethod === "GET" ? (
|
||||
{/*currentActionMethod === "GET" ? (
|
||||
<Button
|
||||
color="primary"
|
||||
variant={fileDownloadEnabled ? "contained" : "outlined"}
|
||||
@@ -3831,7 +3946,7 @@ const AppCreator = (defaultprops) => {
|
||||
>
|
||||
Download as file
|
||||
</Button>
|
||||
) : null}
|
||||
) : null*/}
|
||||
{fileUploadEnabled ? (
|
||||
<TextField
|
||||
required
|
||||
@@ -3864,37 +3979,41 @@ const AppCreator = (defaultprops) => {
|
||||
/>
|
||||
) : null}
|
||||
<div />
|
||||
<b>Headers</b>: static for the action
|
||||
<TextField
|
||||
required
|
||||
style={{
|
||||
flex: "1",
|
||||
marginRight: "15px",
|
||||
marginTop: "5px",
|
||||
backgroundColor: inputColor,
|
||||
}}
|
||||
fullWidth={true}
|
||||
placeholder={
|
||||
"Accept: application/json\r\nContent-Type: application/json"
|
||||
}
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
id="standard-required"
|
||||
defaultValue={currentAction["headers"]}
|
||||
multiline
|
||||
rows="2"
|
||||
onChange={(e) => setActionField("headers", e.target.value)}
|
||||
helperText={
|
||||
<span style={{ color: "white", marginBottom: "2px" }}>
|
||||
Headers that are part of the request. Default: EMPTY
|
||||
</span>
|
||||
}
|
||||
InputProps={{
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{fileUploadEnabled ? null :
|
||||
<span>
|
||||
<b>Headers</b>
|
||||
<TextField
|
||||
required
|
||||
style={{
|
||||
flex: "1",
|
||||
marginRight: "15px",
|
||||
marginTop: "5px",
|
||||
backgroundColor: inputColor,
|
||||
}}
|
||||
fullWidth={true}
|
||||
placeholder={
|
||||
"Accept: application/json\r\nContent-Type: application/json"
|
||||
}
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
id="standard-required"
|
||||
defaultValue={currentAction["headers"]}
|
||||
multiline
|
||||
minRows="2"
|
||||
onChange={(e) => setActionField("headers", e.target.value)}
|
||||
helperText={
|
||||
<span style={{ color: "white", marginBottom: "2px" }}>
|
||||
Headers that are part of the request. Default: EMPTY
|
||||
</span>
|
||||
}
|
||||
InputProps={{
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
}
|
||||
{bodyInfo}
|
||||
<Divider
|
||||
style={{
|
||||
@@ -4582,6 +4701,12 @@ const AppCreator = (defaultprops) => {
|
||||
<div style={{ marginTop: 10, marginBottom: 10 }}>
|
||||
{projectCategories.map((tag, index) => {
|
||||
const newname = tag.charAt(0).toUpperCase() + tag.slice(1);
|
||||
|
||||
//var regex = /_shuffle_replace_\d/i;
|
||||
////console.log("NEW: ",
|
||||
//newname = newname.replaceAll(regex, "")
|
||||
//console.log("Replaced: ", newname)
|
||||
|
||||
return (
|
||||
<Chip
|
||||
key={index}
|
||||
@@ -4814,6 +4939,7 @@ const AppCreator = (defaultprops) => {
|
||||
imageUploadError.length > 0 ? (
|
||||
<div style={{ marginTop: 10 }}>Error: {imageUploadError}</div>
|
||||
) : null;
|
||||
|
||||
const imageUploadModalView = openImageModal ? (
|
||||
<Dialog
|
||||
open={openImageModal}
|
||||
|
||||
+142
-46
@@ -39,6 +39,10 @@ import {
|
||||
Delete as DeleteIcon,
|
||||
} from "@material-ui/icons";
|
||||
|
||||
import {
|
||||
ForkRight as ForkRightIcon,
|
||||
} from '@mui/icons-material';
|
||||
|
||||
import { useTheme } from "@material-ui/core/styles";
|
||||
|
||||
import YAML from "yaml";
|
||||
@@ -70,7 +74,77 @@ export const FixName = (name) => {
|
||||
return newAppname;
|
||||
};
|
||||
|
||||
// Takes input of e.g. $node.data.#.asd and a matching value from a json blob
|
||||
// Returns
|
||||
export const FindJsonPath = (path, inputdata) => {
|
||||
const splitkey = ".";
|
||||
var parsedValues = [];
|
||||
|
||||
if (inputdata === undefined || inputdata === null) {
|
||||
console.log("Input is ", inputdata, ". Returning.")
|
||||
return inputdata
|
||||
}
|
||||
|
||||
if (typeof inputdata !== "object") {
|
||||
console.log("Input is NOT an object. Returning.")
|
||||
return inputdata
|
||||
}
|
||||
|
||||
var keysplit = path.split(splitkey)
|
||||
if (path.startsWith("$") && keysplit.length > 1) {
|
||||
keysplit = keysplit.slice(1,)
|
||||
}
|
||||
|
||||
if (keysplit.length === 0) {
|
||||
console.log("Couldn't find key: length is 0 for keysplit.")
|
||||
return inputdata
|
||||
}
|
||||
|
||||
// FIXME: Check list - always getting FIRST item, not digging too deep.
|
||||
// If object, send further
|
||||
if (keysplit[0].includes("#")) {
|
||||
if (Object.prototype.toString.call(inputdata) === '[object Array]') {
|
||||
if (inputdata.length === 0) {
|
||||
return ""
|
||||
} else {
|
||||
|
||||
// Fix the list
|
||||
if (keysplit.length === 1) {
|
||||
return inputdata[0]
|
||||
} else {
|
||||
const joinedsplit = keysplit.slice(1,).join(".")
|
||||
return FindJsonPath(joinedsplit, inputdata[0])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
var found = false
|
||||
for (const [key, value] of Object.entries(inputdata)) {
|
||||
const newkey = key.valueOf().toLowerCase().replaceAll(" ", "_")
|
||||
if (key === keysplit[0] || newkey === keysplit[0]) {
|
||||
found = true
|
||||
|
||||
// Return if no more keys
|
||||
// Else, dig deeper
|
||||
if (keysplit.length === 1) {
|
||||
return value
|
||||
} else {
|
||||
const joinedsplit = keysplit.slice(1,).join(".")
|
||||
return FindJsonPath(joinedsplit, value)
|
||||
}
|
||||
} else {
|
||||
//console.log("N: ", key)
|
||||
}
|
||||
}
|
||||
|
||||
return inputdata
|
||||
}
|
||||
|
||||
// Parses JSON data into keys that can be used everywhere :)
|
||||
// Reverse of this is FindJsonPath
|
||||
export const GetParsedPaths = (inputdata, basekey) => {
|
||||
const splitkey = ".";
|
||||
var parsedValues = [];
|
||||
@@ -92,12 +166,18 @@ export const GetParsedPaths = (inputdata, basekey) => {
|
||||
|
||||
// Handle direct loop!
|
||||
if (!isNaN(key) && basekey === "") {
|
||||
console.log("Handling direct loop.");
|
||||
parsedValues.push({
|
||||
type: "object",
|
||||
name: "Node",
|
||||
autocomplete: `${basekey.replaceAll(" ", "_")}`,
|
||||
});
|
||||
|
||||
//parsedValues.push({
|
||||
// type: "value",
|
||||
// name: `${basekey} length`,
|
||||
// autocomplete: `{{ ${basekey.replaceAll(" ", "_")} | size }}`,
|
||||
//});
|
||||
|
||||
parsedValues.push({
|
||||
type: "list",
|
||||
name: `${splitkey}list`,
|
||||
@@ -120,6 +200,13 @@ export const GetParsedPaths = (inputdata, basekey) => {
|
||||
name: basekeyname,
|
||||
autocomplete: `${basekey}.${key.replaceAll(" ", "_")}`,
|
||||
});
|
||||
|
||||
//parsedValues.push({
|
||||
// type: "value",
|
||||
// name: `${basekeyname} length`,
|
||||
// autocomplete: "{{ "+`${basekey}.${key.replaceAll(" ", "_")} | size }}`,
|
||||
//});
|
||||
|
||||
parsedValues.push({
|
||||
type: "list",
|
||||
name: `${basekeyname}${splitkey}list`,
|
||||
@@ -724,9 +811,11 @@ const Apps = (props) => {
|
||||
</Tooltip>
|
||||
) : null;
|
||||
|
||||
// FIXME: Add /apps/new?id=<PUBLIC> to allow for changes of the original
|
||||
// Should always reference the original ID.
|
||||
var editButton =
|
||||
//if (selectedApp.name !== undefined && selectedApp.name !== null && selectedApp.name.includes("New")) {
|
||||
//}
|
||||
|
||||
var editButton =
|
||||
selectedApp.activated &&
|
||||
selectedApp.private_id !== undefined &&
|
||||
selectedApp.private_id.length > 0 &&
|
||||
@@ -746,21 +835,19 @@ const Apps = (props) => {
|
||||
) : null;
|
||||
|
||||
//var editNewButton = editButton === null ?
|
||||
var editNewButton = selectedApp.generated && selectedApp.activated && props.userdata.id !== selectedApp.owner ?
|
||||
isCloud ?
|
||||
var editNewButton = selectedApp.generated && selectedApp.activated && props.userdata.id !== selectedApp.owner && isCloud ?
|
||||
<Link to={activateUrl} style={{ textDecoration: "none" }}>
|
||||
<Tooltip title={"Edit this public app to your liking"}>
|
||||
<Tooltip title={"Fork and Edit this public app to your liking"}>
|
||||
<Button
|
||||
variant="contained"
|
||||
component="label"
|
||||
color="primary"
|
||||
style={{ marginTop: 10, marginRight: 10 }}
|
||||
>
|
||||
<EditIcon />
|
||||
<ForkRightIcon />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Link>
|
||||
: null
|
||||
: null
|
||||
|
||||
const activateButton =
|
||||
@@ -796,8 +883,7 @@ const Apps = (props) => {
|
||||
((selectedApp.private_id !== undefined &&
|
||||
selectedApp.private_id.length > 0 &&
|
||||
selectedApp.generated) ||
|
||||
(selectedApp.downloaded !== undefined &&
|
||||
selectedApp.downloaded == true) ||
|
||||
(selectedApp.downloaded !== undefined && selectedApp.downloaded == true) ||
|
||||
!selectedApp.generated) &&
|
||||
activateButton === null ? (
|
||||
<Tooltip title={"Delete app"}>
|
||||
@@ -899,6 +985,10 @@ const Apps = (props) => {
|
||||
|
||||
const userRoles = ["you", isCloud ? "public" : "everyone"];
|
||||
|
||||
// Admin in org or creator of app
|
||||
// FIXME: Missing check for if same creator account
|
||||
const canEditApp = userdata !== undefined && (userdata.admin === "true" || userdata.id === selectedApp.owner || selectedApp.owner === "" || (userdata.admin === "true" && userdata.active_org.id === selectedApp.reference_org)) || !selectedApp.generated
|
||||
|
||||
//fetch(globalUrl+"/api/v1/get_openapi/"+urlParams.get("id"),
|
||||
var baseInfo =
|
||||
newAppname.length > 0 ? (
|
||||
@@ -1003,18 +1093,20 @@ const Apps = (props) => {
|
||||
) : null}
|
||||
|
||||
{activateButton}
|
||||
{editNewButton}
|
||||
{(props.userdata !== undefined &&
|
||||
(props.userdata.role === "admin" ||
|
||||
props.userdata.id === selectedApp.owner ||
|
||||
selectedApp.owner === ""
|
||||
)) || !selectedApp.generated ? (
|
||||
|
||||
{ /* editNewButton === null && */ }
|
||||
|
||||
{canEditApp ? (
|
||||
<div>
|
||||
{editButton}
|
||||
{downloadButton}
|
||||
{deleteButton}
|
||||
</div>
|
||||
) : null}
|
||||
) :
|
||||
<div>
|
||||
{editNewButton}
|
||||
</div>
|
||||
}
|
||||
{selectedApp.tags !== undefined && selectedApp.tags !== null ? (
|
||||
<div
|
||||
style={{
|
||||
@@ -1040,8 +1132,8 @@ const Apps = (props) => {
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
{props.userdata !== undefined &&
|
||||
props.userdata.id === selectedApp.owner ? (
|
||||
{canEditApp
|
||||
? (
|
||||
<div style={{ marginTop: 15 }}>
|
||||
{/*<p><b>ID:</b> {selectedApp.id}</p>*/}
|
||||
<b style={{ marginRight: 15 }}>Sharing </b>
|
||||
@@ -1394,6 +1486,7 @@ const Apps = (props) => {
|
||||
}, [appValidation, isDropzone]);
|
||||
|
||||
var appDelay = -75
|
||||
|
||||
const appView = isLoggedIn ? (
|
||||
<Dropzone
|
||||
style={{ width: viewWidth * 2 + 20, margin: "auto", padding: 20 }}
|
||||
@@ -1442,7 +1535,7 @@ const Apps = (props) => {
|
||||
</div>
|
||||
{isCloud ? null : (
|
||||
<span>
|
||||
{isLoading ? null : (
|
||||
{userdata === undefined || userdata === null || isLoading ? null : (
|
||||
<Tooltip
|
||||
title={"Reload apps locally"}
|
||||
style={{ marginTop: "28px", width: "100%" }}
|
||||
@@ -1466,29 +1559,32 @@ const Apps = (props) => {
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Tooltip
|
||||
title={"Download from Github"}
|
||||
style={{ marginTop: "28px", width: "100%" }}
|
||||
aria-label={"Upload"}
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
component="label"
|
||||
color="primary"
|
||||
style={{ margin: 5, maxHeight: 50, marginTop: 10 }}
|
||||
disabled={isLoading}
|
||||
onClick={() => {
|
||||
setOpenApi(baseRepository);
|
||||
setLoadAppsModalOpen(true);
|
||||
}}
|
||||
>
|
||||
{isLoading ? (
|
||||
<CircularProgress size={25} />
|
||||
) : (
|
||||
<CloudDownloadIcon />
|
||||
)}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
{userdata === undefined || userdata === null || userdata.admin === "false" ? null :
|
||||
<Tooltip
|
||||
title={"Download from Github"}
|
||||
style={{ marginTop: "28px", width: "100%" }}
|
||||
aria-label={"Upload"}
|
||||
>
|
||||
<Button
|
||||
variant="outlined"
|
||||
component="label"
|
||||
color="primary"
|
||||
style={{ margin: 5, maxHeight: 50, marginTop: 10 }}
|
||||
disabled={isLoading}
|
||||
onClick={() => {
|
||||
setOpenApi(baseRepository);
|
||||
setLoadAppsModalOpen(true);
|
||||
}}
|
||||
>
|
||||
{isLoading ? (
|
||||
<CircularProgress size={25} />
|
||||
) : (
|
||||
<CloudDownloadIcon />
|
||||
)}
|
||||
</Button>
|
||||
</Tooltip>
|
||||
}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -1511,7 +1607,7 @@ const Apps = (props) => {
|
||||
fullWidth
|
||||
color="primary"
|
||||
id="app_search_field"
|
||||
placeholder={"Search apps"}
|
||||
placeholder={"Search your apps"}
|
||||
onChange={(event) => {
|
||||
handleSearchChange(event.target.value);
|
||||
setCursearch(event.target.value);
|
||||
@@ -1794,7 +1890,7 @@ const Apps = (props) => {
|
||||
getApps();
|
||||
}, 1000);
|
||||
} else {
|
||||
alert.error("Failed deleting app");
|
||||
alert.error("Failed deleting app. Does it still exist?");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -2187,7 +2283,7 @@ const Apps = (props) => {
|
||||
<FormControl>
|
||||
<DialogTitle>
|
||||
<div style={{ color: "rgba(255,255,255,0.9)" }}>
|
||||
Create a new integration
|
||||
Create a new app
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<DialogContent style={{ color: "rgba(255,255,255,0.65)" }}>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useInterval } from "react-powerhooks";
|
||||
import DetectionFramework from "../components/DetectionFramework.jsx";
|
||||
import AppFramework from "../components/AppFramework.jsx";
|
||||
import { makeStyles, useTheme } from "@material-ui/core/styles";
|
||||
// nodejs library that concatenates classes
|
||||
import classNames from "classnames";
|
||||
import theme from '../theme';
|
||||
@@ -9,6 +10,7 @@ import { useNavigate, Link, useParams } from "react-router-dom";
|
||||
// react plugin used to create charts
|
||||
//import { Line, Bar } from "react-chartjs-2";
|
||||
import { useAlert } from "react-alert";
|
||||
import Autocomplete from "@material-ui/lab/Autocomplete";
|
||||
|
||||
import {
|
||||
Tooltip,
|
||||
@@ -19,6 +21,7 @@ import {
|
||||
Grid,
|
||||
Paper,
|
||||
Chip,
|
||||
Checkbox,
|
||||
} from "@material-ui/core";
|
||||
|
||||
import {
|
||||
@@ -27,9 +30,13 @@ import {
|
||||
Description as DescriptionIcon,
|
||||
PlayArrow as PlayArrowIcon,
|
||||
Edit as EditIcon,
|
||||
CheckBox as CheckBoxIcon,
|
||||
CheckBoxOutlineBlank as CheckBoxOutlineBlankIcon,
|
||||
OpenInNew as OpenInNewIcon,
|
||||
} from "@material-ui/icons";
|
||||
|
||||
import WorkflowPaper from "../components/WorkflowPaper.jsx"
|
||||
import { removeParam } from "../views/AngularWorkflow.jsx"
|
||||
|
||||
// core components
|
||||
//import {
|
||||
@@ -57,7 +64,33 @@ import {
|
||||
TreeMapRect,
|
||||
} from 'reaviz';
|
||||
|
||||
const UsecaseListComponent = ({keys, isCloud, globalUrl, frameworkData, isLoggedIn}) => {
|
||||
const useStyles = makeStyles({
|
||||
notchedOutline: {
|
||||
borderColor: "#f85a3e !important",
|
||||
},
|
||||
root: {
|
||||
"& .MuiAutocomplete-listbox": {
|
||||
border: "2px solid #f85a3e",
|
||||
color: "white",
|
||||
fontSize: 18,
|
||||
"& li:nth-child(even)": {
|
||||
backgroundColor: "#CCC",
|
||||
},
|
||||
"& li:nth-child(odd)": {
|
||||
backgroundColor: "#FFF",
|
||||
},
|
||||
},
|
||||
},
|
||||
inputRoot: {
|
||||
color: "white",
|
||||
// This matches the specificity of the default styles at https://github.com/mui-org/material-ui/blob/v4.11.3/packages/material-ui-lab/src/Autocomplete/Autocomplete.js#L90
|
||||
"&:hover .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: "#f86a3e",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const UsecaseListComponent = ({keys, isCloud, globalUrl, frameworkData, isLoggedIn, workflows, setWorkflows}) => {
|
||||
const [expandedIndex, setExpandedIndex] = useState(-1);
|
||||
const [expandedItem, setExpandedItem] = useState(-1);
|
||||
const [inputUsecase, setInputUsecase] = useState({});
|
||||
@@ -67,8 +100,13 @@ const UsecaseListComponent = ({keys, isCloud, globalUrl, frameworkData, isLogged
|
||||
const [video, setVideo] = useState("");
|
||||
const [blogpost, setBlogpost] = useState("");
|
||||
|
||||
const [mitreTags, setMitreTags] = useState([]);
|
||||
const [selectedWorkflows, setSelectedWorkflows] = useState([])
|
||||
const [firstLoad, setFirstLoad] = useState(true)
|
||||
|
||||
const classes = useStyles();
|
||||
let navigate = useNavigate();
|
||||
|
||||
const [mitreTags, setMitreTags] = useState([]);
|
||||
if (keys === undefined || keys === null || keys.length === 0) {
|
||||
return null
|
||||
}
|
||||
@@ -104,7 +142,6 @@ const UsecaseListComponent = ({keys, isCloud, globalUrl, frameworkData, isLogged
|
||||
setTimeout(() => {
|
||||
//console.log("Scroll!")
|
||||
const found = document.getElementById("selected_box");
|
||||
console.log("Found to scroll: ", found)
|
||||
if (found !== undefined && found !== null) {
|
||||
//console.log("FOUND!!")
|
||||
found.scrollTo({
|
||||
@@ -112,6 +149,9 @@ const UsecaseListComponent = ({keys, isCloud, globalUrl, frameworkData, isLogged
|
||||
behavior: "smooth",
|
||||
})
|
||||
}
|
||||
|
||||
setFirstLoad(true)
|
||||
setSelectedWorkflows([])
|
||||
}, 100);
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -119,6 +159,9 @@ const UsecaseListComponent = ({keys, isCloud, globalUrl, frameworkData, isLogged
|
||||
setInputUsecase({})
|
||||
setExpandedIndex(index)
|
||||
setExpandedItem(subindex)
|
||||
|
||||
setFirstLoad(true)
|
||||
setSelectedWorkflows([])
|
||||
})
|
||||
}
|
||||
|
||||
@@ -178,6 +221,44 @@ const UsecaseListComponent = ({keys, isCloud, globalUrl, frameworkData, isLogged
|
||||
})
|
||||
}
|
||||
|
||||
const setWorkflow = (workflowdata) => {
|
||||
const new_url = `${globalUrl}/api/v1/workflows/${workflowdata.id}`
|
||||
|
||||
fetch(new_url, {
|
||||
method: "PUT",
|
||||
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;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson.success === false) {
|
||||
if (responseJson.reason !== undefined) {
|
||||
alert.error("Error updating workflow: ", responseJson.reason)
|
||||
} else {
|
||||
alert.error("Error updating workflow.")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
return responseJson;
|
||||
})
|
||||
.catch((error) => {
|
||||
alert.error("Problem setting workflow: ", error.toString());
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{marginTop: 25, minHeight: 1000,}}>
|
||||
<Typography variant="h1">
|
||||
@@ -194,14 +275,19 @@ const UsecaseListComponent = ({keys, isCloud, globalUrl, frameworkData, isLogged
|
||||
</Typography>
|
||||
<Grid container spacing={3} style={{marginTop: 25}}>
|
||||
{usecase.list.map((subcase, subindex) => {
|
||||
const selectedItem = subindex === expandedItem && index === expandedIndex
|
||||
|
||||
if (subcase.matches === undefined || subcase.matches === null) {
|
||||
subcase.matches = []
|
||||
} else {
|
||||
if (selectedItem && subcase.matches.length > 0 && selectedWorkflows.length === 0 && firstLoad === true) {
|
||||
setFirstLoad(false)
|
||||
setSelectedWorkflows(subcase.matches)
|
||||
}
|
||||
}
|
||||
|
||||
const selectedItem = subindex === expandedItem && index === expandedIndex
|
||||
if (selectedItem && subcase.name !== undefined && inputUsecase.name !== undefined) {
|
||||
if (subcase.name.toLowerCase().replaceAll(" ", "_") === inputUsecase.name.toLowerCase().replaceAll(" ", "_")) {
|
||||
console.log("Input: ", inputUsecase)
|
||||
if (inputUsecase.description !== undefined && inputUsecase.description !== null) {
|
||||
subcase.description = inputUsecase.description
|
||||
}
|
||||
@@ -213,24 +299,35 @@ const UsecaseListComponent = ({keys, isCloud, globalUrl, frameworkData, isLogged
|
||||
if (inputUsecase.video !== undefined && inputUsecase.video !== null) {
|
||||
subcase.video = inputUsecase.video
|
||||
}
|
||||
|
||||
if (inputUsecase.extra_buttons !== undefined && inputUsecase.extra_buttons !== null) {
|
||||
subcase.extra_buttons = inputUsecase.extra_buttons
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const finished = subcase.matches.length > 0
|
||||
//const backgroundColor = selectedItem ? "inherit" : finished ? "inherit" : usecase.color
|
||||
const backgroundColor = "inherit"
|
||||
const finished = subcase.matches.length > 0
|
||||
const backgroundColor = theme.palette.surfaceColor
|
||||
//"inherit"
|
||||
const itemBorder = `${selectedItem ? "3px" : expandedItem >= 0 ? "0px" : "1px"} solid ${usecase.color}`
|
||||
|
||||
const fixedName = subcase.name.toLowerCase().replace("_", " ")
|
||||
|
||||
return (
|
||||
<Grid item xs={selectedItem ? 12 : 4} key={subindex} style={{minHeight: 110,}} onClick={() => {
|
||||
<Grid id={fixedName} item xs={selectedItem ? 12 : 4} key={subindex} style={{minHeight: 110,}} onClick={() => {
|
||||
|
||||
//setSelectedWorkflows([])
|
||||
if (selectedItem) {
|
||||
} else {
|
||||
//if (subcase.description !== undefined && subcase.description !== null && subcase.description.length > 0) {
|
||||
getUsecase(subcase.name, index, subindex)
|
||||
//}
|
||||
navigate(`/usecases?selected_object=${fixedName}`)
|
||||
|
||||
//const newitem = removeParam("selected_object", cursearch);
|
||||
//navigate(curpath + newitem)
|
||||
}
|
||||
}}>
|
||||
<Paper style={{padding: "30px 30px 30px 30px", minHeight: 75, cursor: !selectedItem ? "pointer" : "default", border: itemBorder, backgroundColor: backgroundColor,}} onClick={() => {
|
||||
<Paper style={{padding: 25, minHeight: 75, cursor: !selectedItem ? "pointer" : "default", border: itemBorder, backgroundColor: backgroundColor,}} onClick={() => {
|
||||
}}>
|
||||
{!selectedItem ?
|
||||
<div style={{textAlign: "left", position: "relative",}}>
|
||||
@@ -307,7 +404,7 @@ const UsecaseListComponent = ({keys, isCloud, globalUrl, frameworkData, isLogged
|
||||
: null}
|
||||
</div>
|
||||
:
|
||||
<div style={{textAlign: "left", position: "relative",}} id="selected_box">
|
||||
<div style={{textAlign: "left", position: "relative", }} id="selected_box">
|
||||
<Typography variant="h6">
|
||||
<b>{subcase.name}</b>
|
||||
</Typography>
|
||||
@@ -402,7 +499,7 @@ const UsecaseListComponent = ({keys, isCloud, globalUrl, frameworkData, isLogged
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div style={{marginTop: 25, display: "flex", minHeight: 400, maxHeight: 400, }}>
|
||||
<div style={{marginTop: 25, display: "flex", minHeight: 400, maxHeight: 400, marginRight: 15, }}>
|
||||
{editing ?
|
||||
<div style={{flex: 1, marginRight: 50, }}>
|
||||
<Typography variant="h6">
|
||||
@@ -506,14 +603,152 @@ const UsecaseListComponent = ({keys, isCloud, globalUrl, frameworkData, isLogged
|
||||
</div>
|
||||
:
|
||||
<div style={{flex: 1, textAlign: "left", marginRight: 10, }}>
|
||||
<Typography variant="body1">
|
||||
<Typography variant="body1" color="textSecondary">
|
||||
{subcase.description}
|
||||
</Typography>
|
||||
<Typography variant="h6" style={{marginTop: 15, }}>
|
||||
Your workflow{subcase.matches.length === 1 ? "" : "s"} ({subcase.matches.length})
|
||||
</Typography>
|
||||
{subcase.matches.length > 0 ?
|
||||
<Grid container xs={3} style={{maxWidth: 325, marginTop: 10, }}>
|
||||
|
||||
{workflows !== undefined && workflows !== null && workflows.length > 0 ?
|
||||
<Typography variant="body1" style={{marginTop: 15, marginBottom: 10, }}>
|
||||
Select relevant workflows
|
||||
</Typography>
|
||||
: null}
|
||||
|
||||
{workflows !== undefined && workflows !== null && workflows.length > 0 ?
|
||||
<Autocomplete
|
||||
multiple
|
||||
id="workflow_matching"
|
||||
options={workflows}
|
||||
autoHighlight
|
||||
value={selectedWorkflows}
|
||||
classes={{ inputRoot: classes.inputRoot }}
|
||||
ListboxProps={{
|
||||
style: {
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
getOptionSelected={(option, value) => option.id === value.id}
|
||||
getOptionLabel={(option) => {
|
||||
|
||||
if (
|
||||
option === undefined ||
|
||||
option === null ||
|
||||
option.name === undefined ||
|
||||
option.name === null
|
||||
) {
|
||||
return "No Workflow Selected";
|
||||
}
|
||||
|
||||
const newname = (option.name.charAt(0).toUpperCase() + option.name.substring(1)).replaceAll("_", " ");
|
||||
|
||||
return newname;
|
||||
}}
|
||||
fullWidth
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
height: 50,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
}}
|
||||
onChange={(event, newValue) => {
|
||||
console.log("CLICK: ", newValue)
|
||||
//handleWorkflowSelectionUpdate({ target: { value: newValue} })
|
||||
//setSelectedWorkflows=
|
||||
//var newvalue = []
|
||||
//for (var key in newValue) {
|
||||
// if (newValue[key].id !== undefined) {
|
||||
// newvalue.push(newValue[key].id)
|
||||
// }
|
||||
//}
|
||||
|
||||
// Doing this way as you may want to remove some too
|
||||
for (var key in workflows) {
|
||||
if (!newValue.find(data => data.id === workflows[key].id)) {
|
||||
// Check if it has the one in it
|
||||
if (workflows[key]["usecase_ids"] !== undefined && workflows[key]["usecase_ids"] !== null && workflows[key]["usecase_ids"].includes(subcase.name)) {
|
||||
const filtered = workflows[key]["usecase_ids"].filter(data => data !== subcase.name)
|
||||
if (filtered !== undefined && filtered !== null) {
|
||||
//console.log("Removing: ", workflows[key].name, workflows[key])
|
||||
workflows[key]["usecase_ids"] = filtered
|
||||
|
||||
setWorkflow(workflows[key])
|
||||
}
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if (workflows[key]["usecase_ids"] === undefined || workflows[key]["usecase_ids"] === null) {
|
||||
workflows[key]["usecase_ids"] = [subcase.name]
|
||||
console.log("Setting: ", workflows[key].name)
|
||||
setWorkflow(workflows[key])
|
||||
|
||||
} else if (!workflows[key]["usecase_ids"].includes(subcase.name)) {
|
||||
workflows[key]["usecase_ids"].push(subcase.name)
|
||||
console.log("Adding: ", workflows[key].name)
|
||||
setWorkflow(workflows[key])
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
setWorkflows(workflows)
|
||||
console.log("New: ", newValue)
|
||||
setSelectedWorkflows(newValue)
|
||||
//setUpdate(Math.random())
|
||||
}}
|
||||
renderOption={(props, option) => {
|
||||
//console.log("In options?: ", props, option)
|
||||
|
||||
var newname = props.name
|
||||
if (newname === undefined || newname === null) {
|
||||
newname = "placeholder"
|
||||
}
|
||||
|
||||
if (newname.length > 2) {
|
||||
newname = newname.charAt(0).toUpperCase() + newname.substring(1)
|
||||
}
|
||||
return (
|
||||
<li {...props}>
|
||||
<Tooltip arrow placement="left" title={
|
||||
<span style={{}}>
|
||||
{props.image !== undefined && props.image !== null && props.image.length > 0 ?
|
||||
<img src={props.image} alt={newname} style={{backgroundColor: theme.palette.surfaceColor, maxHeight: 200, minHeigth: 200, borderRadius: theme.palette.borderRadius, }} />
|
||||
: null}
|
||||
<Typography>
|
||||
Choose {newname}
|
||||
</Typography>
|
||||
</span>
|
||||
} placement="bottom">
|
||||
<span>
|
||||
<Checkbox
|
||||
icon={<CheckBoxOutlineBlankIcon fontSize="small" />}
|
||||
checkedIcon={<CheckBoxIcon fontSize="small" />}
|
||||
style={{ marginRight: 8 }}
|
||||
checked={option.selected}
|
||||
/>
|
||||
{newname}
|
||||
</span>
|
||||
</Tooltip>
|
||||
</li>
|
||||
)
|
||||
}}
|
||||
renderInput={(params) => {
|
||||
return (
|
||||
<TextField
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
}}
|
||||
{...params}
|
||||
label="Find your workflows"
|
||||
variant="outlined"
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
: null}
|
||||
|
||||
{/*subcase.matches.length > 0 ?
|
||||
<Grid container style={{maxWidth: 325, marginTop: 10, }}>
|
||||
{subcase.matches.map((workflow, workflowindex) => {
|
||||
return (
|
||||
<Grid key={workflowindex} item index={workflowindex} xs={12}>
|
||||
@@ -528,13 +763,59 @@ const UsecaseListComponent = ({keys, isCloud, globalUrl, frameworkData, isLogged
|
||||
No workflow selected yet.
|
||||
</Typography>
|
||||
</div>
|
||||
}
|
||||
{isCloud !== false ?
|
||||
<div>
|
||||
<Typography variant="h6" style={{marginTop: 15, cursor: "pointer",}} onClick={() => {
|
||||
navigate("/search?tab=workflows&q="+subcase.name)
|
||||
}}>
|
||||
Public workflows
|
||||
*/}
|
||||
|
||||
{subcase.extra_buttons !== undefined && subcase.extra_buttons !== null && subcase.extra_buttons.length > 0 ?
|
||||
<div style={{marginTop: 25, }}>
|
||||
<Typography variant="body1" style={{marginTop: 0,}} onClick={() => {}}>
|
||||
Examples
|
||||
</Typography>
|
||||
<div style={{display: "flex"}}>
|
||||
{subcase.extra_buttons.map((subdata, index) => {
|
||||
var highlight = false
|
||||
var baseTypeInfo = subcase.type !== undefined ? subcase.type : "communication"
|
||||
if (frameworkData !== undefined && frameworkData !== null) {
|
||||
if (frameworkData[baseTypeInfo] !== undefined && frameworkData[baseTypeInfo] !== null && subdata.app !== undefined && subdata.app !== null) {
|
||||
if (frameworkData[baseTypeInfo].name !== undefined && frameworkData[baseTypeInfo].name.toLowerCase().replaceAll("_", " ") === subdata.app.toLowerCase().replaceAll("_", " ")) {
|
||||
highlight = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var marginTop = 6
|
||||
if (subdata.name.includes(" ") && subdata.name.length > 10) {
|
||||
marginTop = 0
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
key={index}
|
||||
href={subdata.link}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
style={{ textDecoration: "none", color: "rgba(255,255,255,0.7)", marginRight: 5, }}
|
||||
>
|
||||
<div style={{width: 160, display: "flex", borderRadius: theme.palette.borderRadius, cursor: "pointer", border: highlight ? "2px solid #f86a3e" : "1px solid rgba(255,255,255,0.7)", backgroundColor: theme.palette.inputColor, padding: "0px 0px 15px 15px", overflow: "hidden",}}>
|
||||
<img src={subdata.image} style={{width: 40, height: 40, borderRadius: theme.palette.borderRadius, marginTop: 15, }} />
|
||||
<Typography variant="body1" style={{lineHeight: "95%", marginLeft: 12, marginTop: marginTop === 0 ? 19 : 25, maxHeight: 34, }}>
|
||||
{subdata.name}
|
||||
</Typography>
|
||||
</div>
|
||||
</a>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
: null}
|
||||
<div style={{marginTop: 20}}>
|
||||
<a
|
||||
href={`https://shuffler.io/search?tab=workflows&q=${subcase.name}`}
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
style={{ textDecoration: "none", color: "white", marginRight: 5, }}
|
||||
>
|
||||
<Typography variant="body1" style={{marginTop: 15, cursor: "pointer",}} onClick={() => {}}>
|
||||
See other Public Workflows for {} <OpenInNewIcon style={{marginTop: 5, marginLeft: 15, }}/>
|
||||
</Typography>
|
||||
{/*
|
||||
<div>
|
||||
@@ -543,17 +824,17 @@ const UsecaseListComponent = ({keys, isCloud, globalUrl, frameworkData, isLogged
|
||||
</Typography>
|
||||
</div>
|
||||
*/}
|
||||
</div>
|
||||
: null}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div style={{
|
||||
height: 400,
|
||||
width: 400,
|
||||
height: 350,
|
||||
width: 350,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
border: "1px solid rgba(255,255,255,0.3)",
|
||||
}}>
|
||||
<DetectionFramework
|
||||
<AppFramework
|
||||
inputUsecase={inputUsecase}
|
||||
frameworkData={frameworkData}
|
||||
selectedOption={"Draw"}
|
||||
@@ -561,7 +842,7 @@ const UsecaseListComponent = ({keys, isCloud, globalUrl, frameworkData, isLogged
|
||||
isLoaded={true}
|
||||
isLoggedIn={true}
|
||||
globalUrl={globalUrl}
|
||||
size={0.7}
|
||||
size={0.6}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -726,10 +1007,63 @@ const Dashboard = (props) => {
|
||||
const [workflows, setWorkflows] = useState([]);
|
||||
const [frameworkData, setFrameworkData] = useState(undefined);
|
||||
|
||||
let navigate = useNavigate();
|
||||
const isCloud =
|
||||
window.location.host === "localhost:3002" ||
|
||||
window.location.host === "shuffler.io";
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedUsecaseCategory.length === 0) {
|
||||
setSelectedUsecases(usecases)
|
||||
} else {
|
||||
const foundUsecase = usecases.find(data => data.name === selectedUsecaseCategory)
|
||||
if (foundUsecase !== undefined && foundUsecase !== null) {
|
||||
setSelectedUsecases([foundUsecase])
|
||||
}
|
||||
}
|
||||
}, [selectedUsecaseCategory])
|
||||
|
||||
const checkSelectedParams = () => {
|
||||
const urlSearchParams = new URLSearchParams(window.location.search)
|
||||
const params = Object.fromEntries(urlSearchParams.entries())
|
||||
|
||||
const curpath = typeof window === "undefined" || window.location === undefined ? "" : window.location.pathname;
|
||||
const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search;
|
||||
|
||||
const foundQuery = params["selected"]
|
||||
if (foundQuery !== null && foundQuery !== undefined) {
|
||||
setSelectedUsecaseCategory(foundQuery)
|
||||
|
||||
const newitem = removeParam("selected", cursearch);
|
||||
navigate(curpath + newitem)
|
||||
}
|
||||
|
||||
const foundQuery2 = params["selected_object"]
|
||||
if (foundQuery2 !== null && foundQuery2 !== undefined) {
|
||||
console.log("Got selected_object: ", foundQuery2)
|
||||
|
||||
const queryName = foundQuery2.toLowerCase().replaceAll("_", " ")
|
||||
// Waiting a bit for it to render
|
||||
setTimeout(() => {
|
||||
const foundItem = document.getElementById(queryName)
|
||||
if (foundItem !== undefined && foundItem !== null) {
|
||||
foundItem.click()
|
||||
} else {
|
||||
//console.log("Couldn't find item with name ", queryName)
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (usecases.length > 0) {
|
||||
console.log(usecases)
|
||||
checkSelectedParams()
|
||||
}
|
||||
}, [usecases])
|
||||
|
||||
const getFramework = () => {
|
||||
fetch(globalUrl + "/api/v1/apps/frameworkConfiguration", {
|
||||
method: "GET",
|
||||
@@ -783,10 +1117,8 @@ const Dashboard = (props) => {
|
||||
.then((responseJson) => {
|
||||
fetchUsecases(responseJson)
|
||||
|
||||
console.log("Resp: ", responseJson)
|
||||
if (responseJson !== undefined) {
|
||||
//setWorkflows(responseJson);
|
||||
//fetchUsecases(responseJson)
|
||||
setWorkflows(responseJson);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -795,18 +1127,6 @@ const Dashboard = (props) => {
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
console.log("Changed: ", selectedUsecaseCategory)
|
||||
if (selectedUsecaseCategory.length === 0) {
|
||||
setSelectedUsecases(usecases)
|
||||
} else {
|
||||
const foundUsecase = usecases.find(data => data.name === selectedUsecaseCategory)
|
||||
if (foundUsecase !== undefined && foundUsecase !== null) {
|
||||
console.log("FOUND: ", foundUsecase)
|
||||
setSelectedUsecases([foundUsecase])
|
||||
}
|
||||
}
|
||||
}, [selectedUsecaseCategory])
|
||||
|
||||
document.title = "Shuffle - usecases";
|
||||
var dayGraphLabels = [60, 80, 65, 130, 80, 105, 90, 130, 70, 115, 60, 130];
|
||||
@@ -845,10 +1165,9 @@ const Dashboard = (props) => {
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
// Matching workflows with usecases
|
||||
if (responseJson.success !== false) {
|
||||
console.log("Usecases: ", responseJson)
|
||||
if (workflows !== undefined && workflows !== null && workflows.length > 0) {
|
||||
console.log("Got workflows: ", workflows)
|
||||
var categorydata = responseJson
|
||||
|
||||
var newcategories = []
|
||||
@@ -866,7 +1185,6 @@ const Dashboard = (props) => {
|
||||
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()) {
|
||||
console.log("Got match: ", workflow.usecase_ids[usecasekey])
|
||||
|
||||
category.matches.push({
|
||||
"workflow": workflow.id,
|
||||
@@ -888,7 +1206,6 @@ const Dashboard = (props) => {
|
||||
newcategories.push(category)
|
||||
}
|
||||
|
||||
console.log("Categories: ", newcategories)
|
||||
if (newcategories !== undefined && newcategories !== null && newcategories.length > 0) {
|
||||
handleKeysetting(newcategories)
|
||||
setUsecases(newcategories)
|
||||
@@ -1064,7 +1381,6 @@ const Dashboard = (props) => {
|
||||
});
|
||||
|
||||
if (firstRequest) {
|
||||
console.log("HELO");
|
||||
setFirstRequest(false);
|
||||
//start();
|
||||
//runUpdate();
|
||||
@@ -1213,6 +1529,8 @@ const Dashboard = (props) => {
|
||||
keys={selectedUsecases}
|
||||
isCloud={isCloud}
|
||||
globalUrl={globalUrl}
|
||||
workflows={workflows}
|
||||
setWorkflows={setWorkflows}
|
||||
/>
|
||||
|
||||
{treeKeys.length > 0 ?
|
||||
|
||||
@@ -0,0 +1,759 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useInterval } from "react-powerhooks";
|
||||
import { makeStyles, useTheme } from "@material-ui/core/styles";
|
||||
// nodejs library that concatenates classes
|
||||
import classNames from "classnames";
|
||||
import theme from '../theme';
|
||||
import { useNavigate, Link, useParams } from "react-router-dom";
|
||||
|
||||
// react plugin used to create charts
|
||||
//import { Line, Bar } from "react-chartjs-2";
|
||||
import { useAlert } from "react-alert";
|
||||
import Autocomplete from "@material-ui/lab/Autocomplete";
|
||||
import Draggable from "react-draggable";
|
||||
|
||||
import {
|
||||
Tooltip,
|
||||
TextField,
|
||||
IconButton,
|
||||
Button,
|
||||
Typography,
|
||||
Grid,
|
||||
Paper,
|
||||
Chip,
|
||||
Checkbox,
|
||||
} from "@material-ui/core";
|
||||
|
||||
import {
|
||||
Close as CloseIcon,
|
||||
DoneAll as DoneAllIcon,
|
||||
Description as DescriptionIcon,
|
||||
PlayArrow as PlayArrowIcon,
|
||||
Edit as EditIcon,
|
||||
CheckBox as CheckBoxIcon,
|
||||
CheckBoxOutlineBlank as CheckBoxOutlineBlankIcon,
|
||||
OpenInNew as OpenInNewIcon,
|
||||
} from "@material-ui/icons";
|
||||
|
||||
import WorkflowPaper from "../components/WorkflowPaper.jsx"
|
||||
import { removeParam } from "../views/AngularWorkflow.jsx"
|
||||
|
||||
// core components
|
||||
//import {
|
||||
// chartExample1,
|
||||
// chartExample2,
|
||||
// chartExample3,
|
||||
// chartExample4,
|
||||
//} from "../charts.js";
|
||||
|
||||
import {
|
||||
RadialBarChart,
|
||||
RadialAreaChart,
|
||||
RadialAxis,
|
||||
StackedBarSeries,
|
||||
TooltipArea,
|
||||
ChartTooltip,
|
||||
TooltipTemplate,
|
||||
RadialAreaSeries,
|
||||
RadialPointSeries,
|
||||
RadialArea,
|
||||
RadialLine,
|
||||
TreeMap,
|
||||
TreeMapSeries,
|
||||
TreeMapLabel,
|
||||
TreeMapRect,
|
||||
Line,
|
||||
LineChart,
|
||||
LineSeries,
|
||||
LinearYAxis,
|
||||
LinearXAxis,
|
||||
LinearYAxisTickSeries,
|
||||
LinearXAxisTickSeries,
|
||||
AreaChart,
|
||||
AreaSeries,
|
||||
PointSeries,
|
||||
} from 'reaviz';
|
||||
|
||||
const useStyles = makeStyles({
|
||||
notchedOutline: {
|
||||
borderColor: "#f85a3e !important",
|
||||
},
|
||||
root: {
|
||||
"& .MuiAutocomplete-listbox": {
|
||||
border: "2px solid #f85a3e",
|
||||
color: "white",
|
||||
fontSize: 18,
|
||||
"& li:nth-child(even)": {
|
||||
backgroundColor: "#CCC",
|
||||
},
|
||||
"& li:nth-child(odd)": {
|
||||
backgroundColor: "#FFF",
|
||||
},
|
||||
},
|
||||
},
|
||||
inputRoot: {
|
||||
color: "white",
|
||||
// This matches the specificity of the default styles at https://github.com/mui-org/material-ui/blob/v4.11.3/packages/material-ui-lab/src/Autocomplete/Autocomplete.js#L90
|
||||
"&:hover .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: "#f86a3e",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const inputdata = [
|
||||
{
|
||||
"key": "Threat Intel",
|
||||
"value": 18,
|
||||
"x": "2020-02-17T08:00:00.000Z",
|
||||
"x0": "2020-02-17T08:00:00.000Z",
|
||||
"x1": "2020-02-17T08:00:00.000Z",
|
||||
"y": 18,
|
||||
"y0": 0,
|
||||
"y1": 18
|
||||
},
|
||||
{
|
||||
"key": "Threat Intel",
|
||||
"value": 3,
|
||||
"x": "2020-02-21T08:00:00.000Z",
|
||||
"x0": "2020-02-21T08:00:00.000Z",
|
||||
"x1": "2020-02-21T08:00:00.000Z",
|
||||
"y": 3,
|
||||
"y0": 0,
|
||||
"y1": 3
|
||||
},
|
||||
{
|
||||
"key": "Threat Intel",
|
||||
"value": 14,
|
||||
"x": "2020-02-26T08:00:00.000Z",
|
||||
"x0": "2020-02-26T08:00:00.000Z",
|
||||
"x1": "2020-02-26T08:00:00.000Z",
|
||||
"y": 14,
|
||||
"y0": 0,
|
||||
"y1": 14
|
||||
},
|
||||
{
|
||||
"key": "Threat Intel",
|
||||
"value": 18,
|
||||
"x": "2020-02-29T08:00:00.000Z",
|
||||
"x0": "2020-02-29T08:00:00.000Z",
|
||||
"x1": "",
|
||||
"y": 18,
|
||||
"y0": 0,
|
||||
"y1": 18
|
||||
}
|
||||
]
|
||||
|
||||
const LineChartWrapper = ({keys, height, width}) => {
|
||||
const [hovered, setHovered] = useState("");
|
||||
|
||||
//console.log("Date: ", new Date("2019-11-14T08:00:00.000Z"))
|
||||
console.log("Keys: ", keys)
|
||||
var inputdata = keys.data
|
||||
|
||||
/*
|
||||
const inputdata = [{
|
||||
"key": "Intel",
|
||||
"data": [
|
||||
{ key: new Date('11/22/2019'), data: 3, metadata: {color: "orange", "name": "Intel"}},
|
||||
{ key: new Date('11/24/2019'), data: 8, metadata: {color: "orange", "name": "Intel"}},
|
||||
{ key: new Date('11/29/2019'), data: 2, metadata: {color: "orange", "name": "Intel"}},
|
||||
]},
|
||||
{
|
||||
"key": "Popper",
|
||||
"data": [
|
||||
{ key: new Date('11/24/2019'), data: 9, },
|
||||
{ key: new Date('11/29/2019'), data: 3, },
|
||||
]
|
||||
}
|
||||
]
|
||||
*/
|
||||
|
||||
return (
|
||||
<div style={{}}>
|
||||
<Typography variant="h6" style={{marginBotton: 15}}>
|
||||
{keys.title}
|
||||
</Typography>
|
||||
<AreaChart
|
||||
style={{marginTop: 15}}
|
||||
height={height}
|
||||
width={width}
|
||||
data={inputdata}
|
||||
series={
|
||||
<AreaSeries
|
||||
type="grouped"
|
||||
symbols={
|
||||
<PointSeries show={true} />
|
||||
}
|
||||
colorScheme={(colorInput) => {
|
||||
var color = "cybertron"
|
||||
if (colorInput !== undefined && colorInput.length > 0) {
|
||||
color = colorInput[0].metadata !== undefined && colorInput[0].metadata.color !== undefined ? colorInput[0].metadata.color : color
|
||||
}
|
||||
|
||||
return color
|
||||
}}
|
||||
tooltip={
|
||||
<TooltipArea
|
||||
color={"#000000"}
|
||||
style={{
|
||||
backgroundColor: "red",
|
||||
}}
|
||||
isRadial={true}
|
||||
onValueEnter={(event) => {
|
||||
if (hovered !== event.value.x) {
|
||||
//setHovered(event.value.x)
|
||||
}
|
||||
}}
|
||||
tooltip={
|
||||
<ChartTooltip
|
||||
followCursor={true}
|
||||
modifiers={{
|
||||
offset: '5px, 5px'
|
||||
}}
|
||||
content={(data, color) => {
|
||||
console.log("DATA: ", data)
|
||||
const name = data.metadata !== undefined && data.metadata.name !== undefined ? data.metadata.name : "No"
|
||||
|
||||
return (
|
||||
<div style={{borderRadius: theme.palette.borderRadius, backgroundColor: theme.palette.inputColor, border: "1px solid rgba(255,255,255,0.3)", color: "white", padding: 5, cursor: "pointer",}}>
|
||||
<Typography variant="body1">
|
||||
{name}
|
||||
</Typography>
|
||||
</div>
|
||||
)
|
||||
/*
|
||||
<TooltipTemplate
|
||||
color={"#ffffff"}
|
||||
value={{
|
||||
x: data.x,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
*/
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const RadialChart = ({keys, setSelectedCategory}) => {
|
||||
const [hovered, setHovered] = useState("");
|
||||
|
||||
return (
|
||||
<div style={{cursor: "pointer",}} onClick={() => {
|
||||
console.log("Click: ", hovered)
|
||||
if (setSelectedCategory !== undefined) {
|
||||
setSelectedCategory(hovered)
|
||||
}
|
||||
}}>
|
||||
<RadialAreaChart
|
||||
id="workflow_categories"
|
||||
height={500}
|
||||
width={500}
|
||||
data={keys}
|
||||
axis={<RadialAxis type="category" />}
|
||||
series={
|
||||
<RadialAreaSeries
|
||||
interpolation="smooth"
|
||||
colorScheme={(colorInput) => {
|
||||
return '#f86a3e'
|
||||
}}
|
||||
animated={false}
|
||||
id="workflow_series_id"
|
||||
style={{cursor: "pointer",}}
|
||||
line={
|
||||
<RadialLine
|
||||
color={"#000000"}
|
||||
data={(data, color) => {
|
||||
console.log("INFO: ", data, color)
|
||||
return (
|
||||
null
|
||||
)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
tooltip={
|
||||
<TooltipArea
|
||||
color={"#000000"}
|
||||
style={{
|
||||
backgroundColor: "red",
|
||||
}}
|
||||
isRadial={true}
|
||||
onValueEnter={(event) => {
|
||||
if (hovered !== event.value.x) {
|
||||
setHovered(event.value.x)
|
||||
}
|
||||
}}
|
||||
tooltip={
|
||||
<ChartTooltip
|
||||
followCursor={true}
|
||||
modifiers={{
|
||||
offset: '5px, 5px'
|
||||
}}
|
||||
content={(data, color) => {
|
||||
return (
|
||||
<div style={{borderRadius: theme.palette.borderRadius, backgroundColor: theme.palette.inputColor, border: "1px solid rgba(255,255,255,0.3)", color: "white", padding: 5, cursor: "pointer",}}>
|
||||
<Typography variant="body1">
|
||||
{data.x}
|
||||
</Typography>
|
||||
</div>
|
||||
)
|
||||
/*
|
||||
<TooltipTemplate
|
||||
color={"#ffffff"}
|
||||
value={{
|
||||
x: data.x,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
*/
|
||||
}
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
//axis={<RadialAxis type="category" />}
|
||||
}
|
||||
|
||||
// This is the start of a dashboard that can be used.
|
||||
// What data do we fill in here? Idk
|
||||
const Dashboard = (props) => {
|
||||
const { globalUrl, isLoggedIn } = props;
|
||||
const alert = useAlert();
|
||||
const [bigChartData, setBgChartData] = useState("data1");
|
||||
const [dayAmount, setDayAmount] = useState(7);
|
||||
const [firstRequest, setFirstRequest] = useState(true);
|
||||
const [stats, setStats] = useState({});
|
||||
const [changeme, setChangeme] = useState("");
|
||||
const [statsRan, setStatsRan] = useState(false);
|
||||
const [keys, setKeys] = useState([])
|
||||
const [treeKeys, setTreeKeys] = useState([])
|
||||
|
||||
const [selectedUsecaseCategory, setSelectedUsecaseCategory] = useState("");
|
||||
const [selectedUsecases, setSelectedUsecases] = useState([]);
|
||||
const [usecases, setUsecases] = useState([]);
|
||||
const [workflows, setWorkflows] = useState([]);
|
||||
const [frameworkData, setFrameworkData] = useState(undefined);
|
||||
|
||||
const [widgetData, setWidgetData] = useState([]);
|
||||
|
||||
let navigate = useNavigate();
|
||||
const isCloud =
|
||||
window.location.host === "localhost:3002" ||
|
||||
window.location.host === "shuffler.io";
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedUsecaseCategory.length === 0) {
|
||||
setSelectedUsecases(usecases)
|
||||
} else {
|
||||
const foundUsecase = usecases.find(data => data.name === selectedUsecaseCategory)
|
||||
if (foundUsecase !== undefined && foundUsecase !== null) {
|
||||
setSelectedUsecases([foundUsecase])
|
||||
}
|
||||
}
|
||||
}, [selectedUsecaseCategory])
|
||||
|
||||
const checkSelectedParams = () => {
|
||||
const urlSearchParams = new URLSearchParams(window.location.search)
|
||||
const params = Object.fromEntries(urlSearchParams.entries())
|
||||
|
||||
const curpath = typeof window === "undefined" || window.location === undefined ? "" : window.location.pathname;
|
||||
const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search;
|
||||
|
||||
const foundQuery = params["selected"]
|
||||
if (foundQuery !== null && foundQuery !== undefined) {
|
||||
setSelectedUsecaseCategory(foundQuery)
|
||||
|
||||
const newitem = removeParam("selected", cursearch);
|
||||
navigate(curpath + newitem)
|
||||
}
|
||||
|
||||
const foundQuery2 = params["selected_object"]
|
||||
if (foundQuery2 !== null && foundQuery2 !== undefined) {
|
||||
console.log("Got selected_object: ", foundQuery2)
|
||||
|
||||
const queryName = foundQuery2.toLowerCase().replaceAll("_", " ")
|
||||
// Waiting a bit for it to render
|
||||
setTimeout(() => {
|
||||
const foundItem = document.getElementById(queryName)
|
||||
if (foundItem !== undefined && foundItem !== null) {
|
||||
foundItem.click()
|
||||
} else {
|
||||
//console.log("Couldn't find item with name ", queryName)
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (usecases.length > 0) {
|
||||
console.log(usecases)
|
||||
checkSelectedParams()
|
||||
}
|
||||
}, [usecases])
|
||||
|
||||
const getWidget = (dashboard, widget) => {
|
||||
fetch(`${globalUrl}/api/v1/dashboards/${dashboard}/widgets/${widget}`, {
|
||||
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) => {
|
||||
console.log("Resp: ", responseJson)
|
||||
if (responseJson.success === false) {
|
||||
if (responseJson.reason !== undefined) {
|
||||
//alert.error("Failed loading: " + responseJson.reason)
|
||||
} else {
|
||||
//alert.error("Failed to load framework for your org.")
|
||||
}
|
||||
} else {
|
||||
var tmpdata = responseJson
|
||||
for (var key in tmpdata.data) {
|
||||
for (var subkey in tmpdata.data[key].data) {
|
||||
tmpdata.data[key].data[subkey].key = new Date(tmpdata.data[key].data[subkey].key)
|
||||
}
|
||||
}
|
||||
|
||||
const foundWidget = widgetData.findIndex(data => data.title === widget)
|
||||
console.log("Found: ", foundWidget)
|
||||
if (foundWidget !== undefined && foundWidget !== null && foundWidget >= 0) {
|
||||
widgetData[foundWidget] = tmpdata
|
||||
} else {
|
||||
widgetData.push(tmpdata)
|
||||
}
|
||||
|
||||
console.log("Data: ", widgetData)
|
||||
setWidgetData(widgetData)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
//alert.error(error.toString());
|
||||
})
|
||||
}
|
||||
|
||||
document.title = "Shuffle - Dashboard";
|
||||
var dayGraphLabels = [60, 80, 65, 130, 80, 105, 90, 130, 70, 115, 60, 130];
|
||||
var dayGraphData = [60, 80, 65, 130, 80, 105, 90, 130, 70, 115, 60, 130];
|
||||
|
||||
const handleKeysetting = (categorydata) => {
|
||||
var allCategories = []
|
||||
var treeCategories = []
|
||||
for (key in categorydata) {
|
||||
const category = categorydata[key]
|
||||
allCategories.push({"key": category.name, "data": category.list.length, "color": category.color})
|
||||
treeCategories.push({"key": category.name, "data": 100, "color": category.color,})
|
||||
for (var subkey in category.list) {
|
||||
treeCategories.push({"key": category.list[subkey].name, "data": 20, "color": category.color})
|
||||
}
|
||||
}
|
||||
|
||||
setKeys(allCategories)
|
||||
setTreeKeys(treeCategories)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getWidget("main", "Overall")
|
||||
getWidget("main", "Overall2")
|
||||
}, []);
|
||||
|
||||
const fetchdata = (stats_id) => {
|
||||
fetch(globalUrl + "/api/v1/stats/" + stats_id, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status !== 200) {
|
||||
console.log("Status not 200 for " + stats_id);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
stats[stats_id] = responseJson;
|
||||
setStats(stats);
|
||||
// Used to force updates
|
||||
setChangeme(stats_id);
|
||||
})
|
||||
.catch((error) => {
|
||||
//alert.error("ERROR: " + error.toString());
|
||||
console.log("ERROR: " + error.toString());
|
||||
});
|
||||
};
|
||||
|
||||
let chart1_2_options = {
|
||||
maintainAspectRatio: false,
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
tooltips: {
|
||||
backgroundColor: "#f5f5f5",
|
||||
titleFontColor: "#333",
|
||||
bodyFontColor: "#666",
|
||||
bodySpacing: 4,
|
||||
xPadding: 12,
|
||||
mode: "nearest",
|
||||
intersect: 0,
|
||||
position: "nearest",
|
||||
},
|
||||
responsive: true,
|
||||
scales: {
|
||||
yAxes: [
|
||||
{
|
||||
barPercentage: 1.6,
|
||||
gridLines: {
|
||||
drawBorder: false,
|
||||
color: "rgba(29,140,248,0.0)",
|
||||
zeroLineColor: "transparent",
|
||||
},
|
||||
ticks: {
|
||||
suggestedMin: 60,
|
||||
suggestedMax: 125,
|
||||
padding: 20,
|
||||
fontColor: "#9a9a9a",
|
||||
},
|
||||
},
|
||||
],
|
||||
xAxes: [
|
||||
{
|
||||
barPercentage: 1.6,
|
||||
gridLines: {
|
||||
drawBorder: false,
|
||||
color: "rgba(29,140,248,0.1)",
|
||||
zeroLineColor: "transparent",
|
||||
},
|
||||
ticks: {
|
||||
padding: 20,
|
||||
fontColor: "#9a9a9a",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const dayGraph = {
|
||||
data: (canvas) => {
|
||||
let ctx = canvas.getContext("2d");
|
||||
|
||||
let gradientStroke = ctx.createLinearGradient(0, 230, 0, 50);
|
||||
|
||||
gradientStroke.addColorStop(1, "rgba(29,140,248,0.2)");
|
||||
gradientStroke.addColorStop(0.4, "rgba(29,140,248,0.0)");
|
||||
gradientStroke.addColorStop(0, "rgba(29,140,248,0)"); //blue colors
|
||||
|
||||
return {
|
||||
labels: dayGraphLabels,
|
||||
datasets: [
|
||||
{
|
||||
label: "My First dataset",
|
||||
fill: true,
|
||||
backgroundColor: gradientStroke,
|
||||
borderColor: "#1f8ef1",
|
||||
borderWidth: 2,
|
||||
borderDash: [],
|
||||
borderDashOffset: 0.0,
|
||||
pointBackgroundColor: "#1f8ef1",
|
||||
pointBorderColor: "rgba(255,255,255,0)",
|
||||
pointHoverBackgroundColor: "#1f8ef1",
|
||||
pointBorderWidth: 20,
|
||||
pointHoverRadius: 4,
|
||||
pointHoverBorderWidth: 15,
|
||||
pointRadius: 4,
|
||||
data: dayGraphData,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
options: chart1_2_options,
|
||||
};
|
||||
|
||||
// All these are currently tracked.
|
||||
const variables = [
|
||||
"backend_executions",
|
||||
"workflow_executions",
|
||||
"workflow_executions_aborted",
|
||||
"workflow_executions_success",
|
||||
"total_apps_created",
|
||||
"total_apps_loaded",
|
||||
"openapi_apps_created",
|
||||
"total_apps_deleted",
|
||||
"total_webhooks_ran",
|
||||
"total_workflows",
|
||||
"total_workflow_actions",
|
||||
"total_workflow_triggers",
|
||||
];
|
||||
|
||||
const runUpdate = () => {
|
||||
for (var key in variables) {
|
||||
fetchdata(variables[key]);
|
||||
}
|
||||
};
|
||||
|
||||
// Refresh every 60 seconds
|
||||
const autoUpdate = 60000;
|
||||
const { start, stop } = useInterval({
|
||||
duration: autoUpdate,
|
||||
startImmediate: false,
|
||||
callback: () => {
|
||||
runUpdate();
|
||||
},
|
||||
});
|
||||
|
||||
if (firstRequest) {
|
||||
setFirstRequest(false);
|
||||
//start();
|
||||
//runUpdate();
|
||||
} else if (!statsRan) {
|
||||
// FIXME: Run this under runUpdate schedule?
|
||||
// 1. Fix labels in dayGraphy.data
|
||||
// 2. Add data to the daygraph
|
||||
|
||||
// Every time there's an update :)
|
||||
|
||||
// This should probably be done in the backend.. bleh
|
||||
if (
|
||||
stats["workflow_executions"] !== undefined &&
|
||||
stats["workflow_executions"] !== null &&
|
||||
stats["workflow_executions"].data !== undefined
|
||||
) {
|
||||
setStatsRan(true);
|
||||
//console.log("NEW DATA?: ", stats)
|
||||
console.log("SET WORKFLOW: ", stats["workflow_executions"]);
|
||||
//var curday = startDate.getDate()
|
||||
|
||||
// Index = what day are we on
|
||||
|
||||
// 0 = today
|
||||
var newDayGraphLabels = [];
|
||||
var newDayGraphData = [];
|
||||
for (var i = dayAmount; i > 0; i--) {
|
||||
var enddate = new Date();
|
||||
enddate.setDate(-i);
|
||||
enddate.setHours(23, 59, 59, 999);
|
||||
|
||||
var startdate = new Date();
|
||||
startdate.setDate(-i);
|
||||
startdate.setHours(0, 0, 0, 0);
|
||||
|
||||
var endtime = enddate.getTime() / 1000;
|
||||
var starttime = startdate.getTime() / 1000;
|
||||
|
||||
console.log(
|
||||
"START: ",
|
||||
starttime,
|
||||
"END: ",
|
||||
endtime,
|
||||
"Data: ",
|
||||
stats["workflow_executions"]
|
||||
);
|
||||
for (var key in stats["workflow_executions"].data) {
|
||||
const item = stats["workflow_executions"]["data"][key];
|
||||
console.log("ITEM: ", item.timestamp, endtime);
|
||||
console.log(endtime - starttime);
|
||||
if (
|
||||
endtime - starttime > endtime - item.timestamp &&
|
||||
endtime.timestamp >= 0
|
||||
) {
|
||||
console.log("HIT? ");
|
||||
}
|
||||
console.log(item.timestamp - endtime);
|
||||
//console.log(item.timestamp-endtime)
|
||||
break;
|
||||
if (item.timestamp > endtime && item.timestamp < starttime) {
|
||||
if (newDayGraphData[i - 1] === undefined) {
|
||||
newDayGraphData[i - 1] = 1;
|
||||
} else {
|
||||
newDayGraphData[i - 1] += 1;
|
||||
}
|
||||
|
||||
//break
|
||||
}
|
||||
}
|
||||
|
||||
newDayGraphLabels.push(i);
|
||||
}
|
||||
|
||||
console.log(newDayGraphLabels);
|
||||
console.log(newDayGraphData);
|
||||
}
|
||||
}
|
||||
|
||||
const newdata =
|
||||
Object.getOwnPropertyNames(stats).length > 0 ? (
|
||||
<div>
|
||||
Autoupdate every {autoUpdate / 1000} seconds
|
||||
{variables.map((data) => {
|
||||
if (stats[data] === undefined || stats[data] === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (stats[data].total === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{data}: {stats[data].total}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
const data = (
|
||||
<div className="content" style={{width: 1000, margin: "auto", paddingBottom: 200, textAlign: "center",}}>
|
||||
<div style={{width: 500, margin: "auto"}}>
|
||||
{keys.length > 0 ?
|
||||
<span>
|
||||
<RadialChart keys={keys} setSelectedCategory={setSelectedUsecaseCategory} />
|
||||
</span>
|
||||
: null}
|
||||
</div>
|
||||
|
||||
{widgetData === undefined || widgetData === null || widgetData === [] || widgetData.length === 0 ? null :
|
||||
<Draggable>
|
||||
<Paper style={{height: 350, width: 500, padding: "15px 15px 15px 15px", }}>
|
||||
<LineChartWrapper keys={widgetData[0]} height={280} width={470} />
|
||||
</Paper>
|
||||
</Draggable>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
|
||||
const dataWrapper = (
|
||||
<div style={{ maxWidth: 1366, margin: "auto" }}>{data}</div>
|
||||
);
|
||||
|
||||
return dataWrapper;
|
||||
};
|
||||
|
||||
export default Dashboard;
|
||||
+146
-117
@@ -4,6 +4,7 @@ import { useTheme } from "@material-ui/core/styles";
|
||||
import ReactMarkdown from "react-markdown";
|
||||
import { BrowserView, MobileView } from "react-device-detect";
|
||||
import { useParams, useNavigate, Link } from "react-router-dom";
|
||||
import { isMobile } from "react-device-detect";
|
||||
|
||||
import {
|
||||
Grid,
|
||||
@@ -25,9 +26,10 @@ import {
|
||||
} from "@material-ui/icons";
|
||||
|
||||
const Body = {
|
||||
maxWidth: 1000,
|
||||
minWidth: 768,
|
||||
margin: "auto",
|
||||
//maxWidth: 1000,
|
||||
//minWidth: 768,
|
||||
maxWidth: "100%",
|
||||
minWidth: "100%",
|
||||
display: "flex",
|
||||
height: "100%",
|
||||
color: "white",
|
||||
@@ -52,14 +54,15 @@ const innerHrefStyle = {
|
||||
};
|
||||
|
||||
const Docs = (defaultprops) => {
|
||||
const { globalUrl, selectedDoc, serverside, isMobile } = defaultprops;
|
||||
const { globalUrl, selectedDoc, serverside, serverMobile } = defaultprops;
|
||||
|
||||
let navigate = useNavigate();
|
||||
const theme = useTheme();
|
||||
|
||||
// Quickfix for react router 5 -> 6
|
||||
const params = useParams();
|
||||
var props = JSON.parse(JSON.stringify(defaultprops))
|
||||
//var props = JSON.parse(JSON.stringify(defaultprops))
|
||||
var props = Object.assign({selected: false}, defaultprops);
|
||||
props.match = {}
|
||||
props.match.params = params
|
||||
|
||||
@@ -71,7 +74,7 @@ const Docs = (defaultprops) => {
|
||||
}, [])
|
||||
//console.log("PARAMS: ", params)
|
||||
|
||||
const [mobile, setMobile] = useState(isMobile === true ? true : false);
|
||||
const [mobile, setMobile] = useState(serverMobile === true || isMobile === true ? true : false);
|
||||
const [data, setData] = useState("");
|
||||
const [firstrequest, setFirstrequest] = useState(true);
|
||||
const [list, setList] = useState([]);
|
||||
@@ -102,16 +105,19 @@ const Docs = (defaultprops) => {
|
||||
padding: 30,
|
||||
paddingTop: 15,
|
||||
marginTop: 15,
|
||||
minHeight: "50vh",
|
||||
minHeight: "80vh",
|
||||
//height: "50vh",
|
||||
};
|
||||
|
||||
const SideBar = {
|
||||
maxWidth: 250,
|
||||
flex: 1,
|
||||
position: "sticky",
|
||||
top: 100,
|
||||
maxHeight: "83vh",
|
||||
minWidth: 250,
|
||||
maxWidth: 300,
|
||||
borderRight: "1px solid rgba(255,255,255,0.3)",
|
||||
left: 0,
|
||||
position: "sticky",
|
||||
top: 50,
|
||||
minHeight: "90vh",
|
||||
maxHeight: "90vh",
|
||||
overflowX: "hidden",
|
||||
overflowY: "auto",
|
||||
zIndex: 1000,
|
||||
@@ -151,7 +157,11 @@ const Docs = (defaultprops) => {
|
||||
.then((responseJson) => {
|
||||
if (responseJson.success) {
|
||||
setData(responseJson.reason);
|
||||
document.title = "Shuffle " + docId + " documentation";
|
||||
if (docId === undefined) {
|
||||
document.title = "Shuffle documentation introduction";
|
||||
} else {
|
||||
document.title = "Shuffle " + docId + " documentation";
|
||||
}
|
||||
|
||||
if (responseJson.reason !== undefined && responseJson.reason !== null && responseJson.reason.includes("404: Not Found")) {
|
||||
navigate("/docs")
|
||||
@@ -244,6 +254,7 @@ const Docs = (defaultprops) => {
|
||||
if (props.match.params.key === undefined) {
|
||||
|
||||
} else {
|
||||
console.log("DOCID: ", props.match.params.key)
|
||||
fetchDocs(props.match.params.key)
|
||||
}
|
||||
}
|
||||
@@ -271,7 +282,8 @@ const Docs = (defaultprops) => {
|
||||
.split("_")
|
||||
.join(" ")
|
||||
.split("-")
|
||||
.join(" ");
|
||||
.join(" ")
|
||||
.split("?")[0]
|
||||
|
||||
//console.log(name)
|
||||
var found = false;
|
||||
@@ -343,11 +355,12 @@ const Docs = (defaultprops) => {
|
||||
|
||||
const markdownStyle = {
|
||||
color: "rgba(255, 255, 255, 0.65)",
|
||||
flex: "1",
|
||||
maxWidth: mobile ? "100%" : 750,
|
||||
overflow: "hidden",
|
||||
paddingBottom: 100,
|
||||
marginLeft: mobile ? 0 : 50,
|
||||
margin: "auto",
|
||||
maxWidth: "100%",
|
||||
minWidth: "100%",
|
||||
overflow: "hidden",
|
||||
};
|
||||
|
||||
function OuterLink(props) {
|
||||
@@ -412,7 +425,7 @@ const Docs = (defaultprops) => {
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
<div style={{ flex: 3, display: "flex", vAlign: "center" }}>
|
||||
<div style={{ flex: 3, display: "flex", vAlign: "center", position: "sticky", top: 50, }}>
|
||||
{mobile ? null : (
|
||||
<Typography style={{ display: "inline", marginTop: 6 }}>
|
||||
<a
|
||||
@@ -533,7 +546,20 @@ const Docs = (defaultprops) => {
|
||||
target="_blank"
|
||||
style={{ textDecoration: "none", color: "inherit", flex: 1, margin: 10, }}
|
||||
>
|
||||
<div style={{cursor: hover ? "pointer" : "default", borderRadius: theme.palette.borderRadius, flex: 1, border: "1px solid rgba(255,255,255,0.3)", backgroundColor: hover ? theme.palette.surfaceColor : theme.palette.inputColor, padding: 25, }} onMouseOver={() => {
|
||||
<div style={{cursor: hover ? "pointer" : "default", borderRadius: theme.palette.borderRadius, flex: 1, border: "1px solid rgba(255,255,255,0.3)", backgroundColor: hover ? theme.palette.surfaceColor : theme.palette.inputColor, padding: 25, }}
|
||||
onClick={(event) => {
|
||||
if (link === "" || link === undefined) {
|
||||
event.preventDefault()
|
||||
console.log("IN CLICK!")
|
||||
if (window.drift !== undefined) {
|
||||
window.drift.api.startInteraction({ interactionId: 340043 })
|
||||
} else {
|
||||
console.log("Couldn't find drift in window.drift and not .drift-open-chat with querySelector: ", window.drift)
|
||||
}
|
||||
} else {
|
||||
console.log("Link defined: ", link)
|
||||
}
|
||||
}} onMouseOver={() => {
|
||||
setHover(true)
|
||||
}}
|
||||
onMouseOut={() => {
|
||||
@@ -586,105 +612,105 @@ const Docs = (defaultprops) => {
|
||||
<div style={{
|
||||
color: "rgba(255, 255, 255, 0.65)",
|
||||
flex: "1",
|
||||
maxWidth: mobile ? "100%" : 750,
|
||||
overflow: "hidden",
|
||||
paddingBottom: 100,
|
||||
marginLeft: mobile ? 0 : 50,
|
||||
marginTop: 50,
|
||||
textAlign: "center",
|
||||
maxWidth: 500,
|
||||
margin: "auto",
|
||||
marginTop: 50,
|
||||
}}>
|
||||
<Typography variant="h4" style={{textAlign: "center",}}>
|
||||
Documentation
|
||||
</Typography>
|
||||
<div style={{display: "flex", marginTop: 25, }}>
|
||||
<CustomButton title="Open a Ticket" icon=<img src="/images/Shuffle_logo_new.png" style={{height: 35, width: 35, border: "", borderRadius: theme.palette.borderRadius, }} /> link="https://support.shuffler.io" />
|
||||
<CustomButton title="Ask the community" icon=<img src="/images/social/discord.png" style={{height: 35, width: 35, border: "", borderRadius: theme.palette.borderRadius, }} /> link="https://discord.gg/B2CBzUm" />
|
||||
</div>
|
||||
|
||||
<div style={{textAlign: "left"}}>
|
||||
<Typography variant="h6" style={headerStyle} >Tutorial</Typography>
|
||||
<Typography variant="body1">
|
||||
<b>Dive in.</b> Hands-on is the best approach to see how Shuffle can transform your security operations. Our set of tutorials and videos teach you how to build your skills. Check out the <Link to="/docs/getting-started" style={hrefStyle2}>getting started</Link> section to give it a go!
|
||||
<Typography variant="h4" style={{textAlign: "center",}}>
|
||||
Documentation
|
||||
</Typography>
|
||||
<div style={{display: "flex", marginTop: 25, }}>
|
||||
<CustomButton title="Talk to Support" icon=<img src="/images/Shuffle_logo_new.png" style={{height: 35, width: 35, border: "", borderRadius: theme.palette.borderRadius, }} /> />
|
||||
<CustomButton title="Ask the community" icon=<img src="/images/social/discord.png" style={{height: 35, width: 35, border: "", borderRadius: theme.palette.borderRadius, }} /> link="https://discord.gg/B2CBzUm" />
|
||||
</div>
|
||||
|
||||
<Typography variant="h6" style={headerStyle}>Why Shuffle?</Typography>
|
||||
<Typography variant="body1">
|
||||
<b>Security first.</b> We incentivize trying before buying, and give you the full set of tools you need to automate your operations. What's more is we also help you <a href="https://shuffler.io/pricing?tag=docs" target="_blank" style={hrefStyle2}>find usecases</a> that fit your your unique needs. Accessibility is key, and we intend to help every SOC globally use and share their usecases.
|
||||
</Typography>
|
||||
<div style={{textAlign: "left"}}>
|
||||
<Typography variant="h6" style={headerStyle} >Tutorial</Typography>
|
||||
<Typography variant="body1">
|
||||
<b>Dive in.</b> Hands-on is the best approach to see how Shuffle can transform your security operations. Our set of tutorials and videos teach you how to build your skills. Check out the <Link to="/docs/getting-started" style={hrefStyle2}>getting started</Link> section to give it a go!
|
||||
</Typography>
|
||||
|
||||
<Typography variant="h6" style={headerStyle}>Get help</Typography>
|
||||
<Typography variant="body1">
|
||||
<b>Our promise</b> is to make it easier and easier to automate your operations. In some cases however, it may be good with a helping hand. That's where <a href="https://shuffler.io/pricing?tag=docs" target="_blank" style={hrefStyle2}>Shuffle's consultancy and support</a> services come in handy. We help you build and automate your operational processes to a level you haven't seen before with the help of our <a href="https://shuffler.io/usecases?tag=docs" target="_blank" style={hrefStyle2}>usecases</a>.
|
||||
</Typography>
|
||||
<Typography variant="h6" style={headerStyle}>Why Shuffle?</Typography>
|
||||
<Typography variant="body1">
|
||||
<b>Security first.</b> We incentivize trying before buying, and give you the full set of tools you need to automate your operations. What's more is we also help you <a href="https://shuffler.io/pricing?tag=docs" target="_blank" style={hrefStyle2}>find usecases</a> that fit your your unique needs. Accessibility is key, and we intend to help every SOC globally use and share their usecases.
|
||||
</Typography>
|
||||
|
||||
<Typography variant="h6" style={headerStyle}>APIs</Typography>
|
||||
<Typography variant="body1">
|
||||
<b>Learn.</b> We're all about learning, and are continuously creating documentation and video tutorials to better understand how to get started. APIs are an extremely important part of how the internet works today, and our goal is helping every security professional learn about them.
|
||||
</Typography>
|
||||
<Typography variant="h6" style={headerStyle}>Get help</Typography>
|
||||
<Typography variant="body1">
|
||||
<b>Our promise</b> is to make it easier and easier to automate your operations. In some cases however, it may be good with a helping hand. That's where <a href="https://shuffler.io/pricing?tag=docs" target="_blank" style={hrefStyle2}>Shuffle's consultancy and support</a> services come in handy. We help you build and automate your operational processes to a level you haven't seen before with the help of our <a href="https://shuffler.io/usecases?tag=docs" target="_blank" style={hrefStyle2}>usecases</a>.
|
||||
</Typography>
|
||||
|
||||
<Typography variant="h6" style={headerStyle}>Workflow building</Typography>
|
||||
<Typography variant="body1">
|
||||
<b>Build.</b> Creating workflows has never been easier. Jump into things with our <Link to="/getting-started" style={hrefStyle2}>getting Started</Link> section and build to your hearts content. Workflows make it all come together, with an easy to use area.
|
||||
</Typography>
|
||||
<Typography variant="h6" style={headerStyle}>APIs</Typography>
|
||||
<Typography variant="body1">
|
||||
<b>Learn.</b> We're all about learning, and are continuously creating documentation and video tutorials to better understand how to get started. APIs are an extremely important part of how the internet works today, and our goal is helping every security professional learn about them.
|
||||
</Typography>
|
||||
|
||||
<Typography variant="h6" style={headerStyle}>Managing Shuffle</Typography>
|
||||
<Typography variant="body1">
|
||||
<b>Organize.</b> Whether an organization of 1000 or 1, management tools are necessary. In Shuffle we offer full user management, MFA and single-signon options, multi-tenancy and a lot more - for free!
|
||||
</Typography>
|
||||
</div>
|
||||
<Typography variant="h6" style={headerStyle}>Workflow building</Typography>
|
||||
<Typography variant="body1">
|
||||
<b>Build.</b> Creating workflows has never been easier. Jump into things with our <Link to="/getting-started" style={hrefStyle2}>getting Started</Link> section and build to your hearts content. Workflows make it all come together, with an easy to use area.
|
||||
</Typography>
|
||||
|
||||
{/*
|
||||
<Grid container spacing={2} style={{marginTop: 50, }}>
|
||||
{list.map((data, index) => {
|
||||
const item = data.name;
|
||||
if (item === undefined) {
|
||||
return null;
|
||||
}
|
||||
<Typography variant="h6" style={headerStyle}>Managing Shuffle</Typography>
|
||||
<Typography variant="body1">
|
||||
<b>Organize.</b> Whether an organization of 1000 or 1, management tools are necessary. In Shuffle we offer full user management, MFA and single-signon options, multi-tenancy and a lot more - for free!
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
const path = "/docs/" + item;
|
||||
const newname =
|
||||
item.charAt(0).toUpperCase() +
|
||||
item.substring(1).split("_").join(" ").split("-").join(" ");
|
||||
{/*
|
||||
<Grid container spacing={2} style={{marginTop: 50, }}>
|
||||
{list.map((data, index) => {
|
||||
const item = data.name;
|
||||
if (item === undefined) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const itemMatching = props.match.params.key === undefined ? false :
|
||||
props.match.params.key.toLowerCase() === item.toLowerCase();
|
||||
const path = "/docs/" + item;
|
||||
const newname =
|
||||
item.charAt(0).toUpperCase() +
|
||||
item.substring(1).split("_").join(" ").split("-").join(" ");
|
||||
|
||||
return (
|
||||
<Grid key={index} item xs={4}>
|
||||
<DocumentationButton key={index} item={newname} link={"/docs/"+data.name} />
|
||||
</Grid>
|
||||
)
|
||||
})}
|
||||
</Grid>
|
||||
*/}
|
||||
const itemMatching = props.match.params.key === undefined ? false :
|
||||
props.match.params.key.toLowerCase() === item.toLowerCase();
|
||||
|
||||
{/*
|
||||
<TextField
|
||||
required
|
||||
style={{
|
||||
flex: "1",
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
height: 50,
|
||||
}}
|
||||
InputProps={{
|
||||
style:{
|
||||
color: "white",
|
||||
return (
|
||||
<Grid key={index} item xs={4}>
|
||||
<DocumentationButton key={index} item={newname} link={"/docs/"+data.name} />
|
||||
</Grid>
|
||||
)
|
||||
})}
|
||||
</Grid>
|
||||
*/}
|
||||
|
||||
{/*
|
||||
<TextField
|
||||
required
|
||||
style={{
|
||||
flex: "1",
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
height: 50,
|
||||
},
|
||||
}}
|
||||
placeholder={"Search Knowledgebase"}
|
||||
color="primary"
|
||||
fullWidth={true}
|
||||
type="firstname"
|
||||
id={"Searchfield"}
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
onChange={(event) => {
|
||||
console.log("Change: ", event.target.value)
|
||||
}}
|
||||
/>
|
||||
*/}
|
||||
}}
|
||||
InputProps={{
|
||||
style:{
|
||||
color: "white",
|
||||
height: 50,
|
||||
},
|
||||
}}
|
||||
placeholder={"Search Knowledgebase"}
|
||||
color="primary"
|
||||
fullWidth={true}
|
||||
type="firstname"
|
||||
id={"Searchfield"}
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
onChange={(event) => {
|
||||
console.log("Change: ", event.target.value)
|
||||
}}
|
||||
/>
|
||||
*/}
|
||||
</div>
|
||||
|
||||
const postDataBrowser =
|
||||
@@ -757,23 +783,26 @@ const Docs = (defaultprops) => {
|
||||
</List>
|
||||
</Paper>
|
||||
</div>
|
||||
{props.match.params.key === undefined ?
|
||||
mainpageInfo
|
||||
:
|
||||
<div id="markdown_wrapper_outer" style={markdownStyle}>
|
||||
<ReactMarkdown
|
||||
id="markdown_wrapper"
|
||||
escapeHtml={false}
|
||||
source={data}
|
||||
renderers={{
|
||||
link: OuterLink,
|
||||
image: Img,
|
||||
code: CodeHandler,
|
||||
heading: Heading,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
<div style={{maxWidth: 750, minWidth: 750, margin: "auto", overflow: "hidden", marginTop: 50, }}>
|
||||
{props.match.params.key === undefined ?
|
||||
mainpageInfo
|
||||
:
|
||||
<div id="markdown_wrapper_outer" style={markdownStyle}>
|
||||
<ReactMarkdown
|
||||
id="markdown_wrapper"
|
||||
escapeHtml={false}
|
||||
source={data}
|
||||
style={{maxWidth: "100%", minWidth: "100%", }}
|
||||
renderers={{
|
||||
link: OuterLink,
|
||||
image: Img,
|
||||
code: CodeHandler,
|
||||
heading: Heading,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import ReactDOM from "react-dom"
|
||||
|
||||
import DetectionFramework from "../components/DetectionFramework.jsx";
|
||||
import AppFramework from "../components/AppFramework.jsx";
|
||||
import { useAlert } from "react-alert";
|
||||
import { Link, useParams } from "react-router-dom";
|
||||
import theme from '../theme';
|
||||
@@ -69,7 +69,7 @@ const Framework = (props) => {
|
||||
</Link>
|
||||
</div>
|
||||
{frameworkLoaded === true && isLoaded ?
|
||||
<DetectionFramework
|
||||
<AppFramework
|
||||
frameworkData={frameworkData}
|
||||
selectedOption={"Draw"}
|
||||
showOptions={false}
|
||||
|
||||
@@ -62,7 +62,7 @@ import NestedMenuItem from "material-ui-nested-menu-item";
|
||||
//import {Search as SearchIcon, ArrowUpward as ArrowUpwardIcon, Visibility as VisibilityIcon, Done as DoneIcon, Close as CloseIcon, Error as ErrorIcon, FindReplace as FindreplaceIcon, ArrowLeft as ArrowLeftIcon, Cached as CachedIcon, DirectionsRun as DirectionsRunIcon, Add as AddIcon, Polymer as PolymerIcon, FormatListNumbered as FormatListNumberedIcon, Create as CreateIcon, PlayArrow as PlayArrowIcon, AspectRatio as AspectRatioIcon, MoreVert as MoreVertIcon, Apps as AppsIcon, Schedule as ScheduleIcon, FavoriteBorder as FavoriteBorderIcon, Pause as PauseIcon, Delete as DeleteIcon, AddCircleOutline as AddCircleOutlineIcon, Save as SaveIcon, KeyboardArrowLeft as KeyboardArrowLeftIcon, KeyboardArrowRight as KeyboardArrowRightIcon, ArrowBack as ArrowBackIcon, Settings as SettingsIcon, LockOpen as LockOpenIcon, ExpandMore as ExpandMoreIcon, VpnKey as VpnKeyIcon} from '@material-ui/icons';
|
||||
|
||||
//https://next.material-ui.com/components/material-icons/
|
||||
import { DataGrid, GridToolbar } from "@material-ui/data-grid";
|
||||
import { DataGrid, GridToolbar } from "@mui/x-data-grid";
|
||||
|
||||
//import JSONPretty from 'react-json-pretty';
|
||||
//import JSONPrettyMon from 'react-json-pretty/dist/monikai'
|
||||
@@ -148,9 +148,9 @@ const GettingStarted = (props) => {
|
||||
const [downloadUrl, setDownloadUrl] = React.useState(
|
||||
"https://github.com/frikky/shuffle-workflows"
|
||||
);
|
||||
const [videoViewOpen, setVideoViewOpen] = React.useState(false)
|
||||
const [downloadBranch, setDownloadBranch] = React.useState("master");
|
||||
const [loadWorkflowsModalOpen, setLoadWorkflowsModalOpen] = React.useState(false);
|
||||
const [videoViewOpen, setVideoViewOpen] = React.useState(false);
|
||||
const [exportModalOpen, setExportModalOpen] = React.useState(false);
|
||||
const [exportData, setExportData] = React.useState("");
|
||||
|
||||
@@ -515,8 +515,6 @@ const GettingStarted = (props) => {
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
setVideoViewOpen(true)
|
||||
|
||||
if (response.status !== 200) {
|
||||
console.log("Status not 200 for workflows :O!: ", response.status);
|
||||
|
||||
@@ -561,6 +559,7 @@ const GettingStarted = (props) => {
|
||||
// Ensures the zooming happens only once per load
|
||||
setTimeout(() => {
|
||||
setFirstLoad(false)
|
||||
setVideoViewOpen(true)
|
||||
}, 100)
|
||||
} else {
|
||||
if (isLoggedIn) {
|
||||
@@ -2152,7 +2151,7 @@ const GettingStarted = (props) => {
|
||||
const steps = [
|
||||
{
|
||||
html: (
|
||||
<Typography variant={textType} style={{marginTop: textSpacingDiff}} onClick={() => {
|
||||
<Typography variant={textType} style={{marginTop: textSpacingDiff, textAlign: "left",}} onClick={() => {
|
||||
if (isCloud) {
|
||||
ReactGA.event({
|
||||
category: "getting-started",
|
||||
@@ -2160,15 +2159,15 @@ const GettingStarted = (props) => {
|
||||
})
|
||||
}
|
||||
}}>
|
||||
<Link to="/detectionframework" style={{textDecoration: "none", color: "#f86a3e",}}>Find relevant apps</Link> and start your automation journey
|
||||
<Link to="/welcome?tab=2" style={{textDecoration: "none", color: "#f86a3e",}}>Find relevant apps</Link> and start your automation journey
|
||||
</Typography>
|
||||
),
|
||||
tutorial: "find_integrations",
|
||||
},
|
||||
{
|
||||
html:
|
||||
<Typography variant={textType} style={{marginTop: textSpacingDiff}}>
|
||||
Discover <Link to="/usecases" style={{cursor: "pointer", textDecoration: "none", color: "#f86a3e",}}>Use Case ideas</Link> and
|
||||
<Typography variant={textType} style={{marginTop: textSpacingDiff, textAlign: "left",}}>
|
||||
Discover <Link to="/welcome?tab=3" style={{cursor: "pointer", textDecoration: "none", color: "#f86a3e",}}>Use-Case ideas</Link> and
|
||||
<span style={{cursor: "pointer", textDecoration: "none", color: "#f86a3e",}} onClick={() => {
|
||||
|
||||
if (isCloud) {
|
||||
@@ -2200,7 +2199,7 @@ const GettingStarted = (props) => {
|
||||
},
|
||||
{
|
||||
html: (
|
||||
<Typography variant={textType} style={{marginTop: textSpacingDiff}} onClick={() => {
|
||||
<Typography variant={textType} style={{marginTop: textSpacingDiff, textAlign: "left",}} onClick={() => {
|
||||
if (isCloud) {
|
||||
ReactGA.event({
|
||||
category: "getting-started",
|
||||
@@ -2218,8 +2217,8 @@ const GettingStarted = (props) => {
|
||||
},
|
||||
{
|
||||
html:
|
||||
<Typography variant={textType} style={{marginTop: textSpacingDiff}}>
|
||||
Configure your organization <Link to="/admin" style={{textDecoration: "none", color: "#f86a3e",}}>in the admin panel</Link>
|
||||
<Typography variant={textType} style={{marginTop: textSpacingDiff, textAlign: "left",}}>
|
||||
Configure your organization name <Link to="/admin" style={{textDecoration: "none", color: "#f86a3e",}}>in the admin panel</Link> and <Link to="/admin?tab=users" style={{textDecoration: "none", color: "#f86a3e",}}>invite your team</Link>
|
||||
</Typography>,
|
||||
tutorial: "configure_organization",
|
||||
}
|
||||
@@ -2227,55 +2226,53 @@ const GettingStarted = (props) => {
|
||||
|
||||
return (
|
||||
<div style={viewStyle}>
|
||||
{isCloud ?
|
||||
<Dialog
|
||||
open={videoViewOpen}
|
||||
onClose={() => {
|
||||
setVideoViewOpen(false)
|
||||
}}
|
||||
PaperProps={{
|
||||
style: {
|
||||
backgroundColor: surfaceColor,
|
||||
color: "white",
|
||||
minWidth: 560,
|
||||
minHeight: 415,
|
||||
textAlign: "center",
|
||||
},
|
||||
}}
|
||||
<Dialog
|
||||
open={videoViewOpen}
|
||||
onClose={() => {
|
||||
setVideoViewOpen(false)
|
||||
}}
|
||||
PaperProps={{
|
||||
style: {
|
||||
backgroundColor: surfaceColor,
|
||||
color: "white",
|
||||
minWidth: 560,
|
||||
minHeight: 415,
|
||||
textAlign: "center",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DialogTitle>
|
||||
Welcome to Shuffle!
|
||||
</DialogTitle>
|
||||
|
||||
<Tooltip
|
||||
title="Close window"
|
||||
placement="top"
|
||||
style={{ zIndex: 10011 }}
|
||||
>
|
||||
<DialogTitle>
|
||||
Welcome to Shuffle!
|
||||
</DialogTitle>
|
||||
|
||||
<Tooltip
|
||||
title="Close window"
|
||||
placement="top"
|
||||
style={{ zIndex: 10011 }}
|
||||
>
|
||||
<IconButton
|
||||
style={{ zIndex: 5000, position: "absolute", top: 10, right: 34 }}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setVideoViewOpen(false)
|
||||
}}
|
||||
>
|
||||
<CloseIcon style={{ color: "white" }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<iframe
|
||||
width="560"
|
||||
height="315"
|
||||
style={{margin: "0px auto 0px auto", width: 560, height: 315,}}
|
||||
src="https://www.youtube-nocookie.com/embed/rO7k9q3OgC0"
|
||||
title="Introduction video"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen
|
||||
<IconButton
|
||||
style={{ zIndex: 5000, position: "absolute", top: 10, right: 34 }}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
setVideoViewOpen(false)
|
||||
}}
|
||||
>
|
||||
</iframe>
|
||||
</Dialog>
|
||||
: null}
|
||||
<CloseIcon style={{ color: "white" }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<iframe
|
||||
width="560"
|
||||
height="315"
|
||||
style={{margin: "0px auto 0px auto", width: 560, height: 315,}}
|
||||
src="https://www.youtube-nocookie.com/embed/rO7k9q3OgC0"
|
||||
title="Introduction video"
|
||||
frameborder="0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowfullscreen
|
||||
>
|
||||
</iframe>
|
||||
</Dialog>
|
||||
<div style={workflowViewStyle}>
|
||||
<Typography variant="h1" style={{fontSize: 30, marginTop: 25, }}>
|
||||
Getting Started with Shuffle
|
||||
@@ -2292,8 +2289,34 @@ const GettingStarted = (props) => {
|
||||
console.log("Found tutorial for ", data.tutorial)
|
||||
tutorialFound = true
|
||||
}
|
||||
}
|
||||
|
||||
if (tutorialFound === false) {
|
||||
if (data.tutorial === "discover_workflows") {
|
||||
if (workflows.length > 0) {
|
||||
for (var key in workflows) {
|
||||
const tmpworkflow = workflows[key]
|
||||
if (tmpworkflow.published_id !== undefined && tmpworkflow.published_id !== null && tmpworkflow.published_id.length > 0) {
|
||||
tutorialFound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (data.tutorial === "learn_shuffle") {
|
||||
//tutorial: "discover_workflows",
|
||||
if (workflows.length > 0) {
|
||||
tutorialFound = true
|
||||
}
|
||||
}
|
||||
|
||||
if (data.tutorial === "configure_organization") {
|
||||
if (userdata.active_org.name !== userdata.username) {
|
||||
tutorialFound = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={index} style={{display: "flex", marginBottom: index === steps.length-1 ? 0 : 20, }}>
|
||||
@@ -2350,6 +2373,14 @@ const GettingStarted = (props) => {
|
||||
</Button>
|
||||
</a>
|
||||
</div>
|
||||
{/*
|
||||
<div style={{position: "fixed", bottom: 110, right: 110, display: "flex", }}>
|
||||
<Typography variant="body1" color="textSecondary" style={{marginRight: 0, maxWidth: 150, }}>
|
||||
Need assistance? Ask our support team (it's free!).
|
||||
</Typography>
|
||||
<img src="/images/Arrow.png" style={{width: 150}} />
|
||||
</div>
|
||||
*/}
|
||||
</div>
|
||||
{/*
|
||||
<div style={flexContainerStyle}>
|
||||
|
||||
@@ -184,10 +184,22 @@ const LoginDialog = (props) => {
|
||||
);
|
||||
}
|
||||
|
||||
setIsLoggedIn(true);
|
||||
if (responseJson.tutorials === undefined || responseJson.tutorials === null || !responseJson.tutorials.includes("welcome")) {
|
||||
console.log("RUN Welcome!!")
|
||||
window.location.pathname = "/welcome"
|
||||
return
|
||||
}
|
||||
|
||||
//navigate("/workflows")
|
||||
window.location.href = "/workflows"
|
||||
const tmpView = new URLSearchParams(window.location.search).get("view")
|
||||
if (tmpView !== undefined && tmpView !== null) {
|
||||
//const newUrl = `/${tmpView}${decodeURIComponent(window.location.search)}`
|
||||
const newUrl = `/${tmpView}`
|
||||
window.location.pathname = newUrl
|
||||
} else {
|
||||
window.location.pathname = "/workflows"
|
||||
}
|
||||
|
||||
setIsLoggedIn(true);
|
||||
}
|
||||
})
|
||||
)
|
||||
@@ -285,7 +297,7 @@ const LoginDialog = (props) => {
|
||||
{loginInfo === undefined ||
|
||||
loginInfo === null ||
|
||||
loginInfo.length === 0 ? null : (
|
||||
<div style={{ marginTop: "10px" }}>Response: {loginInfo}</div>
|
||||
<div style={{ marginTop: "10px" }}>Database Response: {loginInfo}</div>
|
||||
)}
|
||||
<CircularProgress color="secondary" style={{ color: "white" }} />
|
||||
|
||||
|
||||
@@ -51,7 +51,8 @@ import {
|
||||
GridToolbarContainer,
|
||||
GridDensitySelector,
|
||||
GridToolbar,
|
||||
} from "@material-ui/data-grid";
|
||||
} from "@mui/x-data-grid";
|
||||
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
|
||||
import ListIcon from "@material-ui/icons/List";
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
import theme from '../theme';
|
||||
import {isMobile} from "react-device-detect";
|
||||
import AppGrid from "../components/AppGrid.jsx"
|
||||
import WorkflowGrid from "../components/WorkflowGrid.jsx"
|
||||
import CreatorGrid from "../components/CreatorGrid.jsx"
|
||||
import DocsGrid from "../components/DocsGrid.jsx"
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import {
|
||||
Tabs,
|
||||
Tab,
|
||||
} from "@material-ui/core";
|
||||
|
||||
import {
|
||||
Apps as AppsIcon,
|
||||
Polymer as PolymerIcon,
|
||||
EmojiObjects as EmojiObjectsIcon,
|
||||
Description as DescriptionIcon,
|
||||
} from "@material-ui/icons";
|
||||
|
||||
|
||||
const bodyDivStyle = {
|
||||
margin: "auto",
|
||||
maxWidth: 1024,
|
||||
scrollX: "hidden",
|
||||
overflowX: "hidden",
|
||||
}
|
||||
|
||||
// Should be different if logged in :|
|
||||
const Search = (props) => {
|
||||
const { globalUrl, isLoaded, serverside, userdata, hidemargins, } = props;
|
||||
let navigate = useNavigate();
|
||||
|
||||
const [curTab, setCurTab] = useState(0);
|
||||
const iconStyle = { marginRight: 10 };
|
||||
|
||||
useEffect(() => {
|
||||
if (serverside !== true && window.location.search !== undefined && window.location.search !== null) {
|
||||
const urlSearchParams = new URLSearchParams(window.location.search)
|
||||
const params = Object.fromEntries(urlSearchParams.entries())
|
||||
const foundTab = params["tab"]
|
||||
if (foundTab !== null && foundTab !== undefined) {
|
||||
for (var key in Object.keys(views)) {
|
||||
const value = views[key]
|
||||
console.log(key, value)
|
||||
if (value === foundTab) {
|
||||
setConfig("", key)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (serverside === true) {
|
||||
return null
|
||||
}
|
||||
|
||||
const boxStyle = {
|
||||
color: "white",
|
||||
flex: "1",
|
||||
marginLeft: 10,
|
||||
marginRight: 10,
|
||||
paddingLeft: 30,
|
||||
paddingRight: 30,
|
||||
paddingBottom: 30,
|
||||
paddingTop: hidemargins === true ? 0 : 30,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflowX: "hidden",
|
||||
minHeight: 400,
|
||||
}
|
||||
|
||||
const views = {
|
||||
0: "apps",
|
||||
1: "workflows",
|
||||
2: "docs",
|
||||
3: "creators",
|
||||
}
|
||||
|
||||
const setConfig = (event, inputValue) => {
|
||||
const newValue = parseInt(inputValue)
|
||||
|
||||
setCurTab(newValue)
|
||||
if (newValue === 0) {
|
||||
document.title = "Shuffle - search - apps";
|
||||
} else if (newValue === 1) {
|
||||
document.title = "Shuffle - search - workflows";
|
||||
} else if (newValue === 2) {
|
||||
document.title = "Shuffle - search - documentation";
|
||||
} else if (newValue === 3) {
|
||||
document.title = "Shuffle - search - creators";
|
||||
} else {
|
||||
document.title = "Shuffle - search";
|
||||
}
|
||||
|
||||
|
||||
const urlSearchParams = new URLSearchParams(window.location.search)
|
||||
const params = Object.fromEntries(urlSearchParams.entries())
|
||||
const foundQuery = params["q"]
|
||||
var extraQ = ""
|
||||
if (foundQuery !== null && foundQuery !== undefined) {
|
||||
extraQ = "&q="+foundQuery
|
||||
}
|
||||
|
||||
|
||||
if ((serverside === false || serverside === undefined) && window.location.pathname.includes("/search")) {
|
||||
navigate(`/search?tab=${views[newValue]}`+extraQ)
|
||||
}
|
||||
}
|
||||
|
||||
if (isLoaded === false) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
// Random names for type & autoComplete. Didn't research :^)
|
||||
const landingpageDataBrowser =
|
||||
<div style={{paddingBottom: hidemargins === true ? 0 : 100, color: "white", backgroundColor: theme.palette.surfacColor}}>
|
||||
<div style={boxStyle}>
|
||||
<Tabs
|
||||
style={{width: 610, margin: "auto", marginTop: hidemargins === true ? 0 : 25, }}
|
||||
value={curTab}
|
||||
indicatorColor="primary"
|
||||
textColor="secondary"
|
||||
onChange={setConfig}
|
||||
aria-label="disabled tabs example"
|
||||
>
|
||||
<Tab
|
||||
label=<span>
|
||||
<AppsIcon style={iconStyle} /> Apps
|
||||
</span>
|
||||
/>
|
||||
<Tab
|
||||
label=<span>
|
||||
<PolymerIcon style={iconStyle} /> Workflows
|
||||
</span>
|
||||
/>
|
||||
<Tab
|
||||
label=<span>
|
||||
<DescriptionIcon style={iconStyle} /> Docs
|
||||
</span>
|
||||
/>
|
||||
<Tab
|
||||
label=<span>
|
||||
<EmojiObjectsIcon style={iconStyle} /> Creators
|
||||
</span>
|
||||
/>
|
||||
</Tabs>
|
||||
{curTab === 0 ?
|
||||
<AppGrid maxRows={3} showSuggestion={true} globalUrl={globalUrl} isMobile={isMobile} userdata={userdata} />
|
||||
:
|
||||
curTab === 1 ?
|
||||
window.location.pathname === "/search" ?
|
||||
<WorkflowGrid maxRows={3} showSuggestion={true} globalUrl={globalUrl} isMobile={isMobile} userdata={userdata} />
|
||||
:
|
||||
<WorkflowGrid maxRows={3} showSuggestion={true} globalUrl={globalUrl} isMobile={isMobile} userdata={userdata} />
|
||||
:
|
||||
curTab === 2 ?
|
||||
<DocsGrid maxRows={6} parsedXs={12} showSuggestion={true} globalUrl={globalUrl} isMobile={isMobile} userdata={userdata} />
|
||||
:
|
||||
curTab === 3 ?
|
||||
<CreatorGrid parsedXs={4} showSuggestion={true} globalUrl={globalUrl} isMobile={isMobile} userdata={userdata} />
|
||||
:
|
||||
null}
|
||||
</div>
|
||||
</div>
|
||||
//{/*alternativeView={true} />*/}
|
||||
|
||||
const loadedCheck = isLoaded ?
|
||||
<div>
|
||||
<div style={bodyDivStyle}>{landingpageDataBrowser}</div>
|
||||
</div>
|
||||
:
|
||||
<div>
|
||||
</div>
|
||||
|
||||
// #1f2023?
|
||||
return(
|
||||
<div style={{backgroundColor: "#1f2023",}}>
|
||||
{loadedCheck}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Search;
|
||||
@@ -1,10 +1,10 @@
|
||||
import React, { useRef, useState, useEffect, useLayoutEffect } from "react";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { Typography, CircularProgress } from "@material-ui/core";
|
||||
import theme from '../theme';
|
||||
|
||||
const SetAuthentication = (props) => {
|
||||
const { globalUrl, isLoggedIn, isLoaded, userdata } = props;
|
||||
const { globalUrl } = props;
|
||||
|
||||
const [firstRequest, setFirstRequest] = useState(true);
|
||||
const [finished, setFinished] = useState(false);
|
||||
@@ -20,7 +20,7 @@ const SetAuthentication = (props) => {
|
||||
const params = Object.fromEntries(urlSearchParams.entries());
|
||||
console.log("PARAMS: ", params)
|
||||
|
||||
const authenticationStore = [];
|
||||
//const authenticationStore = [];
|
||||
var appAuthData = {
|
||||
label: "",
|
||||
app: {
|
||||
@@ -60,6 +60,7 @@ const SetAuthentication = (props) => {
|
||||
externalData.code = params.code
|
||||
}
|
||||
|
||||
var foundScope = ""
|
||||
if (params.state !== undefined && params.state !== null) {
|
||||
const paramsplit = params.state.split("&");
|
||||
console.log(paramsplit);
|
||||
@@ -112,6 +113,7 @@ const SetAuthentication = (props) => {
|
||||
|
||||
if (query[0] === "scope") {
|
||||
appAuthData.fields.push({ key: "scope", value: query[1] });
|
||||
foundScope = query[1]
|
||||
}
|
||||
|
||||
if (query[0] === "client_id") {
|
||||
@@ -136,145 +138,157 @@ const SetAuthentication = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (externalData.handleExternal) {
|
||||
console.log("RUN EXTERNAL!!: ", externalData)
|
||||
|
||||
fetch(globalUrl + "/api/v1/triggers/github/register", {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify(externalData),
|
||||
})
|
||||
.then((response) => {
|
||||
const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search;
|
||||
const tmpView = new URLSearchParams(cursearch).get("state");
|
||||
if (
|
||||
tmpView !== undefined &&
|
||||
tmpView !== null &&
|
||||
tmpView.length > 0
|
||||
) {
|
||||
console.log("State to find app name from: ", tmpView)
|
||||
if (foundScope !== undefined && foundScope !== null && foundScope.length > 0) {
|
||||
appAuthData.label = `${foundScope}`
|
||||
}
|
||||
|
||||
var foundTab = params["error"];
|
||||
if (foundTab !== null && foundTab !== undefined && foundTab.length > 0) {
|
||||
console.log("Found error: ", foundTab, "! Skipping Shuffle requests to validate Oauth2")
|
||||
var errorDesc = params["error_description"]
|
||||
if (errorDesc !== null && errorDesc !== undefined && errorDesc.length > 0) {
|
||||
foundTab += "\n\n"+errorDesc
|
||||
}
|
||||
|
||||
setFailed(true)
|
||||
setResponse(`${foundTab}`)
|
||||
} else {
|
||||
if (externalData.handleExternal) {
|
||||
console.log("RUN EXTERNAL!!: ", externalData)
|
||||
|
||||
fetch(globalUrl + "/api/v1/triggers/github/register", {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify(externalData),
|
||||
})
|
||||
.then((response) => {
|
||||
const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search;
|
||||
const tmpView = new URLSearchParams(cursearch).get("state");
|
||||
if (
|
||||
tmpView !== undefined &&
|
||||
tmpView !== null &&
|
||||
tmpView.length > 0
|
||||
) {
|
||||
console.log("State to find app name from: ", tmpView)
|
||||
}
|
||||
|
||||
if (response.status !== 200) {
|
||||
console.log("Status not 200 for oauth2 authentication");
|
||||
setFailed(true);
|
||||
} else {
|
||||
setFinished(true);
|
||||
//setTimeout(() => {
|
||||
// window.close();
|
||||
//}, 2500);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
//setUserSettings(responseJson)
|
||||
console.log("Resp: ", responseJson);
|
||||
|
||||
if (responseJson.reason !== undefined) {
|
||||
setResponse(responseJson.reason);
|
||||
setFinished(true);
|
||||
|
||||
} else {
|
||||
const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search;
|
||||
var tmpView = new URLSearchParams(cursearch).get("error_description");
|
||||
if (
|
||||
tmpView !== undefined &&
|
||||
tmpView !== null &&
|
||||
tmpView.length > 0
|
||||
) {
|
||||
setResponse(tmpView)
|
||||
} else {
|
||||
tmpView = new URLSearchParams(cursearch).get("error");
|
||||
if (
|
||||
tmpView !== undefined &&
|
||||
tmpView !== null &&
|
||||
tmpView.length > 0
|
||||
) {
|
||||
setResponse(tmpView)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
fetch(globalUrl + "/api/v1/apps/authentication", {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify(appAuthData),
|
||||
})
|
||||
.then((response) => {
|
||||
const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search;
|
||||
const tmpView = new URLSearchParams(cursearch).get("state");
|
||||
if (
|
||||
tmpView !== undefined &&
|
||||
tmpView !== null &&
|
||||
tmpView.length > 0
|
||||
) {
|
||||
console.log("State to find app name from: ", tmpView)
|
||||
}
|
||||
|
||||
if (response.status !== 200) {
|
||||
console.log("Status not 200 for oauth2 authentication");
|
||||
setFailed(true);
|
||||
} else {
|
||||
setFinished(true);
|
||||
setTimeout(() => {
|
||||
window.close();
|
||||
}, 2500);
|
||||
}
|
||||
|
||||
if (response.status !== 200) {
|
||||
console.log("Status not 200 for oauth2 authentication");
|
||||
setFailed(true);
|
||||
} else {
|
||||
setFinished(true);
|
||||
//setTimeout(() => {
|
||||
// window.close();
|
||||
//}, 2500);
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
//setUserSettings(responseJson)
|
||||
if (responseJson.reason !== undefined) {
|
||||
setResponse(responseJson.reason);
|
||||
setFinished(true);
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
//setUserSettings(responseJson)
|
||||
console.log("Resp: ", responseJson);
|
||||
|
||||
if (responseJson.reason !== undefined) {
|
||||
setResponse(responseJson.reason);
|
||||
setFinished(true);
|
||||
|
||||
} else {
|
||||
const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search;
|
||||
var tmpView = new URLSearchParams(cursearch).get("error_description");
|
||||
if (
|
||||
tmpView !== undefined &&
|
||||
tmpView !== null &&
|
||||
tmpView.length > 0
|
||||
) {
|
||||
setResponse(tmpView)
|
||||
} else {
|
||||
tmpView = new URLSearchParams(cursearch).get("error");
|
||||
if (
|
||||
tmpView !== undefined &&
|
||||
tmpView !== null &&
|
||||
tmpView.length > 0
|
||||
) {
|
||||
const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search;
|
||||
var tmpView = new URLSearchParams(cursearch).get("error_description");
|
||||
if (
|
||||
tmpView !== undefined &&
|
||||
tmpView !== null &&
|
||||
tmpView.length > 0
|
||||
) {
|
||||
setResponse(tmpView)
|
||||
} else {
|
||||
tmpView = new URLSearchParams(cursearch).get("error");
|
||||
if (
|
||||
tmpView !== undefined &&
|
||||
tmpView !== null &&
|
||||
tmpView.length > 0
|
||||
) {
|
||||
setResponse(tmpView)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
console.log(appAuthData);
|
||||
|
||||
fetch(globalUrl + "/api/v1/apps/authentication", {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify(appAuthData),
|
||||
})
|
||||
.then((response) => {
|
||||
const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search;
|
||||
const tmpView = new URLSearchParams(cursearch).get("state");
|
||||
if (
|
||||
tmpView !== undefined &&
|
||||
tmpView !== null &&
|
||||
tmpView.length > 0
|
||||
) {
|
||||
console.log("State to find app name from: ", tmpView)
|
||||
}
|
||||
|
||||
if (response.status !== 200) {
|
||||
console.log("Status not 200 for oauth2 authentication");
|
||||
setFailed(true);
|
||||
} else {
|
||||
setFinished(true);
|
||||
setTimeout(() => {
|
||||
window.close();
|
||||
}, 2500);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
//setUserSettings(responseJson)
|
||||
console.log("Resp: ", responseJson);
|
||||
|
||||
if (responseJson.reason !== undefined) {
|
||||
setResponse(responseJson.reason);
|
||||
setFinished(true);
|
||||
|
||||
} else {
|
||||
const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search;
|
||||
var tmpView = new URLSearchParams(cursearch).get("error_description");
|
||||
if (
|
||||
tmpView !== undefined &&
|
||||
tmpView !== null &&
|
||||
tmpView.length > 0
|
||||
) {
|
||||
setResponse(tmpView)
|
||||
} else {
|
||||
tmpView = new URLSearchParams(cursearch).get("error");
|
||||
if (
|
||||
tmpView !== undefined &&
|
||||
tmpView !== null &&
|
||||
tmpView.length > 0
|
||||
) {
|
||||
setResponse(tmpView)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -298,6 +312,9 @@ const SetAuthentication = (props) => {
|
||||
)}
|
||||
<div />
|
||||
{failed ? "Failed setup. Error: " : ""} {response}
|
||||
<br/>
|
||||
<br/>
|
||||
{failed ? "If the error persists, try to use fewer scopes. Contact our support at support@shuffler.io if you need further assistance. You may close this window." : ""}
|
||||
</Typography>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { useRef, useState, useEffect, useLayoutEffect } from "react";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import { Typography, CircularProgress } from "@material-ui/core";
|
||||
|
||||
const SetAuthentication = (props) => {
|
||||
const { globalUrl, isLoggedIn, isLoaded, userdata } = props;
|
||||
const { globalUrl } = props;
|
||||
|
||||
const [firstRequest, setFirstRequest] = useState(true);
|
||||
const [finished, setFinished] = useState(false);
|
||||
@@ -18,7 +18,7 @@ const SetAuthentication = (props) => {
|
||||
const urlSearchParams = new URLSearchParams(window.location.search);
|
||||
const params = Object.fromEntries(urlSearchParams.entries());
|
||||
|
||||
const authenticationStore = [];
|
||||
// const authenticationStore = [];
|
||||
var appAuthData = {
|
||||
label: "",
|
||||
app: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import {
|
||||
Grid,
|
||||
Typography,
|
||||
@@ -8,7 +9,6 @@ import {
|
||||
Divider,
|
||||
TextField,
|
||||
} from "@material-ui/core";
|
||||
import { Link } from "react-router-dom";
|
||||
import { useAlert } from "react-alert";
|
||||
import { useTheme } from "@material-ui/core/styles";
|
||||
|
||||
@@ -18,23 +18,21 @@ const Settings = (props) => {
|
||||
const { globalUrl, isLoaded, userdata, setUserData } = props;
|
||||
const theme = useTheme();
|
||||
const alert = useAlert();
|
||||
let navigate = useNavigate();
|
||||
|
||||
const [username, setUsername] = useState("");
|
||||
const [firstname, setFirstname] = useState("");
|
||||
const [lastname, setLastname] = useState("");
|
||||
const [title, setTitle] = useState("");
|
||||
const [companyname, setCompanyname] = useState("");
|
||||
const [email, setEmail] = useState("");
|
||||
const [phone, setPhone] = useState("");
|
||||
const [currentPassword, setCurrentPassword] = useState("");
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
const [newPassword2, setNewPassword2] = useState("");
|
||||
const [file, setFile] = React.useState("");
|
||||
const [fileBase64, setFileBase64] = React.useState(
|
||||
userdata.image === undefined || userdata.image === null
|
||||
? theme.palette.defaultImage
|
||||
: userdata.image
|
||||
);
|
||||
// const [file, setFile] = React.useState("");
|
||||
// const [fileBase64, setFileBase64] = React.useState(
|
||||
// userdata.image === undefined || userdata.image === null
|
||||
// ? theme.palette.defaultImage
|
||||
// : userdata.image
|
||||
// );
|
||||
const [loadedValidationWorkflows, setLoadedValidationWorkflows] =
|
||||
React.useState([]);
|
||||
const [selfOwnedWorkflows, setSelfOwnedWorkflows] = React.useState([]);
|
||||
@@ -42,7 +40,6 @@ const Settings = (props) => {
|
||||
React.useState([]);
|
||||
|
||||
// Used for error messages etc
|
||||
const [formMessage] = useState("");
|
||||
const [passwordFormMessage, setPasswordFormMessage] = useState("");
|
||||
|
||||
const [firstrequest, setFirstRequest] = useState(true);
|
||||
@@ -295,58 +292,58 @@ const Settings = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const registerProviders = (userdata) => {
|
||||
// Register hooks here
|
||||
detectEthereumProvider().then((provider) => {
|
||||
if (provider) {
|
||||
if (!provider.isMetaMask) {
|
||||
alert.error("Only MetaMask is supported as of now.");
|
||||
return;
|
||||
}
|
||||
// const registerProviders = (userdata) => {
|
||||
// // Register hooks here
|
||||
// detectEthereumProvider().then((provider) => {
|
||||
// if (provider) {
|
||||
// if (!provider.isMetaMask) {
|
||||
// alert.error("Only MetaMask is supported as of now.");
|
||||
// return;
|
||||
// }
|
||||
|
||||
// Find the ethereum network
|
||||
// Get the users' account(s)
|
||||
//alert.info("Connecting to MetaMask")
|
||||
//console.log("Connected: ", provider.isConnected())
|
||||
// // Find the ethereum network
|
||||
// // Get the users' account(s)
|
||||
// //alert.info("Connecting to MetaMask")
|
||||
// //console.log("Connected: ", provider.isConnected())
|
||||
|
||||
if (!provider.isConnected()) {
|
||||
alert.error("Metamask is not connected.");
|
||||
return;
|
||||
}
|
||||
// if (!provider.isConnected()) {
|
||||
// alert.error("Metamask is not connected.");
|
||||
// return;
|
||||
// }
|
||||
|
||||
provider.on("message", (event) => {
|
||||
alert.info("Ethereum message: ", event);
|
||||
});
|
||||
// provider.on("message", (event) => {
|
||||
// alert.info("Ethereum message: ", event);
|
||||
// });
|
||||
|
||||
provider.on("chainChanged", (chainId) => {
|
||||
console.log("Changed chain to: ", chainId);
|
||||
// provider.on("chainChanged", (chainId) => {
|
||||
// console.log("Changed chain to: ", chainId);
|
||||
|
||||
const method = "eth_getBalance";
|
||||
const params = [userdata.eth_info.account, "latest"];
|
||||
provider
|
||||
.request({
|
||||
method: method,
|
||||
params,
|
||||
})
|
||||
.then((result) => {
|
||||
console.log("Got result: ", result);
|
||||
if (result !== undefined && result !== null) {
|
||||
userdata.eth_info.balance = result;
|
||||
userdata.eth_info.parsed_balance = result / 1000000000000000000;
|
||||
console.log("INFO: ", userdata);
|
||||
setUserData(userdata);
|
||||
} else {
|
||||
alert.error("Couldn't find balance: ", result);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
// If the request fails, the Promise will reject with an error.
|
||||
alert.error("Failed getting info from ethereum API: " + error);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
||||
// const method = "eth_getBalance";
|
||||
// const params = [userdata.eth_info.account, "latest"];
|
||||
// provider
|
||||
// .request({
|
||||
// method: method,
|
||||
// params,
|
||||
// })
|
||||
// .then((result) => {
|
||||
// console.log("Got result: ", result);
|
||||
// if (result !== undefined && result !== null) {
|
||||
// userdata.eth_info.balance = result;
|
||||
// userdata.eth_info.parsed_balance = result / 1000000000000000000;
|
||||
// console.log("INFO: ", userdata);
|
||||
// setUserData(userdata);
|
||||
// } else {
|
||||
// alert.error("Couldn't find balance: ", result);
|
||||
// }
|
||||
// })
|
||||
// .catch((error) => {
|
||||
// // If the request fails, the Promise will reject with an error.
|
||||
// alert.error("Failed getting info from ethereum API: " + error);
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
// });
|
||||
// };
|
||||
|
||||
// This should "always" have data
|
||||
useEffect(() => {
|
||||
@@ -414,7 +411,15 @@ const Settings = (props) => {
|
||||
src={imageData}
|
||||
alt="Click to upload an image (174x174)"
|
||||
id="logo"
|
||||
onClick={() => {
|
||||
if (imageData !== theme.palette.defaultImage) {
|
||||
navigate(`/creators/${userdata.public_username}`)
|
||||
} else {
|
||||
navigate(`/creators`)
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
maxWidth: 100,
|
||||
maxHeight: 100,
|
||||
minWidth: 100,
|
||||
@@ -733,7 +738,7 @@ const Settings = (props) => {
|
||||
{isCloud ?
|
||||
<span>
|
||||
<Typography variant="body1" color="textSecondary">
|
||||
By connecting your Github account, 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 non-sensitive data will be turned into a <a target="_blank" style={{ textDecoration: "none", color: "#f86a3e"}} href="https://shuffler.io/search?tab=creators">creator account</a>. This enables you to earn a passive income from Shuffle. This IS reversible.
|
||||
By connecting your Github or Metamask account, 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 non-sensitive data will be turned into a <a target="_blank" style={{ textDecoration: "none", color: "#f86a3e"}} href="https://shuffler.io/search?tab=creators">creator account</a>. This enables you to earn a passive income from Shuffle. This IS reversible. Support: support@shuffler.io
|
||||
</Typography>
|
||||
<Button
|
||||
style={{ height: 40, marginTop: 10 }}
|
||||
@@ -810,7 +815,7 @@ const Settings = (props) => {
|
||||
) : null}
|
||||
</div>
|
||||
<div style={{ flex: 1, marginTop: 20 }}>
|
||||
{userdata !== undefined &&
|
||||
{/*userdata !== undefined &&
|
||||
userdata.eth_info !== undefined &&
|
||||
userdata.eth_info.account !== undefined &&
|
||||
userdata.eth_info.account.length > 0 ? (
|
||||
@@ -868,7 +873,7 @@ const Settings = (props) => {
|
||||
>
|
||||
Authenticate Metamask Wallet
|
||||
</Button>
|
||||
)}
|
||||
)*/}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -0,0 +1,298 @@
|
||||
import React from "react";
|
||||
import { Grid, Container, Divider } from "@mui/material";
|
||||
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import Card from "@material-ui/core/Card";
|
||||
import CardContent from "@material-ui/core/CardContent";
|
||||
import Typography from "@material-ui/core/Typography";
|
||||
|
||||
import Table from "@material-ui/core/Table";
|
||||
import TableBody from "@material-ui/core/TableBody";
|
||||
import TableCell from "@material-ui/core/TableCell";
|
||||
import TableContainer from "@material-ui/core/TableContainer";
|
||||
import TableHead from "@material-ui/core/TableHead";
|
||||
import TableRow from "@material-ui/core/TableRow";
|
||||
import Paper from "@material-ui/core/Paper";
|
||||
|
||||
import { LineChart, LineSeries, BarChart } from "reaviz";
|
||||
import { GridStripe } from "reaviz";
|
||||
//import { GridlineSeries } from "reaviz";
|
||||
|
||||
import InputLabel from '@material-ui/core/InputLabel';
|
||||
import FormControl from '@material-ui/core/FormControl';
|
||||
import Select from '@material-ui/core/Select';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
|
||||
const data = [
|
||||
{
|
||||
key: new Date("11/29/2019"),
|
||||
data: 10,
|
||||
},
|
||||
{
|
||||
key: new Date("11/30/2019"),
|
||||
data: 14,
|
||||
},
|
||||
{
|
||||
key: new Date("12/01/2019"),
|
||||
data: 5,
|
||||
},
|
||||
{
|
||||
key: new Date("12/02/2019"),
|
||||
data: 18,
|
||||
},
|
||||
];
|
||||
|
||||
const useStyles1 = makeStyles((theme) => ({
|
||||
formControl: {
|
||||
margin: theme.spacing(1),
|
||||
minWidth: 120,
|
||||
},
|
||||
selectEmpty: {
|
||||
marginTop: theme.spacing(2),
|
||||
},
|
||||
}));
|
||||
|
||||
|
||||
const useStyles = makeStyles({
|
||||
table: {
|
||||
minWidth: 650,
|
||||
},
|
||||
root: {
|
||||
minWidth: 275,
|
||||
},
|
||||
bullet: {
|
||||
display: "inline-block",
|
||||
margin: "0 2px",
|
||||
transform: "scale(0.8)",
|
||||
},
|
||||
title: {
|
||||
fontSize: 14,
|
||||
},
|
||||
pos: {
|
||||
marginBottom: 12,
|
||||
},
|
||||
});
|
||||
|
||||
function createData(name, calories, fat, carbs, protein) {
|
||||
return { name, calories, fat, carbs, protein };
|
||||
}
|
||||
|
||||
const rows = [
|
||||
createData("Frozen yoghurt", 159, 6.0, 24, 4.0),
|
||||
createData("Ice cream sandwich", 237, 9.0, 37, 4.3),
|
||||
createData("Eclair", 262, 16.0, 24, 6.0),
|
||||
createData("Cupcake", 305, 3.7, 67, 4.3),
|
||||
createData("Gingerbread", 356, 16.0, 49, 3.9),
|
||||
];
|
||||
|
||||
const DashboardPage = () => {
|
||||
const classes = useStyles();
|
||||
const classes1 = useStyles1();
|
||||
|
||||
const [age, setAge] = React.useState(0);
|
||||
|
||||
const handleChange = (event) => {
|
||||
setAge(event.target.value);
|
||||
|
||||
};
|
||||
|
||||
return (
|
||||
<Container maxWidth="xl">
|
||||
<Grid>
|
||||
<Grid item xl={8} style={{"border":"20px"}}>
|
||||
<center>
|
||||
<Typography type="title" variant="h1" color="inherit">
|
||||
Dashboard
|
||||
</Typography>
|
||||
<div style={{
|
||||
"paddingLeft": "50px"
|
||||
}}>
|
||||
<FormControl className={classes1.formControl}>
|
||||
<InputLabel id="demo-simple-select-label">Organization</InputLabel>
|
||||
<Select
|
||||
labelId="demo-simple-select-label"
|
||||
onChange={handleChange}
|
||||
>
|
||||
<MenuItem value={10}>Ten</MenuItem>
|
||||
<MenuItem value={20}>Twenty</MenuItem>
|
||||
<MenuItem value={30}>Thirty</MenuItem>
|
||||
</Select>
|
||||
</FormControl>
|
||||
</div>
|
||||
</center>
|
||||
</Grid>
|
||||
<Divider />
|
||||
</Grid>
|
||||
<Grid
|
||||
container
|
||||
spacing={2}
|
||||
style={{
|
||||
maxWidth: "1250px",
|
||||
margin: "auto auto 10px",
|
||||
padding: "10px",
|
||||
}}
|
||||
>
|
||||
<Grid item xs={4}>
|
||||
<Card
|
||||
className={classes.root}
|
||||
style={{
|
||||
color: "white",
|
||||
backgroundColor: "RGB(31, 32, 36)",
|
||||
height: "100px",
|
||||
border: "2px solid rgb(197, 17, 82)",
|
||||
}}
|
||||
>
|
||||
<CardContent>
|
||||
<Typography
|
||||
className={classes.title}
|
||||
color="textSecondary"
|
||||
gutterBottom
|
||||
>
|
||||
Total workflows executions
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h1"
|
||||
className={classes.pos}
|
||||
color="textSecondary"
|
||||
>
|
||||
456
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<Card
|
||||
className={classes.root}
|
||||
style={{
|
||||
color: "white",
|
||||
backgroundColor: "RGB(31, 32, 36)",
|
||||
height: "100px",
|
||||
border: "2px solid rgb(244, 194, 13)",
|
||||
}}
|
||||
>
|
||||
<CardContent>
|
||||
<Typography
|
||||
className={classes.title}
|
||||
color="textSecondary"
|
||||
gutterBottom
|
||||
>
|
||||
Total Apps executions
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h1"
|
||||
className={classes.pos}
|
||||
color="textSecondary"
|
||||
>
|
||||
587
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
<Grid item xs={4}>
|
||||
<Card
|
||||
className={classes.root}
|
||||
style={{
|
||||
color: "white",
|
||||
backgroundColor: "RGB(31, 32, 36)",
|
||||
height: "100px",
|
||||
border: "2px solid rgb(72, 133, 237)",
|
||||
}}
|
||||
>
|
||||
<CardContent>
|
||||
<Typography
|
||||
className={classes.title}
|
||||
color="textSecondary"
|
||||
gutterBottom
|
||||
>
|
||||
Total failed executions
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h1"
|
||||
className={classes.pos}
|
||||
color="textSecondary"
|
||||
>
|
||||
999
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid
|
||||
container
|
||||
spacing={3}
|
||||
style={{
|
||||
maxWidth: "1250px",
|
||||
margin: "auto auto 10px",
|
||||
color: "white",
|
||||
backgroundColor: "rgb(39, 41, 45)",
|
||||
padding: "20px",
|
||||
}}
|
||||
>
|
||||
<Grid item md={6}>
|
||||
<BarChart width={600} height={400} data={data} />
|
||||
</Grid>
|
||||
<Grid item md={6}>
|
||||
<LineChart
|
||||
width={600}
|
||||
height={400}
|
||||
data={data}
|
||||
line={<GridStripe fill={"a#393c3e"} />}
|
||||
series={<LineSeries symbols={null} />}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid
|
||||
container
|
||||
spacing={1}
|
||||
style={{
|
||||
maxWidth: "1250px",
|
||||
margin: "auto auto 10px",
|
||||
color: "white",
|
||||
backgroundColor: "rgb(39, 41, 45)",
|
||||
padding: "20px",
|
||||
}}
|
||||
>
|
||||
<Grid item md={12}>
|
||||
<TableContainer
|
||||
component={Paper}
|
||||
style={{
|
||||
color: "white",
|
||||
backgroundColor: "rgb(39, 41, 45) ",
|
||||
}}
|
||||
>
|
||||
<Table
|
||||
className={classes.table}
|
||||
size="small"
|
||||
aria-label="a dense table"
|
||||
>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>Dessert (100g serving)</TableCell>
|
||||
<TableCell align="right">Calories</TableCell>
|
||||
<TableCell align="right">Fat (g)</TableCell>
|
||||
<TableCell align="right">Carbs (g)</TableCell>
|
||||
<TableCell align="right">Protein (g)</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{rows.map((row) => (
|
||||
<TableRow key={row.name}>
|
||||
<TableCell component="th" scope="row">
|
||||
{row.name}
|
||||
</TableCell>
|
||||
<TableCell align="right">{row.calories}</TableCell>
|
||||
<TableCell align="right">{row.fat}</TableCell>
|
||||
<TableCell align="right">{row.carbs}</TableCell>
|
||||
<TableCell align="right">{row.protein}</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardPage;
|
||||
@@ -0,0 +1,483 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import ReactGA from 'react-ga';
|
||||
import WelcomeForm2 from "../components/WelcomeForm2.jsx";
|
||||
import Stepper from "@material-ui/core/Stepper";
|
||||
import Step from "@material-ui/core/Step";
|
||||
import StepLabel from "@material-ui/core/StepLabel";
|
||||
import AppFramework from "../components/AppFramework.jsx";
|
||||
import {
|
||||
Grid,
|
||||
Container,
|
||||
Fade,
|
||||
Typography,
|
||||
Paper,
|
||||
Button,
|
||||
Card,
|
||||
CardContent,
|
||||
CardActionArea,
|
||||
} from '@mui/material';
|
||||
import theme from '../theme';
|
||||
import { useNavigate, Link } from "react-router-dom";
|
||||
|
||||
const Welcome = (props) => {
|
||||
const { globalUrl, surfaceColor, newColor, mini, inputColor, userdata, isLoggedIn, isLoaded } = props;
|
||||
const [skipped, setSkipped] = React.useState(new Set());
|
||||
const [inputUsecase, setInputUsecase] = useState({});
|
||||
const [frameworkData, setFrameworkData] = useState(undefined);
|
||||
const [discoveryWrapper, setDiscoveryWrapper] = useState(undefined);
|
||||
const [activeStep, setActiveStep] = React.useState(0);
|
||||
const [apps, setApps] = React.useState([]);
|
||||
const [defaultSearch, setDefaultSearch] = React.useState("")
|
||||
const [selectionOpen, setSelectionOpen] = React.useState(false)
|
||||
const [showWelcome, setShowWelcome] = React.useState(false)
|
||||
const [usecases, setUsecases] = React.useState([]);
|
||||
const [workflows, setWorkflows] = React.useState([]);
|
||||
|
||||
const isCloud =
|
||||
window.location.host === "localhost:3002" ||
|
||||
window.location.host === "shuffler.io";
|
||||
|
||||
const [steps, setSteps] = useState([
|
||||
"Help us get to know you",
|
||||
"Find your Apps",
|
||||
"Discover Usecases",
|
||||
])
|
||||
|
||||
let navigate = useNavigate();
|
||||
|
||||
const handleKeysetting = (categorydata, workflows) => {
|
||||
//workflows[0].category = ["detect"]
|
||||
//workflows[0].usecase_ids = ["Correlate tickets"]
|
||||
|
||||
if (workflows !== undefined && workflows !== null) {
|
||||
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()) {
|
||||
//console.log("Got match: ", workflow.usecase_ids[usecasekey])
|
||||
|
||||
category.matches.push({
|
||||
"workflow": workflow.id,
|
||||
"category": subcategory.name,
|
||||
})
|
||||
subcategory.matches.push(workflow.id)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (subcategory.matches.length > 0) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newcategories.push(category)
|
||||
}
|
||||
|
||||
setUsecases(newcategories)
|
||||
} else {
|
||||
setUsecases(categorydata)
|
||||
}
|
||||
|
||||
setWorkflows(workflows)
|
||||
}
|
||||
|
||||
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) => {
|
||||
if (responseJson.success !== false) {
|
||||
handleKeysetting(responseJson, workflows)
|
||||
} else {
|
||||
//setWorkflows(workflows);
|
||||
//setWorkflowDone(true);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("Usecase 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) {
|
||||
console.log("Status not 200 for workflows :O!: ", response.status);
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson !== undefined) {
|
||||
var newarray = []
|
||||
for (var key in responseJson) {
|
||||
const wf = responseJson[key]
|
||||
if (wf.public === true) {
|
||||
continue
|
||||
}
|
||||
|
||||
newarray.push(wf)
|
||||
}
|
||||
|
||||
// Workflows are set in here
|
||||
fetchUsecases(newarray)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("err in get workflows: ", error.toString());
|
||||
})
|
||||
}
|
||||
|
||||
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) {
|
||||
setFrameworkData({})
|
||||
|
||||
if (responseJson.reason !== undefined) {
|
||||
//alert.error("Failed loading: " + responseJson.reason)
|
||||
} else {
|
||||
//alert.error("Failed to load framework for your org.")
|
||||
}
|
||||
} else {
|
||||
setFrameworkData(responseJson)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("err in framework: ", error.toString());
|
||||
})
|
||||
}
|
||||
|
||||
const getApps = () => {
|
||||
fetch(globalUrl + "/api/v1/apps", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status !== 200) {
|
||||
console.log("Status not 200 for apps :O!");
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
setApps(responseJson);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("App loading error: "+error.toString());
|
||||
});
|
||||
}
|
||||
|
||||
const usecaseButtons = [{
|
||||
"name": "Phishing",
|
||||
"usecase": "Email management",
|
||||
"color": "#C51152",
|
||||
}, {
|
||||
"name": "Enrichment",
|
||||
"usecase": "2. Enrich",
|
||||
"color": "#F4C20D",
|
||||
}, {
|
||||
"name": "Detection",
|
||||
"usecase": "3. Detect",
|
||||
"color": "#3CBA54",
|
||||
}, {
|
||||
"name": "Response",
|
||||
"usecase": "4. Respond",
|
||||
"color": "#4885ED",
|
||||
}]
|
||||
|
||||
const handleSetSearch = (input, orgupdate) => {
|
||||
console.log("INPUT & ORGUPDATE: ", input, orgupdate, defaultSearch)
|
||||
if (input !== defaultSearch) {
|
||||
setDefaultSearch(input)
|
||||
setSelectionOpen(false)
|
||||
setTimeout(function(){
|
||||
setSelectionOpen(true)
|
||||
}, 150);
|
||||
|
||||
//if (userdata !== undefined && userdata.active_org !== undefined && userdata.active_org.id !== undefined) {
|
||||
// sendOrgUpdate("", "", userdata.active_org.id, orgupdate)
|
||||
//}
|
||||
} else {
|
||||
setDefaultSearch("")
|
||||
setSelectionOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getFramework()
|
||||
getApps()
|
||||
getAvailableWorkflows()
|
||||
|
||||
if (
|
||||
window.location.search !== undefined &&
|
||||
window.location.search !== null
|
||||
) {
|
||||
const urlSearchParams = new URLSearchParams(window.location.search);
|
||||
const params = Object.fromEntries(urlSearchParams.entries());
|
||||
const foundTab = params["tab"];
|
||||
if (foundTab !== null && foundTab !== undefined && !isNaN(foundTab)) {
|
||||
console.log("FOUND TAB: ", foundTab)
|
||||
setShowWelcome(true)
|
||||
if (foundTab === 3 || foundTab === "3") {
|
||||
console.log("SET SEARCH!!")
|
||||
|
||||
handleSetSearch(usecaseButtons[0].name, usecaseButtons[0].usecase)
|
||||
}
|
||||
|
||||
setActiveStep(foundTab-1)
|
||||
} else {
|
||||
navigate(`/welcome?tab=1`)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const isStepSkipped = step => {
|
||||
return skipped.has(step)
|
||||
}
|
||||
|
||||
const paperObject = {
|
||||
flex: 1,
|
||||
padding: 0,
|
||||
textAlign: "center",
|
||||
maxWidth: 300,
|
||||
minWidth: 300,
|
||||
backgroundColor: theme.palette.surfaceColor,
|
||||
color: "white",
|
||||
}
|
||||
|
||||
const actionObject = {
|
||||
padding: "50px 35px 50px 35px",
|
||||
}
|
||||
|
||||
const imageStyle = {
|
||||
width: 150,
|
||||
height: 150,
|
||||
margin: "auto",
|
||||
marginTop: 30,
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{width: 1000, margin: "auto", backgroundColor: theme.palette.platformColor, paddingBottom: 150, minHeight: 1500, }}>
|
||||
{/*
|
||||
<div style={{position: "fixed", bottom: 110, right: 110, display: "flex", }}>
|
||||
<img src="/images/Arrow.png" style={{width: 250, height: "100%",}} />
|
||||
</div>
|
||||
*/}
|
||||
{showWelcome === true ?
|
||||
<div>
|
||||
<div style={{minWidth: 500, maxWidth: 500, margin: "auto", marginTop: isCloud ? "auto" : 20, }}>
|
||||
<Stepper
|
||||
activeStep={activeStep}
|
||||
color="primary"
|
||||
style={{
|
||||
backgroundColor: theme.palette.platformColor,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
padding: 12,
|
||||
border: "1px solid rgba(255,255,255,0.3)",
|
||||
maxWidth: 500,
|
||||
color: "white",
|
||||
}}
|
||||
>
|
||||
{steps.map((label, index) => {
|
||||
const stepProps = {}
|
||||
const labelProps = {}
|
||||
//if (isStepOptional(index)) {
|
||||
// labelProps.optional = "optional"
|
||||
//}
|
||||
|
||||
if (isStepSkipped(index)) {
|
||||
stepProps.completed = false;
|
||||
}
|
||||
|
||||
return (
|
||||
<Step key={label} {...stepProps} style={{maxWidth: 160, color: "white", }}>
|
||||
<StepLabel {...labelProps} style={{marginLeft: 10, color: "white",}}>
|
||||
{label}
|
||||
</StepLabel>
|
||||
</Step>
|
||||
)
|
||||
})}
|
||||
</Stepper>
|
||||
</div>
|
||||
<Grid container spacing={2} style={{ padding: 0, maxWidth: 1000, minWidth: 1000, margin: "auto", }}>
|
||||
<Grid item xs={window.location.href.includes("tab=2") ? 6 : 12}>
|
||||
<div>
|
||||
{/*
|
||||
<WelcomeForm
|
||||
userdata={userdata}
|
||||
globalUrl={globalUrl}
|
||||
discoveryWrapper={discoveryWrapper}
|
||||
setDiscoveryWrapper={setDiscoveryWrapper}
|
||||
/>
|
||||
*/}
|
||||
<WelcomeForm2
|
||||
userdata={userdata}
|
||||
globalUrl={globalUrl}
|
||||
discoveryWrapper={discoveryWrapper}
|
||||
setDiscoveryWrapper={setDiscoveryWrapper}
|
||||
appFramework={frameworkData}
|
||||
getFramework={getFramework}
|
||||
steps={steps}
|
||||
skipped={skipped}
|
||||
setSkipped={setSkipped}
|
||||
activeStep={activeStep}
|
||||
setActiveStep={setActiveStep}
|
||||
getApps={getApps}
|
||||
apps={apps}
|
||||
handleSetSearch={handleSetSearch}
|
||||
usecaseButtons={usecaseButtons}
|
||||
defaultSearch={defaultSearch}
|
||||
setDefaultSearch={setDefaultSearch}
|
||||
selectionOpen={selectionOpen}
|
||||
setSelectionOpen={setSelectionOpen}
|
||||
/>
|
||||
</div>
|
||||
</Grid>
|
||||
{frameworkData === undefined || window.location.href.includes("tab=1") || window.location.href.includes("tab=3") ? null :
|
||||
<div style={{marginTop: 25, }}>
|
||||
<Typography variant="h6" style={{textAlign: "center", marginBottom: 25, }}>
|
||||
App Framework
|
||||
</Typography>
|
||||
<Fade>
|
||||
<AppFramework
|
||||
inputUsecase={inputUsecase}
|
||||
frameworkData={frameworkData}
|
||||
setFrameworkData={setFrameworkData}
|
||||
selectedOption={"Draw"}
|
||||
showOptions={false}
|
||||
isLoaded={true}
|
||||
isLoggedIn={true}
|
||||
globalUrl={globalUrl}
|
||||
size={0.78}
|
||||
color={theme.palette.platformColor}
|
||||
discoveryWrapper={discoveryWrapper}
|
||||
setDiscoveryWrapper={setDiscoveryWrapper}
|
||||
apps={apps}
|
||||
inputUsecases={usecases}
|
||||
setInputUsecases={setUsecases}
|
||||
/>
|
||||
</Fade>
|
||||
</div>
|
||||
}
|
||||
</Grid>
|
||||
</div>
|
||||
:
|
||||
<Fade in={true}>
|
||||
<div style={{maxWidth: 700, margin: "auto", marginTop: 50, }}>
|
||||
<Typography variant="h4" style={{color: "white", textAlign: "center"}}>
|
||||
Welcome to Shuffle
|
||||
</Typography>
|
||||
<Typography variant="body1" style={{textAlign: "center", marginBottom: 50, }}>
|
||||
Who do you identify with the most?
|
||||
</Typography>
|
||||
<div style={{display: "flex", marginTop: 70, width: 700, margin: "auto",}}>
|
||||
<Card style={paperObject} onClick={() => {
|
||||
if (isCloud) {
|
||||
ReactGA.event({
|
||||
category: "welcome",
|
||||
action: "click_welcome_continue",
|
||||
label: "",
|
||||
})
|
||||
} else {
|
||||
//setActiveStep(1)
|
||||
}
|
||||
|
||||
setShowWelcome(true)
|
||||
}}>
|
||||
<CardActionArea style={actionObject}>
|
||||
<Typography variant="h4" style={{color: "#49A928"}}>
|
||||
New to Shuffle
|
||||
</Typography>
|
||||
<img src="/images/welcome_cog.png" style={imageStyle} />
|
||||
<Typography variant="body1" style={{marginTop: 30, color: "rgba(255,255,255,0.8)"}}>
|
||||
Follow our short introduction and learn some tips and tricks
|
||||
</Typography>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
<div style={{marginLeft: 25, marginRight: 25, }}>
|
||||
<Typography style={{marginTop: 200, }}>
|
||||
OR
|
||||
</Typography>
|
||||
</div>
|
||||
<Card style={paperObject} onClick={() => {
|
||||
if (isCloud) {
|
||||
ReactGA.event({
|
||||
category: "welcome",
|
||||
action: "click_getting_started",
|
||||
label: "",
|
||||
})
|
||||
}
|
||||
|
||||
navigate("/workflows?message=Skipped intro")
|
||||
}}>
|
||||
<CardActionArea style={actionObject}>
|
||||
<Typography variant="h4" style={{color: "#f86a3e"}}>
|
||||
Experienced
|
||||
</Typography>
|
||||
<img src="/images/social/shuffle_logo_round.png" style={imageStyle} />
|
||||
<Typography variant="body1" style={{marginTop: 30, color: "rgba(255,255,255,0.8)"}}>
|
||||
You know Shuffle well. Head to the product right away!
|
||||
</Typography>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</Fade>
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Welcome;
|
||||
+664
-273
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python3
|
||||
# Created by Shuffle, AS. <frikky@shuffler.io>.
|
||||
# Based on the Slack integration using Webhooks
|
||||
|
||||
@@ -23,8 +23,7 @@ except Exception as e:
|
||||
# </integration>
|
||||
|
||||
# Global vars
|
||||
|
||||
debug_enabled = False
|
||||
debug_enabled = False
|
||||
pwd = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
|
||||
json_alert = {}
|
||||
now = time.strftime("%a %b %d %H:%M:%S %Z %Y")
|
||||
@@ -32,6 +31,12 @@ now = time.strftime("%a %b %d %H:%M:%S %Z %Y")
|
||||
# Set paths
|
||||
log_file = '{0}/logs/integrations.log'.format(pwd)
|
||||
|
||||
try:
|
||||
with open("/tmp/shuffle_start.txt", "w+") as tmp:
|
||||
tmp.write("Script started")
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def main(args):
|
||||
debug("# Starting")
|
||||
@@ -47,10 +52,18 @@ def main(args):
|
||||
debug(alert_file_location)
|
||||
|
||||
# Load alert. Parse JSON object.
|
||||
with open(alert_file_location) as alert_file:
|
||||
json_alert = json.load(alert_file)
|
||||
try:
|
||||
with open(alert_file_location) as alert_file:
|
||||
json_alert = json.load(alert_file)
|
||||
except:
|
||||
debug("# Alert file %s doesn't exist" % alert_file_location)
|
||||
|
||||
debug("# Processing alert")
|
||||
debug(json_alert)
|
||||
try:
|
||||
debug(json_alert)
|
||||
except Exception as e:
|
||||
debug("Failed getting json_alert %s" % e)
|
||||
sys.exit(1)
|
||||
|
||||
debug("# Generating message")
|
||||
msg = generate_msg(json_alert)
|
||||
@@ -60,6 +73,14 @@ def main(args):
|
||||
debug(msg)
|
||||
|
||||
debug("# Sending message")
|
||||
|
||||
try:
|
||||
with open("/tmp/shuffle_end.txt", "w+") as tmp:
|
||||
tmp.write("Script done pre-msg sending")
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
send_msg(msg, webhook)
|
||||
|
||||
|
||||
@@ -137,9 +158,10 @@ def generate_msg(alert):
|
||||
|
||||
|
||||
def send_msg(msg, url):
|
||||
debug("# In send msg")
|
||||
headers = {'content-type': 'application/json', 'Accept-Charset': 'UTF-8'}
|
||||
res = requests.post(url, data=msg, headers=headers)
|
||||
debug(res)
|
||||
res = requests.post(url, data=msg, headers=headers, verify=False)
|
||||
debug("# After send msg: %s" % res)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
@@ -154,18 +176,26 @@ if __name__ == "__main__":
|
||||
sys.argv[3],
|
||||
sys.argv[4] if len(sys.argv) > 4 else '',
|
||||
)
|
||||
debug_enabled = (len(sys.argv) > 4 and sys.argv[4] == 'debug')
|
||||
#debug_enabled = (len(sys.argv) > 4 and sys.argv[4] == 'debug')
|
||||
debug_enabled = True
|
||||
else:
|
||||
msg = '{0} Wrong arguments'.format(now)
|
||||
bad_arguments = True
|
||||
|
||||
# Logging the call
|
||||
try:
|
||||
f = open(log_file, 'a')
|
||||
except:
|
||||
f = open(log_file, 'w+')
|
||||
f.write("")
|
||||
f.close()
|
||||
|
||||
f = open(log_file, 'a')
|
||||
f.write(msg + '\n')
|
||||
f.close()
|
||||
|
||||
if bad_arguments:
|
||||
debug("# Exiting: Bad arguments.")
|
||||
debug("# Exiting: Bad arguments. Inputted: %s" % sys.argv)
|
||||
sys.exit(1)
|
||||
|
||||
# Main function
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
NAME=shuffle-orborus
|
||||
VERSION=0.9.71
|
||||
VERSION=1.0.9
|
||||
|
||||
echo "Running docker build with $NAME:$VERSION"
|
||||
#docker rmi frikky/shuffle:$NAME --force
|
||||
@@ -10,3 +10,7 @@ docker build . -t frikky/shuffle:$NAME -t docker.pkg.github.com/frikky/shuffle/$
|
||||
docker push frikky/shuffle:$NAME
|
||||
docker push ghcr.io/frikky/$NAME:$VERSION
|
||||
docker push ghcr.io/frikky/$NAME:nightly
|
||||
|
||||
docker push shuffle/shuffle:$NAME
|
||||
docker push ghcr.io/shuffle/$NAME:$VERSION
|
||||
docker push ghcr.io/shuffle/$NAME:nightly
|
||||
|
||||
@@ -8,5 +8,5 @@ require (
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/mackerelio/go-osstat v0.2.1
|
||||
github.com/satori/go.uuid v1.2.0
|
||||
github.com/shuffle/shuffle-shared v0.2.27
|
||||
github.com/shuffle/shuffle-shared v0.3.24
|
||||
)
|
||||
|
||||
@@ -130,6 +130,8 @@ github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJm
|
||||
github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||
github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk=
|
||||
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20221031212613-62deef7fc822 h1:hjXJeBcAMS1WGENGqDpzvmgS43oECTx8UXq31UBu0Jw=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20221031212613-62deef7fc822/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
|
||||
github.com/bradfitz/slice v0.0.0-20180809154707-2b758aa73013 h1:/P9/RL0xgWE+ehnCUUN5h3RpG3dmoMCOONO1CCvq23Y=
|
||||
github.com/bradfitz/slice v0.0.0-20180809154707-2b758aa73013/go.mod h1:pccXHIvs3TV/TUqSNyEvF99sxjX2r4FFRIyw6TZY9+w=
|
||||
github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk=
|
||||
@@ -786,6 +788,18 @@ github.com/shuffle/shuffle-shared v0.2.9 h1:fh2eOD7olifW2uyC3Vlp8u3dqhgIxtYaDVja
|
||||
github.com/shuffle/shuffle-shared v0.2.9/go.mod h1:YuMle0RjwXb3hxR5PdaOOD9e+hUyK34OABS0UbrT/Sk=
|
||||
github.com/shuffle/shuffle-shared v0.2.27 h1:YT9MtXyMSxIGMpNovjp9pCKFyt2gk40EdAXqDvldhM8=
|
||||
github.com/shuffle/shuffle-shared v0.2.27/go.mod h1:YuMle0RjwXb3hxR5PdaOOD9e+hUyK34OABS0UbrT/Sk=
|
||||
github.com/shuffle/shuffle-shared v0.2.41 h1:1TBP/47Xzh7ysi6I++wJxRxKpZYp7NBe7YF1DrFL2mA=
|
||||
github.com/shuffle/shuffle-shared v0.2.41/go.mod h1:YuMle0RjwXb3hxR5PdaOOD9e+hUyK34OABS0UbrT/Sk=
|
||||
github.com/shuffle/shuffle-shared v0.2.63 h1:IF82o5WS4+6wEIirqAd1qEm/pBCuQMbn2CJeuAt2qFI=
|
||||
github.com/shuffle/shuffle-shared v0.2.63/go.mod h1:YuMle0RjwXb3hxR5PdaOOD9e+hUyK34OABS0UbrT/Sk=
|
||||
github.com/shuffle/shuffle-shared v0.2.64 h1:WpCKiL5tNt7wTJaHkf1zXhjeUB5ltFap1dXgU6ijKq4=
|
||||
github.com/shuffle/shuffle-shared v0.2.64/go.mod h1:YuMle0RjwXb3hxR5PdaOOD9e+hUyK34OABS0UbrT/Sk=
|
||||
github.com/shuffle/shuffle-shared v0.2.82 h1:V3bYw7MxHPQgydUuWLGHONpX7NwbRPPAALovKqsdaW0=
|
||||
github.com/shuffle/shuffle-shared v0.2.82/go.mod h1:YuMle0RjwXb3hxR5PdaOOD9e+hUyK34OABS0UbrT/Sk=
|
||||
github.com/shuffle/shuffle-shared v0.3.5 h1:erfXVKjeSkmpoGczZ6hPETg8gDdZeXYgXsrEgVO/uqg=
|
||||
github.com/shuffle/shuffle-shared v0.3.5/go.mod h1:YuMle0RjwXb3hxR5PdaOOD9e+hUyK34OABS0UbrT/Sk=
|
||||
github.com/shuffle/shuffle-shared v0.3.24 h1:zBDZan4u2XjC6TAi5BdFoVroBPGYd6PAha+3/cSfD6w=
|
||||
github.com/shuffle/shuffle-shared v0.3.24/go.mod h1:yI6HCog/R3Kq1FvCIVbXedLl87rtSuDOyzolmuMswB4=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/sirupsen/logrus v1.0.4-0.20170822132746-89742aefa4b2/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||
github.com/sirupsen/logrus v1.0.6/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc=
|
||||
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -57,6 +58,7 @@ var workerTimeoutEnv = os.Getenv("SHUFFLE_ORBORUS_EXECUTION_TIMEOUT")
|
||||
var concurrencyEnv = os.Getenv("SHUFFLE_ORBORUS_EXECUTION_CONCURRENCY")
|
||||
var appSdkVersion = os.Getenv("SHUFFLE_APP_SDK_VERSION")
|
||||
var workerVersion = os.Getenv("SHUFFLE_WORKER_VERSION")
|
||||
var newWorkerImage = os.Getenv("SHUFFLE_WORKER_IMAGE")
|
||||
|
||||
//var baseimagename = "docker.pkg.github.com/frikky/shuffle"
|
||||
//var baseimagename = "ghcr.io/frikky"
|
||||
@@ -282,6 +284,35 @@ func deployServiceWorkers(image string) {
|
||||
}
|
||||
}
|
||||
|
||||
if len(os.Getenv("DOCKER_HOST")) > 0 {
|
||||
log.Printf("[DEBUG] Deploying docker socket proxy to the network %s as the DOCKER_HOST variable is set", networkName)
|
||||
//if err == nil {
|
||||
containers, err := dockercli.ContainerList(ctx, types.ContainerListOptions{
|
||||
All: true,
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
for _, container := range containers {
|
||||
if strings.Contains(strings.ToLower(container.Image), "docker-socket-proxy") {
|
||||
networkConfig := &network.EndpointSettings{}
|
||||
err := dockercli.NetworkConnect(ctx, networkName, container.ID, networkConfig)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed connecting Docker socket proxy to docker network %s: %s", networkName, err)
|
||||
} else {
|
||||
log.Printf("[INFO] Attached the docker socket proxy to the execution network")
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Printf("[ERROR] Failed listing containers when deploying socket proxy on swarm: %s", err)
|
||||
}
|
||||
//} else {
|
||||
// log.Printf("[ERROR] Failed listing and finding the right image for docker socket proxy: %s", err)
|
||||
//}
|
||||
}
|
||||
|
||||
//serviceOptions := types.ServiceCreateOptions{}
|
||||
//service, err := dockercli.ServiceCreate(
|
||||
// context.Background(),
|
||||
@@ -385,13 +416,15 @@ func deployServiceWorkers(image string) {
|
||||
},
|
||||
}
|
||||
|
||||
if defaultNetworkAttach == true {
|
||||
if defaultNetworkAttach == true || strings.ToLower(os.Getenv("SHUFFLE_DEFAULT_NETWORK_ATTACH")) == "true" {
|
||||
targetName := "shuffle_shuffle"
|
||||
log.Printf("[DEBUG] Adding network attach for network %s to worker in swarm", targetName)
|
||||
serviceSpec.Networks = append(serviceSpec.Networks, swarm.NetworkAttachmentConfig{
|
||||
Target: "shuffle_shuffle",
|
||||
Target: targetName,
|
||||
})
|
||||
|
||||
// FIXM: Remove this if deployment fails?
|
||||
serviceSpec.TaskTemplate.ContainerSpec.Env = append(serviceSpec.TaskTemplate.ContainerSpec.Env, fmt.Sprintf("SHUFFLE_SWARM_OTHER_NETWORK=shuffle_shuffle"))
|
||||
serviceSpec.TaskTemplate.ContainerSpec.Env = append(serviceSpec.TaskTemplate.ContainerSpec.Env, fmt.Sprintf("SHUFFLE_SWARM_OTHER_NETWORK=%s", targetName))
|
||||
}
|
||||
|
||||
if dockerApiVersion != "" {
|
||||
@@ -411,12 +444,23 @@ func deployServiceWorkers(image string) {
|
||||
if len(os.Getenv("DOCKER_HOST")) > 0 {
|
||||
serviceSpec.TaskTemplate.ContainerSpec.Env = append(serviceSpec.TaskTemplate.ContainerSpec.Env, fmt.Sprintf("DOCKER_HOST=%s", os.Getenv("DOCKER_HOST")))
|
||||
} else {
|
||||
serviceSpec.TaskTemplate.ContainerSpec.Mounts = []mount.Mount{
|
||||
mount.Mount{
|
||||
Source: "/var/run/docker.sock",
|
||||
Target: "/var/run/docker.sock",
|
||||
Type: mount.TypeBind,
|
||||
},
|
||||
if runtime.GOOS == "windows" {
|
||||
serviceSpec.TaskTemplate.ContainerSpec.Mounts = []mount.Mount{
|
||||
mount.Mount{
|
||||
Source: `\\.\pipe\docker_engine`,
|
||||
Target: `\\.\pipe\docker_engine`,
|
||||
Type: mount.TypeBind,
|
||||
},
|
||||
}
|
||||
} else {
|
||||
serviceSpec.TaskTemplate.ContainerSpec.Mounts = []mount.Mount{
|
||||
mount.Mount{
|
||||
Source: "/var/run/docker.sock",
|
||||
Target: "/var/run/docker.sock",
|
||||
Type: mount.TypeBind,
|
||||
},
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -475,7 +519,11 @@ func deployWorker(image string, identifier string, env []string, executionReques
|
||||
}
|
||||
|
||||
if len(os.Getenv("DOCKER_HOST")) == 0 {
|
||||
hostConfig.Binds = []string{"/var/run/docker.sock:/var/run/docker.sock:rw"}
|
||||
if runtime.GOOS == "windows" {
|
||||
hostConfig.Binds = []string{`\\.\pipe\docker_engine:\\.\pipe\docker_engine`}
|
||||
} else {
|
||||
hostConfig.Binds = []string{"/var/run/docker.sock:/var/run/docker.sock:rw"}
|
||||
}
|
||||
}
|
||||
|
||||
hostConfig.NetworkMode = container.NetworkMode(fmt.Sprintf("container:%s", containerId))
|
||||
@@ -642,12 +690,12 @@ func initializeImages() {
|
||||
ctx := context.Background()
|
||||
|
||||
if appSdkVersion == "" {
|
||||
appSdkVersion = "0.8.97"
|
||||
appSdkVersion = "1.1.0"
|
||||
log.Printf("[WARNING] SHUFFLE_APP_SDK_VERSION not defined. Defaulting to %s", appSdkVersion)
|
||||
}
|
||||
|
||||
if workerVersion == "" {
|
||||
workerVersion = "nightly"
|
||||
workerVersion = "1.1.0"
|
||||
log.Printf("[WARNING] SHUFFLE_WORKER_VERSION not defined. Defaulting to %s", workerVersion)
|
||||
}
|
||||
|
||||
@@ -657,24 +705,23 @@ func initializeImages() {
|
||||
log.Printf("[DEBUG] Setting baseimageregistry")
|
||||
}
|
||||
if baseimagename == "" {
|
||||
baseimagename = "frikky/shuffle"
|
||||
baseimagename = "frikky"
|
||||
baseimagename = "shuffle/shuffle" // Dockerhub
|
||||
baseimagename = "shuffle" // Github
|
||||
log.Printf("[DEBUG] Setting baseimagename")
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] Setting swarm config to %#v. Default is empty.", swarmConfig)
|
||||
|
||||
newWorker := fmt.Sprintf("%s/%s/shuffle-worker:%s", baseimageregistry, baseimagename, workerVersion)
|
||||
if len(newWorkerImage) > 0 {
|
||||
newWorker = newWorkerImage
|
||||
}
|
||||
|
||||
// check whether they are the same first
|
||||
images := []string{
|
||||
fmt.Sprintf("frikky/shuffle:app_sdk"),
|
||||
fmt.Sprintf("shuffle/shuffle:app_sdk"),
|
||||
fmt.Sprintf("%s/%s/shuffle-app_sdk:%s", baseimageregistry, baseimagename, appSdkVersion),
|
||||
fmt.Sprintf("%s/%s/shuffle-worker:%s", baseimageregistry, baseimagename, workerVersion),
|
||||
// fmt.Sprintf("docker.io/%s:app_sdk", baseimagename),
|
||||
// fmt.Sprintf("docker.io/%s:worker", baseimagename),
|
||||
|
||||
//fmt.Sprintf("%s/worker:%s", baseimagename, workerVersion),
|
||||
//fmt.Sprintf("%s/app_sdk:%s", baseimagename, appSdkVersion),
|
||||
//fmt.Sprintf("frikky/shuffle:app_sdk"),
|
||||
newWorker,
|
||||
}
|
||||
|
||||
pullOptions := types.ImagePullOptions{}
|
||||
@@ -787,6 +834,18 @@ func checkSwarmService(ctx context.Context) {
|
||||
|
||||
// Initial loop etc
|
||||
func main() {
|
||||
startupDelay := os.Getenv("SHUFFLE_ORBORUS_STARTUP_DELAY")
|
||||
if len(startupDelay) > 0 {
|
||||
log.Printf("[DEBUG] Setting startup delay to %#v", startupDelay)
|
||||
|
||||
tmpInt, err := strconv.Atoi(startupDelay)
|
||||
if err == nil {
|
||||
time.Sleep(time.Duration(tmpInt) * time.Second)
|
||||
} else {
|
||||
log.Printf("[WARNING] Env SHUFFLE_ORBORUS_STARTUP_DELAY must be a number, not %s", startupDelay)
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("[INFO] Setting up execution environment")
|
||||
|
||||
//FIXME
|
||||
@@ -835,8 +894,9 @@ func main() {
|
||||
|
||||
if len(os.Getenv("DOCKER_HOST")) > 0 {
|
||||
log.Printf("[DEBUG] Running docker with socket proxy %s instead of default", os.Getenv("DOCKER_HOST"))
|
||||
|
||||
} else {
|
||||
log.Printf("[DEBUG] Running docker with default socket /var/run/docker.sock")
|
||||
log.Printf(`[DEBUG] Running docker with default socket /var/run/docker.sock or `)
|
||||
}
|
||||
|
||||
ctx := context.Background()
|
||||
@@ -859,6 +919,10 @@ func main() {
|
||||
initializeImages()
|
||||
|
||||
workerImage := fmt.Sprintf("%s/%s/shuffle-worker:%s", baseimageregistry, baseimagename, workerVersion)
|
||||
if len(newWorkerImage) > 0 {
|
||||
workerImage = newWorkerImage
|
||||
}
|
||||
|
||||
if swarmConfig == "run" || swarmConfig == "swarm" {
|
||||
checkSwarmService(ctx)
|
||||
|
||||
@@ -896,7 +960,7 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
client.Timeout = 10 * time.Second
|
||||
client.Timeout = 30 * time.Second
|
||||
|
||||
fullUrl := fmt.Sprintf("%s/api/v1/workflows/queue", baseUrl)
|
||||
req, err := http.NewRequest(
|
||||
@@ -922,15 +986,15 @@ func main() {
|
||||
req.Header.Add("Org", org)
|
||||
}
|
||||
|
||||
log.Printf("[INFO] Waiting for executions at %s with Environment %s", fullUrl, environment)
|
||||
log.Printf("[INFO] Waiting for executions at %s with Environment %#v", fullUrl, environment)
|
||||
hasStarted := false
|
||||
for {
|
||||
//go getStats()
|
||||
//log.Printf("Prerequest")
|
||||
//log.Printf("Postrequest")
|
||||
//log.Printf("[DEBUG] Prerequest - queue")
|
||||
newresp, err := client.Do(req)
|
||||
//log.Printf("[DEBUG] Postrequest - queue")
|
||||
if err != nil {
|
||||
log.Printf("[WARNING] Failed making request: %s", err)
|
||||
log.Printf("[WARNING] Failed making request to %s: %s", fullUrl, err)
|
||||
|
||||
zombiecounter += 1
|
||||
if zombiecounter*sleepTime > workerTimeout {
|
||||
@@ -1330,7 +1394,6 @@ func sendWorkerRequest(workflowExecution shuffle.ExecutionRequest) error {
|
||||
|
||||
//log.Printf("[DEBUG] Data: %s", string(data))
|
||||
|
||||
//streamUrl := fmt.Sprintf("http://shuffle-workers:33333/api/v1/execute", parsedBaseurl)
|
||||
streamUrl := fmt.Sprintf("http://shuffle-workers:33333/api/v1/execute")
|
||||
if containerId == "" || containerId == "shuffle-orborus" {
|
||||
streamUrl = fmt.Sprintf("%s:33333/api/v1/execute", parsedBaseurl)
|
||||
@@ -1347,6 +1410,10 @@ func sendWorkerRequest(workflowExecution shuffle.ExecutionRequest) error {
|
||||
log.Printf("[ERROR] Failed creating worker request: %s", err)
|
||||
if strings.Contains(fmt.Sprintf("%s", err), "connection refused") || strings.Contains(fmt.Sprintf("%s", err), "EOF") {
|
||||
workerImage := fmt.Sprintf("%s/%s/shuffle-worker:%s", baseimageregistry, baseimagename, workerVersion)
|
||||
|
||||
if len(newWorkerImage) > 0 {
|
||||
workerImage = newWorkerImage
|
||||
}
|
||||
deployServiceWorkers(workerImage)
|
||||
|
||||
time.Sleep(time.Duration(10) * time.Second)
|
||||
@@ -1361,6 +1428,11 @@ func sendWorkerRequest(workflowExecution shuffle.ExecutionRequest) error {
|
||||
log.Printf("[ERROR] Error running worker request to %s (1): %s", streamUrl, err)
|
||||
if strings.Contains(fmt.Sprintf("%s", err), "connection refused") || strings.Contains(fmt.Sprintf("%s", err), "EOF") {
|
||||
workerImage := fmt.Sprintf("%s/%s/shuffle-worker:%s", baseimageregistry, baseimagename, workerVersion)
|
||||
|
||||
if len(newWorkerImage) > 0 {
|
||||
workerImage = newWorkerImage
|
||||
}
|
||||
|
||||
deployServiceWorkers(workerImage)
|
||||
|
||||
time.Sleep(time.Duration(10) * time.Second)
|
||||
@@ -1384,12 +1456,6 @@ func sendWorkerRequest(workflowExecution shuffle.ExecutionRequest) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
//workerImage := fmt.Sprintf("%s/%s/shuffle-worker:%s", baseimageregistry, baseimagename, workerVersion)
|
||||
//deployServiceWorkers(workerImage)
|
||||
|
||||
//time.Sleep(time.Duration(10) * time.Second)
|
||||
//err = sendWorkerRequest(executionRequest)
|
||||
|
||||
return errors.New(fmt.Sprintf("Bad statuscode from worker: %d - expecting 200", newresp.StatusCode))
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,19 @@
|
||||
#docker run \
|
||||
# --env DOCKER_API_VERSION=1.40 \
|
||||
# --env ENVIRONMENT_NAME="Shuffle" \
|
||||
# --env BASE_URL="http://192.168.86.45:5001" \
|
||||
# --env HTTP_PROXY="http://192.168.86.45:8082" \
|
||||
# --env HTTPS_PROXY="https://192.168.86.45:8082" \
|
||||
# --env SHUFFLE_PASS_WORKER_PROXY=true \
|
||||
# --env SHUFFLE_PASS_APP_PROXY=true \
|
||||
# -v /var/run/docker.sock:/var/run/docker.sock \
|
||||
# ghcr.io/frikky/shuffle-orborus:nightly
|
||||
|
||||
docker run \
|
||||
--env DOCKER_API_VERSION=1.40 \
|
||||
--env ENVIRONMENT_NAME="Shuffle" \
|
||||
--env BASE_URL="http://192.168.86.45:5001" \
|
||||
--env HTTP_PROXY="http://192.168.86.45:8082" \
|
||||
--env HTTPS_PROXY="https://192.168.86.45:8082" \
|
||||
--env SHUFFLE_PASS_WORKER_PROXY=true \
|
||||
--env SHUFFLE_PASS_APP_PROXY=true \
|
||||
--env ENVIRONMENT_NAME="Another env" \
|
||||
--env ORG="2e7b6a08-b63b-4fc2-bd70-718091509db1" \
|
||||
--env AUTH="env auth" \
|
||||
--env BASE_URL="https://shuffler.io" \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
ghcr.io/frikky/shuffle-orborus:nightly
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
NAME=shuffle-worker
|
||||
VERSION=0.9.71
|
||||
VERSION=1.1.0
|
||||
|
||||
echo "Running docker build with $NAME:$VERSION"
|
||||
#CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o worker.bin .
|
||||
|
||||
@@ -10,6 +10,6 @@ require (
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/shuffle/shuffle-shared v0.2.27
|
||||
github.com/shuffle/shuffle-shared v0.3.24
|
||||
go4.org v0.0.0-20201209231011-d4a079459e60 // indirect
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -16,7 +16,6 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -24,7 +23,6 @@ import (
|
||||
"github.com/docker/docker/api/types/container"
|
||||
//"github.com/docker/docker/api/types/filters"
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
dockerclient "github.com/docker/docker/client"
|
||||
//"github.com/go-git/go-billy/v5/memfs"
|
||||
|
||||
@@ -113,31 +111,6 @@ func shutdown(workflowExecution shuffle.WorkflowExecution, nodeId string, reason
|
||||
}
|
||||
|
||||
// Might not be necessary because of cleanupEnv hostconfig autoremoval
|
||||
//if cleanupEnv == "true" && len(containerIds) > 0 && (os.Getenv("SHUFFLE_SWARM_CONFIG") != "run" && os.Getenv("SHUFFLE_SWARM_CONFIG") != "swarm") {
|
||||
if cleanupEnv == "true" && (os.Getenv("SHUFFLE_SWARM_CONFIG") != "run" && os.Getenv("SHUFFLE_SWARM_CONFIG") != "swarm") {
|
||||
/*
|
||||
ctx := context.Background()
|
||||
dockercli, err := dockerclient.NewEnvClient()
|
||||
if err == nil {
|
||||
log.Printf("[INFO] Cleaning up %d containers", len(containerIds))
|
||||
removeOptions := types.ContainerRemoveOptions{
|
||||
RemoveVolumes: true,
|
||||
Force: true,
|
||||
}
|
||||
|
||||
for _, containername := range containerIds {
|
||||
log.Printf("[INFO] Should stop and and remove container %s (deprecated)", containername)
|
||||
//dockercli.ContainerStop(ctx, containername, nil)
|
||||
//dockercli.ContainerRemove(ctx, containername, removeOptions)
|
||||
//removeContainers = append(removeContainers, containername)
|
||||
}
|
||||
}
|
||||
*/
|
||||
} else {
|
||||
if os.Getenv("SHUFFLE_SWARM_CONFIG") != "run" && os.Getenv("SHUFFLE_SWARM_CONFIG") != "swarm" {
|
||||
log.Printf("[DEBUG][%s] NOT cleaning up containers. IDS: %d, CLEANUP env: %s", workflowExecution.ExecutionId, 0, cleanupEnv)
|
||||
}
|
||||
}
|
||||
|
||||
if len(reason) > 0 && len(nodeId) > 0 {
|
||||
//log.Printf("[INFO] Running abort of workflow because it should be finished")
|
||||
@@ -165,16 +138,11 @@ func shutdown(workflowExecution shuffle.WorkflowExecution, nodeId string, reason
|
||||
log.Printf("[INFO][%s] Failed building request: %s", workflowExecution.ExecutionId, err)
|
||||
}
|
||||
|
||||
// FIXME: Add an API call to the backend
|
||||
if os.Getenv("SHUFFLE_SWARM_CONFIG") != "run" && os.Getenv("SHUFFLE_SWARM_CONFIG") != "swarm" {
|
||||
authorization := os.Getenv("AUTHORIZATION")
|
||||
if len(authorization) > 0 {
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", authorization))
|
||||
} else {
|
||||
log.Printf("[ERROR][%s] No authorization specified for abort", workflowExecution.ExecutionId)
|
||||
}
|
||||
authorization := os.Getenv("AUTHORIZATION")
|
||||
if len(authorization) > 0 {
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", authorization))
|
||||
} else {
|
||||
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", workflowExecution.Authorization))
|
||||
log.Printf("[ERROR][%s] No authorization specified for abort", workflowExecution.ExecutionId)
|
||||
}
|
||||
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
@@ -210,29 +178,8 @@ func shutdown(workflowExecution shuffle.WorkflowExecution, nodeId string, reason
|
||||
//Finished shutdown (after %d seconds). ", sleepDuration)
|
||||
|
||||
// Allows everything to finish in subprocesses (apps)
|
||||
if os.Getenv("SHUFFLE_SWARM_CONFIG") != "run" && os.Getenv("SHUFFLE_SWARM_CONFIG") != "swarm" {
|
||||
time.Sleep(time.Duration(sleepDuration) * time.Second)
|
||||
os.Exit(3)
|
||||
} else {
|
||||
log.Printf("[DEBUG][%s] Sending result and resetting values (K8s & Swarm).", workflowExecution.ExecutionId)
|
||||
//UpdateExecutionVariables(ctx, workflowExecution.ExecutionId, startAction, children, parents, visited, executed, nextActions, environments, extra)
|
||||
|
||||
/*
|
||||
environments = []string{}
|
||||
parents = map[string][]string{}
|
||||
children = map[string][]string{}
|
||||
visited = []string{}
|
||||
executed = []string{}
|
||||
nextActions = []string{}
|
||||
containerIds = []string{}
|
||||
extra = 0
|
||||
startAction = ""
|
||||
results = []shuffle.ActionResult{}
|
||||
allLogs = map[string]string{}
|
||||
*/
|
||||
//requestsSent = 0
|
||||
//executionRunning = false
|
||||
}
|
||||
time.Sleep(time.Duration(sleepDuration) * time.Second)
|
||||
os.Exit(3)
|
||||
//cacheKey := fmt.Sprintf("workflowexecution-%s", workflowExecution.ExecutionId)
|
||||
}
|
||||
|
||||
@@ -241,58 +188,6 @@ func deployApp(cli *dockerclient.Client, image string, identifier string, env []
|
||||
// form basic hostConfig
|
||||
ctx := context.Background()
|
||||
|
||||
if os.Getenv("SHUFFLE_SWARM_CONFIG") == "run" || os.Getenv("SHUFFLE_SWARM_CONFIG") == "swarm" {
|
||||
//identifier := fmt.Sprintf("%s_%s_%s_%s", appname, appversion, action.ID, workflowExecution.ExecutionId)
|
||||
|
||||
appName := strings.Replace(identifier, fmt.Sprintf("_%s", action.ID), "", -1)
|
||||
appName = strings.Replace(appName, fmt.Sprintf("_%s", workflowExecution.ExecutionId), "", -1)
|
||||
appName = strings.ToLower(appName)
|
||||
//log.Printf("[INFO][%s] New appname: %s, image: %s", workflowExecution.ExecutionId, appName, image)
|
||||
|
||||
if !shuffle.ArrayContains(downloadedImages, image) {
|
||||
log.Printf("[DEBUG] Downloading image %s from backend as it's first iteration for this image on the worker.", image)
|
||||
// FIXME: Not caring if it's ok or not. Just continuing
|
||||
// This is working as intended, just designed to download an updated
|
||||
// image on every Orborus/new worker restart.
|
||||
|
||||
// Running as coroutine for eventual completeness
|
||||
//go downloadDockerImageBackend(&http.Client{}, image)
|
||||
// FIXME: With goroutines it got too much trouble of deploying with an older version
|
||||
// Allowing slow startups, as long as it's eventually fast, and uses the same registry as on host.
|
||||
downloadDockerImageBackend(&http.Client{}, image)
|
||||
}
|
||||
|
||||
exposedPort, err := findAppInfo(image, appName)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed finding and creating port for %s: %s", appName, err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG][%s] Should run towards port %d for app %s. DELAY: %d", workflowExecution.ExecutionId, exposedPort, appName, action.ExecutionDelay)
|
||||
if action.ExecutionDelay > 0 {
|
||||
//log.Printf("[DEBUG] Running app %s with delay of %d", action.Name, action.ExecutionDelay)
|
||||
waitTime := time.Duration(action.ExecutionDelay) * time.Second
|
||||
|
||||
time.AfterFunc(waitTime, func() {
|
||||
err = sendAppRequest(baseUrl, appName, exposedPort, action, workflowExecution)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed sending SCHEDULED request to app %s on port %d: %s", appName, exposedPort, err)
|
||||
}
|
||||
})
|
||||
|
||||
} else {
|
||||
//log.Printf("[DEBUG] Running app %s NORMALLY as there is no delay set", action.Name)
|
||||
err = sendAppRequest(baseUrl, appName, exposedPort, action, workflowExecution)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed sending request to app %s on port %d: %s", appName, exposedPort, err)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
//log.Printf("[DEBUG] Successfully ran request towards port %d for app %s", exposedPort, appName)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Max 10% CPU every second
|
||||
//CPUShares: 128,
|
||||
//CPUQuota: 10000,
|
||||
@@ -307,10 +202,7 @@ func deployApp(cli *dockerclient.Client, image string, identifier string, env []
|
||||
Resources: container.Resources{},
|
||||
}
|
||||
|
||||
if os.Getenv("SHUFFLE_SWARM_CONFIG") != "run" && os.Getenv("SHUFFLE_SWARM_CONFIG") != "swarm" {
|
||||
hostConfig.NetworkMode = container.NetworkMode(fmt.Sprintf("container:worker-%s", workflowExecution.ExecutionId))
|
||||
//log.Printf("Environments: %#v", env)
|
||||
}
|
||||
hostConfig.NetworkMode = container.NetworkMode(fmt.Sprintf("container:worker-%s", workflowExecution.ExecutionId))
|
||||
|
||||
// Removing because log extraction should happen first
|
||||
if cleanupEnv == "true" {
|
||||
@@ -2104,7 +1996,7 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl
|
||||
resultLength := len(workflowExecution.Results)
|
||||
setExecution := true
|
||||
|
||||
workflowExecution, dbSave, err := shuffle.ParsedExecutionResult(ctx, *workflowExecution, actionResult, true)
|
||||
workflowExecution, dbSave, err := shuffle.ParsedExecutionResult(ctx, *workflowExecution, actionResult, true, 0)
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG] Rerunning transaction? %s", err)
|
||||
if strings.Contains(fmt.Sprintf("%s", err), "Rerun this transaction") {
|
||||
@@ -2119,7 +2011,7 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl
|
||||
resultLength = len(workflowExecution.Results)
|
||||
setExecution = true
|
||||
|
||||
workflowExecution, dbSave, err = shuffle.ParsedExecutionResult(ctx, *workflowExecution, actionResult, false)
|
||||
workflowExecution, dbSave, err = shuffle.ParsedExecutionResult(ctx, *workflowExecution, actionResult, false, 0)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed execution of parsedexecution (2): %s", err)
|
||||
resp.WriteHeader(401)
|
||||
@@ -2171,13 +2063,6 @@ func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workfl
|
||||
return
|
||||
}
|
||||
|
||||
if os.Getenv("SHUFFLE_SWARM_CONFIG") == "run" || os.Getenv("SHUFFLE_SWARM_CONFIG") == "swarm" {
|
||||
finished := validateFinished(*workflowExecution)
|
||||
if !finished {
|
||||
log.Printf("[DEBUG][%s] Handling next node since it's not finished!", workflowExecution.ExecutionId)
|
||||
handleExecutionResult(*workflowExecution)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log.Printf("[INFO][%s] Skipping setexec with status %s", workflowExecution.ExecutionId, workflowExecution.Status)
|
||||
|
||||
@@ -2211,7 +2096,7 @@ func getWorkflowExecution(ctx context.Context, id string) (*shuffle.WorkflowExec
|
||||
}
|
||||
|
||||
func sendResult(workflowExecution shuffle.WorkflowExecution, data []byte) {
|
||||
if workflowExecution.ExecutionSource == "default" && os.Getenv("SHUFFLE_SWARM_CONFIG") != "run" && os.Getenv("SHUFFLE_SWARM_CONFIG") != "swarm" {
|
||||
if workflowExecution.ExecutionSource == "default" {
|
||||
log.Printf("[INFO][%s] Not sending backend info since source is default", workflowExecution.ExecutionId)
|
||||
return
|
||||
}
|
||||
@@ -2253,7 +2138,7 @@ func validateFinished(workflowExecution shuffle.WorkflowExecution) bool {
|
||||
log.Printf("[INFO][%s] VALIDATION. Status: %s, shuffle.Actions: %d, Extra: %d, Results: %d. Parent: %#v\n", workflowExecution.ExecutionId, workflowExecution.Status, len(workflowExecution.Workflow.Actions), extra, len(workflowExecution.Results), workflowExecution.ExecutionParent)
|
||||
|
||||
//if len(workflowExecution.Results) == len(workflowExecution.Workflow.Actions)+extra {
|
||||
if (len(environments) == 1 && requestsSent == 0 && len(workflowExecution.Results) >= 1 && os.Getenv("SHUFFLE_SWARM_CONFIG") != "run" && os.Getenv("SHUFFLE_SWARM_CONFIG") != "swarm") || (len(workflowExecution.Results) >= len(workflowExecution.Workflow.Actions)+extra && len(workflowExecution.Workflow.Actions) > 0) {
|
||||
if (len(environments) == 1 && requestsSent == 0 && len(workflowExecution.Results) >= 1) || (len(workflowExecution.Results) >= len(workflowExecution.Workflow.Actions)+extra && len(workflowExecution.Workflow.Actions) > 0) {
|
||||
if workflowExecution.Status == "FINISHED" {
|
||||
for _, result := range workflowExecution.Results {
|
||||
if result.Status == "EXECUTING" || result.Status == "WAITING" {
|
||||
@@ -2263,9 +2148,7 @@ func validateFinished(workflowExecution shuffle.WorkflowExecution) bool {
|
||||
}
|
||||
}
|
||||
|
||||
if os.Getenv("SHUFFLE_SWARM_CONFIG") != "run" && os.Getenv("SHUFFLE_SWARM_CONFIG") != "swarm" {
|
||||
requestsSent += 1
|
||||
}
|
||||
requestsSent += 1
|
||||
|
||||
log.Printf("[DEBUG][%s] Should send full result to %s", workflowExecution.ExecutionId, baseUrl)
|
||||
|
||||
@@ -2340,10 +2223,6 @@ func setWorkflowExecution(ctx context.Context, workflowExecution shuffle.Workflo
|
||||
cacheKey := fmt.Sprintf("workflowexecution-%s", workflowExecution.ExecutionId)
|
||||
requestCache.Set(cacheKey, &workflowExecution, cache.DefaultExpiration)
|
||||
|
||||
if os.Getenv("SHUFFLE_SWARM_CONFIG") == "run" || os.Getenv("SHUFFLE_SWARM_CONFIG") == "swarm" {
|
||||
return nil
|
||||
}
|
||||
|
||||
handleExecutionResult(workflowExecution)
|
||||
validateFinished(workflowExecution)
|
||||
|
||||
@@ -2366,69 +2245,6 @@ func setWorkflowExecution(ctx context.Context, workflowExecution shuffle.Workflo
|
||||
// GetLocalIP returns the non loopback local IP of the host
|
||||
func getLocalIP() string {
|
||||
|
||||
if os.Getenv("SHUFFLE_SWARM_CONFIG") == "run" || os.Getenv("SHUFFLE_SWARM_CONFIG") == "swarm" {
|
||||
name, err := os.Hostname()
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Couldn't find hostanme of worker: %s", err)
|
||||
os.Exit(3)
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] Found hostname %s since worker is running with \"run\" command", name)
|
||||
return name
|
||||
|
||||
/**
|
||||
Everything below was a test to see if we needed to match directly to a network interface. May require docker network API.
|
||||
**/
|
||||
|
||||
log.Printf("[DEBUG] Looking for IP for the external docker-network %s", swarmNetworkName)
|
||||
// Different process to ensure we find the right IP.
|
||||
// Necessary due to Ingress being added to docker ser
|
||||
ifaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] FATAL: networks the container is listening in %s: %s", swarmNetworkName, err)
|
||||
os.Exit(3)
|
||||
}
|
||||
|
||||
foundIP := ""
|
||||
for _, i := range ifaces {
|
||||
log.Printf("NETWORK: %s", i.Name)
|
||||
//If i.Name != swarmNetworkName {
|
||||
// continue
|
||||
//}
|
||||
|
||||
addrs, err := i.Addrs()
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] FATAL: Failed getting address for listener in network %s: %s", swarmNetworkName, err)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, addr := range addrs {
|
||||
var ip net.IP
|
||||
switch v := addr.(type) {
|
||||
case *net.IPNet:
|
||||
ip = v.IP
|
||||
case *net.IPAddr:
|
||||
ip = v.IP
|
||||
}
|
||||
|
||||
log.Printf("%s: IP: %#v", i.Name, ip)
|
||||
|
||||
// FIXME: Allow for IPv6 too!
|
||||
//if strings.Count(ip.String(), ".") == 3 {
|
||||
// foundIP = ip.String()
|
||||
// break
|
||||
//}
|
||||
// process IP address
|
||||
}
|
||||
}
|
||||
|
||||
if len(foundIP) == 0 {
|
||||
log.Printf("[ERROR] FATAL: No valid IP found for network %s. Defaulting to base IP", swarmNetworkName)
|
||||
} else {
|
||||
return foundIP
|
||||
}
|
||||
}
|
||||
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
return ""
|
||||
@@ -2470,23 +2286,10 @@ func webserverSetup(workflowExecution shuffle.WorkflowExecution) net.Listener {
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] OLD HOSTNAME: %s", appCallbackUrl)
|
||||
if os.Getenv("SHUFFLE_SWARM_CONFIG") == "run" || os.Getenv("SHUFFLE_SWARM_CONFIG") == "swarm" {
|
||||
log.Printf("\n\nStarting webserver on port %d with hostname: %s\n\n", baseport, hostname)
|
||||
port := listener.Addr().(*net.TCPAddr).Port
|
||||
|
||||
appCallbackUrl = fmt.Sprintf("http://%s:%d", hostname, baseport)
|
||||
listener, err = net.Listen("tcp", fmt.Sprintf(":%d", baseport))
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed to assign port to %d: %s", baseport, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
return listener
|
||||
} else {
|
||||
port := listener.Addr().(*net.TCPAddr).Port
|
||||
|
||||
log.Printf("\n\nStarting webserver on port %d with hostname: %s\n\n", port, hostname)
|
||||
appCallbackUrl = fmt.Sprintf("http://%s:%d", hostname, port)
|
||||
}
|
||||
log.Printf("\n\nStarting webserver on port %d with hostname: %s\n\n", port, hostname)
|
||||
appCallbackUrl = fmt.Sprintf("http://%s:%d", hostname, port)
|
||||
log.Printf("NEW HOSTNAME: %s", appCallbackUrl)
|
||||
|
||||
return listener
|
||||
@@ -2570,420 +2373,6 @@ func downloadDockerImageBackend(client *http.Client, imageName string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func deploySwarmService(dockercli *dockerclient.Client, name, image string, deployport int) error {
|
||||
log.Printf("[DEBUG] Deploying service for %s to swarm on port %d", name, deployport)
|
||||
//containerName := fmt.Sprintf("shuffle-worker-%s", parsedUuid)
|
||||
|
||||
if len(baseimagename) == 0 {
|
||||
baseimagename = "frikky/shuffle"
|
||||
//var baseimagename = "frikky/shuffle"
|
||||
//var registryName = "registry.hub.docker.com"
|
||||
}
|
||||
|
||||
//image := fmt.Sprintf("%s:%s", baseimagename, name)
|
||||
networkName := "shuffle-executions"
|
||||
if len(swarmNetworkName) > 0 {
|
||||
networkName = swarmNetworkName
|
||||
}
|
||||
|
||||
replicatedJobs := uint64(1)
|
||||
|
||||
// Sent from Orborus
|
||||
// Should be equal to
|
||||
scaleReplicas := os.Getenv("SHUFFLE_APP_REPLICAS")
|
||||
if len(scaleReplicas) > 0 {
|
||||
tmpInt, err := strconv.Atoi(scaleReplicas)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] %s is not a valid number for replication", scaleReplicas)
|
||||
} else {
|
||||
replicatedJobs = uint64(tmpInt)
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] SHUFFLE_APP_REPLICAS set to value %#v. Trying to overwrite default (%d/node)", scaleReplicas, replicatedJobs)
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] Deploying app with name %s with image %s", name, image)
|
||||
|
||||
containerName := fmt.Sprintf(strings.Replace(name, ".", "-", -1))
|
||||
serviceSpec := swarm.ServiceSpec{
|
||||
Annotations: swarm.Annotations{
|
||||
Name: containerName,
|
||||
Labels: map[string]string{},
|
||||
},
|
||||
Mode: swarm.ServiceMode{
|
||||
Replicated: &swarm.ReplicatedService{
|
||||
// Max total
|
||||
Replicas: &replicatedJobs,
|
||||
},
|
||||
},
|
||||
Networks: []swarm.NetworkAttachmentConfig{
|
||||
swarm.NetworkAttachmentConfig{
|
||||
Target: networkName,
|
||||
},
|
||||
},
|
||||
EndpointSpec: &swarm.EndpointSpec{
|
||||
Ports: []swarm.PortConfig{
|
||||
swarm.PortConfig{
|
||||
Protocol: swarm.PortConfigProtocolTCP,
|
||||
PublishMode: swarm.PortConfigPublishModeIngress,
|
||||
Name: "app-port",
|
||||
PublishedPort: uint32(deployport),
|
||||
TargetPort: uint32(deployport),
|
||||
},
|
||||
},
|
||||
},
|
||||
TaskTemplate: swarm.TaskSpec{
|
||||
Resources: &swarm.ResourceRequirements{
|
||||
Reservations: &swarm.Resources{},
|
||||
},
|
||||
LogDriver: &swarm.Driver{
|
||||
Name: "json-file",
|
||||
Options: map[string]string{
|
||||
"max-size": "10m",
|
||||
},
|
||||
},
|
||||
ContainerSpec: &swarm.ContainerSpec{
|
||||
Image: image,
|
||||
Env: []string{
|
||||
fmt.Sprintf("SHUFFLE_APP_EXPOSED_PORT=%d", deployport),
|
||||
fmt.Sprintf("SHUFFLE_SWARM_CONFIG=%s", os.Getenv("SHUFFLE_SWARM_CONFIG")),
|
||||
fmt.Sprintf("SHUFFLE_LOGS_DISABLED=%s", os.Getenv("SHUFFLE_LOGS_DISABLED")),
|
||||
},
|
||||
Hosts: []string{
|
||||
containerName,
|
||||
},
|
||||
},
|
||||
RestartPolicy: &swarm.RestartPolicy{
|
||||
Condition: swarm.RestartPolicyConditionNone,
|
||||
},
|
||||
Placement: &swarm.Placement{
|
||||
// Max per node
|
||||
MaxReplicas: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if len(os.Getenv("SHUFFLE_SWARM_OTHER_NETWORK")) > 0 {
|
||||
serviceSpec.Networks = append(serviceSpec.Networks, swarm.NetworkAttachmentConfig{
|
||||
Target: "shuffle_shuffle",
|
||||
})
|
||||
}
|
||||
|
||||
if strings.ToLower(os.Getenv("SHUFFLE_PASS_APP_PROXY")) == "true" {
|
||||
serviceSpec.TaskTemplate.ContainerSpec.Env = append(serviceSpec.TaskTemplate.ContainerSpec.Env, fmt.Sprintf("HTTP_PROXY=%s", os.Getenv("HTTP_PROXY")))
|
||||
serviceSpec.TaskTemplate.ContainerSpec.Env = append(serviceSpec.TaskTemplate.ContainerSpec.Env, fmt.Sprintf("HTTPS_PROXY=%s", os.Getenv("HTTPS_PROXY")))
|
||||
serviceSpec.TaskTemplate.ContainerSpec.Env = append(serviceSpec.TaskTemplate.ContainerSpec.Env, fmt.Sprintf("NO_PROXY=%s", os.Getenv("NO_PROXY")))
|
||||
}
|
||||
|
||||
/*
|
||||
Mounts: []mount.Mount{
|
||||
mount.Mount{
|
||||
Source: "/var/run/docker.sock",
|
||||
Target: "/var/run/docker.sock",
|
||||
Type: mount.TypeBind,
|
||||
},
|
||||
},
|
||||
*/
|
||||
|
||||
if dockerApiVersion != "" {
|
||||
serviceSpec.TaskTemplate.ContainerSpec.Env = append(serviceSpec.TaskTemplate.ContainerSpec.Env, fmt.Sprintf("DOCKER_API_VERSION=%s", dockerApiVersion))
|
||||
}
|
||||
|
||||
// Required for certain apps
|
||||
if timezone == "" {
|
||||
timezone = "Europe/Amsterdam"
|
||||
}
|
||||
|
||||
serviceSpec.TaskTemplate.ContainerSpec.Env = append(serviceSpec.TaskTemplate.ContainerSpec.Env, fmt.Sprintf("TZ=%s", timezone))
|
||||
|
||||
serviceOptions := types.ServiceCreateOptions{}
|
||||
service, err := dockercli.ServiceCreate(
|
||||
context.Background(),
|
||||
serviceSpec,
|
||||
serviceOptions,
|
||||
)
|
||||
_ = service
|
||||
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG] Failed deploying %s with image %s: %s", name, image, err)
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] Successfully deployed service %s with image %s on port %d", name, image, deployport)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Runs data discovery
|
||||
func findAppInfo(image, name string) (int, error) {
|
||||
dockercli, err := dockerclient.NewEnvClient()
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Unable to create docker client (2): %s", err)
|
||||
return -1, err
|
||||
}
|
||||
|
||||
highest := baseport
|
||||
exposedPort := -1
|
||||
|
||||
// Exists as a "cache" layer
|
||||
if portMappings != nil {
|
||||
for key, value := range portMappings {
|
||||
if value > highest {
|
||||
highest = value
|
||||
}
|
||||
|
||||
if key == name {
|
||||
exposedPort = value
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
portMappings = make(map[string]int)
|
||||
}
|
||||
|
||||
//Filters:
|
||||
if exposedPort == -1 {
|
||||
serviceListOptions := types.ServiceListOptions{}
|
||||
services, err := dockercli.ServiceList(
|
||||
context.Background(),
|
||||
serviceListOptions,
|
||||
)
|
||||
|
||||
// Basic self-correction
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Unable to list services: %s (may continue anyway?)", err)
|
||||
if strings.Contains(fmt.Sprintf("%s", err), "is too new") {
|
||||
// Static for some reason
|
||||
defaultVersion := "1.40"
|
||||
dockerApiVersion = defaultVersion
|
||||
os.Setenv("DOCKER_API_VERSION", defaultVersion)
|
||||
log.Printf("[DEBUG] Setting Docker API to %s default and retrying listing requests", defaultVersion)
|
||||
} else {
|
||||
return -1, err
|
||||
}
|
||||
|
||||
services, err = dockercli.ServiceList(
|
||||
context.Background(),
|
||||
serviceListOptions,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Unable to list services (2): %s", err)
|
||||
return -1, err
|
||||
}
|
||||
}
|
||||
|
||||
for _, service := range services {
|
||||
//log.Printf("[INFO] Service: %#v", service.Spec.Annotations.Name)
|
||||
|
||||
for _, endpoint := range service.Spec.EndpointSpec.Ports {
|
||||
if strings.Contains(endpoint.Name, "port") {
|
||||
portMappings[service.Spec.Annotations.Name] = int(endpoint.PublishedPort)
|
||||
if int(endpoint.PublishedPort) > highest {
|
||||
highest = int(endpoint.PublishedPort)
|
||||
}
|
||||
|
||||
if service.Spec.Annotations.Name == name || service.Spec.Annotations.Name == strings.Replace(name, ".", "-", -1) {
|
||||
exposedPort = int(endpoint.PublishedPort)
|
||||
//break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//log.Printf("%s - %s", service.Spec.Annotations.Name, strings.Replace(name, ".", "-", -1))
|
||||
if service.Spec.Annotations.Name != name && service.Spec.Annotations.Name != strings.Replace(name, ".", "-", -1) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Break if it's the correct port, as it's the right service
|
||||
if exposedPort >= 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//log.Printf("[DEBUG] Portmappings: %#v", portMappings)
|
||||
|
||||
if exposedPort >= 0 {
|
||||
//log.Printf("[INFO] Found service %s on port %d - no need to deploy another", name, exposedPort)
|
||||
} else {
|
||||
// Increment by 1 for highest port
|
||||
if highest <= baseport {
|
||||
highest = baseport
|
||||
}
|
||||
|
||||
highest += 1
|
||||
err = deploySwarmService(dockercli, name, image, highest)
|
||||
if err != nil {
|
||||
log.Printf("[WARNING] NOT Found service: %s. error: %s", name, err)
|
||||
return highest, err
|
||||
} else {
|
||||
log.Printf("[INFO] Deployed app with name %s", name)
|
||||
}
|
||||
|
||||
exposedPort = highest
|
||||
|
||||
if appsInitialized {
|
||||
log.Printf("[DEBUG] Waiting 30 seconds before moving on to let app start")
|
||||
time.Sleep(time.Duration(30) * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
return exposedPort, nil
|
||||
}
|
||||
|
||||
func sendAppRequest(incomingUrl, appName string, port int, action shuffle.Action, workflowExecution shuffle.WorkflowExecution) error {
|
||||
parsedRequest := shuffle.OrborusExecutionRequest{
|
||||
ExecutionId: workflowExecution.ExecutionId,
|
||||
Authorization: workflowExecution.Authorization,
|
||||
EnvironmentName: os.Getenv("ENVIRONMENT_NAME"),
|
||||
Timezone: os.Getenv("TZ"),
|
||||
Cleanup: os.Getenv("CLEANUP"),
|
||||
HTTPProxy: os.Getenv("HTTP_PROXY"),
|
||||
HTTPSProxy: os.Getenv("HTTPS_PROXY"),
|
||||
ShufflePassProxyToApp: os.Getenv("SHUFFLE_PASS_APP_PROXY"),
|
||||
BaseUrl: baseUrl,
|
||||
Action: action,
|
||||
FullExecution: workflowExecution,
|
||||
}
|
||||
//var baseUrl = os.Getenv("BASE_URL")
|
||||
//var appCallbackUrl = os.Getenv("BASE_URL")
|
||||
|
||||
parsedBaseurl := incomingUrl
|
||||
if strings.Count(baseUrl, ":") >= 2 {
|
||||
baseUrlSplit := strings.Split(baseUrl, ":")
|
||||
if len(baseUrlSplit) >= 3 {
|
||||
parsedBaseurl = strings.Join(baseUrlSplit[0:2], ":")
|
||||
//parsedRequest.BaseUrl = fmt.Sprintf("%s:33333", parsedBaseurl)
|
||||
}
|
||||
}
|
||||
|
||||
if len(parsedRequest.Url) == 0 {
|
||||
// Fixed callback url to the worker itself
|
||||
if strings.Count(parsedBaseurl, ":") >= 2 {
|
||||
parsedRequest.Url = parsedBaseurl
|
||||
} else {
|
||||
// Callback to worker
|
||||
parsedRequest.Url = fmt.Sprintf("%s:%d", parsedBaseurl, baseport)
|
||||
|
||||
//parsedRequest.Url
|
||||
}
|
||||
|
||||
//log.Printf("[DEBUG][%s] Should add a baseurl for the app to get back to: %s", workflowExecution.ExecutionId, parsedRequest.Url)
|
||||
}
|
||||
|
||||
// FIXME: Swapping because this was confusing during dev
|
||||
tmp := parsedRequest.Url
|
||||
parsedRequest.Url = parsedRequest.BaseUrl
|
||||
parsedRequest.BaseUrl = tmp
|
||||
|
||||
//http://3e05d1e7d7a0:33333,
|
||||
|
||||
// Run with proper hostname, but set to shuffle-worker to avoid specific host target.
|
||||
// This means running with VIP instead.
|
||||
if len(hostname) > 0 {
|
||||
parsedRequest.BaseUrl = fmt.Sprintf("http://%s:%d", hostname, baseport)
|
||||
//parsedRequest.BaseUrl = fmt.Sprintf("http://shuffle-workers:%d", baseport)
|
||||
//log.Printf("[DEBUG][%s] Changing hostname to local hostname in Docker network for WORKER URL: %s", workflowExecution.ExecutionId, parsedRequest.BaseUrl)
|
||||
}
|
||||
|
||||
data, err := json.Marshal(parsedRequest)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed marshalling worker request: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
//streamUrl := fmt.Sprintf("%s:%d/api/v1/run", parsedBaseurl, port)
|
||||
streamUrl := fmt.Sprintf("http://%s:%d/api/v1/run", appName, port)
|
||||
log.Printf("[DEBUG][%s] Worker URL: %s, Backend URL: %s, Target App: %s", workflowExecution.ExecutionId, parsedRequest.BaseUrl, parsedRequest.Url, streamUrl)
|
||||
req, err := http.NewRequest(
|
||||
"POST",
|
||||
streamUrl,
|
||||
bytes.NewBuffer([]byte(data)),
|
||||
)
|
||||
|
||||
client := &http.Client{}
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed creating app run request: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Checking as LATE as possible, ensuring we don't rerun what's already ran
|
||||
ctx := context.Background()
|
||||
newExecId := fmt.Sprintf("%s_%s", workflowExecution.ExecutionId, action.ID)
|
||||
_, err = shuffle.GetCache(ctx, newExecId)
|
||||
if err == nil {
|
||||
log.Printf("\n\n[DEBUG] Result for %s already found (PRE REQUEST) - returning\n\n", newExecId)
|
||||
return nil
|
||||
}
|
||||
|
||||
cacheData := []byte("1")
|
||||
err = shuffle.SetCache(ctx, newExecId, cacheData)
|
||||
if err != nil {
|
||||
log.Printf("[WARNING] Failed setting cache for action %s: %s", newExecId, err)
|
||||
} else {
|
||||
log.Printf("[DEBUG] Adding %s to cache (%s)", newExecId, action.Name)
|
||||
}
|
||||
|
||||
// FIXME:
|
||||
|
||||
newresp, err := client.Do(req)
|
||||
if err != nil {
|
||||
if strings.Contains(fmt.Sprintf("%s", err), "timeout awaiting response") {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Printf("[ERROR] Error running app run request: %s", err)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(newresp.Body)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed reading app request body body: %s", err)
|
||||
return err
|
||||
} else {
|
||||
log.Printf("[INFO][%s] NEWRESP (from app): %s", workflowExecution.ExecutionId, string(body))
|
||||
}
|
||||
|
||||
// FIXME: Remove
|
||||
/*
|
||||
if len(hostname) > 0 {
|
||||
//streamUrl := fmt.Sprintf("%s:%d/api/v1/run", parsedBaseurl, port)
|
||||
streamUrl := fmt.Sprintf("http://%s:%d/api/v1/run", appName, port)
|
||||
log.Printf("\n\n[DEBUG] Trying execution towards %s", streamUrl)
|
||||
req, err := http.NewRequest(
|
||||
"POST",
|
||||
streamUrl,
|
||||
bytes.NewBuffer([]byte(data)),
|
||||
)
|
||||
|
||||
client := &http.Client{}
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed creating app run request: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
newresp, err := client.Do(req)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Error running app run request: %s", err)
|
||||
return err
|
||||
}
|
||||
|
||||
body, err := ioutil.ReadAll(newresp.Body)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed reading body: %s", err)
|
||||
return err
|
||||
} else {
|
||||
log.Printf("[INFO] NEWRESP (from app): %s", string(body))
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Function to auto-deploy certain apps if "run" is set
|
||||
// Has some issues with loading when running multiple workers and such.
|
||||
func baseDeploy() {
|
||||
//return
|
||||
@@ -3048,22 +2437,6 @@ func baseDeploy() {
|
||||
|
||||
// Initial loop etc
|
||||
func main() {
|
||||
/*
|
||||
appName := "shuffle-tools_1.1.0"
|
||||
image := "frikky/shuffle:shuffle-tools_1.1.0"
|
||||
exposedPort, err := findAppInfo(image, appName)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed finding and creating port for %s: %s", appName, err)
|
||||
os.Exit(3)
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] Should run towards port %d for app %s", exposedPort, appName)
|
||||
err = sendAppRequest(appCallbackUrl, exposedPort, shuffle.Action{}, shuffle.WorkflowExecution{})
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed sending request to app %s on port %d: %s", appName, exposedPort, err)
|
||||
os.Exit(3)
|
||||
}
|
||||
*/
|
||||
|
||||
// Elasticsearch necessary to ensure we'ren ot running with Datastore configurations for minimal/maximal data sizes
|
||||
_, err := shuffle.RunInit(datastore.Client{}, storage.Client{}, "", "", true, "elasticsearch")
|
||||
@@ -3099,21 +2472,6 @@ func main() {
|
||||
}
|
||||
|
||||
log.Printf("[INFO] Running with timezone %s and swarm config %#v", timezone, os.Getenv("SHUFFLE_SWARM_CONFIG"))
|
||||
if os.Getenv("SHUFFLE_SWARM_CONFIG") == "run" || os.Getenv("SHUFFLE_SWARM_CONFIG") == "swarm" {
|
||||
// Forcing download just in case on the first iteration.
|
||||
workflowExecution := shuffle.WorkflowExecution{}
|
||||
|
||||
//var autoDeploy = []string{"frikky/shuffle:shuffle-subflow_1.0.0", "frikky/shuffle:http_1.1.0", "frikky/shuffle:shuffle-tools_1.1.0", "frikky/shuffle:testing_1.0.0"}
|
||||
|
||||
go baseDeploy()
|
||||
//baseDeploy()
|
||||
|
||||
listener := webserverSetup(workflowExecution)
|
||||
runWebserver(listener)
|
||||
log.Printf("[ERROR] Stopped listener %#v - exiting.", listener)
|
||||
os.Exit(3)
|
||||
}
|
||||
|
||||
//imageName := fmt.Sprintf("%s/%s:shuffle_openapi_1.0.0", registryName, baseimagename)
|
||||
|
||||
// WORKER_TESTING_WORKFLOW should be a workflow ID
|
||||
@@ -3467,17 +2825,6 @@ func runWebserver(listener net.Listener) {
|
||||
r.HandleFunc("/api/v1/streams", handleWorkflowQueue).Methods("POST", "OPTIONS")
|
||||
r.HandleFunc("/api/v1/streams/results", handleGetStreamResults).Methods("POST", "OPTIONS")
|
||||
|
||||
if os.Getenv("SHUFFLE_SWARM_CONFIG") == "run" || os.Getenv("SHUFFLE_SWARM_CONFIG") == "swarm" {
|
||||
/*
|
||||
err = dockercli.ServiceRemove(ctx, "shuffle-workers")
|
||||
if err != nil {}
|
||||
*/
|
||||
|
||||
requestCache = cache.New(60*time.Minute, 120*time.Minute)
|
||||
log.Printf("[DEBUG] Running webserver config for SWARM and K8s")
|
||||
r.HandleFunc("/api/v1/execute", handleRunExecution).Methods("POST", "OPTIONS")
|
||||
}
|
||||
|
||||
//log.Fatal(http.ListenAndServe(port, nil))
|
||||
http.Handle("/", r)
|
||||
log.Fatal(http.Serve(listener, nil))
|
||||
|
||||
Generated
-1182
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user