Merge pull request #996 from Shuffle/launch

v1.1.0 - Creators & Workflow Templates
This commit is contained in:
Frikky
2022-12-06 19:34:05 +01:00
committed by GitHub
90 changed files with 19445 additions and 4720 deletions
+5 -3
View File
@@ -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
+5 -5
View File
@@ -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)
+47
View File
@@ -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"
+75
View File
@@ -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:
+3
View File
@@ -4,6 +4,9 @@
Shuffle Automation
[![CodeQL](https://github.com/Shuffle/Shuffle/actions/workflows/codeql-analysis.yml/badge.svg?branch=launch)](https://github.com/Shuffle/Shuffle/actions/workflows/codeql-analysis.yml)
[![Autobuild](https://github.com/Shuffle/Shuffle/actions/workflows/dockerbuild.yaml/badge.svg?branch=launch)](https://github.com/Shuffle/Shuffle/actions/workflows/dockerbuild.yaml)
</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
View File
@@ -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 -1
View File
@@ -1,4 +1,4 @@
FROM peterclemenko/blackarch as base
FROM blackarchlinux/blackarch as base
FROM base as builder
+341 -40
View File
@@ -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:
+7 -2
View File
@@ -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
+2 -1
View File
@@ -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
+98 -8
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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 -3
View File
@@ -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
+2 -2
View File
@@ -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'
+4 -1
View File
@@ -1 +1,4 @@
#
# hello
this is line 2
and 3
Is it a python problem?
+11 -9
View File
@@ -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
View File
@@ -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
View File
@@ -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
```
+6 -2
View File
@@ -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

+1
View File
@@ -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
View File
@@ -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="/"
@@ -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;
+378
View File
@@ -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;
+245
View File
@@ -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;
+189
View File
@@ -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
+554 -127
View File
@@ -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>
);
};
+434
View File
@@ -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
+306
View File
@@ -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;
+356
View File
@@ -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;
+490
View File
@@ -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;
+27
View File
@@ -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
+22 -8
View File
@@ -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 :
+233 -11
View File
@@ -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
+295 -195
View File
@@ -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
+16 -13
View File
@@ -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;
+648
View File
@@ -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
+830
View File
@@ -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
+370
View File
@@ -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;
+291
View File
@@ -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
+17 -3
View File
@@ -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
+216
View File
@@ -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;
+12 -2
View File
@@ -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: {
+1
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+256 -130
View File
@@ -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
View File
@@ -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)" }}>
+371 -53
View File
@@ -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 ?
+759
View File
@@ -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
View File
@@ -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>
);
+2 -2
View File
@@ -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}
+90 -59
View File
@@ -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&nbsp;
<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&nbsp;
<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}>
+16 -4
View File
@@ -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" }} />
+2 -1
View File
@@ -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";
+188
View File
@@ -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;
+151 -134
View File
@@ -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>
);
+3 -3
View File
@@ -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: {
+66 -61
View File
@@ -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>
+298
View File
@@ -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&nbsp;(g)</TableCell>
<TableCell align="right">Carbs&nbsp;(g)</TableCell>
<TableCell align="right">Protein&nbsp;(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;
+483
View File
@@ -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;
File diff suppressed because it is too large Load Diff
+40 -10
View File
@@ -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
+5 -1
View File
@@ -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
+1 -1
View File
@@ -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
)
+14
View File
@@ -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=
+101 -35
View File
@@ -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))
}
+15 -6
View File
@@ -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 -1
View File
@@ -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 .
+1 -1
View File
@@ -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
+15 -668
View File
@@ -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))
-1182
View File
File diff suppressed because it is too large Load Diff