Merge branch 'frikky:launch' into launch
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
ORG_ID=Shuffle
|
||||
ENVIRONMENT_NAME=Shuffle
|
||||
|
||||
|
||||
# Remote github config for first load
|
||||
SHUFFLE_DOWNLOAD_WORKFLOW_LOCATION=
|
||||
SHUFFLE_DOWNLOAD_WORKFLOW_USERNAME=
|
||||
@@ -60,10 +61,10 @@ SHUFFLE_ELASTIC=true
|
||||
|
||||
# DATABASE CONFIGURATIONS
|
||||
DATASTORE_EMULATOR_HOST=shuffle-database:8000
|
||||
#SHUFFLE_OPENSEARCH_URL=https://shuffle-opensearch:9200
|
||||
SHUFFLE_OPENSEARCH_URL=http://shuffle-opensearch:9200
|
||||
SHUFFLE_OPENSEARCH_USERNAME=
|
||||
SHUFFLE_OPENSEARCH_PASSWORD=
|
||||
#SHUFFLE_OPENSEARCH_URL=http://shuffle-opensearch:9200
|
||||
SHUFFLE_OPENSEARCH_URL=https://shuffle-opensearch:9200
|
||||
SHUFFLE_OPENSEARCH_USERNAME=admin
|
||||
SHUFFLE_OPENSEARCH_PASSWORD=admin
|
||||
SHUFFLE_OPENSEARCH_CERTIFICATE_FILE=
|
||||
SHUFFLE_OPENSEARCH_APIKEY=
|
||||
SHUFFLE_OPENSEARCH_CLOUDID=
|
||||
|
||||
@@ -10,18 +10,19 @@ The Docker setup is done with docker-compose
|
||||
|
||||
1. Make sure you have [Docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/install/) installed.
|
||||
2. Download Shuffle
|
||||
```
|
||||
```bash
|
||||
git clone https://github.com/frikky/Shuffle
|
||||
cd Shuffle
|
||||
```
|
||||
|
||||
3. Fix prerequisites for the Opensearch database (Elasticsearch):
|
||||
```
|
||||
sudo chown -R 1000:1000 shuffle-database # Required for Opensearch
|
||||
```bash
|
||||
mkdir shuffle-database
|
||||
sudo chown -R 1000:1000 shuffle-database
|
||||
```
|
||||
|
||||
4. Run docker-compose.
|
||||
```
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
@@ -38,20 +39,20 @@ This step is for setting up with Docker on windows from scratch.
|
||||
|
||||
4. Open the .env file and change the line with "OUTER_HOSTNAME" to contain your IP:
|
||||
|
||||
```
|
||||
```bash
|
||||
OUTER_HOSTNAME=YOUR.IP.HERE
|
||||
```
|
||||
|
||||
6. Run docker-compose
|
||||
```
|
||||
docker compose up -d
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
### Configurations (proxies, default users etc.)
|
||||
https://shuffler.io/docs/configuration
|
||||
|
||||
### After installation
|
||||
1. After installation, go to http://localhost:3001/adminsetup (or your servername - https is on port 3443)
|
||||
1. After installation, go to http://localhost:3001 (or your servername - https is on port 3443)
|
||||
2. Now set up your admin account (username & password). Shuffle doesn't have a default username and password.
|
||||
3. Sign in with the same Username & Password! Go to /apps and see if you have any apps yet. If not - you may need to [configure proxies](https://shuffler.io/docs/configuration#production_readiness)
|
||||
4. Check out https://shuffler.io/docs/configuration as it has a lot of useful information to get started
|
||||
|
||||
@@ -6,7 +6,7 @@ Shuffle Automation
|
||||
|
||||
</h1><h4 align="center">
|
||||
|
||||
 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.
|
||||
[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.
|
||||
|
||||
[_Key Features_](https://shuffler.io/docs/features) —
|
||||
[_Community & Support_](https://discord.gg/B2CBzUm) —
|
||||
|
||||
+328
-61
@@ -3,6 +3,7 @@ import copy
|
||||
import sys
|
||||
import re
|
||||
import time
|
||||
import base64
|
||||
import json
|
||||
import liquid
|
||||
import logging
|
||||
@@ -14,17 +15,122 @@ import requests
|
||||
import http.client
|
||||
import urllib.parse
|
||||
import jinja2
|
||||
from io import StringIO as StringBuffer
|
||||
from io import BytesIO
|
||||
from liquid import Liquid
|
||||
from liquid import Liquid, defaults
|
||||
|
||||
runtime = os.getenv("SHUFFLE_SWARM_CONFIG", "")
|
||||
|
||||
###
|
||||
###
|
||||
###
|
||||
#### Filters for liquidpy
|
||||
###
|
||||
###
|
||||
###
|
||||
|
||||
defaults.MODE = 'wild'
|
||||
defaults.FROM_FILE = False
|
||||
from liquid.filters.manager import FilterManager
|
||||
from liquid.filters.standard import standard_filter_manager
|
||||
|
||||
shuffle_filters = FilterManager()
|
||||
for key, value in standard_filter_manager.filters.items():
|
||||
shuffle_filters.filters[key] = value
|
||||
|
||||
#@shuffle_filters.register
|
||||
#def plus(a, b):
|
||||
# try:
|
||||
# a = int(a)
|
||||
# except:
|
||||
# a = 0
|
||||
#
|
||||
# try:
|
||||
# b = int(b)
|
||||
# except:
|
||||
# b = 0
|
||||
#
|
||||
# return standard_filter_manager.filters["plus"](a, b)
|
||||
#
|
||||
#@shuffle_filters.register
|
||||
#def minus(a, b):
|
||||
# a = int(a)
|
||||
# b = int(b)
|
||||
# return standard_filter_manager.filters["minus"](a, b)
|
||||
#
|
||||
#@shuffle_filters.register
|
||||
#def multiply(a, b):
|
||||
# a = int(a)
|
||||
# b = int(b)
|
||||
# return standard_filter_manager.filters["multiply"](a, b)
|
||||
#
|
||||
#@shuffle_filters.register
|
||||
#def divide(a, b):
|
||||
# a = int(a)
|
||||
# b = int(b)
|
||||
# return standard_filter_manager.filters["divide"](a, b)
|
||||
|
||||
@shuffle_filters.register
|
||||
def md5(a):
|
||||
a = str(a)
|
||||
return hashlib.md5(a.encode('utf-8')).hexdigest()
|
||||
|
||||
@shuffle_filters.register
|
||||
def sha256(a):
|
||||
a = str(a)
|
||||
return hashlib.sha256(str(a).encode("utf-8")).hexdigest()
|
||||
|
||||
@shuffle_filters.register
|
||||
def md5_base64(a):
|
||||
a = str(a)
|
||||
foundhash = hashlib.md5(a.encode('utf-8')).hexdigest()
|
||||
return base64.b64encode(foundhash.encode('utf-8'))
|
||||
|
||||
@shuffle_filters.register
|
||||
def base64_encode(a):
|
||||
a = str(a)
|
||||
try:
|
||||
return base64.b64encode(a.encode('utf-8')).decode()
|
||||
except:
|
||||
return base64.b64encode(a).decode()
|
||||
|
||||
@shuffle_filters.register
|
||||
def base64_decode(a):
|
||||
a = str(a)
|
||||
try:
|
||||
return base64.b64decode(a).decode()
|
||||
except:
|
||||
return base64.b64decode(a)
|
||||
|
||||
#print(standard_filter_manager.filters)
|
||||
#print(shuffle_filters.filters)
|
||||
#print(Liquid("{{ '10' | plus: 1}}", filters=shuffle_filters.filters).render())
|
||||
#print(Liquid("{{ '10' | minus: 1}}", filters=shuffle_filters.filters).render())
|
||||
#print(Liquid("{{ asd | size }}", filters=shuffle_filters.filters).render())
|
||||
#print(Liquid("{{ 'asd' | md5 }}", filters=shuffle_filters.filters).render())
|
||||
#print(Liquid("{{ 'asd' | sha256 }}", filters=shuffle_filters.filters).render())
|
||||
#print(Liquid("{{ 'asd' | md5_base64 | base64_decode }}", filters=shuffle_filters.filters).render())
|
||||
|
||||
###
|
||||
###
|
||||
###
|
||||
###
|
||||
###
|
||||
###
|
||||
###
|
||||
|
||||
class AppBase:
|
||||
__version__ = None
|
||||
app_name = None
|
||||
|
||||
def __init__(self, redis=None, logger=None, console_logger=None):#, docker_client=None):
|
||||
self.logger = logger if logger is not None else logging.getLogger("AppBaseLogger")
|
||||
self.log_capture_string = StringBuffer()
|
||||
ch = logging.StreamHandler(self.log_capture_string)
|
||||
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
ch.setFormatter(formatter)
|
||||
logger.addHandler(ch)
|
||||
|
||||
self.redis=redis
|
||||
self.console_logger = logger if logger is not None else logging.getLogger("AppBaseLogger")
|
||||
|
||||
@@ -139,15 +245,15 @@ class AppBase:
|
||||
try:
|
||||
new_input = input_data.split()
|
||||
except Exception as e:
|
||||
self.logger.info(f"[ERROR] Failed to run magic parser (1): {e}")
|
||||
self.logger.info(f"[ERROR] Failed to run magic parser during split (1): {e}")
|
||||
return input_data
|
||||
|
||||
# Won't ever touch this one?
|
||||
if isinstance(input_data, list) or isinstance(input_data, object):
|
||||
if isinstance(new_input, list) or isinstance(new_input, object):
|
||||
try:
|
||||
return json.dumps(new_input)
|
||||
except Exception as e:
|
||||
self.logger.info(f"[ERROR] Failed to run magic parser: {e}")
|
||||
self.logger.info(f"[ERROR] Failed to run magic parser (3): {e}")
|
||||
|
||||
return new_input
|
||||
|
||||
@@ -165,12 +271,13 @@ class AppBase:
|
||||
else:
|
||||
self.logger.warning(f"[ERROR] Magic output not defined.")
|
||||
except Exception as e:
|
||||
self.logger.warning(f"[ERROR] Failed to run magic autoparser: {e}")
|
||||
self.logger.warning(f"[ERROR] Failed to run magic autoparser (send result): {e}")
|
||||
pass
|
||||
|
||||
# Try it with some magic
|
||||
|
||||
self.logger.info(f"""[DEBUG] Inside Send result with status {action_result["status"]}""")
|
||||
#if isinstance(action_result,
|
||||
|
||||
# FIXME: Add cleanup of parameters to not send to frontend here
|
||||
params = {}
|
||||
@@ -188,11 +295,64 @@ class AppBase:
|
||||
self.logger.info(f"[DEBUG] Before last stream result")
|
||||
url = "%s%s" % (self.base_url, stream_path)
|
||||
self.logger.info("[INFO] URL FOR RESULT (URL): %s" % url)
|
||||
|
||||
try:
|
||||
ret = requests.post(url, headers=headers, json=action_result)
|
||||
#self.logger.info(f"[DEBUG] Result: {ret.status_code}")
|
||||
#if ret.status_code != 200:
|
||||
# self.logger.info(f"[DEBUG] Shuffle Response: {ret.text}")
|
||||
log_contents = self.log_capture_string.getvalue()
|
||||
#print("RESULTS: %s" % log_contents)
|
||||
self.logger.info("[WARNING] Got logs of length {len(log_contents)}")
|
||||
if len(action_result["action"]["parameters"]) == 0:
|
||||
action_result["action"]["parameters"] = []
|
||||
|
||||
param_found = False
|
||||
for param in action_result["action"]["parameters"]:
|
||||
if param["name"] == "shuffle_action_logs":
|
||||
param_found = True
|
||||
break
|
||||
|
||||
if not param_found:
|
||||
action_result["action"]["parameters"].append({
|
||||
"name": "shuffle_action_logs",
|
||||
"value": log_contents,
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
print(f"Failed adding parameter: {e}")
|
||||
|
||||
# FIXME: Adding retries here.
|
||||
try:
|
||||
finished = False
|
||||
for i in range (0, 5):
|
||||
try:
|
||||
ret = requests.post(url, headers=headers, json=action_result, timeout=10)
|
||||
|
||||
self.logger.info(f"[DEBUG] Result: {ret.status_code} (break on 200)")
|
||||
if ret.status_code == 200 or ret.status_code == 201:
|
||||
finished = True
|
||||
break
|
||||
else:
|
||||
self.logger.info(f"[DEBUG] RESP: {ret.text}")
|
||||
|
||||
except (requests.exceptions.RequestException, TimeoutError) as e:
|
||||
time.sleep(5)
|
||||
continue
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
time.sleep(5)
|
||||
continue
|
||||
except http.client.RemoteDisconnected as e:
|
||||
time.sleep(5)
|
||||
continue
|
||||
except urllib3.exceptions.ProtocolError as e:
|
||||
time.sleep(5)
|
||||
continue
|
||||
|
||||
time.sleep(5)
|
||||
|
||||
if not finished:
|
||||
# Not sure why this would work tho :)
|
||||
action_result["status"] = "FAILURE"
|
||||
action_result["result"] = f"POST error: {e}"
|
||||
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] Successful request result request: Status= {ret.status_code} & Response= {ret.text}. Action status: {action_result["status"]}""")
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
@@ -821,7 +981,7 @@ class AppBase:
|
||||
returns = []
|
||||
for item in value:
|
||||
self.logger.info("VALUE: %s" % item)
|
||||
if len(item) != 36:
|
||||
if len(item) != 36 and not item.startswith("file_"):
|
||||
self.logger.info("Bad length for file value %s" % item)
|
||||
continue
|
||||
#return {
|
||||
@@ -924,6 +1084,7 @@ class AppBase:
|
||||
#return value.json()
|
||||
return {"success": False}
|
||||
|
||||
# Wrapper for set_files
|
||||
def set_file(self, infiles):
|
||||
return self.set_files(infiles)
|
||||
|
||||
@@ -1010,19 +1171,23 @@ class AppBase:
|
||||
}
|
||||
|
||||
# Simple validation of parameters in general
|
||||
replace_params = False
|
||||
try:
|
||||
tmp_parameters = action["parameters"]
|
||||
for param in tmp_parameters:
|
||||
if param["value"] == "SHUFFLE_AUTO_REMOVED":
|
||||
replace_params = True
|
||||
except KeyError:
|
||||
action["parameters"] = []
|
||||
except TypeError:
|
||||
pass
|
||||
|
||||
self.action = copy.deepcopy(action)
|
||||
self.logger.info("[DEBUG] Sending starting action result (EXECUTING)")
|
||||
self.logger.info(f"[DEBUG] Sending starting action result (EXECUTING). Param replace: {replace_params}")
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": "Bearer %s" % self.authorization
|
||||
"Authorization": f"Bearer {self.authorization}"
|
||||
}
|
||||
|
||||
if len(self.action) == 0:
|
||||
@@ -1118,6 +1283,32 @@ class AppBase:
|
||||
|
||||
|
||||
self.full_execution = fullexecution
|
||||
|
||||
try:
|
||||
if replace_params == True:
|
||||
for inner_action in self.full_execution["workflow"]["actions"]:
|
||||
self.logger.info("[DEBUG] ID: %s vs %s" % (inner_action["id"], self.action["id"]))
|
||||
|
||||
# In case of some kind of magic, we're just doing params
|
||||
if inner_action["id"] == self.action["id"]:
|
||||
self.logger.info("FOUND!")
|
||||
|
||||
if isinstance(self.action, str):
|
||||
self.logger.info("Params is in string object for self.action?")
|
||||
else:
|
||||
self.action["parameters"] = inner_action["parameters"]
|
||||
self.action_result["action"]["parameters"] = inner_action["parameters"]
|
||||
|
||||
if isinstance(self.original_action, str):
|
||||
self.logger.info("Params for original actions is in string object?")
|
||||
else:
|
||||
self.original_action["parameters"] = inner_action["parameters"]
|
||||
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
self.logger.info(f"[WARNING] Failed in replace params action parsing: {e}")
|
||||
|
||||
self.logger.info("[DEBUG] AFTER FULLEXEC stream result (init)")
|
||||
|
||||
# Gets the value at the parenthesis level you want
|
||||
@@ -1489,6 +1680,11 @@ class AppBase:
|
||||
newvalue = []
|
||||
firstitem = actualitem[0][0]
|
||||
seconditem = actualitem[0][1]
|
||||
if isinstance(firstitem, int):
|
||||
firstitem = str(firstitem)
|
||||
if isinstance(seconditem, int):
|
||||
seconditem = str(seconditem)
|
||||
|
||||
print("[DEBUG] ACTUAL PARSED: %s" % actualitem)
|
||||
|
||||
# Means it's a single item -> continue
|
||||
@@ -1509,17 +1705,23 @@ class AppBase:
|
||||
newvalue, is_loop = (tmpitem, parsersplit[outercnt+1:])
|
||||
else:
|
||||
print("[INFO] In ELSE - handling %s and %s" % (firstitem, seconditem))
|
||||
if firstitem.lower() == "max" or firstitem.lower() == "last" or firstitem.lower() == "end":
|
||||
firstitem = len(basejson)-1
|
||||
elif firstitem.lower() == "min" or firstitem.lower() == "first":
|
||||
firstitem = 0
|
||||
if isinstance(firstitem, str):
|
||||
if firstitem.lower() == "max" or firstitem.lower() == "last" or firstitem.lower() == "end":
|
||||
firstitem = len(basejson)-1
|
||||
elif firstitem.lower() == "min" or firstitem.lower() == "first":
|
||||
firstitem = 0
|
||||
else:
|
||||
firstitem = int(firstitem)
|
||||
else:
|
||||
firstitem = int(firstitem)
|
||||
|
||||
if seconditem.lower() == "max" or seconditem.lower() == "last" or firstitem.lower() == "end":
|
||||
seconditem = len(basejson)-1
|
||||
elif seconditem.lower() == "min" or seconditem.lower() == "first":
|
||||
seconditem = 0
|
||||
if isinstance(seconditem, str):
|
||||
if seconditem.lower() == "max" or seconditem.lower() == "last" or firstitem.lower() == "end":
|
||||
seconditem = len(basejson)-1
|
||||
elif seconditem.lower() == "min" or seconditem.lower() == "first":
|
||||
seconditem = 0
|
||||
else:
|
||||
seconditem = int(seconditem)
|
||||
else:
|
||||
seconditem = int(seconditem)
|
||||
|
||||
@@ -1704,7 +1906,7 @@ class AppBase:
|
||||
basejson = json.loads(baseresult)
|
||||
except json.decoder.JSONDecodeError as e:
|
||||
try:
|
||||
baseresult = baseresult.replace("\'", "\"")
|
||||
#baseresult = baseresult.replace("\'", "\"")
|
||||
basejson = json.loads(baseresult)
|
||||
except json.decoder.JSONDecodeError as e:
|
||||
print("Parser issue with JSON: %s" % e)
|
||||
@@ -1768,9 +1970,10 @@ class AppBase:
|
||||
# return template
|
||||
|
||||
#self.logger.info(globals())
|
||||
self.logger.info("[DEBUG] Running liquid with data of length %d" % len(template))
|
||||
if len(template) > 100:
|
||||
self.logger.info("[DEBUG] Running liquid with data of length %d" % len(template))
|
||||
#self.logger.info(f"[DEBUG] Data: {template}")
|
||||
run = Liquid(template, mode="wild", from_file=False)
|
||||
run = Liquid(template, mode="wild", from_file=False, filters=shuffle_filters.filters)
|
||||
|
||||
# Can't handle self yet (?)
|
||||
ret = run.render(**globals())
|
||||
@@ -1822,8 +2025,8 @@ class AppBase:
|
||||
self.action_result["status"] = "FAILURE"
|
||||
data = {
|
||||
"success": False,
|
||||
"input": template,
|
||||
"reason": f"Failed to parse LiquidPy: {error_msg}",
|
||||
"input": template,
|
||||
}
|
||||
try:
|
||||
self.action_result["result"] = json.dumps(data)
|
||||
@@ -1945,7 +2148,11 @@ class AppBase:
|
||||
except:
|
||||
self.logger.info("Error in initial replacement of escaped dollar!")
|
||||
|
||||
#self.logger.info("POST input value: %s" % parameter["value"])
|
||||
# Basic fix in case variant isn't set
|
||||
try:
|
||||
self.logger.info("[DEBUG] Parameter variant: %s" % parameter["variant"])
|
||||
except:
|
||||
parameter["variant"] = "STATIC_VALUE"
|
||||
|
||||
# Regex to find all the things
|
||||
if parameter["variant"] == "STATIC_VALUE":
|
||||
@@ -2176,7 +2383,7 @@ class AppBase:
|
||||
return True
|
||||
else:
|
||||
print("[DEBUG] Condition: can't handle %s yet. Setting to true" % check)
|
||||
|
||||
|
||||
return False
|
||||
|
||||
def check_branch_conditions(action, fullexecution, self):
|
||||
@@ -2187,21 +2394,48 @@ class AppBase:
|
||||
except KeyError:
|
||||
return True, ""
|
||||
|
||||
|
||||
available_checks = [
|
||||
"=",
|
||||
"equals",
|
||||
"!=",
|
||||
"does not equal",
|
||||
">",
|
||||
"larger than",
|
||||
"<",
|
||||
"less than",
|
||||
">=",
|
||||
"<=",
|
||||
"startswith",
|
||||
"endswith",
|
||||
"contains",
|
||||
"contains_any_of",
|
||||
"re",
|
||||
"matches regex",
|
||||
]
|
||||
|
||||
relevantbranches = []
|
||||
correct_branches = 0
|
||||
matching_branches = 0
|
||||
for branch in fullexecution["workflow"]["branches"]:
|
||||
if branch["destination_id"] != action["id"]:
|
||||
continue
|
||||
|
||||
matching_branches += 1
|
||||
# Remove anything without a condition
|
||||
try:
|
||||
if (branch["conditions"]) == 0 or branch["conditions"] == None:
|
||||
correct_branches += 1
|
||||
continue
|
||||
except KeyError:
|
||||
correct_branches += 1
|
||||
continue
|
||||
|
||||
self.logger.info("[DEBUG] Relevant conditions: %s" % branch["conditions"])
|
||||
successful_conditions = []
|
||||
failed_conditions = []
|
||||
successful_conditions = 0
|
||||
total_conditions = len(branch["conditions"])
|
||||
for condition in branch["conditions"]:
|
||||
self.logger.info("[DEBUG] Getting condition value of %s" % condition)
|
||||
|
||||
@@ -2209,6 +2443,7 @@ class AppBase:
|
||||
sourcevalue = condition["source"]["value"]
|
||||
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)}
|
||||
|
||||
#sourcevalue = sourcevalue.encode("utf-8")
|
||||
@@ -2217,28 +2452,11 @@ 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)}
|
||||
|
||||
#destinationvalue = destinationvalue.encode("utf-8")
|
||||
destinationvalue = parse_wrapper_start(destinationvalue, self)
|
||||
available_checks = [
|
||||
"=",
|
||||
"equals",
|
||||
"!=",
|
||||
"does not equal",
|
||||
">",
|
||||
"larger than",
|
||||
"<",
|
||||
"less than",
|
||||
">=",
|
||||
"<=",
|
||||
"startswith",
|
||||
"endswith",
|
||||
"contains",
|
||||
"contains_any_of",
|
||||
"re",
|
||||
"matches regex",
|
||||
]
|
||||
|
||||
if not condition["condition"]["value"] in available_checks:
|
||||
self.logger.warning("Skipping %s %s %s because %s is invalid." % (sourcevalue, condition["condition"]["value"], destinationvalue, condition["condition"]["value"]))
|
||||
@@ -2255,22 +2473,58 @@ class AppBase:
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
if not validation:
|
||||
self.logger.info("Failed condition check for %s %s %s." % (sourcevalue, condition["condition"]["value"], destinationvalue))
|
||||
return False, {"success": False, "reason": "Failed condition (3): %s %s %s" % (sourcevalue, condition["condition"]["value"], destinationvalue)}
|
||||
if validation == True:
|
||||
successful_conditions += 1
|
||||
|
||||
#if not validation:
|
||||
# self.logger.info("Failed condition check for %s %s %s." % (sourcevalue, condition["condition"]["value"], destinationvalue))
|
||||
# return False, {"success": False, "reason": "Failed condition (3): %s %s %s" % (sourcevalue, condition["condition"]["value"], destinationvalue)}
|
||||
|
||||
# Make a general parser here, at least to get param["name"] = param["value"] in maparameter[string]string
|
||||
#for condition in branch.conditons:
|
||||
self.logger.info("CONDITIONS VS SUCCESS: %d vs %d" % (total_conditions, successful_conditions))
|
||||
|
||||
if total_conditions == successful_conditions:
|
||||
correct_branches += 1
|
||||
|
||||
if matching_branches == 0:
|
||||
return True, ""
|
||||
|
||||
if matching_branches > 0 and correct_branches > 0:
|
||||
return True, ""
|
||||
|
||||
self.logger.info("[DEBUG] Correct branches vs matching branches: %d vs %d" % (correct_branches, matching_branches))
|
||||
return False, {"success": False, "reason": "Minimum of one branch's conditions must be correct to continue. Total: %d of %d" % (correct_branches, matching_branches)}
|
||||
|
||||
#Correct branches vs matching branches: 1 vs 1
|
||||
#if
|
||||
return True, ""
|
||||
|
||||
|
||||
#
|
||||
#
|
||||
#
|
||||
#
|
||||
# CONT
|
||||
# CONT
|
||||
# CONT
|
||||
# CONT
|
||||
# CONT
|
||||
# CONT
|
||||
# CONT
|
||||
# CONT
|
||||
# CONT
|
||||
# CONT
|
||||
# CONT
|
||||
# CONT
|
||||
# CONT
|
||||
#
|
||||
#
|
||||
#
|
||||
#
|
||||
|
||||
# THE START IS ACTUALLY RIGHT HERE :O
|
||||
# Checks whether conditions are met, otherwise set
|
||||
branchcheck, tmpresult = check_branch_conditions(action, fullexecution, self)
|
||||
if isinstance(tmpresult, object) or isinstance(tmpresult, list):
|
||||
if isinstance(tmpresult, object) or isinstance(tmpresult, list) or isinstance(tmpresult, dict):
|
||||
self.logger.info("[DEBUG] Fixing branch return as object -> string")
|
||||
try:
|
||||
#tmpresult = tmpresult.replace("'", "\"")
|
||||
@@ -2278,19 +2532,14 @@ class AppBase:
|
||||
except json.decoder.JSONDecodeError as e:
|
||||
self.logger.info(f"[WARNING] Failed condition parsing {tmpresult} to string")
|
||||
|
||||
# IF branches fail: Exit!
|
||||
if not branchcheck:
|
||||
self.logger.info("Failed one or more branch conditions.")
|
||||
self.action_result["result"] = tmpresult
|
||||
self.action_result["status"] = "SKIPPED"
|
||||
try:
|
||||
ret = requests.post("%s%s" % (self.base_url, stream_path), headers=headers, json=self.action_result)
|
||||
self.logger.info("Result: %d" % ret.status_code)
|
||||
if ret.status_code != 200:
|
||||
self.logger.info(ret.text)
|
||||
except requests.exceptions.ConnectionError as e:
|
||||
self.logger.exception(e)
|
||||
self.action_result["completed_at"] = int(time.time())
|
||||
|
||||
self.logger.info("\n\n[DEBUG] RETURNING BECAUSE A BRANCH FAILED: %s\n\n" % tmpresult)
|
||||
self.send_result(self.action_result, headers, stream_path)
|
||||
return
|
||||
|
||||
# Replace name cus there might be issues
|
||||
@@ -2612,7 +2861,13 @@ class AppBase:
|
||||
for i in range(0, curminlength):
|
||||
tmpitem = json.loads(json.dumps(parameter["value"]))
|
||||
for key, value in replacements.items():
|
||||
replacement = json.dumps(json.loads(value)[i])
|
||||
replacement = value
|
||||
try:
|
||||
replacement = json.dumps(json.loads(value)[i])
|
||||
except IndexError as e:
|
||||
self.logger.info(f"[ERROR] Failed handling value parsing with index: {e}")
|
||||
pass
|
||||
|
||||
if replacement.startswith("\"") and replacement.endswith("\""):
|
||||
replacement = replacement[1:len(replacement)-1]
|
||||
#except json.decoder.JSONDecodeError as e:
|
||||
@@ -2667,13 +2922,20 @@ class AppBase:
|
||||
multi_parameters[parameter["name"]] = resultarray
|
||||
else:
|
||||
# Parses things like int(value)
|
||||
self.logger.info("[DEBUG] Normal parsing (not looping)")#with data %s" % value)
|
||||
#self.logger.info("[DEBUG] Normal parsing (not looping)")#with data %s" % value)
|
||||
# This part has fucked over so many random JSON usages because of weird paranthesis parsing
|
||||
|
||||
value = parse_wrapper_start(value, self)
|
||||
#self.logger.info("[DEBUG] Post return: %s" % value)
|
||||
|
||||
#self.logger.info("POST data value: %s" % value)
|
||||
|
||||
try:
|
||||
if str(value).startswith("b'") and str(value).endswith("'"):
|
||||
value = value[2:-1]
|
||||
except Exception as e:
|
||||
print(f"Value rawbytes Exception: {e}")
|
||||
|
||||
params[parameter["name"]] = value
|
||||
multi_parameters[parameter["name"]] = value
|
||||
|
||||
@@ -2803,7 +3065,7 @@ class AppBase:
|
||||
except Exception as e:
|
||||
self.logger.warning("[ERROR] Failed to parse coroutine value for old app: {e}")
|
||||
|
||||
self.logger.info("\n[INFO] Returned from execution with types %s" % type(newres))
|
||||
self.logger.info("\n[INFO] Returned from execution with type(s) %s" % type(newres))
|
||||
#self.logger.info("\n[INFO] Returned from execution with %s of types %s" % (newres, type(newres)))#, newres)
|
||||
if isinstance(newres, tuple):
|
||||
self.logger.info(f"[INFO] Handling return as tuple: {newres}")
|
||||
@@ -2946,6 +3208,11 @@ class AppBase:
|
||||
|
||||
# Send the result :)
|
||||
self.send_result(self.action_result, headers, stream_path)
|
||||
try:
|
||||
self.log_capture_string.close()
|
||||
except:
|
||||
pass
|
||||
|
||||
return
|
||||
|
||||
@classmethod
|
||||
|
||||
+13
-13
@@ -3,7 +3,7 @@
|
||||
|
||||
### DEFAULT
|
||||
NAME=shuffle-app_sdk
|
||||
VERSION=0.9.50
|
||||
VERSION=0.9.61
|
||||
|
||||
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
|
||||
@@ -19,17 +19,17 @@ docker push ghcr.io/frikky/$NAME:nightly
|
||||
docker push ghcr.io/frikky/$NAME:latest
|
||||
|
||||
#### KALI ###
|
||||
NAME=shuffle-app_sdk_kali
|
||||
docker build . -f Dockerfile_kali -t frikky/shuffle:app_sdk_kali -t frikky/$NAME:$VERSION -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION
|
||||
|
||||
docker push frikky/shuffle:app_sdk_kali
|
||||
docker push ghcr.io/frikky/$NAME:$VERSION
|
||||
docker push ghcr.io/frikky/$NAME:nightly
|
||||
#NAME=shuffle-app_sdk_kali
|
||||
#docker build . -f Dockerfile_kali -t frikky/shuffle:app_sdk_kali -t frikky/$NAME:$VERSION -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION
|
||||
#
|
||||
#docker push frikky/shuffle:app_sdk_kali
|
||||
#docker push ghcr.io/frikky/$NAME:$VERSION
|
||||
#docker push ghcr.io/frikky/$NAME:nightly
|
||||
|
||||
### BLACKARCH ###
|
||||
NAME=shuffle-app_sdk_blackarch
|
||||
docker build . -f Dockerfile_blackarch -t frikky/shuffle:app_sdk_blackarch -t frikky/$NAME:$VERSION -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION
|
||||
|
||||
docker push frikky/shuffle:app_sdk_blackarch
|
||||
docker push ghcr.io/frikky/$NAME:$VERSION
|
||||
docker push ghcr.io/frikky/$NAME:nightly
|
||||
#NAME=shuffle-app_sdk_blackarch
|
||||
#docker build . -f Dockerfile_blackarch -t frikky/shuffle:app_sdk_blackarch -t frikky/$NAME:$VERSION -t docker.pkg.github.com/frikky/shuffle/$NAME:$VERSION -t ghcr.io/frikky/$NAME:$VERSION
|
||||
#
|
||||
#docker push frikky/shuffle:app_sdk_blackarch
|
||||
#docker push ghcr.io/frikky/$NAME:$VERSION
|
||||
#docker push ghcr.io/frikky/$NAME:nightly
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
//"github.com/docker/docker"
|
||||
"github.com/docker/docker/api/types"
|
||||
//"github.com/docker/docker/api/types/container"
|
||||
@@ -417,7 +418,7 @@ func stopWebhook(image string, identifier string) error {
|
||||
|
||||
// Starts a new webhook
|
||||
func handleStopHookDocker(resp http.ResponseWriter, request *http.Request) {
|
||||
cors := handleCors(resp, request)
|
||||
cors := shuffle.HandleCors(resp, request)
|
||||
if cors {
|
||||
return
|
||||
}
|
||||
@@ -502,7 +503,7 @@ var webhook = `{
|
||||
// Starts a new webhook
|
||||
func handleDeleteHookDocker(resp http.ResponseWriter, request *http.Request) {
|
||||
ctx := context.Background()
|
||||
cors := handleCors(resp, request)
|
||||
cors := shuffle.HandleCors(resp, request)
|
||||
if cors {
|
||||
return
|
||||
}
|
||||
@@ -619,7 +620,7 @@ func hookTest() {
|
||||
|
||||
//https://stackoverflow.com/questions/23935141/how-to-copy-docker-images-from-one-host-to-another-without-using-a-repository
|
||||
func getDockerImage(resp http.ResponseWriter, request *http.Request) {
|
||||
cors := handleCors(resp, request)
|
||||
cors := shuffle.HandleCors(resp, request)
|
||||
if cors {
|
||||
return
|
||||
}
|
||||
|
||||
+12
-11
@@ -1,34 +1,35 @@
|
||||
module main
|
||||
|
||||
go 1.15
|
||||
go 1.16
|
||||
|
||||
replace github.com/shuffle/shuffle-shared => ../../../../git/shuffle-shared
|
||||
//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/pubsub v1.17.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
|
||||
github.com/basgys/goxml2json v1.1.0
|
||||
github.com/carlescere/scheduler v0.0.0-20170109141437-ee74d2f83d82
|
||||
github.com/docker/docker v20.10.9+incompatible
|
||||
github.com/docker/docker v20.10.12+incompatible
|
||||
github.com/frikky/kin-openapi v0.41.0
|
||||
github.com/fsouza/go-dockerclient v1.7.4
|
||||
github.com/fsouza/go-dockerclient v1.7.7
|
||||
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/gorilla/mux v1.8.0
|
||||
github.com/h2non/filetype v1.1.1
|
||||
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.1.79
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
|
||||
github.com/shuffle/shuffle-shared v0.2.7
|
||||
go4.org v0.0.0-20201209231011-d4a079459e60 // indirect
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
|
||||
google.golang.org/api v0.58.0
|
||||
golang.org/x/crypto v0.0.0-20220112180741-5e0467b6c7ce
|
||||
google.golang.org/api v0.65.0
|
||||
google.golang.org/appengine v1.6.7
|
||||
google.golang.org/grpc v1.41.0
|
||||
google.golang.org/grpc v1.43.0
|
||||
gopkg.in/src-d/go-git.v4 v4.13.1
|
||||
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b
|
||||
)
|
||||
|
||||
+131
-32
@@ -1,14 +1,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
uuid "github.com/satori/go.uuid"
|
||||
"github.com/shuffle/shuffle-shared"
|
||||
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/md5"
|
||||
|
||||
//"crypto/tls"
|
||||
//"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
@@ -16,11 +19,13 @@ import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
|
||||
//"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -49,12 +54,14 @@ import (
|
||||
"github.com/go-git/go-git/v5"
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/storage/memory"
|
||||
|
||||
//cv "github.com/nirasan/go-oauth-pkce-code-verifier"
|
||||
|
||||
//githttp "gopkg.in/src-d/go-git.v4/plumbing/transport/http"
|
||||
|
||||
// Random
|
||||
xj "github.com/basgys/goxml2json"
|
||||
newscheduler "github.com/carlescere/scheduler"
|
||||
"github.com/satori/go.uuid"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
@@ -706,7 +713,7 @@ func createNewUser(username, password, role, apikey string, org shuffle.OrgMini)
|
||||
}
|
||||
|
||||
func handleRegister(resp http.ResponseWriter, request *http.Request) {
|
||||
cors := handleCors(resp, request)
|
||||
cors := shuffle.HandleCors(resp, request)
|
||||
if cors {
|
||||
return
|
||||
}
|
||||
@@ -835,7 +842,7 @@ func handleCookie(request *http.Request) bool {
|
||||
}
|
||||
|
||||
func handleInfo(resp http.ResponseWriter, request *http.Request) {
|
||||
cors := handleCors(resp, request)
|
||||
cors := shuffle.HandleCors(resp, request)
|
||||
if cors {
|
||||
return
|
||||
}
|
||||
@@ -1118,7 +1125,7 @@ func increaseStatisticsField(ctx context.Context, fieldname, id string, amount i
|
||||
|
||||
// FIXME - forward this to emails or whatever CRM system in use
|
||||
func handleContact(resp http.ResponseWriter, request *http.Request) {
|
||||
cors := handleCors(resp, request)
|
||||
cors := shuffle.HandleCors(resp, request)
|
||||
if cors {
|
||||
return
|
||||
}
|
||||
@@ -1163,8 +1170,17 @@ func handleContact(resp http.ResponseWriter, request *http.Request) {
|
||||
resp.Write([]byte(fmt.Sprintf(`{"success": true, "message": "Thanks for reaching out. We will contact you soon!"}`)))
|
||||
}
|
||||
|
||||
func verifier() (*shuffle.CodeVerifier, error) {
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
b := make([]byte, 32, 32)
|
||||
for i := 0; i < 32; i++ {
|
||||
b[i] = byte(r.Intn(255))
|
||||
}
|
||||
return shuffle.CreateCodeVerifierFromBytes(b)
|
||||
}
|
||||
|
||||
func checkAdminLogin(resp http.ResponseWriter, request *http.Request) {
|
||||
cors := handleCors(resp, request)
|
||||
cors := shuffle.HandleCors(resp, request)
|
||||
if cors {
|
||||
return
|
||||
}
|
||||
@@ -1186,14 +1202,74 @@ func checkAdminLogin(resp http.ResponseWriter, request *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
//ssoUrl = org.SSOConfig.SOSOEntrypoint
|
||||
redirectUri := shuffle.SSOUrl
|
||||
baseSSOUrl := ""
|
||||
handled := []string{}
|
||||
for _, user := range users {
|
||||
if shuffle.ArrayContains(handled, user.ActiveOrg.Id) {
|
||||
continue
|
||||
}
|
||||
|
||||
handled = append(handled, user.ActiveOrg.Id)
|
||||
org, err := shuffle.GetOrg(ctx, user.ActiveOrg.Id)
|
||||
if err != nil {
|
||||
log.Printf("[WARNING] Error getting org in admin check: %s", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// No childorg setup, only parent org
|
||||
if len(org.ManagerOrgs) > 0 || len(org.CreatorOrg) > 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
// 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()
|
||||
//h.Write([]byte(v.Value))
|
||||
verifier, verifiererr := verifier()
|
||||
if verifiererr == nil {
|
||||
codeChallenge = verifier.Value
|
||||
}
|
||||
|
||||
//log.Printf("[DEBUG] Got challenge value %s (pre state)", codeChallenge)
|
||||
|
||||
// https://192.168.55.222:3443/api/v1/login_openid
|
||||
//location := strings.Split(request.URL.String(), "/")
|
||||
//redirectUrl := url.QueryEscape("http://localhost:5001/api/v1/login_openid")
|
||||
redirectUrl := url.QueryEscape(fmt.Sprintf("http://%s/api/v1/login_openid", request.Host))
|
||||
if strings.Contains(request.Host, "shuffle-backend") && !strings.Contains(os.Getenv("BASE_URL"), "shuffle-backend") {
|
||||
redirectUrl = url.QueryEscape(fmt.Sprintf("%s/api/v1/login_openid", os.Getenv("BASE_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
|
||||
if verifiererr == nil {
|
||||
codeChallenge = verifier.CodeChallengeS256()
|
||||
}
|
||||
|
||||
//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)
|
||||
break
|
||||
}
|
||||
|
||||
if len(org.SSOConfig.SSOEntrypoint) > 0 {
|
||||
log.Printf("[DEBUG] Found SAML SSO url")
|
||||
baseSSOUrl = org.SSOConfig.SSOEntrypoint
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
//log.Printf("[DEBUG] OpenID URL: %s", baseSSOUrl)
|
||||
resp.WriteHeader(200)
|
||||
resp.Write([]byte(fmt.Sprintf(`{"success": true, "reason": "redirect", "sso_url": "%s"}`, redirectUri)))
|
||||
resp.Write([]byte(fmt.Sprintf(`{"success": true, "reason": "redirect", "sso_url": "%s"}`, baseSSOUrl)))
|
||||
}
|
||||
|
||||
func handleLogin(resp http.ResponseWriter, request *http.Request) {
|
||||
cors := handleCors(resp, request)
|
||||
cors := shuffle.HandleCors(resp, request)
|
||||
if cors {
|
||||
return
|
||||
}
|
||||
@@ -1418,8 +1494,12 @@ func fixUserOrg(ctx context.Context, user *shuffle.User) *shuffle.User {
|
||||
}
|
||||
|
||||
// Used for testing only. Shouldn't impact production.
|
||||
func handleCors(resp http.ResponseWriter, request *http.Request) bool {
|
||||
allowedOrigins := "http://localhost:3000"
|
||||
/*
|
||||
func shuffle.HandleCors(resp http.ResponseWriter, request *http.Request) bool {
|
||||
// Used for Codespace dev
|
||||
allowedOrigins := "https://frikky-shuffle-5gvr4xx62w64-3000.githubpreview.dev"
|
||||
//origin := request.Header["Origin"]
|
||||
//log.Printf("Origin: %s", origin)
|
||||
//allowedOrigins := "http://localhost:3002"
|
||||
|
||||
resp.Header().Set("Vary", "Origin")
|
||||
@@ -1436,6 +1516,7 @@ func handleCors(resp http.ResponseWriter, request *http.Request) bool {
|
||||
|
||||
return false
|
||||
}
|
||||
*/
|
||||
|
||||
func parseWorkflowParameters(resp http.ResponseWriter, request *http.Request) (map[string]interface{}, error) {
|
||||
body, err := ioutil.ReadAll(request.Body)
|
||||
@@ -1580,7 +1661,7 @@ func SearchNested(obj interface{}, key string) (interface{}, bool) {
|
||||
}
|
||||
|
||||
func handleSetHook(resp http.ResponseWriter, request *http.Request) {
|
||||
cors := handleCors(resp, request)
|
||||
cors := shuffle.HandleCors(resp, request)
|
||||
if cors {
|
||||
return
|
||||
}
|
||||
@@ -1768,7 +1849,7 @@ func verifyHook(hook shuffle.Hook) (bool, string) {
|
||||
}
|
||||
|
||||
func setSpecificSchedule(resp http.ResponseWriter, request *http.Request) {
|
||||
cors := handleCors(resp, request)
|
||||
cors := shuffle.HandleCors(resp, request)
|
||||
if cors {
|
||||
return
|
||||
}
|
||||
@@ -1828,7 +1909,7 @@ func setSpecificSchedule(resp http.ResponseWriter, request *http.Request) {
|
||||
}
|
||||
|
||||
func getSpecificWebhook(resp http.ResponseWriter, request *http.Request) {
|
||||
cors := handleCors(resp, request)
|
||||
cors := shuffle.HandleCors(resp, request)
|
||||
if cors {
|
||||
return
|
||||
}
|
||||
@@ -1880,7 +1961,7 @@ func getSpecificWebhook(resp http.ResponseWriter, request *http.Request) {
|
||||
|
||||
// Starts a new webhook
|
||||
func handleDeleteSchedule(resp http.ResponseWriter, request *http.Request) {
|
||||
cors := handleCors(resp, request)
|
||||
cors := shuffle.HandleCors(resp, request)
|
||||
if cors {
|
||||
return
|
||||
}
|
||||
@@ -1935,7 +2016,7 @@ func handleDeleteSchedule(resp http.ResponseWriter, request *http.Request) {
|
||||
|
||||
// Starts a new webhook
|
||||
func handleNewSchedule(resp http.ResponseWriter, request *http.Request) {
|
||||
cors := handleCors(resp, request)
|
||||
cors := shuffle.HandleCors(resp, request)
|
||||
if cors {
|
||||
return
|
||||
}
|
||||
@@ -2229,7 +2310,7 @@ func getSpecificSchedule(resp http.ResponseWriter, request *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
cors := handleCors(resp, request)
|
||||
cors := shuffle.HandleCors(resp, request)
|
||||
if cors {
|
||||
return
|
||||
}
|
||||
@@ -2295,7 +2376,7 @@ func loadYaml(fileLocation string) (ApiYaml, error) {
|
||||
|
||||
// This should ALWAYS come from an OUTPUT
|
||||
func executeSchedule(resp http.ResponseWriter, request *http.Request) {
|
||||
cors := handleCors(resp, request)
|
||||
cors := shuffle.HandleCors(resp, request)
|
||||
if cors {
|
||||
return
|
||||
}
|
||||
@@ -2788,7 +2869,7 @@ type Result struct {
|
||||
// r.HandleFunc("/api/v1/docs/{key}", getDocs).Methods("GET", "OPTIONS")
|
||||
|
||||
func getOpenapi(resp http.ResponseWriter, request *http.Request) {
|
||||
cors := handleCors(resp, request)
|
||||
cors := shuffle.HandleCors(resp, request)
|
||||
if cors {
|
||||
return
|
||||
}
|
||||
@@ -3254,7 +3335,7 @@ func buildSwaggerApp(resp http.ResponseWriter, body []byte, user shuffle.User) {
|
||||
|
||||
// Creates an app from the app builder
|
||||
func verifySwagger(resp http.ResponseWriter, request *http.Request) {
|
||||
cors := handleCors(resp, request)
|
||||
cors := shuffle.HandleCors(resp, request)
|
||||
if cors {
|
||||
return
|
||||
}
|
||||
@@ -3316,11 +3397,6 @@ func createFs(basepath, pathname string) (billy.Filesystem, error) {
|
||||
return err
|
||||
}
|
||||
|
||||
//if strings.Contains(path, "yaml") {
|
||||
// log.Printf("PATH: %s -> %s", path, fullpath)
|
||||
// //log.Printf("DATA: %s", string(srcData))
|
||||
//}
|
||||
|
||||
dst, err := fs.Create(fullpath)
|
||||
if err != nil {
|
||||
log.Printf("Dst error: %s", err)
|
||||
@@ -3863,7 +3939,7 @@ func runInitEs(ctx context.Context) {
|
||||
}
|
||||
|
||||
for _, schedule := range schedules {
|
||||
if schedule.Environment == "cloud" {
|
||||
if strings.ToLower(schedule.Environment) == "cloud" {
|
||||
log.Printf("Skipping cloud schedule")
|
||||
continue
|
||||
}
|
||||
@@ -4999,7 +5075,7 @@ func handleStopCloudSync(syncUrl string, org shuffle.Org) (*shuffle.Org, error)
|
||||
This is here to both enable and disable cloud sync features for an organization
|
||||
*/
|
||||
func handleCloudSetup(resp http.ResponseWriter, request *http.Request) {
|
||||
cors := handleCors(resp, request)
|
||||
cors := shuffle.HandleCors(resp, request)
|
||||
if cors {
|
||||
return
|
||||
}
|
||||
@@ -5101,8 +5177,19 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) {
|
||||
|
||||
_, err = handleStopCloudSync(syncPath, *org)
|
||||
if err != nil {
|
||||
ret := shuffle.ResultChecker{
|
||||
Success: false,
|
||||
Reason: fmt.Sprintf("%s", err),
|
||||
}
|
||||
|
||||
resp.WriteHeader(401)
|
||||
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err)))
|
||||
b, err := json.Marshal(ret)
|
||||
if err != nil {
|
||||
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err)))
|
||||
return
|
||||
}
|
||||
|
||||
resp.Write(b)
|
||||
} else {
|
||||
resp.WriteHeader(200)
|
||||
resp.Write([]byte(fmt.Sprintf(`{"success": true, "reason": "Successfully disabled cloud sync for org."}`)))
|
||||
@@ -5687,10 +5774,15 @@ func initHandlers() {
|
||||
dbclient, err = datastore.NewClient(ctx, gceProject, option.WithGRPCDialOption(grpc.WithNoProxy()))
|
||||
if err != nil {
|
||||
if elasticConfig == "" {
|
||||
log.Fatalf("[ERROR] Database client error during init: %s. Env: SHUFFLE_ELASTIC=false", err)
|
||||
log.Printf("[ERROR] Database client error during init: %s. Env: SHUFFLE_ELASTIC=false", err)
|
||||
} else {
|
||||
log.Printf("[DEBUG] Database client error during init: %s. Here for backwards compatibility: not critical.", err)
|
||||
if !strings.Contains(fmt.Sprintf("%s", err), "find default credentials") {
|
||||
log.Printf("[DEBUG] Database client error info during init: %s. Here for backwards compatibility: not critical.", err)
|
||||
}
|
||||
dbclient = &datastore.Client{}
|
||||
}
|
||||
} else {
|
||||
//log.Printf("Database client initiated: %s", dbclient)
|
||||
}
|
||||
|
||||
for {
|
||||
@@ -5783,10 +5875,13 @@ func initHandlers() {
|
||||
r.HandleFunc("/api/v1/apps/authentication/{appauthId}/config", shuffle.SetAuthenticationConfig).Methods("POST", "OPTIONS")
|
||||
r.HandleFunc("/api/v1/apps/authentication/{appauthId}", shuffle.DeleteAppAuthentication).Methods("DELETE", "OPTIONS")
|
||||
|
||||
// Related to
|
||||
// Related to NFT things
|
||||
r.HandleFunc("/api/v1/workflows/collections/load", shuffle.LoadCollections).Methods("POST", "OPTIONS")
|
||||
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", shuffle.LoadUsecases).Methods("GET", "OPTIONS")
|
||||
|
||||
// Legacy app things
|
||||
r.HandleFunc("/api/v1/workflows/apps/validate", validateAppInput).Methods("POST", "OPTIONS")
|
||||
r.HandleFunc("/api/v1/workflows/apps", getWorkflowApps).Methods("GET", "OPTIONS")
|
||||
@@ -5868,7 +5963,8 @@ func initHandlers() {
|
||||
// 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("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")
|
||||
|
||||
// Important for email, IDS etc. Create this by:
|
||||
// PS: For cloud, this has to use cloud storage.
|
||||
@@ -5887,6 +5983,9 @@ func initHandlers() {
|
||||
r.HandleFunc("/api/v1/notifications/clear", shuffle.HandleClearNotifications).Methods("GET", "OPTIONS")
|
||||
r.HandleFunc("/api/v1/notifications/{notificationId}/markasread", shuffle.HandleMarkAsRead).Methods("GET", "OPTIONS")
|
||||
//r.HandleFunc("/api/v1/notifications/{notificationId}/markasread", shuffle.HandleMarkAsRead).Methods("GET", "OPTIONS")
|
||||
r.HandleFunc("/api/v1/users/notifications", shuffle.HandleGetNotifications).Methods("GET", "OPTIONS")
|
||||
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")
|
||||
|
||||
http.Handle("/", r)
|
||||
}
|
||||
|
||||
+17
-15
@@ -21,6 +21,7 @@ import (
|
||||
|
||||
"github.com/docker/docker/api/types"
|
||||
dockerclient "github.com/docker/docker/client"
|
||||
|
||||
//gyaml "github.com/ghodss/yaml"
|
||||
|
||||
"github.com/h2non/filetype"
|
||||
@@ -34,6 +35,7 @@ import (
|
||||
"github.com/go-git/go-git/v5/plumbing"
|
||||
"github.com/go-git/go-git/v5/storage/memory"
|
||||
http2 "gopkg.in/src-d/go-git.v4/plumbing/transport/http"
|
||||
|
||||
//"github.com/gorilla/websocket"
|
||||
//"google.golang.org/appengine"
|
||||
//"google.golang.org/appengine/memcache"
|
||||
@@ -142,7 +144,7 @@ func createSchedule(ctx context.Context, scheduleId, workflowId, name, startNode
|
||||
}
|
||||
|
||||
func handleGetWorkflowqueueConfirm(resp http.ResponseWriter, request *http.Request) {
|
||||
cors := handleCors(resp, request)
|
||||
cors := shuffle.HandleCors(resp, request)
|
||||
if cors {
|
||||
return
|
||||
}
|
||||
@@ -243,7 +245,7 @@ func handleGetWorkflowqueueConfirm(resp http.ResponseWriter, request *http.Reque
|
||||
// FIXME: Authenticate this one? Can org ID be auth enough?
|
||||
// (especially since we have a default: shuffle)
|
||||
func handleGetWorkflowqueue(resp http.ResponseWriter, request *http.Request) {
|
||||
cors := handleCors(resp, request)
|
||||
cors := shuffle.HandleCors(resp, request)
|
||||
if cors {
|
||||
return
|
||||
}
|
||||
@@ -335,7 +337,7 @@ func handleGetWorkflowqueue(resp http.ResponseWriter, request *http.Request) {
|
||||
}
|
||||
|
||||
func handleGetStreamResults(resp http.ResponseWriter, request *http.Request) {
|
||||
cors := handleCors(resp, request)
|
||||
cors := shuffle.HandleCors(resp, request)
|
||||
if cors {
|
||||
return
|
||||
}
|
||||
@@ -393,7 +395,7 @@ func handleGetStreamResults(resp http.ResponseWriter, request *http.Request) {
|
||||
}
|
||||
|
||||
func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) {
|
||||
cors := handleCors(resp, request)
|
||||
cors := shuffle.HandleCors(resp, request)
|
||||
if cors {
|
||||
return
|
||||
}
|
||||
@@ -666,7 +668,7 @@ func handleExecutionStatistics(execution shuffle.WorkflowExecution) {
|
||||
}
|
||||
|
||||
func deleteWorkflow(resp http.ResponseWriter, request *http.Request) {
|
||||
cors := handleCors(resp, request)
|
||||
cors := shuffle.HandleCors(resp, request)
|
||||
if cors {
|
||||
return
|
||||
}
|
||||
@@ -1034,7 +1036,7 @@ func cloudExecuteAction(execution shuffle.WorkflowExecution) error {
|
||||
}
|
||||
|
||||
func executeWorkflow(resp http.ResponseWriter, request *http.Request) {
|
||||
cors := handleCors(resp, request)
|
||||
cors := shuffle.HandleCors(resp, request)
|
||||
if cors {
|
||||
return
|
||||
}
|
||||
@@ -1131,7 +1133,7 @@ func executeWorkflow(resp http.ResponseWriter, request *http.Request) {
|
||||
}
|
||||
|
||||
func stopSchedule(resp http.ResponseWriter, request *http.Request) {
|
||||
cors := handleCors(resp, request)
|
||||
cors := shuffle.HandleCors(resp, request)
|
||||
if cors {
|
||||
return
|
||||
}
|
||||
@@ -1281,7 +1283,7 @@ func stopSchedule(resp http.ResponseWriter, request *http.Request) {
|
||||
}
|
||||
|
||||
func stopScheduleGCP(resp http.ResponseWriter, request *http.Request) {
|
||||
cors := handleCors(resp, request)
|
||||
cors := shuffle.HandleCors(resp, request)
|
||||
if cors {
|
||||
return
|
||||
}
|
||||
@@ -1390,7 +1392,7 @@ func deleteSchedule(ctx context.Context, id string) error {
|
||||
}
|
||||
|
||||
func scheduleWorkflow(resp http.ResponseWriter, request *http.Request) {
|
||||
cors := handleCors(resp, request)
|
||||
cors := shuffle.HandleCors(resp, request)
|
||||
if cors {
|
||||
return
|
||||
}
|
||||
@@ -1641,7 +1643,7 @@ func setExampleresult(ctx context.Context, result shuffle.AppExecutionExample) e
|
||||
}
|
||||
|
||||
func getWorkflowApps(resp http.ResponseWriter, request *http.Request) {
|
||||
cors := handleCors(resp, request)
|
||||
cors := shuffle.HandleCors(resp, request)
|
||||
if cors {
|
||||
return
|
||||
}
|
||||
@@ -1714,7 +1716,7 @@ func handleGetfile(resp http.ResponseWriter, request *http.Request) ([]byte, err
|
||||
|
||||
// Basically a search for apps that aren't activated yet
|
||||
func getSpecificApps(resp http.ResponseWriter, request *http.Request) {
|
||||
cors := handleCors(resp, request)
|
||||
cors := shuffle.HandleCors(resp, request)
|
||||
if cors {
|
||||
return
|
||||
}
|
||||
@@ -1789,7 +1791,7 @@ func getSpecificApps(resp http.ResponseWriter, request *http.Request) {
|
||||
}
|
||||
|
||||
func validateAppInput(resp http.ResponseWriter, request *http.Request) {
|
||||
cors := handleCors(resp, request)
|
||||
cors := shuffle.HandleCors(resp, request)
|
||||
if cors {
|
||||
return
|
||||
}
|
||||
@@ -1910,7 +1912,7 @@ func loadGithubWorkflows(url, username, password, userId, branch, orgId string)
|
||||
}
|
||||
|
||||
func loadSpecificWorkflows(resp http.ResponseWriter, request *http.Request) {
|
||||
cors := handleCors(resp, request)
|
||||
cors := shuffle.HandleCors(resp, request)
|
||||
if cors {
|
||||
return
|
||||
}
|
||||
@@ -1972,7 +1974,7 @@ func loadSpecificWorkflows(resp http.ResponseWriter, request *http.Request) {
|
||||
}
|
||||
|
||||
func handleAppHotloadRequest(resp http.ResponseWriter, request *http.Request) {
|
||||
cors := handleCors(resp, request)
|
||||
cors := shuffle.HandleCors(resp, request)
|
||||
if cors {
|
||||
return
|
||||
}
|
||||
@@ -2290,7 +2292,7 @@ func iterateWorkflowGithubFolders(fs billy.Filesystem, dir []os.FileInfo, extra
|
||||
}
|
||||
|
||||
func setNewWorkflowApp(resp http.ResponseWriter, request *http.Request) {
|
||||
cors := handleCors(resp, request)
|
||||
cors := shuffle.HandleCors(resp, request)
|
||||
if cors {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -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/files/e19cffe4-e2da-47e9-809e-904f5cb03687/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"
|
||||
|
||||
+2
-5
@@ -48,7 +48,6 @@ services:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
environment:
|
||||
- SHUFFLE_WORKER_VERSION=nightly
|
||||
- ORG_ID=${ORG_ID}
|
||||
- ENVIRONMENT_NAME=${ENVIRONMENT_NAME}
|
||||
- BASE_URL=http://${OUTER_HOSTNAME}:${BACKEND_PORT}
|
||||
- DOCKER_API_VERSION=1.40
|
||||
@@ -64,13 +63,12 @@ services:
|
||||
- SHUFFLE_SWARM_CONFIG=runn
|
||||
restart: unless-stopped
|
||||
opensearch:
|
||||
image: opensearchproject/opensearch:1.2.3
|
||||
image: opensearchproject/opensearch:1.2.4
|
||||
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
|
||||
- plugins.security.disabled=true
|
||||
- "OPENSEARCH_JAVA_OPTS=-Xms2048m -Xmx2048m" # minimum and maximum Java heap size, recommend setting both to 50% of system RAM
|
||||
- cluster.routing.allocation.disk.threshold_enabled=false
|
||||
- cluster.name=shuffle-cluster
|
||||
- node.name=shuffle-opensearch
|
||||
@@ -96,4 +94,3 @@ networks:
|
||||
driver: bridge
|
||||
|
||||
#driver: overlay
|
||||
#driver: bridge
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"name": "shuffler",
|
||||
"homepage": "https://shuffler.io",
|
||||
"version": "0.9.50",
|
||||
"version": "0.9.61",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.15.8",
|
||||
"@emotion/is-prop-valid": "^1.1.1",
|
||||
"@emotion/react": "^11.7.0",
|
||||
"@emotion/styled": "^11.6.0",
|
||||
"@material-ui/core": "^4.5.2",
|
||||
@@ -66,6 +67,7 @@
|
||||
"react-scripts": "^4.0.1",
|
||||
"react-shepherd": "^3.3.6",
|
||||
"reactstrap": "^7.1.0",
|
||||
"reaviz": "^12.1.0",
|
||||
"shellwords": "^0.1.1",
|
||||
"simplebar": "^4.2.3",
|
||||
"styled-components": "^4.4.0",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 109 KiB |
+14
-6
@@ -15,7 +15,7 @@ import theme from "./theme";
|
||||
import Apps from "./views/Apps";
|
||||
import AppCreator from "./views/AppCreator";
|
||||
|
||||
import Dashboard from "./views/Dashboard";
|
||||
import Dashboard from "./views/Dashboard.jsx";
|
||||
import AdminSetup from "./views/AdminSetup";
|
||||
import Admin from "./views/Admin";
|
||||
import Docs from "./views/Docs";
|
||||
@@ -49,6 +49,12 @@ if (window.location.port === "3000") {
|
||||
//globalUrl = "http://localhost:5002"
|
||||
}
|
||||
|
||||
if (globalUrl.includes("githubpreview.dev")) {
|
||||
//globalUrl = globalUrl.replace("3000", "5001")
|
||||
globalUrl = "https://frikky-shuffle-5gvr4xx62w64-5001.githubpreview.dev"
|
||||
}
|
||||
console.log("global: ", globalUrl)
|
||||
|
||||
const App = (message, props) => {
|
||||
|
||||
const [userdata, setUserData] = useState({});
|
||||
@@ -70,7 +76,7 @@ const App = (message, props) => {
|
||||
checkLogin();
|
||||
setDataset(true);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
if (
|
||||
isLoaded &&
|
||||
@@ -78,17 +84,19 @@ const App = (message, props) => {
|
||||
!window.location.pathname.startsWith("/login") &&
|
||||
!window.location.pathname.startsWith("/docs") &&
|
||||
!window.location.pathname.startsWith("/detectionframework") &&
|
||||
!window.location.pathname.startsWith("/adminsetup")
|
||||
!window.location.pathname.startsWith("/adminsetup") &&
|
||||
!window.location.pathname.startsWith("/usecases")
|
||||
) {
|
||||
window.location = "/login";
|
||||
}
|
||||
|
||||
const getUserNotifications = () => {
|
||||
fetch(`${globalUrl}/api/v1/notifications`, {
|
||||
fetch(`${globalUrl}/api/v1/users/notifications`, {
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
cors: "cors",
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((responseJson) => {
|
||||
@@ -109,7 +117,7 @@ const App = (message, props) => {
|
||||
|
||||
const checkLogin = () => {
|
||||
var baseurl = globalUrl;
|
||||
fetch(baseurl + "/api/v1/users/getinfo", {
|
||||
fetch(`${globalUrl}/api/v1/getinfo`, {
|
||||
credentials: "include",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -399,7 +407,7 @@ const App = (message, props) => {
|
||||
/>
|
||||
<Route
|
||||
exact
|
||||
path="/dashboard"
|
||||
path="/usecases"
|
||||
element={
|
||||
<Dashboard
|
||||
isLoaded={isLoaded}
|
||||
|
||||
@@ -556,7 +556,7 @@ const ConfigureWorkflow = (props) => {
|
||||
{action.must_activate ? (
|
||||
<Button
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
variant="contained"
|
||||
onClick={() => {
|
||||
console.log("ACTION: ", action)
|
||||
activateApp(action.action.app_id, action.app_name, action.app_version);
|
||||
@@ -666,7 +666,7 @@ const ConfigureWorkflow = (props) => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
Finish setup
|
||||
Close window
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
|
||||
@@ -1499,7 +1499,7 @@ const Framework = (props) => {
|
||||
/>
|
||||
:
|
||||
<div>
|
||||
TBD: Coming in 1.0.0.
|
||||
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>
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
|
||||
import {
|
||||
Analytics as AnalyticsIcon,
|
||||
Lightbulb as LightbulbIcon,
|
||||
} from "@mui/icons-material";
|
||||
//import LogoutIcon from '@mui/icons-material/Logout';
|
||||
import { useAlert } from "react-alert";
|
||||
@@ -466,6 +467,16 @@ const Header = (props) => {
|
||||
<AnalyticsIcon style={{marginRight: 5 }}/> Get Started
|
||||
</Link>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
<Link to="/usecases" style={hrefStyle}>
|
||||
<LightbulbIcon style={{marginRight: 5 }}/> Use Cases
|
||||
</Link>
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={(event) => {
|
||||
event.preventDefault();
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
import React, {useState, useRef, useImperativeHandle} from 'react'
|
||||
import {makeStyles} from '@material-ui/core/styles'
|
||||
import Menu, {MenuProps} from '@material-ui/core/Menu'
|
||||
import MenuItem, {MenuItemProps} from '@material-ui/core/MenuItem'
|
||||
import ArrowRight from '@material-ui/icons/ArrowRight'
|
||||
import clsx from 'clsx'
|
||||
|
||||
export interface NestedMenuItemProps extends Omit<MenuItemProps, 'button'> {
|
||||
/**
|
||||
* Open state of parent `<Menu />`, used to close decendent menus when the
|
||||
* root menu is closed.
|
||||
*/
|
||||
parentMenuOpen: boolean
|
||||
/**
|
||||
* Component for the container element.
|
||||
* @default 'div'
|
||||
*/
|
||||
component?: React.ElementType
|
||||
/**
|
||||
* Effectively becomes the `children` prop passed to the `<MenuItem/>`
|
||||
* element.
|
||||
*/
|
||||
label?: React.ReactNode
|
||||
/**
|
||||
* @default <ArrowRight />
|
||||
*/
|
||||
rightIcon?: React.ReactNode
|
||||
/**
|
||||
* Props passed to container element.
|
||||
*/
|
||||
ContainerProps?: React.HTMLAttributes<HTMLElement> &
|
||||
React.RefAttributes<HTMLElement | null>
|
||||
/**
|
||||
* Props passed to sub `<Menu/>` element
|
||||
*/
|
||||
MenuProps?: Omit<MenuProps, 'children'>
|
||||
/**
|
||||
* @see https://material-ui.com/api/list-item/
|
||||
*/
|
||||
button?: true | undefined
|
||||
}
|
||||
|
||||
const TRANSPARENT = 'rgba(0,0,0,0)'
|
||||
const useMenuItemStyles = makeStyles((theme) => ({
|
||||
root: (props: any) => ({
|
||||
backgroundColor: props.open ? theme.palette.action.hover : TRANSPARENT
|
||||
})
|
||||
}))
|
||||
|
||||
/**
|
||||
* Use as a drop-in replacement for `<MenuItem>` when you need to add cascading
|
||||
* menu elements as children to this component.
|
||||
*/
|
||||
const NestedMenuItem = React.forwardRef<
|
||||
HTMLLIElement | null,
|
||||
NestedMenuItemProps
|
||||
>(function NestedMenuItem(props, ref) {
|
||||
const {
|
||||
parentMenuOpen,
|
||||
component = 'div',
|
||||
label,
|
||||
rightIcon = <ArrowRight />,
|
||||
children,
|
||||
className,
|
||||
tabIndex: tabIndexProp,
|
||||
MenuProps = {},
|
||||
ContainerProps: ContainerPropsProp = {},
|
||||
...MenuItemProps
|
||||
} = props
|
||||
|
||||
const {ref: containerRefProp, ...ContainerProps} = ContainerPropsProp
|
||||
|
||||
const menuItemRef = useRef<HTMLLIElement>(null)
|
||||
useImperativeHandle(ref, () => menuItemRef.current)
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
useImperativeHandle(containerRefProp, () => containerRef.current)
|
||||
|
||||
const menuContainerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const [isSubMenuOpen, setIsSubMenuOpen] = useState(false)
|
||||
|
||||
const handleMouseEnter = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setIsSubMenuOpen(true)
|
||||
|
||||
if (ContainerProps?.onMouseEnter) {
|
||||
ContainerProps.onMouseEnter(event)
|
||||
}
|
||||
}
|
||||
const handleMouseLeave = (event: React.MouseEvent<HTMLElement>) => {
|
||||
setIsSubMenuOpen(false)
|
||||
|
||||
if (ContainerProps?.onMouseLeave) {
|
||||
ContainerProps.onMouseLeave(event)
|
||||
}
|
||||
}
|
||||
|
||||
// Check if any immediate children are active
|
||||
const isSubmenuFocused = () => {
|
||||
const active = containerRef.current?.ownerDocument?.activeElement
|
||||
for (const child of menuContainerRef.current?.children ?? []) {
|
||||
if (child === active) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const handleFocus = (event: React.FocusEvent<HTMLElement>) => {
|
||||
if (event.target === containerRef.current) {
|
||||
setIsSubMenuOpen(true)
|
||||
}
|
||||
|
||||
if (ContainerProps?.onFocus) {
|
||||
ContainerProps.onFocus(event)
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
||||
if (event.key === 'Escape') {
|
||||
return
|
||||
}
|
||||
|
||||
if (isSubmenuFocused()) {
|
||||
event.stopPropagation()
|
||||
}
|
||||
|
||||
const active = containerRef.current?.ownerDocument?.activeElement
|
||||
|
||||
if (event.key === 'ArrowLeft' && isSubmenuFocused()) {
|
||||
containerRef.current?.focus()
|
||||
}
|
||||
|
||||
if (
|
||||
event.key === 'ArrowRight' &&
|
||||
event.target === containerRef.current &&
|
||||
event.target === active
|
||||
) {
|
||||
const firstChild = menuContainerRef.current?.children[0] as
|
||||
| HTMLElement
|
||||
| undefined
|
||||
firstChild?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
const open = isSubMenuOpen && parentMenuOpen
|
||||
const menuItemClasses = useMenuItemStyles({open})
|
||||
|
||||
// Root element must have a `tabIndex` attribute for keyboard navigation
|
||||
let tabIndex
|
||||
if (!props.disabled) {
|
||||
tabIndex = tabIndexProp !== undefined ? tabIndexProp : -1
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
{...ContainerProps}
|
||||
ref={containerRef}
|
||||
onFocus={handleFocus}
|
||||
tabIndex={tabIndex}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
<MenuItem
|
||||
{...MenuItemProps}
|
||||
className={clsx(menuItemClasses.root, className)}
|
||||
ref={menuItemRef}
|
||||
>
|
||||
{label}
|
||||
{rightIcon}
|
||||
</MenuItem>
|
||||
<Menu
|
||||
// Set pointer events to 'none' to prevent the invisible Popover div
|
||||
// from capturing events for clicks and hovers
|
||||
style={{pointerEvents: 'none'}}
|
||||
anchorEl={menuItemRef.current}
|
||||
anchorOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'right'
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left'
|
||||
}}
|
||||
open={open}
|
||||
autoFocus={false}
|
||||
disableAutoFocus
|
||||
disableEnforceFocus
|
||||
onClose={() => {
|
||||
setIsSubMenuOpen(false)
|
||||
}}
|
||||
>
|
||||
<div ref={menuContainerRef} style={{pointerEvents: 'auto'}}>
|
||||
{children}
|
||||
</div>
|
||||
</Menu>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
export default NestedMenuItem
|
||||
@@ -49,6 +49,8 @@ const MenuProps = {
|
||||
scrollX: "auto",
|
||||
},
|
||||
},
|
||||
variant: "menu",
|
||||
getContentAnchorEl: null,
|
||||
};
|
||||
|
||||
const AuthenticationOauth2 = (props) => {
|
||||
@@ -84,6 +86,7 @@ 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 : [];
|
||||
|
||||
@@ -111,8 +114,19 @@ const AuthenticationOauth2 = (props) => {
|
||||
setButtonClicked(true);
|
||||
console.log("SCOPES: ", scopes);
|
||||
|
||||
client_id = client_id.trim()
|
||||
client_secret = client_secret.trim()
|
||||
oauth_url = oauth_url.trim()
|
||||
|
||||
var resources = "";
|
||||
if (scopes !== undefined && (scopes !== null) & (scopes.length > 0)) {
|
||||
if (offlineAccess === true && !scopes.includes("offline_access")) {
|
||||
if (authenticationType.redirect_uri.includes("microsoft")) {
|
||||
console.log("Appending offline access")
|
||||
scopes.push("offline_access")
|
||||
}
|
||||
}
|
||||
|
||||
resources = scopes.join(" ");
|
||||
//resources = scopes.join(",");
|
||||
}
|
||||
@@ -496,34 +510,49 @@ const AuthenticationOauth2 = (props) => {
|
||||
}}
|
||||
/>
|
||||
{allscopes.length === 0 ? null : (
|
||||
<span style={{marginTop: 10}}>
|
||||
Scopes
|
||||
<Select
|
||||
multiple
|
||||
value={selectedScopes}
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
color: "white",
|
||||
padding: 5,
|
||||
}}
|
||||
onChange={(e) => {
|
||||
handleScopeChange(e);
|
||||
}}
|
||||
fullWidth
|
||||
input={<Input id="select-multiple-native" />}
|
||||
renderValue={(selected) => selected.join(", ")}
|
||||
MenuProps={MenuProps}
|
||||
>
|
||||
{allscopes.map((data, index) => {
|
||||
return (
|
||||
<MenuItem key={index} value={data}>
|
||||
<Checkbox checked={selectedScopes.indexOf(data) > -1} />
|
||||
<ListItemText primary={data} />
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</span>
|
||||
<div style={{width: "100%", marginTop: 10, display: "flex"}}>
|
||||
<span>
|
||||
Scopes
|
||||
<Select
|
||||
multiple
|
||||
value={selectedScopes}
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
color: "white",
|
||||
padding: 5,
|
||||
minWidth: 300,
|
||||
maxWidth: 300,
|
||||
}}
|
||||
onChange={(e) => {
|
||||
handleScopeChange(e)
|
||||
}}
|
||||
fullWidth
|
||||
input={<Input id="select-multiple-native" />}
|
||||
renderValue={(selected) => selected.join(", ")}
|
||||
MenuProps={MenuProps}
|
||||
>
|
||||
{allscopes.map((data, index) => {
|
||||
return (
|
||||
<MenuItem key={index} value={data}>
|
||||
<Checkbox checked={selectedScopes.indexOf(data) > -1} />
|
||||
<ListItemText primary={data} />
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</span>
|
||||
<span>
|
||||
<Tooltip
|
||||
color="primary"
|
||||
title={"Automatic Refresh (default: true)"}
|
||||
placement="top"
|
||||
>
|
||||
<Checkbox style={{paddingTop: 20}} color="secondary" checked={offlineAccess} onClick={() => {
|
||||
setOfflineAccess(!offlineAccess)
|
||||
}}/>
|
||||
</Tooltip>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -97,6 +97,30 @@ const OrgHeader = (props) => {
|
||||
? ""
|
||||
: selectedOrganization.defaults.notification_workflow
|
||||
);
|
||||
const [openidClientId, setOpenidClientId] = React.useState(
|
||||
selectedOrganization.sso_config === undefined
|
||||
? ""
|
||||
: selectedOrganization.sso_config.client_id === undefined ||
|
||||
selectedOrganization.sso_config.client_id.length === 0
|
||||
? ""
|
||||
: selectedOrganization.sso_config.client_id
|
||||
);
|
||||
const [openidAuthorization, setOpenidAuthorization] = React.useState(
|
||||
selectedOrganization.sso_config === undefined
|
||||
? ""
|
||||
: selectedOrganization.sso_config.openid_authorization === undefined ||
|
||||
selectedOrganization.sso_config.openid_authorization.length === 0
|
||||
? ""
|
||||
: selectedOrganization.sso_config.openid_authorization
|
||||
);
|
||||
const [openidToken, setOpenidToken] = React.useState(
|
||||
selectedOrganization.sso_config === undefined
|
||||
? ""
|
||||
: selectedOrganization.sso_config.openid_token === undefined ||
|
||||
selectedOrganization.sso_config.openid_token.length === 0
|
||||
? ""
|
||||
: selectedOrganization.sso_config.openid_token
|
||||
)
|
||||
|
||||
const [file, setFile] = React.useState("");
|
||||
const [fileBase64, setFileBase64] = React.useState(
|
||||
@@ -145,6 +169,7 @@ const OrgHeader = (props) => {
|
||||
defaults,
|
||||
sso_config
|
||||
) => {
|
||||
|
||||
const data = {
|
||||
name: name,
|
||||
description: description,
|
||||
@@ -216,6 +241,9 @@ const OrgHeader = (props) => {
|
||||
{
|
||||
sso_entrypoint: ssoEntrypoint,
|
||||
sso_certificate: ssoCertificate,
|
||||
client_id: openidClientId,
|
||||
openid_authorization: openidAuthorization,
|
||||
openid_token: openidToken,
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -548,79 +576,200 @@ const OrgHeader = (props) => {
|
||||
</span>
|
||||
</Grid>
|
||||
)}
|
||||
<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>
|
||||
{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>
|
||||
}
|
||||
{/*
|
||||
<span style={{textAlign: "center"}}>
|
||||
{expanded ?
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import React, {useState, useEffect, useLayoutEffect} from 'react';
|
||||
|
||||
import Draggable from "react-draggable";
|
||||
import {
|
||||
Paper
|
||||
} from "@material-ui/core";
|
||||
|
||||
const PaperComponent = (props) => {
|
||||
return (
|
||||
<Draggable
|
||||
handle="#draggable-dialog-title"
|
||||
cancel={'[class*="MuiDialogContent-root"]'}
|
||||
>
|
||||
<Paper {...props} />
|
||||
</Draggable>
|
||||
)
|
||||
}
|
||||
|
||||
export default PaperComponent;
|
||||
@@ -1,11 +1,12 @@
|
||||
import React, { useState, useEffect, useLayoutEffect } from "react";
|
||||
import { makeStyles, createStyles } from "@material-ui/core/styles";
|
||||
|
||||
import { validateJson, GetIconInfo } from "../views/Workflows.jsx";
|
||||
import { GetParsedPaths } from "../views/Apps.jsx";
|
||||
import { GetIconInfo } from "../views/Workflows.jsx";
|
||||
import { sortByKey } from "../views/AngularWorkflow.jsx";
|
||||
import { useTheme } from "@material-ui/core/styles";
|
||||
import NestedMenuItem from "material-ui-nested-menu-item";
|
||||
import { useAlert } from "react-alert";
|
||||
import theme from '../theme';
|
||||
//import NestedMenuItem from "./NestedMenu.jsx";
|
||||
|
||||
@@ -168,6 +169,7 @@ const ParsedAction = (props) => {
|
||||
|
||||
//const theme = useTheme();
|
||||
const classes = useStyles();
|
||||
const alert = useAlert()
|
||||
|
||||
const [expansionModalOpen, setExpansionModalOpen] = React.useState(false);
|
||||
const [hideBody, setHideBody] = React.useState(true);
|
||||
@@ -178,23 +180,21 @@ const ParsedAction = (props) => {
|
||||
const [hiddenDescription, setHiddenDescription] = React.useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
//if (data.startsWith("${") && data.endsWith("}")) {
|
||||
//}
|
||||
// PARAM FIX - Gonna use the ID field, even though it's a hack
|
||||
const paramcheck = selectedAction.parameters.find(param => param.name === "body")
|
||||
console.log("LOADED! Change hideBody based on input? Action: ", selectedAction, paramcheck)
|
||||
if (paramcheck !== undefined && paramcheck !== null) {
|
||||
if (paramcheck.id === "TOGGLED"){
|
||||
setHideBody(false)
|
||||
setActivateHidingBodyButton(false)
|
||||
console.log("TOGGLED BODY!")
|
||||
} else {
|
||||
setHideBody(true)
|
||||
|
||||
if (paramcheck.id === "UNTOGGLED") {
|
||||
if (selectedAction.parameters !== null && selectedAction.parameters !== undefined) {
|
||||
const paramcheck = selectedAction.parameters.find(param => param.name === "body")
|
||||
//console.log("LOADED! Change hideBody based on input? Action: ", selectedAction, paramcheck)
|
||||
if (paramcheck !== undefined && paramcheck !== null) {
|
||||
if (paramcheck.id === "TOGGLED"){
|
||||
setHideBody(false)
|
||||
setActivateHidingBodyButton(false)
|
||||
console.log("UNTOGGLED!")
|
||||
console.log("TOGGLED BODY!")
|
||||
} else {
|
||||
setHideBody(true)
|
||||
|
||||
if (paramcheck.id === "UNTOGGLED") {
|
||||
setActivateHidingBodyButton(false)
|
||||
console.log("UNTOGGLED!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -304,6 +304,8 @@ const ParsedAction = (props) => {
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
||||
const defineStartnode = () => {
|
||||
if (cy === undefined) {
|
||||
return;
|
||||
@@ -391,14 +393,46 @@ const ParsedAction = (props) => {
|
||||
|
||||
if (actionlist.length === 0) {
|
||||
// FIXME: Have previous execution values in here
|
||||
actionlist.push({
|
||||
type: "Execution Argument",
|
||||
name: "Execution Argument",
|
||||
value: "$exec",
|
||||
highlight: "exec",
|
||||
autocomplete: "exec",
|
||||
example: "",
|
||||
});
|
||||
if (workflowExecutions.length > 0) {
|
||||
for (var key in workflowExecutions) {
|
||||
if (
|
||||
workflowExecutions[key].execution_argument === undefined ||
|
||||
workflowExecutions[key].execution_argument === null ||
|
||||
workflowExecutions[key].execution_argument.length === 0
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
console.log("EXEC: ", workflowExecutions[key].execution_argument)
|
||||
|
||||
const valid = validateJson(workflowExecutions[key].execution_argument)
|
||||
console.log("VALID: ", valid)
|
||||
if (valid.valid) {
|
||||
actionlist.push({
|
||||
type: "Execution Argument",
|
||||
name: "Execution Argument",
|
||||
value: "$exec",
|
||||
highlight: "exec",
|
||||
autocomplete: "exec",
|
||||
example: valid.result,
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (actionlist.length === 0) {
|
||||
actionlist.push({
|
||||
type: "Execution Argument",
|
||||
name: "Execution Argument",
|
||||
value: "$exec",
|
||||
highlight: "exec",
|
||||
autocomplete: "exec",
|
||||
example: "",
|
||||
})
|
||||
}
|
||||
|
||||
actionlist.push({
|
||||
type: "Shuffle DB",
|
||||
name: "Shuffle DB",
|
||||
@@ -454,7 +488,8 @@ const ParsedAction = (props) => {
|
||||
continue;
|
||||
}
|
||||
|
||||
var exampledata = item.example === undefined ? "" : item.example;
|
||||
var exampledata = item.example === undefined || item.example === null ? "" : item.example;
|
||||
console.log("EXAMPLE: ", exampledata)
|
||||
// Find previous execution and their variables
|
||||
//exampledata === "" &&
|
||||
if (workflowExecutions.length > 0) {
|
||||
@@ -471,55 +506,22 @@ const ParsedAction = (props) => {
|
||||
var foundResult = workflowExecutions[key].results.find(
|
||||
(result) => result.action.id === item.id
|
||||
);
|
||||
if (foundResult === undefined) {
|
||||
if (foundResult === undefined || foundResult === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foundResult.result = foundResult.result.trim();
|
||||
foundResult.result = foundResult.result
|
||||
.split(" None")
|
||||
.join(' "None"');
|
||||
foundResult.result = foundResult.result
|
||||
.split(" False")
|
||||
.join(" false");
|
||||
foundResult.result = foundResult.result
|
||||
.split(" True")
|
||||
.join(" true");
|
||||
if (foundResult.result !== undefined && foundResult.result !== null) {
|
||||
foundResult = foundResult.result
|
||||
}
|
||||
console.log("VALID RESULT: ", foundResult)
|
||||
|
||||
var jsonvalid = true;
|
||||
try {
|
||||
const tmp = String(JSON.parse(foundResult.result));
|
||||
if (
|
||||
!foundResult.result.includes("{") &&
|
||||
!foundResult.result.includes("[")
|
||||
) {
|
||||
jsonvalid = false;
|
||||
}
|
||||
} catch (e) {
|
||||
try {
|
||||
foundResult.result = foundResult.result
|
||||
.split("'")
|
||||
.join('"');
|
||||
const tmp = String(JSON.parse(foundResult.result));
|
||||
if (
|
||||
!foundResult.result.includes("{") &&
|
||||
!foundResult.result.includes("[")
|
||||
) {
|
||||
jsonvalid = false;
|
||||
}
|
||||
} catch (e) {
|
||||
jsonvalid = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Finds the FIRST json only
|
||||
if (jsonvalid) {
|
||||
exampledata = JSON.parse(foundResult.result);
|
||||
const valid = validateJson(foundResult)
|
||||
if (valid.valid) {
|
||||
exampledata = valid.result;
|
||||
break;
|
||||
}
|
||||
//else {
|
||||
// console.log("Invalid JSON: ", foundResult.result)
|
||||
//}
|
||||
} else {
|
||||
exampledata = foundResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -528,6 +530,7 @@ const ParsedAction = (props) => {
|
||||
item.label === null || item.label === undefined
|
||||
? ""
|
||||
: item.label.split(" ").join("_");
|
||||
|
||||
const actionvalue = {
|
||||
type: "action",
|
||||
id: item.id,
|
||||
@@ -539,11 +542,65 @@ const ParsedAction = (props) => {
|
||||
}
|
||||
}
|
||||
|
||||
//console.log("ACTIONLIST: ", actionlist)
|
||||
setActionlist(actionlist);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
const calculateHelpertext = (input_data) => {
|
||||
var helperText = ""
|
||||
var looperText = ""
|
||||
const found = input_data.match(/[$]{1}([a-zA-Z0-9_-]+\.?){1}([a-zA-Z0-9#_-]+\.?){0,}/g)
|
||||
|
||||
if (found !== null) {
|
||||
try {
|
||||
// When the found array is empty.
|
||||
for (var i = 0; i < found.length; i++) {
|
||||
const variableSplit = found[i].split(".#")
|
||||
if ((variableSplit.length-1) > 1) {
|
||||
//console.log("Larger than 1: ", variableSplit)
|
||||
if (looperText.length === 0) {
|
||||
looperText += "PS: Double looping (.#) may cause problems."
|
||||
}
|
||||
}
|
||||
|
||||
var foundSlice = false
|
||||
for (var j = 0; j < actionlist.length; j++) {
|
||||
//console.log("ACTION: ", found[i], actionlist[j])
|
||||
//console.log("ACTION :", found[i].split(".")[0].slice(1,).toLowerCase(), actionlist[j].autocomplete.toLowerCase())
|
||||
if(found[i].split(".")[0].slice(1,).toLowerCase() == actionlist[j].autocomplete.toLowerCase()){
|
||||
//console.log("Found: ", found[i])
|
||||
// Validate path?
|
||||
|
||||
foundSlice = true
|
||||
}
|
||||
}
|
||||
|
||||
if (!foundSlice) {
|
||||
if (!helperText.includes("Invalid variables")) {
|
||||
helperText+= "Invalid variables: "
|
||||
}
|
||||
helperText+= found[i] + ", "
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.log("Parsing error: ", e)
|
||||
}
|
||||
}
|
||||
|
||||
if (looperText.length > 0) {
|
||||
if (helperText.length > 0) {
|
||||
helperText += ". "
|
||||
}
|
||||
|
||||
helperText += looperText
|
||||
}
|
||||
|
||||
return helperText
|
||||
}
|
||||
|
||||
const changeActionParameter = (event, count, data) => {
|
||||
//console.log("Action change: ", selectedAction, data)
|
||||
if (data.name.startsWith("${") && data.name.endsWith("}")) {
|
||||
@@ -1282,21 +1339,27 @@ const ParsedAction = (props) => {
|
||||
|
||||
const clickedFieldId = "rightside_field_" + count;
|
||||
|
||||
const shufflecode = <ShuffleCodeEditor
|
||||
fieldCount = {fieldCount}
|
||||
setFieldCount = {setFieldCount}
|
||||
actionlist = {actionlist}
|
||||
changeActionParameterCodeMirror = {changeActionParameterCodeMirror}
|
||||
codedata={codedata}
|
||||
setcodedata={setcodedata}
|
||||
expansionModalOpen={expansionModalOpen}
|
||||
setExpansionModalOpen={setExpansionModalOpen}
|
||||
/>
|
||||
const shufflecode = fieldCount !== count ? null :
|
||||
(
|
||||
<ShuffleCodeEditor
|
||||
fieldCount = {fieldCount}
|
||||
setFieldCount = {setFieldCount}
|
||||
actionlist = {actionlist}
|
||||
changeActionParameterCodeMirror = {changeActionParameterCodeMirror}
|
||||
codedata={codedata}
|
||||
setcodedata={setcodedata}
|
||||
expansionModalOpen={expansionModalOpen}
|
||||
setExpansionModalOpen={setExpansionModalOpen}
|
||||
/>
|
||||
)
|
||||
|
||||
//<TextareaAutosize
|
||||
// <CodeMirror
|
||||
//fullWidth
|
||||
|
||||
var baseHelperText = ""
|
||||
if (data !== undefined && data !== null && data.value !== undefined && data.value !== null && data.value.length > 0) {
|
||||
baseHelperText = calculateHelpertext(data.value)
|
||||
}
|
||||
|
||||
var datafield = (
|
||||
<TextField
|
||||
@@ -1405,7 +1468,7 @@ const ParsedAction = (props) => {
|
||||
//changeActionParameterCodemirror(event, count, data)
|
||||
changeActionParameter(event, count, data);
|
||||
}}
|
||||
helperText={
|
||||
helperText={baseHelperText.length > 0 ? baseHelperText :
|
||||
selectedApp.generated &&
|
||||
selectedApp.activated &&
|
||||
data.name === "body" ? (
|
||||
@@ -1423,15 +1486,7 @@ const ParsedAction = (props) => {
|
||||
) : null
|
||||
}
|
||||
onBlur={(event) => {
|
||||
// Super basic check
|
||||
//if (event.target.value.startsWith("{")) {
|
||||
// console.log("VALIDATING JSON")
|
||||
// try {
|
||||
// JSON.parse(event.target.value)
|
||||
// } catch (e) {
|
||||
// alert.error("Failed to parse json: ", e)
|
||||
// }
|
||||
//}
|
||||
baseHelperText = calculateHelpertext(event.target.value)
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -1536,6 +1591,9 @@ const ParsedAction = (props) => {
|
||||
|
||||
datafield = (
|
||||
<Select
|
||||
MenuProps={{
|
||||
disableScrollLock: true,
|
||||
}}
|
||||
SelectDisplayProps={{
|
||||
style: {
|
||||
marginLeft: 10,
|
||||
@@ -2036,6 +2094,9 @@ const ParsedAction = (props) => {
|
||||
Autocomplete
|
||||
</InputLabel>
|
||||
<Select
|
||||
MenuProps={{
|
||||
disableScrollLock: true,
|
||||
}}
|
||||
labelId="action-autocompleter"
|
||||
SelectDisplayProps={{
|
||||
style: {
|
||||
@@ -2333,11 +2394,17 @@ const ParsedAction = (props) => {
|
||||
selectedApp.versions !== undefined &&
|
||||
selectedApp.versions.length > 1 ? (
|
||||
<Select
|
||||
MenuProps={{
|
||||
disableScrollLock: true,
|
||||
}}
|
||||
defaultValue={selectedAction.app_version}
|
||||
onChange={(event) => {
|
||||
console.log("VAL: ", event.target.value)
|
||||
console.log("App: ", selectedApp)
|
||||
const newversion = selectedApp.versions.find(
|
||||
(tmpApp) => tmpApp.version == event.target.value
|
||||
);
|
||||
|
||||
console.log("NEWVERSION: ", newversion);
|
||||
if (newversion !== undefined && newversion !== null) {
|
||||
getApp(newversion.id, true);
|
||||
@@ -2408,6 +2475,7 @@ const ParsedAction = (props) => {
|
||||
if (param.value.includes(baselabel)) {
|
||||
//if (param.value.toLowerCase().includes(baselabel)) {
|
||||
console.log("FOUND: ", param);
|
||||
|
||||
workflow.actions[key].parameters[subkey].value.replaceAll(
|
||||
baselabel,
|
||||
e.target.value
|
||||
@@ -2497,6 +2565,9 @@ const ParsedAction = (props) => {
|
||||
<Typography>Authentication</Typography>
|
||||
<div style={{ display: "flex" }}>
|
||||
<Select
|
||||
MenuProps={{
|
||||
disableScrollLock: true,
|
||||
}}
|
||||
labelId="select-app-auth"
|
||||
value={
|
||||
Object.getOwnPropertyNames(
|
||||
@@ -2508,6 +2579,7 @@ const ParsedAction = (props) => {
|
||||
SelectDisplayProps={{
|
||||
style: {
|
||||
marginLeft: 10,
|
||||
maxWidth: 250,
|
||||
},
|
||||
}}
|
||||
fullWidth
|
||||
@@ -2581,6 +2653,7 @@ const ParsedAction = (props) => {
|
||||
>
|
||||
<IconButton
|
||||
color="primary"
|
||||
variant="outlined"
|
||||
style={{}}
|
||||
onClick={() => {
|
||||
setAuthenticationModalOpen(true);
|
||||
@@ -2597,6 +2670,9 @@ const ParsedAction = (props) => {
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
<Typography>Environment</Typography>
|
||||
<Select
|
||||
MenuProps={{
|
||||
disableScrollLock: true,
|
||||
}}
|
||||
value={
|
||||
selectedActionEnvironment === undefined ||
|
||||
selectedActionEnvironment.Name === undefined
|
||||
@@ -2651,6 +2727,9 @@ const ParsedAction = (props) => {
|
||||
<div style={{ marginTop: "20px" }}>
|
||||
<Typography>Set execution variable (optional)</Typography>
|
||||
<Select
|
||||
MenuProps={{
|
||||
disableScrollLock: true,
|
||||
}}
|
||||
value={
|
||||
selectedAction.execution_variable !== undefined
|
||||
? selectedAction.execution_variable.name
|
||||
@@ -2843,6 +2922,9 @@ const ParsedAction = (props) => {
|
||||
|
||||
{/*setNewSelectedAction !== undefined ?
|
||||
<Select
|
||||
MenuProps={{
|
||||
disableScrollLock: true,
|
||||
}}
|
||||
value={selectedAction.name}
|
||||
fullWidth
|
||||
onChange={setNewSelectedAction}
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
import { useTheme } from '@material-ui/core/styles';
|
||||
import { validateJson } from "../views/Workflows.jsx";
|
||||
import ReactJson from "react-json-view";
|
||||
import PaperComponent from "../components/PaperComponent.jsx"
|
||||
|
||||
import CodeMirror from '@uiw/react-codemirror';
|
||||
import 'codemirror/keymap/sublime';
|
||||
@@ -89,16 +90,24 @@ const CodeEditor = (props) => {
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
disableEnforceFocus={true}
|
||||
hideBackdrop={true}
|
||||
disableBackdropClick={true}
|
||||
open={expansionModalOpen}
|
||||
onClose={() => {
|
||||
//setExpansionModalOpen(false)
|
||||
console.log("In closer")
|
||||
changeActionParameterCodeMirror({target: {value: ""}}, fieldCount, localcodedata)
|
||||
}}
|
||||
PaperComponent={PaperComponent}
|
||||
aria-labelledby="draggable-dialog-title"
|
||||
PaperProps={{
|
||||
style: {
|
||||
backgroundColor: theme.palette.surfaceColor,
|
||||
color: "white",
|
||||
minWidth: 600,
|
||||
padding: 25,
|
||||
border: theme.palette.defaultBorder,
|
||||
zIndex: 10012,
|
||||
},
|
||||
}}
|
||||
>
|
||||
@@ -109,7 +118,9 @@ const CodeEditor = (props) => {
|
||||
>
|
||||
<div style={{display: "flex"}}>
|
||||
<DialogTitle
|
||||
id="draggable-dialog-title"
|
||||
style={{
|
||||
cursor: "move",
|
||||
paddingBottom:20,
|
||||
paddingLeft: 10,
|
||||
}}
|
||||
|
||||
@@ -0,0 +1,287 @@
|
||||
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)
|
||||
|
||||
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={"/workflows/" + data.objectID}
|
||||
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
|
||||
@@ -16,6 +16,7 @@ const theme = createMuiTheme({
|
||||
surfaceColor: "#27292d",
|
||||
inputColor: "#383B40",
|
||||
borderRadius: 5,
|
||||
defaultBorder: "1px solid rgba(255,255,255,0.3)",
|
||||
jsonTheme: "brewer",
|
||||
reactJsonStyle: {
|
||||
borderRadius: 5,
|
||||
|
||||
@@ -1154,8 +1154,8 @@ const Admin = (props) => {
|
||||
})
|
||||
.then((respdata) => {
|
||||
if (respdata.length === 0) {
|
||||
alert.error("Failed getting file");
|
||||
return;
|
||||
alert.error("Failed getting file. Is it deleted?");
|
||||
return;
|
||||
}
|
||||
|
||||
var blob = new Blob([respdata], {
|
||||
@@ -3101,12 +3101,12 @@ const Admin = (props) => {
|
||||
{fileNamespaces !== undefined &&
|
||||
fileNamespaces !== null &&
|
||||
fileNamespaces.length > 1 ? (
|
||||
<FormControl>
|
||||
<InputLabel id="input-namespace-label">Namespace</InputLabel>
|
||||
<FormControl style={{minWidth: 150, maxWidth: 150,}}>
|
||||
<InputLabel id="input-namespace-label">File Category</InputLabel>
|
||||
<Select
|
||||
labelId="input-namespace-select-label"
|
||||
id="input-namespace-select-id"
|
||||
style={{ color: "white", minWidth: 100, float: "right" }}
|
||||
style={{ color: "white", minWidth: 150, maxWidth: 150, float: "right" }}
|
||||
value={selectedNamespace}
|
||||
onChange={(event) => {
|
||||
console.log("CHANGE NAMESPACE: ", event.target);
|
||||
@@ -3185,7 +3185,7 @@ const Admin = (props) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<ListItem key={index} style={{ backgroundColor: bgColor }}>
|
||||
<ListItem key={index} style={{ backgroundColor: bgColor, maxHeight: 100, overflow: "hidden",}}>
|
||||
<ListItemText
|
||||
style={{
|
||||
maxWidth: 225,
|
||||
@@ -3637,8 +3637,8 @@ const Admin = (props) => {
|
||||
style={{ minWidth: 125, maxWidth: 125, overflow: "hidden" }}
|
||||
/>
|
||||
<ListItemText
|
||||
primary="Last Edited"
|
||||
style={{ minWidth: 225, maxWidth: 225, overflow: "hidden" }}
|
||||
primary="Created"
|
||||
style={{ minWidth: 230, maxWidth: 230, overflow: "hidden" }}
|
||||
/>
|
||||
<ListItemText primary="Actions" />
|
||||
</ListItem>
|
||||
@@ -3650,7 +3650,26 @@ const Admin = (props) => {
|
||||
bgColor = "#1f2023";
|
||||
}
|
||||
|
||||
console.log("Auth data: ", data)
|
||||
//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!",
|
||||
}]
|
||||
}
|
||||
|
||||
return (
|
||||
<ListItem key={index} style={{ backgroundColor: bgColor }}>
|
||||
@@ -3720,11 +3739,11 @@ const Admin = (props) => {
|
||||
/>
|
||||
<ListItemText
|
||||
style={{
|
||||
maxWidth: 225,
|
||||
minWidth: 225,
|
||||
maxWidth: 230,
|
||||
minWidth: 230,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
primary={new Date(data.edited * 1000).toISOString()}
|
||||
primary={new Date(data.created * 1000).toISOString()}
|
||||
/>
|
||||
<ListItemText>
|
||||
<IconButton
|
||||
|
||||
+1826
-1274
File diff suppressed because it is too large
Load Diff
@@ -275,7 +275,6 @@ const AppCreator = (defaultprops) => {
|
||||
const [urlPathQueries, setUrlPathQueries] = useState([]);
|
||||
const [update, setUpdate] = useState("");
|
||||
const [urlPathParameters] = useState([]);
|
||||
const [firstrequest, setFirstrequest] = React.useState(true);
|
||||
const [basedata, setBasedata] = React.useState({});
|
||||
const [actions, setActions] = useState([]);
|
||||
const [filteredActions, setFilteredActions] = useState([]);
|
||||
@@ -343,16 +342,13 @@ const AppCreator = (defaultprops) => {
|
||||
window.location.host === "shuffler.io";
|
||||
|
||||
useEffect(() => {
|
||||
if (firstrequest) {
|
||||
setFirstrequest(false);
|
||||
if (window.location.pathname.includes("apps/edit")) {
|
||||
setIsEditing(true);
|
||||
handleEditApp();
|
||||
} else {
|
||||
checkQuery();
|
||||
}
|
||||
}
|
||||
});
|
||||
if (window.location.pathname.includes("apps/edit")) {
|
||||
setIsEditing(true);
|
||||
handleEditApp();
|
||||
} else {
|
||||
checkQuery();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleEditApp = () => {
|
||||
fetch(globalUrl + "/api/v1/apps/" + props.match.params.appid + "/config", {
|
||||
@@ -484,10 +480,29 @@ const AppCreator = (defaultprops) => {
|
||||
// Sets the data up as it should be at later points
|
||||
// This is the data FROM the database, not what's being saved
|
||||
const parseIncomingOpenapiData = (data) => {
|
||||
|
||||
console.log("Data: ", data)
|
||||
var parsedDecoded = ""
|
||||
try {
|
||||
const decoded = base64_decode(data.openapi)
|
||||
parsedDecoded = decoded
|
||||
} catch (e) {
|
||||
console.log("Failed JSON parsing: ", e)
|
||||
parsedDecoded = data
|
||||
}
|
||||
|
||||
if (data.openapi === null) {
|
||||
alert.info("Failed to load OpenAPI for app. Please contact support if this persists.")
|
||||
setIsAppLoaded(true);
|
||||
return
|
||||
}
|
||||
|
||||
console.log("Decoded: ", parsedDecoded)
|
||||
const parsedapp =
|
||||
data.openapi === undefined
|
||||
data.openapi === undefined || data.openapi === null
|
||||
? data
|
||||
: JSON.parse(base64_decode(data.openapi));
|
||||
: JSON.parse(parsedDecoded);
|
||||
|
||||
data = parsedapp.body === undefined ? parsedapp : parsedapp.body;
|
||||
|
||||
var jsonvalid = false;
|
||||
@@ -618,6 +633,7 @@ const AppCreator = (defaultprops) => {
|
||||
continue;
|
||||
}
|
||||
|
||||
//console.log("METHOD: ", methodvalue)
|
||||
var tmpname = methodvalue.summary;
|
||||
if (
|
||||
methodvalue.operationId !== undefined &&
|
||||
@@ -628,7 +644,13 @@ const AppCreator = (defaultprops) => {
|
||||
tmpname = methodvalue.operationId;
|
||||
}
|
||||
|
||||
tmpname = tmpname.replaceAll(".", " ");
|
||||
if (tmpname !== undefined && tmpname !== null) {
|
||||
tmpname = tmpname.replaceAll(".", " ");
|
||||
}
|
||||
|
||||
if ((tmpname === undefined || tmpname === null) && methodvalue.description !== undefined && methodvalue.description !== null && methodvalue.description.length > 0) {
|
||||
tmpname = methodvalue.description.replaceAll(".", " ").replaceAll("_", " ")
|
||||
}
|
||||
|
||||
var newaction = {
|
||||
name: tmpname,
|
||||
@@ -739,9 +761,9 @@ const AppCreator = (defaultprops) => {
|
||||
var newbody = {};
|
||||
// Can handle default, required, description and type
|
||||
for (var propkey in retRef.properties) {
|
||||
const parsedkey = propkey
|
||||
.replaceAll(" ", "_")
|
||||
.toLowerCase();
|
||||
console.log("replace: ", propkey)
|
||||
|
||||
const parsedkey = propkey.replaceAll(" ", "_").toLowerCase();
|
||||
newbody[parsedkey] = "${" + parsedkey + "}";
|
||||
}
|
||||
|
||||
@@ -885,9 +907,8 @@ const AppCreator = (defaultprops) => {
|
||||
) {
|
||||
var newbody = {};
|
||||
for (var propkey in parameter.properties) {
|
||||
const parsedkey = propkey
|
||||
.replaceAll(" ", "_")
|
||||
.toLowerCase();
|
||||
console.log("propkey2: ", propkey)
|
||||
const parsedkey = propkey.replaceAll(" ", "_").toLowerCase();
|
||||
if (parameter.properties[propkey].type === undefined) {
|
||||
console.log(
|
||||
"Skipping (4): ",
|
||||
@@ -1022,9 +1043,8 @@ const AppCreator = (defaultprops) => {
|
||||
) {
|
||||
var newbody = {};
|
||||
for (var propkey in parameter.properties) {
|
||||
const parsedkey = propkey
|
||||
.replaceAll(" ", "_")
|
||||
.toLowerCase();
|
||||
console.log("propkey3: ", propkey)
|
||||
const parsedkey = propkey.replaceAll(" ", "_").toLowerCase();
|
||||
if (
|
||||
parameter.properties[propkey].type === undefined
|
||||
) {
|
||||
@@ -1112,9 +1132,8 @@ const AppCreator = (defaultprops) => {
|
||||
) {
|
||||
var newbody = {};
|
||||
for (var propkey in parameter.properties) {
|
||||
const parsedkey = propkey
|
||||
.replaceAll(" ", "_")
|
||||
.toLowerCase();
|
||||
console.log("propkey4: ", propkey)
|
||||
const parsedkey = propkey.replaceAll(" ", "_").toLowerCase();
|
||||
if (
|
||||
parameter.properties[propkey].type ===
|
||||
undefined
|
||||
@@ -1197,6 +1216,7 @@ const AppCreator = (defaultprops) => {
|
||||
) {
|
||||
var newbody = {};
|
||||
for (var propkey in parameter.properties) {
|
||||
console.log("propkey5: ", propkey)
|
||||
const parsedkey = propkey
|
||||
.replaceAll(" ", "_")
|
||||
.toLowerCase();
|
||||
@@ -1606,6 +1626,15 @@ const AppCreator = (defaultprops) => {
|
||||
id: props.match.params.appid,
|
||||
};
|
||||
|
||||
if (isEditing === false) {
|
||||
var urlParams = new URLSearchParams(window.location.search);
|
||||
if (urlParams !== undefined && urlParams !== null && urlParams.has("id")) {
|
||||
data.id = urlParams.get("id")
|
||||
}
|
||||
|
||||
//id: props.match.params.appid,
|
||||
}
|
||||
|
||||
if (basedata.info !== undefined && basedata.info.contact !== undefined) {
|
||||
data.info["contact"] = basedata.info.contact;
|
||||
} else if (contact === "") {
|
||||
@@ -2068,6 +2097,7 @@ const AppCreator = (defaultprops) => {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log("Paramname: ", parameterName)
|
||||
var newparamName = parameterName.replaceAll('"', "");
|
||||
newparamName = newparamName.replaceAll("'", "");
|
||||
|
||||
@@ -2077,6 +2107,9 @@ const AppCreator = (defaultprops) => {
|
||||
name: newparamName,
|
||||
description: refreshUrl,
|
||||
}
|
||||
|
||||
console.log("Full auth component: ", data.components.securitySchemes["ApiKeyAuth"])
|
||||
|
||||
} else if (authenticationOption === "Bearer auth") {
|
||||
data.components.securitySchemes["BearerAuth"] = {
|
||||
type: "http",
|
||||
@@ -2098,6 +2131,7 @@ const AppCreator = (defaultprops) => {
|
||||
scheme: "basic",
|
||||
};
|
||||
} else if (authenticationOption === "Oauth2") {
|
||||
console.log("oauth2: ", parameterName)
|
||||
var newparamName = parameterName.replaceAll('"', "");
|
||||
newparamName = newparamName.replaceAll("'", "");
|
||||
|
||||
@@ -5029,6 +5063,8 @@ const AppCreator = (defaultprops) => {
|
||||
marginTop: "5px",
|
||||
marginRight: "15px",
|
||||
backgroundColor: inputColor,
|
||||
maxHeight: 250,
|
||||
overflow: "auto",
|
||||
}}
|
||||
fullWidth={true}
|
||||
type="name"
|
||||
@@ -5228,7 +5264,7 @@ const AppCreator = (defaultprops) => {
|
||||
);
|
||||
|
||||
const loadedCheck =
|
||||
isLoaded && isAppLoaded && !firstrequest ? (
|
||||
isLoaded && isAppLoaded ? (
|
||||
<div>
|
||||
<div style={bodyDivStyle}>{landingpageDataBrowser}</div>
|
||||
{newActionModal}
|
||||
|
||||
+28
-15
@@ -225,7 +225,11 @@ const Apps = (props) => {
|
||||
document.title = "Shuffle - Apps";
|
||||
|
||||
if (!isLoggedIn && isLoaded) {
|
||||
navigate("/login")
|
||||
if (isCloud) {
|
||||
navigate("/search?tab=apps")
|
||||
} else {
|
||||
navigate("/login")
|
||||
}
|
||||
}
|
||||
|
||||
setFirstrequest(false);
|
||||
@@ -287,9 +291,9 @@ const Apps = (props) => {
|
||||
if (response.status !== 200) {
|
||||
console.log("Status not 200 for apps :O!");
|
||||
|
||||
if (isCloud) {
|
||||
window.location.pathname = "/search";
|
||||
}
|
||||
//if (isCloud) {
|
||||
// window.location.pathname = "/search";
|
||||
//}
|
||||
}
|
||||
|
||||
return response.json();
|
||||
@@ -515,7 +519,11 @@ const Apps = (props) => {
|
||||
valid = "false";
|
||||
}
|
||||
|
||||
if (data.actions === null || data.actions.length === 0) {
|
||||
if (data.actions === undefined || data.actions === null) {
|
||||
data.actions = []
|
||||
}
|
||||
|
||||
if (data === undefined || data.actions === undefined || data.actions === null || data.actions.length === 0) {
|
||||
valid = "false";
|
||||
}
|
||||
|
||||
@@ -717,6 +725,8 @@ 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 =
|
||||
selectedApp.activated &&
|
||||
selectedApp.private_id !== undefined &&
|
||||
@@ -737,10 +747,10 @@ const Apps = (props) => {
|
||||
) : null;
|
||||
|
||||
//var editNewButton = editButton === null ?
|
||||
var editNewButton = null
|
||||
/*
|
||||
<Link to={editUrl} style={{ textDecoration: "none" }}>
|
||||
<Tooltip title={"Add your version"}>
|
||||
console.log("User, & genrrated, activate: ", props.userdata, selectedApp.generated, selectedApp.activated)
|
||||
var editNewButton = selectedApp.generated && selectedApp.activated && props.userdata.id !== selectedApp.owner ?
|
||||
<Link to={activateUrl} style={{ textDecoration: "none" }}>
|
||||
<Tooltip title={"Edit this public app to your liking"}>
|
||||
<Button
|
||||
variant="contained"
|
||||
component="label"
|
||||
@@ -751,9 +761,9 @@ const Apps = (props) => {
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Link>
|
||||
*/
|
||||
: null
|
||||
|
||||
const activateButton =
|
||||
const activateButton =
|
||||
selectedApp.generated && !selectedApp.activated ? (
|
||||
<div>
|
||||
<Link to={activateUrl} style={{ textDecoration: "none" }}>
|
||||
@@ -993,15 +1003,14 @@ const Apps = (props) => {
|
||||
) : null}
|
||||
|
||||
{activateButton}
|
||||
{editNewButton}
|
||||
{(props.userdata !== undefined &&
|
||||
(props.userdata.role === "admin" ||
|
||||
props.userdata.id === selectedApp.owner ||
|
||||
selectedApp.owner === ""
|
||||
)) ||
|
||||
!selectedApp.generated ? (
|
||||
)) || !selectedApp.generated ? (
|
||||
<div>
|
||||
{editButton}
|
||||
{editNewButton}
|
||||
{downloadButton}
|
||||
{deleteButton}
|
||||
</div>
|
||||
@@ -1817,7 +1826,11 @@ const Apps = (props) => {
|
||||
if (responseJson.success) {
|
||||
alert.success("Successfully updated app configuration");
|
||||
} else {
|
||||
alert.error("Error updating app configuration");
|
||||
if (responseJson.reason !== undefined && responseJson.reason !== null) {
|
||||
alert.error("Error: "+responseJson.reason);
|
||||
} else {
|
||||
alert.error("Error updating app configuration");
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
|
||||
+524
-185
@@ -1,46 +1,327 @@
|
||||
import React, { useState } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { useInterval } from "react-powerhooks";
|
||||
// nodejs library that concatenates classes
|
||||
import classNames from "classnames";
|
||||
import theme from '../theme';
|
||||
|
||||
// react plugin used to create charts
|
||||
import { Line, Bar } from "react-chartjs-2";
|
||||
//import { Line, Bar } from "react-chartjs-2";
|
||||
import { useAlert } from "react-alert";
|
||||
|
||||
// https://demos.creative-tim.com/black-dashboard-react/?ref=appseed#/admin/dashboard
|
||||
|
||||
// reactstrap components
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Card,
|
||||
CardHeader,
|
||||
CardBody,
|
||||
CardTitle,
|
||||
DropdownToggle,
|
||||
DropdownMenu,
|
||||
DropdownItem,
|
||||
UncontrolledDropdown,
|
||||
Label,
|
||||
FormGroup,
|
||||
Input,
|
||||
Table,
|
||||
Row,
|
||||
Col,
|
||||
UncontrolledTooltip,
|
||||
} from "reactstrap";
|
||||
Tooltip,
|
||||
IconButton,
|
||||
Typography,
|
||||
Grid,
|
||||
Paper,
|
||||
Chip,
|
||||
} from "@material-ui/core";
|
||||
|
||||
import {
|
||||
Close as CloseIcon,
|
||||
DoneAll as DoneAllIcon,
|
||||
} from "@material-ui/icons";
|
||||
|
||||
import WorkflowPaper from "../components/WorkflowPaper.jsx"
|
||||
|
||||
// core components
|
||||
import {
|
||||
chartExample1,
|
||||
chartExample2,
|
||||
chartExample3,
|
||||
chartExample4,
|
||||
} from "../charts.js";
|
||||
//import {
|
||||
// chartExample1,
|
||||
// chartExample2,
|
||||
// chartExample3,
|
||||
// chartExample4,
|
||||
//} from "../charts.js";
|
||||
|
||||
import {
|
||||
RadialBarChart,
|
||||
RadialAreaChart,
|
||||
RadialAxis,
|
||||
StackedBarSeries,
|
||||
TooltipArea,
|
||||
ChartTooltip,
|
||||
TooltipTemplate,
|
||||
RadialAreaSeries,
|
||||
RadialPointSeries,
|
||||
RadialArea,
|
||||
RadialLine,
|
||||
TreeMap,
|
||||
TreeMapSeries,
|
||||
TreeMapLabel,
|
||||
TreeMapRect,
|
||||
} from 'reaviz';
|
||||
|
||||
const UsecaseListComponent = ({keys, isCloud}) => {
|
||||
const [expandedIndex, setExpandedIndex] = useState(-1);
|
||||
const [expandedItem, setExpandedItem] = useState(-1);
|
||||
if (keys === undefined || keys === null || keys.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{marginTop: 25, minHeight: 1000,}}>
|
||||
<Typography variant="h1">
|
||||
Shuffle usecases
|
||||
</Typography>
|
||||
<Typography variant="body1">
|
||||
Usecases in Shuffle are divided into {keys.length} type{keys.length === 1 ? "" : "s"}.
|
||||
</Typography>
|
||||
{keys.map((usecase, index) => {
|
||||
return (
|
||||
<div key={index} style={{marginTop: index === 0 ? 50 : 100}}>
|
||||
<Typography variant="h6">
|
||||
{usecase.name}
|
||||
</Typography>
|
||||
<Grid container spacing={3} style={{marginTop: 25}}>
|
||||
{usecase.list.map((subcase, subindex) => {
|
||||
if (subcase.matches === undefined || subcase.matches === null) {
|
||||
subcase.matches = []
|
||||
}
|
||||
|
||||
const selectedItem = subindex === expandedItem && index === expandedIndex
|
||||
const finished = subcase.matches.length > 0
|
||||
//const backgroundColor = selectedItem ? "inherit" : finished ? "inherit" : usecase.color
|
||||
const backgroundColor = "inherit"
|
||||
const itemBorder = `${selectedItem ? "3px" : expandedItem >= 0 ? "0px" : "1px"} solid ${usecase.color}`
|
||||
|
||||
return (
|
||||
<Grid item xs={selectedItem ? 12 : 4} key={subindex} style={{minHeight: 110,}} onClick={() => {
|
||||
if (selectedItem) {
|
||||
} else {
|
||||
setExpandedIndex(index)
|
||||
setExpandedItem(subindex)
|
||||
}
|
||||
}}>
|
||||
<Paper style={{padding: "30px 30px 30px 30px", minHeight: 75, cursor: !selectedItem ? "pointer" : "default", border: itemBorder, backgroundColor: backgroundColor,}} onClick={() => {
|
||||
console.log("Clicked: ", subcase)
|
||||
}}>
|
||||
{!selectedItem ?
|
||||
<div style={{textAlign: "left", position: "relative",}}>
|
||||
<Typography variant="h6">
|
||||
<b>{subcase.name}</b>
|
||||
</Typography>
|
||||
{finished ?
|
||||
<Tooltip
|
||||
title="A workflow has been assigned"
|
||||
placement="top"
|
||||
>
|
||||
<IconButton
|
||||
style={{ position: "absolute", top: -20, right: -20}}
|
||||
onClick={(e) => {
|
||||
}}
|
||||
>
|
||||
<DoneAllIcon style={{ color: usecase.color }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
: null}
|
||||
</div>
|
||||
:
|
||||
<div style={{textAlign: "left", position: "relative",}}>
|
||||
<Typography variant="h6">
|
||||
<b>{subcase.name}</b>
|
||||
</Typography>
|
||||
<Typography variant="body2">
|
||||
Description: {subcase.description}
|
||||
</Typography>
|
||||
<Tooltip
|
||||
title="Close window"
|
||||
placement="top"
|
||||
style={{ zIndex: 10011 }}
|
||||
>
|
||||
<IconButton
|
||||
style={{ position: "absolute", top: 0, right: 0}}
|
||||
onClick={(e) => {
|
||||
setExpandedItem(-1)
|
||||
setExpandedIndex(-1)
|
||||
}}
|
||||
>
|
||||
<CloseIcon style={{ color: "white" }} />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<div style={{marginTop: 25, display: "flex", minHeight: 400, maxHeight: 400, }}>
|
||||
<img
|
||||
alt={subcase.name}
|
||||
src={"/images/detectionframework.png"}
|
||||
style={{
|
||||
flex: 1,
|
||||
height: 400,
|
||||
width: 400,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
border: "1px solid rgba(255,255,255,0.3)",
|
||||
}}
|
||||
/>
|
||||
<div style={{flex: 1, marginLeft: 10, textAlign: "center",}}>
|
||||
<Typography variant="h6">
|
||||
Your workflow{subcase.matches.length === 1 ? "" : "s"} ({subcase.matches.length})
|
||||
</Typography>
|
||||
{subcase.matches.length > 0 ?
|
||||
<Grid container xs={3} style={{maxWidth: 325, margin: "auto", marginTop: 10, itemAlign: "center", }}>
|
||||
{subcase.matches.map((workflow, workflowindex) => {
|
||||
return (
|
||||
<Grid index={workflowindex} xs={12}>
|
||||
<WorkflowPaper key={workflowindex} data={workflow} />
|
||||
</Grid>
|
||||
)
|
||||
})}
|
||||
</Grid>
|
||||
:
|
||||
<div>
|
||||
<Typography variant="body1" color="textSecondary">
|
||||
No workflow selected yet.
|
||||
</Typography>
|
||||
</div>
|
||||
}
|
||||
{isCloud !== false ?
|
||||
<Typography variant="h6">
|
||||
Public workflows
|
||||
</Typography>
|
||||
: null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</Paper>
|
||||
</Grid>
|
||||
)
|
||||
})}
|
||||
</Grid>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const TreeChart = ({keys}) => {
|
||||
const [hovered, setHovered] = useState("");
|
||||
|
||||
return (
|
||||
<div style={{cursor: "pointer",}} onClick={() => {
|
||||
console.log("Click: ", hovered)
|
||||
}}>
|
||||
<TreeMap
|
||||
id="all_categories"
|
||||
data={keys}
|
||||
margins={10}
|
||||
series={
|
||||
<TreeMapSeries
|
||||
colorScheme={(info) => {
|
||||
return info.color
|
||||
}}
|
||||
label={
|
||||
<TreeMapLabel
|
||||
fontSize="15px"
|
||||
fill="#ffffff"
|
||||
wrap={false}
|
||||
/>
|
||||
}
|
||||
rect={
|
||||
<TreeMapRect
|
||||
cursor="pointer"
|
||||
animated={true}
|
||||
onClick={(event) => {
|
||||
console.log("Click: ", event)
|
||||
}}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
//axis={<RadialAxis type="category" />}
|
||||
}
|
||||
|
||||
|
||||
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 } = props;
|
||||
const { globalUrl, isLoggedIn } = props;
|
||||
const alert = useAlert();
|
||||
const [bigChartData, setBgChartData] = useState("data1");
|
||||
const [dayAmount, setDayAmount] = useState(7);
|
||||
@@ -48,11 +329,172 @@ const Dashboard = (props) => {
|
||||
const [stats, setStats] = useState({});
|
||||
const [changeme, setChangeme] = useState("");
|
||||
const [statsRan, setStatsRan] = useState(false);
|
||||
const [keys, setKeys] = useState([])
|
||||
const [treeKeys, setTreeKeys] = useState([])
|
||||
|
||||
document.title = "Shuffle - dashboard";
|
||||
const [selectedUsecaseCategory, setSelectedUsecaseCategory] = useState("");
|
||||
const [selectedUsecases, setSelectedUsecases] = useState([]);
|
||||
const [usecases, setUsecases] = useState([]);
|
||||
const [workflows, setWorkflows] = useState([]);
|
||||
|
||||
const isCloud =
|
||||
window.location.host === "localhost:3002" ||
|
||||
window.location.host === "shuffler.io";
|
||||
|
||||
const getAvailableWorkflows = () => {
|
||||
fetch(globalUrl + "/api/v1/workflows", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status !== 200) {
|
||||
fetchUsecases()
|
||||
console.log("Status not 200 for workflows :O!: ", response.status);
|
||||
return;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
fetchUsecases(responseJson)
|
||||
|
||||
console.log("Resp: ", responseJson)
|
||||
if (responseJson !== undefined) {
|
||||
//setWorkflows(responseJson);
|
||||
//fetchUsecases(responseJson)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
fetchUsecases()
|
||||
//alert.error(error.toString());
|
||||
});
|
||||
}
|
||||
|
||||
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];
|
||||
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)
|
||||
}
|
||||
|
||||
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)
|
||||
if (workflows !== undefined && workflows !== null && workflows.length > 0) {
|
||||
console.log("Got workflows: ", workflows)
|
||||
var categorydata = responseJson
|
||||
|
||||
var newcategories = []
|
||||
for (var key in categorydata) {
|
||||
var category = categorydata[key]
|
||||
category.matches = []
|
||||
|
||||
for (var subcategorykey in category.list) {
|
||||
var subcategory = category.list[subcategorykey]
|
||||
subcategory.matches = []
|
||||
|
||||
for (var workflowkey in workflows) {
|
||||
const workflow = workflows[workflowkey]
|
||||
|
||||
if (workflow.usecase_ids !== undefined && workflow.usecase_ids !== null) {
|
||||
for (var usecasekey in workflow.usecase_ids) {
|
||||
if (workflow.usecase_ids[usecasekey].toLowerCase() === subcategory.name.toLowerCase()) {
|
||||
console.log("Got match: ", workflow.usecase_ids[usecasekey])
|
||||
|
||||
category.matches.push({
|
||||
"workflow": workflow.id,
|
||||
"category": subcategory.name,
|
||||
})
|
||||
|
||||
subcategory.matches.push(workflow)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (subcategory.matches.length > 0) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newcategories.push(category)
|
||||
}
|
||||
|
||||
console.log("Categories: ", newcategories)
|
||||
if (newcategories !== undefined && newcategories !== null && newcategories.length > 0) {
|
||||
handleKeysetting(newcategories)
|
||||
setUsecases(newcategories)
|
||||
setSelectedUsecases(newcategories)
|
||||
} else {
|
||||
handleKeysetting(responseJson)
|
||||
setUsecases(responseJson)
|
||||
setSelectedUsecases(responseJson)
|
||||
}
|
||||
} else {
|
||||
handleKeysetting(responseJson)
|
||||
setUsecases(responseJson)
|
||||
setSelectedUsecases(responseJson)
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
//alert.error("ERROR: " + error.toString());
|
||||
console.log("ERROR: " + error.toString());
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getAvailableWorkflows()
|
||||
//fetchUsecases()
|
||||
}, []);
|
||||
|
||||
const fetchdata = (stats_id) => {
|
||||
fetch(globalUrl + "/api/v1/stats/" + stats_id, {
|
||||
method: "GET",
|
||||
@@ -76,7 +518,8 @@ const Dashboard = (props) => {
|
||||
setChangeme(stats_id);
|
||||
})
|
||||
.catch((error) => {
|
||||
alert.error("ERROR: " + error.toString());
|
||||
//alert.error("ERROR: " + error.toString());
|
||||
console.log("ERROR: " + error.toString());
|
||||
});
|
||||
};
|
||||
|
||||
@@ -201,8 +644,8 @@ const Dashboard = (props) => {
|
||||
if (firstRequest) {
|
||||
console.log("HELO");
|
||||
setFirstRequest(false);
|
||||
start();
|
||||
runUpdate();
|
||||
//start();
|
||||
//runUpdate();
|
||||
} else if (!statsRan) {
|
||||
// FIXME: Run this under runUpdate schedule?
|
||||
// 1. Fix labels in dayGraphy.data
|
||||
@@ -301,158 +744,54 @@ const Dashboard = (props) => {
|
||||
) : null;
|
||||
|
||||
const data = (
|
||||
<div className="content">
|
||||
<div className="content" style={{width: 1000, margin: "auto", paddingBottom: 200, textAlign: "center",}}>
|
||||
<div style={{width: 500, margin: "auto"}}>
|
||||
{keys.length > 0 ?
|
||||
<RadialChart keys={keys} setSelectedCategory={setSelectedUsecaseCategory} />
|
||||
: null}
|
||||
</div>
|
||||
|
||||
{usecases !== null && usecases !== undefined && usecases.length > 0 ?
|
||||
<div style={{ display: "flex", marginLeft: 120,}}>
|
||||
{usecases.map((usecase, index) => {
|
||||
return (
|
||||
<Chip
|
||||
key={usecase.name}
|
||||
style={{
|
||||
backgroundColor: selectedUsecaseCategory === usecase.name ? usecase.color : theme.palette.surfaceColor,
|
||||
marginRight: 10,
|
||||
paddingLeft: 5,
|
||||
paddingRight: 5,
|
||||
height: 28,
|
||||
cursor: "pointer",
|
||||
border: `1px solid ${usecase.color}`,
|
||||
color: "white",
|
||||
}}
|
||||
label={`${usecase.name} (${usecase.list.length})`}
|
||||
onClick={() => {
|
||||
console.log("Clicked: ", usecase.name)
|
||||
if (selectedUsecaseCategory === usecase.name) {
|
||||
setSelectedUsecaseCategory("")
|
||||
} else {
|
||||
setSelectedUsecaseCategory(usecase.name)
|
||||
}
|
||||
//addFilter(usecase.name.slice(3,usecase.name.length))
|
||||
}}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
: null}
|
||||
|
||||
<UsecaseListComponent keys={selectedUsecases} isCloud={isCloud} />
|
||||
|
||||
{treeKeys.length > 0 ?
|
||||
<TreeChart keys={treeKeys} />
|
||||
: null}
|
||||
|
||||
{newdata}
|
||||
<Row>
|
||||
<Col xs="12">
|
||||
<div className="chart-area">
|
||||
<Line data={dayGraph.data} options={dayGraph.options} />
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs="12">
|
||||
<Card className="card-chart">
|
||||
<CardHeader>
|
||||
<Row>
|
||||
<Col className="text-left" sm="6">
|
||||
<h5 className="card-category">Total Shipments</h5>
|
||||
<CardTitle tag="h2">Workflows</CardTitle>
|
||||
</Col>
|
||||
<Col sm="6">
|
||||
<ButtonGroup
|
||||
className="btn-group-toggle float-right"
|
||||
data-toggle="buttons"
|
||||
>
|
||||
<Button
|
||||
tag="label"
|
||||
className={classNames("btn-simple", {
|
||||
active: bigChartData === "data1",
|
||||
})}
|
||||
color="info"
|
||||
id="0"
|
||||
size="sm"
|
||||
onClick={() => setBgChartData("data1")}
|
||||
>
|
||||
<input
|
||||
defaultChecked
|
||||
className="d-none"
|
||||
name="options"
|
||||
type="radio"
|
||||
/>
|
||||
<span className="d-none d-sm-block d-md-block d-lg-block d-xl-block">
|
||||
Accounts
|
||||
</span>
|
||||
<span className="d-block d-sm-none">
|
||||
<i className="tim-icons icon-single-02" />
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
color="info"
|
||||
id="1"
|
||||
size="sm"
|
||||
tag="label"
|
||||
className={classNames("btn-simple", {
|
||||
active: bigChartData === "data2",
|
||||
})}
|
||||
onClick={() => setBgChartData("data2")}
|
||||
>
|
||||
<input className="d-none" name="options" type="radio" />
|
||||
<span className="d-none d-sm-block d-md-block d-lg-block d-xl-block">
|
||||
Purchases
|
||||
</span>
|
||||
<span className="d-block d-sm-none">
|
||||
<i className="tim-icons icon-gift-2" />
|
||||
</span>
|
||||
</Button>
|
||||
<Button
|
||||
color="info"
|
||||
id="2"
|
||||
size="sm"
|
||||
tag="label"
|
||||
className={classNames("btn-simple", {
|
||||
active: bigChartData === "data3",
|
||||
})}
|
||||
onClick={() => setBgChartData("data3")}
|
||||
>
|
||||
<input className="d-none" name="options" type="radio" />
|
||||
<span className="d-none d-sm-block d-md-block d-lg-block d-xl-block">
|
||||
Sessions
|
||||
</span>
|
||||
<span className="d-block d-sm-none">
|
||||
<i className="tim-icons icon-tap-02" />
|
||||
</span>
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Col>
|
||||
</Row>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<div className="chart-area">
|
||||
<Line
|
||||
data={chartExample1[bigChartData]}
|
||||
options={chartExample1.options}
|
||||
/>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col lg="4">
|
||||
<Card className="card-chart">
|
||||
<CardHeader>
|
||||
<h5 className="card-category">Total Shipments</h5>
|
||||
<CardTitle tag="h3">
|
||||
<i className="tim-icons icon-bell-55 text-info" /> 763,215
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<div className="chart-area">
|
||||
<Line
|
||||
data={chartExample2.data}
|
||||
options={chartExample2.options}
|
||||
/>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col lg="4">
|
||||
<Card className="card-chart">
|
||||
<CardHeader>
|
||||
<h5 className="card-category">Daily Sales</h5>
|
||||
<CardTitle tag="h3">
|
||||
<i className="tim-icons icon-delivery-fast text-primary" />{" "}
|
||||
3,500€
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<div className="chart-area">
|
||||
<Bar
|
||||
data={chartExample3.data}
|
||||
options={chartExample3.options}
|
||||
/>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col lg="4">
|
||||
<Card className="card-chart">
|
||||
<CardHeader>
|
||||
<h5 className="card-category">Completed Tasks</h5>
|
||||
<CardTitle tag="h3">
|
||||
<i className="tim-icons icon-send text-success" /> 12,100K
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<div className="chart-area">
|
||||
<Line
|
||||
data={chartExample4.data}
|
||||
options={chartExample4.options}
|
||||
/>
|
||||
</div>
|
||||
</CardBody>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ const Docs = (defaultprops) => {
|
||||
maxHeight: "83vh",
|
||||
overflowX: "hidden",
|
||||
overflowY: "auto",
|
||||
zIndex: 10003,
|
||||
zIndex: 1000,
|
||||
};
|
||||
|
||||
const fetchDocList = () => {
|
||||
@@ -141,6 +141,11 @@ const Docs = (defaultprops) => {
|
||||
setData(responseJson.reason);
|
||||
document.title = "Shuffle " + docId + " documentation";
|
||||
|
||||
if (responseJson.reason !== undefined && responseJson.reason !== null && responseJson.reason.includes("404: Not Found")) {
|
||||
navigate("/docs")
|
||||
return
|
||||
}
|
||||
|
||||
if (responseJson.meta !== undefined) {
|
||||
setSelectedMeta(responseJson.meta);
|
||||
}
|
||||
@@ -352,7 +357,7 @@ const Docs = (defaultprops) => {
|
||||
}
|
||||
|
||||
function Img(props) {
|
||||
return <img style={{ maxWidth: "100%" }} alt={props.alt} src={props.src} />;
|
||||
return <img style={{ borderRadius: theme.palette.borderRadius, maxWidth: "100%", marginTop: 15, marginBottom: 15, }} alt={props.alt} src={props.src} />;
|
||||
}
|
||||
|
||||
function CodeHandler(props) {
|
||||
@@ -517,7 +522,8 @@ const Docs = (defaultprops) => {
|
||||
const newname =
|
||||
item.charAt(0).toUpperCase() +
|
||||
item.substring(1).split("_").join(" ").split("-").join(" ");
|
||||
const itemMatching =
|
||||
|
||||
const itemMatching = props.match.params.key === undefined ? false :
|
||||
props.match.params.key.toLowerCase() === item.toLowerCase();
|
||||
//const [tocLines, setTocLines] = React.useState([]);
|
||||
return (
|
||||
|
||||
@@ -2,6 +2,7 @@ import React, { useEffect, useContext } from "react";
|
||||
import { makeStyles } from "@material-ui/core/styles";
|
||||
import { useTheme } from "@material-ui/core/styles";
|
||||
|
||||
import ReactGA from 'react-ga';
|
||||
import SecurityFramework from '../components/SecurityFramework.jsx';
|
||||
|
||||
import {
|
||||
@@ -67,7 +68,7 @@ import { DataGrid, GridToolbar } from "@material-ui/data-grid";
|
||||
//import JSONPrettyMon from 'react-json-pretty/dist/monikai'
|
||||
import Dropzone from "../components/Dropzone";
|
||||
|
||||
import { Link } from "react-router-dom";
|
||||
import { useNavigate, Link, useParams } from "react-router-dom";
|
||||
import { useAlert } from "react-alert";
|
||||
import ChipInput from "material-ui-chip-input";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
@@ -109,263 +110,7 @@ const useStyles = makeStyles((theme) => ({
|
||||
},
|
||||
}));
|
||||
|
||||
// Takes an action in Shuffle and
|
||||
// Returns information about the icon, the color etc to be used
|
||||
// This can be used for actions of all types
|
||||
export const GetIconInfo = (action) => {
|
||||
// Finds the icon based on the action. Should be verbs.
|
||||
const iconList = [
|
||||
{ key: "cache_add", values: ["set_cache"] },
|
||||
{ key: "cache_get", values: ["get_cache"] },
|
||||
{ key: "filter", values: ["filter", "route", "router"] },
|
||||
{ key: "merge", values: ["join", "merge"] },
|
||||
{
|
||||
key: "search",
|
||||
values: ["search", "find", "locate", "index", "analyze", "anal", "match", "check cache", "check", "verify", "validate"],
|
||||
},
|
||||
{ key: "list", values: ["list", "head", "options"] },
|
||||
{
|
||||
key: "download",
|
||||
values: [
|
||||
"capture",
|
||||
"get",
|
||||
"download",
|
||||
"return",
|
||||
"hello_world",
|
||||
"curl",
|
||||
"request",
|
||||
"export",
|
||||
"preview",
|
||||
],
|
||||
},
|
||||
{ key: "add", values: ["add", "accept", ] },
|
||||
{ key: "delete", values: ["delete", "remove", "clear", "clean", "dismiss",] },
|
||||
{
|
||||
key: "send",
|
||||
values: [
|
||||
"send",
|
||||
"dispatch",
|
||||
"mail",
|
||||
"forward",
|
||||
"post",
|
||||
"submit",
|
||||
"mark",
|
||||
"set",
|
||||
"release",
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "repeat",
|
||||
values: ["repeat", "retry", "pause", "skip", "copy", "replicat", "demo", ],
|
||||
},
|
||||
{ key: "execute", values: ["execute", "run", "play", "raise"] },
|
||||
{ key: "extract", values: ["extract", "unpack", "decompress", "open"] },
|
||||
{ key: "inflate", values: ["inflate", "pack", "compress"] },
|
||||
{
|
||||
key: "edit",
|
||||
values: [
|
||||
"modify",
|
||||
"update",
|
||||
"create",
|
||||
"edit",
|
||||
"put",
|
||||
"patch",
|
||||
"change",
|
||||
"replace",
|
||||
"conver",
|
||||
"map",
|
||||
"format",
|
||||
"escape",
|
||||
"describe",
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "compare",
|
||||
values: ["compare", "convert", "to", "filter", "translate", "parse"],
|
||||
},
|
||||
{ key: "close", values: ["close", "stop", "cancel", "block"] },
|
||||
];
|
||||
|
||||
var selectedKey = "";
|
||||
if (action.name === undefined || action.name === null) {
|
||||
} else {
|
||||
const actionname = action.name.toLowerCase();
|
||||
for (var key in iconList) {
|
||||
//console.log(iconList[key], actionname)
|
||||
const found = iconList[key].values.find((value) =>
|
||||
actionname.includes(value)
|
||||
);
|
||||
if (found !== null && found !== undefined) {
|
||||
selectedKey = iconList[key].key;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Some of these are manually parsed or created instead of material ui
|
||||
//M8 0C3.58 0 0 1.79 0 4C0 6.21 3.58 8 8 8C12.42 8 16 6.21 16 4C16 1.79 12.42 0 8 0ZM0 6V9C0 11.21 3.58 13 8 13C12.42 13 16 11.21 16 9V6C16 8.21 12.42 10 8 10C3.58 10 0 8.21 0 6ZM0 11V14C0 16.21 3.58 18 8 18C9.41 18 10.79 17.81 12 17.46V14.46C10.79 14.81 9.41 15 8 15C3.58 15 0 13.21 0 11ZM17 11V14H14V16H17V19H19V16H22V14H19V11
|
||||
//https://www.figma.com/file/uCfnMs5w6wnLx6ehPHEV74/Figma-Material-Design-System-v3_0?node-id=834%3A21
|
||||
//COLORS: https://www.pinterest.co.uk/pin/326299935499972946/
|
||||
const defaultColor = "#f76b1c";
|
||||
const defaultGradient = ["#fad961", "#f76b1c"];
|
||||
const parsedIcons = {
|
||||
cache_add: {
|
||||
icon: "M11 3C6.58 3 3 4.79 3 7C3 9.21 6.58 11 11 11C15.42 11 19 9.21 19 7C19 4.79 15.42 3 11 3ZM3 9V12C3 14.21 6.58 16 11 16C15.42 16 19 14.21 19 12V9C19 11.21 15.42 13 11 13C6.58 13 3 11.21 3 9ZM3 14V17C3 19.21 6.58 21 11 21C12.41 21 13.79 20.81 15 20.46V17.46C13.79 17.81 12.41 18 11 18C6.58 18 3 16.21 3 14ZM20 14V17H17V19H20V22H22V19H25V17H22V14",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: "#8acc3f",
|
||||
originalIcon: "",
|
||||
fillGradient: ["#8acc3f", "#459622"],
|
||||
},
|
||||
cache_get: {
|
||||
icon: "M12 2C7.58 2 4 3.79 4 6C4 8.06 7.13 9.74 11.15 9.96C12.45 8.7 14.19 8 16 8C16.8 8 17.59 8.14 18.34 8.41C19.37 7.74 20 6.91 20 6C20 3.79 16.42 2 12 2ZM4 8V11C4 12.68 6.08 14.11 9 14.71C9.06 13.7 9.32 12.72 9.77 11.82C6.44 11.34 4 9.82 4 8ZM15.93 9.94C14.75 9.95 13.53 10.4 12.46 11.46C8.21 15.71 13.71 22.5 18.75 19.17L23.29 23.71L24.71 22.29L20.17 17.75C22.66 13.97 19.47 9.93 15.93 9.94ZM15.9 12C17.47 11.95 19 13.16 19 15C19 15.7956 18.6839 16.5587 18.1213 17.1213C17.5587 17.6839 16.7956 18 16 18C13.33 18 12 14.77 13.88 12.88C14.47 12.29 15.19 12 15.9 12ZM4 13V16C4 18.05 7.09 19.72 11.06 19.95C10.17 19.07 9.54 17.95 9.22 16.74C6.18 16.17 4 14.72 4 13Z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: "#8acc3f",
|
||||
originalIcon: "",
|
||||
fillGradient: ["#8acc3f", "#459622"],
|
||||
},
|
||||
repeat: {
|
||||
icon: "M19 8l-4 4h3c0 3.31-2.69 6-6 6-1.01 0-1.97-.25-2.8-.7l-1.46 1.46C8.97 19.54 10.43 20 12 20c4.42 0 8-3.58 8-8h3l-4-4zM6 12c0-3.31 2.69-6 6-6 1.01 0 1.97.25 2.8.7l1.46-1.46C15.03 4.46 13.57 4 12 4c-4.42 0-8 3.58-8 8H1l4 4 4-4H6z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: defaultColor,
|
||||
originalIcon: <CachedIcon />,
|
||||
},
|
||||
add: {
|
||||
icon: "M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: defaultColor,
|
||||
originalIcon: <AddIcon />,
|
||||
},
|
||||
edit: {
|
||||
icon: "M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.9959.9959 0 00-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: defaultColor,
|
||||
originalIcon: <EditIcon />,
|
||||
},
|
||||
filter: {
|
||||
icon: "M4.25 5.61C6.27 8.2 10 13 10 13v6c0 .55.45 1 1 1h2c.55 0 1-.45 1-1v-6s3.72-4.8 5.74-7.39c.51-.66.04-1.61-.79-1.61H5.04c-.83 0-1.3.95-.79 1.61z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: "#f5515f",
|
||||
originalIcon: "",
|
||||
fillGradient: ["#f5515f", "#a1051d"],
|
||||
},
|
||||
merge: {
|
||||
icon: "M17 20.41 18.41 19 15 15.59 13.59 17 17 20.41zM7.5 8H11v5.59L5.59 19 7 20.41l6-6V8h3.5L12 3.5 7.5 8z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: "#f5515f",
|
||||
originalIcon: "",
|
||||
fillGradient: ["#f5515f", "#a1051d"],
|
||||
},
|
||||
compare: {
|
||||
icon: "M10 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h5v2h2V1h-2v2zm0 15H5l5-6v6zm9-15h-5v2h5v13l-5-6v9h5c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: defaultColor,
|
||||
originalIcon: <CompareIcon />,
|
||||
},
|
||||
extract: {
|
||||
icon: "M3 3h18v2H3z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: defaultColor,
|
||||
originalIcon: <MaximizeIcon />,
|
||||
},
|
||||
inflate: {
|
||||
icon: "M6 19h12v2H6z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: defaultColor,
|
||||
originalIcon: <MinimizeIcon />,
|
||||
},
|
||||
list: {
|
||||
icon: "M3 9h14V7H3v2zm0 4h14v-2H3v2zm0 4h14v-2H3v2zm16 0h2v-2h-2v2zm0-10v2h2V7h-2zm0 6h2v-2h-2v2z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: defaultColor,
|
||||
originalIcon: <TocIcon />,
|
||||
},
|
||||
execute: {
|
||||
icon: "M8 5v14l11-7z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: defaultColor,
|
||||
originalIcon: <PlayArrowIcon />,
|
||||
},
|
||||
delete: {
|
||||
icon: "M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: "#03030e",
|
||||
originalIcon: <DeleteIcon />,
|
||||
fillGradient: ["#03030e", "#205d66"],
|
||||
},
|
||||
close: {
|
||||
icon: "M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: "#03030e",
|
||||
originalIcon: <CloseIcon />,
|
||||
fillGradient: ["#03030e", "#205d66"],
|
||||
},
|
||||
send: {
|
||||
icon: "M2.01 21L23 12 2.01 3 2 10l15 2-15 2z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: "#0373da",
|
||||
originalIcon: <SendIcon />,
|
||||
fillGradient: ["#0bc8bf", "#0373da"],
|
||||
},
|
||||
download: {
|
||||
icon: "M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: "#0373da",
|
||||
originalIcon: <GetAppIcon />,
|
||||
fillGradient: ["#0bc8bf", "#0373da"],
|
||||
},
|
||||
search: {
|
||||
icon: "M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: "green",
|
||||
originalIcon: <SearchIcon />,
|
||||
},
|
||||
};
|
||||
|
||||
var selectedItem = parsedIcons[selectedKey];
|
||||
if (selectedItem === undefined || selectedItem === null) {
|
||||
return {
|
||||
icon: "",
|
||||
iconColor: "",
|
||||
iconBackground: "black",
|
||||
originalIcon: "",
|
||||
};
|
||||
}
|
||||
|
||||
if (selectedItem.fillGradient === undefined) {
|
||||
selectedItem.fillGradient = defaultGradient;
|
||||
selectedItem.iconBackgroundColor = defaultColor;
|
||||
}
|
||||
|
||||
if (selectedItem.icon === "" || selectedItem.icon === undefined) {
|
||||
console.log(
|
||||
`MISSING PATH FOR ${selectedKey} (find in scope): `,
|
||||
selectedItem.originalIcon.type.type
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
(selectedItem.originalIcon === undefined ||
|
||||
selectedItem.originalIcon === "") &&
|
||||
selectedItem.icon !== "" &&
|
||||
selectedItem.icon !== undefined
|
||||
) {
|
||||
const svg_pin = (
|
||||
<svg
|
||||
width={svgSize}
|
||||
height={svgSize}
|
||||
viewBox={`0 0 ${svgSize} ${svgSize}`}
|
||||
version="1.1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path d={selectedItem.icon} fill={selectedItem.iconColor}></path>
|
||||
</svg>
|
||||
);
|
||||
selectedItem.originalIcon = svg_pin;
|
||||
}
|
||||
|
||||
return selectedItem;
|
||||
};
|
||||
|
||||
|
||||
const chipStyle = {
|
||||
backgroundColor: "#3d3f43",
|
||||
marginRight: 5,
|
||||
@@ -377,62 +122,6 @@ const chipStyle = {
|
||||
color: "white",
|
||||
};
|
||||
|
||||
export const validateJson = (showResult) => {
|
||||
//showResult = showResult.split(" None").join(" \"None\"")
|
||||
showResult = showResult.split(" False").join(" false");
|
||||
showResult = showResult.split(" True").join(" true");
|
||||
|
||||
var jsonvalid = true;
|
||||
try {
|
||||
if (!showResult.includes("{") && !showResult.includes("[")) {
|
||||
jsonvalid = false;
|
||||
}
|
||||
} catch (e) {
|
||||
showResult = showResult.split("'").join('"');
|
||||
|
||||
try {
|
||||
if (!showResult.includes("{") && !showResult.includes("[")) {
|
||||
jsonvalid = false;
|
||||
}
|
||||
} catch (e) {
|
||||
jsonvalid = false;
|
||||
}
|
||||
}
|
||||
|
||||
var result = showResult;
|
||||
try {
|
||||
result = jsonvalid ? JSON.parse(showResult) : showResult;
|
||||
} catch (e) {
|
||||
////console.log("Failed parsing JSON even though its valid: ", e)
|
||||
jsonvalid = false;
|
||||
}
|
||||
|
||||
if (jsonvalid === false) {
|
||||
|
||||
if (typeof showResult === 'string') {
|
||||
showResult = showResult.trim()
|
||||
}
|
||||
|
||||
try {
|
||||
var newstr = showResult.replaceAll("'", '"')
|
||||
|
||||
//console.log("Try replacements and trimming with new value: ", newstr)
|
||||
result = JSON.parse(newstr)
|
||||
jsonvalid = true
|
||||
} catch (e) {
|
||||
|
||||
//console.log("Failed parsing JSON even though its valid (2): ", e)
|
||||
jsonvalid = false
|
||||
}
|
||||
}
|
||||
|
||||
//console.log("VALID: ", jsonvalid, result)
|
||||
return {
|
||||
valid: jsonvalid,
|
||||
result: result,
|
||||
};
|
||||
};
|
||||
|
||||
const GettingStarted = (props) => {
|
||||
const { globalUrl, isLoggedIn, isLoaded, userdata } = props;
|
||||
|
||||
@@ -440,6 +129,7 @@ const GettingStarted = (props) => {
|
||||
const theme = useTheme();
|
||||
const alert = useAlert();
|
||||
const classes = useStyles(theme);
|
||||
let navigate = useNavigate();
|
||||
const imgSize = 60;
|
||||
|
||||
const referenceUrl = globalUrl + "/api/v1/hooks/";
|
||||
@@ -459,8 +149,8 @@ const GettingStarted = (props) => {
|
||||
"https://github.com/frikky/shuffle-workflows"
|
||||
);
|
||||
const [downloadBranch, setDownloadBranch] = React.useState("master");
|
||||
const [loadWorkflowsModalOpen, setLoadWorkflowsModalOpen] =
|
||||
React.useState(false);
|
||||
const [loadWorkflowsModalOpen, setLoadWorkflowsModalOpen] = React.useState(false);
|
||||
const [videoViewOpen, setVideoViewOpen] = React.useState(false);
|
||||
const [exportModalOpen, setExportModalOpen] = React.useState(false);
|
||||
const [exportData, setExportData] = React.useState("");
|
||||
|
||||
@@ -825,6 +515,8 @@ const GettingStarted = (props) => {
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
setVideoViewOpen(true)
|
||||
|
||||
if (response.status !== 200) {
|
||||
console.log("Status not 200 for workflows :O!: ", response.status);
|
||||
|
||||
@@ -879,6 +571,8 @@ const GettingStarted = (props) => {
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
setVideoViewOpen(true)
|
||||
|
||||
alert.error(error.toString());
|
||||
});
|
||||
};
|
||||
@@ -2446,70 +2140,27 @@ const GettingStarted = (props) => {
|
||||
maxWidth: 600,
|
||||
};
|
||||
|
||||
const WorkflowView = () => {
|
||||
/*
|
||||
if (workflows.length === 0) {
|
||||
return (
|
||||
<div style={emptyWorkflowStyle}>
|
||||
<Paper style={boxStyle}>
|
||||
<div>
|
||||
<h2>Welcome to Shuffle</h2>
|
||||
</div>
|
||||
<div>
|
||||
<p>
|
||||
<b>Shuffle</b> is a flexible, easy to use, automation platform
|
||||
allowing users to integrate their services and devices freely.
|
||||
It's made to significantly reduce the amount of manual labor,
|
||||
and is focused on security applications.{" "}
|
||||
<a
|
||||
href="/docs/about"
|
||||
style={{ textDecoration: "none", color: "#f85a3e" }}
|
||||
>
|
||||
Click here to learn more.
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
If you want to jump straight into it, click here to create your
|
||||
first workflow:
|
||||
</div>
|
||||
<div style={{ display: "flex" }}>
|
||||
<Button
|
||||
id="second-step"
|
||||
color="primary"
|
||||
style={{ marginTop: "20px" }}
|
||||
variant="outlined"
|
||||
onClick={() => setModalOpen(true)}
|
||||
>
|
||||
New workflow
|
||||
</Button>
|
||||
<span style={{ paddingTop: 20, display: "flex" }}>
|
||||
<Typography
|
||||
style={{ marginTop: 5, marginLeft: 30, marginRight: 15 }}
|
||||
>
|
||||
..OR
|
||||
</Typography>
|
||||
{workflowButtons}
|
||||
</span>
|
||||
</div>
|
||||
</Paper>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
*/
|
||||
|
||||
const WorkflowView = () => {
|
||||
var workflowDelay = -150
|
||||
var appDelay = -75
|
||||
|
||||
const textSpacingDiff = 8
|
||||
const textType = "body2"
|
||||
|
||||
// Discover <a target="_blank" href="https://shuffler.io/creators" style={{textDecoration: "none", color: "#f86a3e",}}>use-cases made by other creators</a>!
|
||||
// Discover <a target="_blank" href="https://shuffler.io/search?tab=workflows" style={{textDecoration: "none", color: "#f86a3e",}}>use-cases made by us and other creators</a>!
|
||||
const steps = [
|
||||
{
|
||||
html: (
|
||||
<Typography variant={textType} style={{marginTop: textSpacingDiff}}>
|
||||
<Link to="/detectionframework" style={{textDecoration: "none", color: "#f86a3e",}}>Find your integrations</Link> by following our simple detection framework!
|
||||
<Typography variant={textType} style={{marginTop: textSpacingDiff}} onClick={() => {
|
||||
if (isCloud) {
|
||||
ReactGA.event({
|
||||
category: "getting-started",
|
||||
action: `integerations_find_click`,
|
||||
})
|
||||
}
|
||||
}}>
|
||||
<Link to="/detectionframework" style={{textDecoration: "none", color: "#f86a3e",}}>Find relevant apps</Link> and start your automation journey
|
||||
</Typography>
|
||||
),
|
||||
tutorial: "find_integrations",
|
||||
@@ -2517,31 +2168,50 @@ const GettingStarted = (props) => {
|
||||
{
|
||||
html:
|
||||
<Typography variant={textType} style={{marginTop: textSpacingDiff}}>
|
||||
Discover <span style={{cursor: "pointer", textDecoration: "none", color: "#f86a3e",}} onClick={() => {
|
||||
Discover <Link to="/usecases" style={{cursor: "pointer", textDecoration: "none", color: "#f86a3e",}}>Use Case ideas</Link> and
|
||||
<span style={{cursor: "pointer", textDecoration: "none", color: "#f86a3e",}} onClick={() => {
|
||||
|
||||
if (isCloud) {
|
||||
navigate(`/search?tab=workflows`)
|
||||
|
||||
ReactGA.event({
|
||||
category: "getting-started",
|
||||
action: `workflow_find_click`,
|
||||
})
|
||||
return
|
||||
} else {
|
||||
alert.success("TBD: Coming in version 1.0.0");
|
||||
}
|
||||
|
||||
const ele = document.getElementById("shuffle_search_field")
|
||||
if (ele !== undefined && ele !== null) {
|
||||
console.log("Found ele: ", ele)
|
||||
ele.focus()
|
||||
ele.style.borderColor = "#f86a3e"
|
||||
ele.style.borderWidth = "2px"
|
||||
const ele = document.getElementById("shuffle_search_field")
|
||||
if (ele !== undefined && ele !== null) {
|
||||
console.log("Found ele: ", ele)
|
||||
ele.focus()
|
||||
ele.style.borderColor = "#f86a3e"
|
||||
ele.style.borderWidth = "2px"
|
||||
|
||||
} else {
|
||||
alert.success("TBD: Coming in version 1.0.0");
|
||||
}
|
||||
}}>
|
||||
use-cases made by other creators</span>!
|
||||
} else {
|
||||
//alert.success("TBD: Coming in version 1.0.0");
|
||||
}
|
||||
}}>
|
||||
workflows made by other creators</span>!
|
||||
</Typography>,
|
||||
tutorial: "discover_workflows",
|
||||
},
|
||||
{
|
||||
html: (
|
||||
<Typography variant={textType} style={{marginTop: textSpacingDiff}}>
|
||||
<Typography variant={textType} style={{marginTop: textSpacingDiff}} onClick={() => {
|
||||
if (isCloud) {
|
||||
ReactGA.event({
|
||||
category: "getting-started",
|
||||
action: `create_workflow_click`,
|
||||
})
|
||||
}
|
||||
}}>
|
||||
Learn to use Shuffle by
|
||||
<span style={{cursor: "pointer", color: "#f86a3e",}} onClick={() => {setModalOpen(true)}}>
|
||||
creating your first workflow
|
||||
</span> and <Link to="/docs" style={{textDecoration: "none", color: "#f86a3e",}}>reading the docs.</Link>
|
||||
</span> and <Link to="/docs/getting_started" style={{textDecoration: "none", color: "#f86a3e",}}>reading the docs.</Link>
|
||||
</Typography>
|
||||
),
|
||||
tutorial: "learn_shuffle",
|
||||
@@ -2557,6 +2227,55 @@ 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",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<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
|
||||
>
|
||||
</iframe>
|
||||
</Dialog>
|
||||
: null}
|
||||
<div style={workflowViewStyle}>
|
||||
<Typography variant="h1" style={{fontSize: 30, marginTop: 25, }}>
|
||||
Getting Started with Shuffle
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/* eslint-disable react/no-multi-comp */
|
||||
import React, { useState } from "react";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { makeStyles } from "@material-ui/styles";
|
||||
import { useInterval } from "react-powerhooks";
|
||||
|
||||
@@ -32,9 +32,6 @@ const useStyles = makeStyles({
|
||||
});
|
||||
|
||||
const LoginDialog = (props) => {
|
||||
const theme = useTheme();
|
||||
let navigate = useNavigate();
|
||||
|
||||
const {
|
||||
globalUrl,
|
||||
isLoaded,
|
||||
@@ -44,6 +41,11 @@ const LoginDialog = (props) => {
|
||||
register,
|
||||
checkLogin,
|
||||
} = props;
|
||||
|
||||
const theme = useTheme();
|
||||
let navigate = useNavigate();
|
||||
const classes = useStyles();
|
||||
|
||||
const [username, setUsername] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [firstRequest, setFirstRequest] = useState(true);
|
||||
@@ -54,9 +56,13 @@ const LoginDialog = (props) => {
|
||||
const [MFAField, setMFAField] = useState(false);
|
||||
const [MFAValue, setMFAValue] = useState("");
|
||||
|
||||
|
||||
// Used to swap from login to register. True = login, false = register
|
||||
|
||||
const classes = useStyles();
|
||||
useEffect(() => {
|
||||
checkAdmin()
|
||||
}, [loginViewLoading])
|
||||
|
||||
// Error messages etc
|
||||
const [loginInfo, setLoginInfo] = useState("");
|
||||
|
||||
@@ -143,7 +149,7 @@ const LoginDialog = (props) => {
|
||||
|
||||
var baseurl = globalUrl;
|
||||
if (register) {
|
||||
var url = baseurl + "/api/v1/users/login";
|
||||
var url = baseurl + "/api/v1/login";
|
||||
fetch(url, {
|
||||
mode: "cors",
|
||||
method: "POST",
|
||||
@@ -180,7 +186,8 @@ const LoginDialog = (props) => {
|
||||
|
||||
setIsLoggedIn(true);
|
||||
|
||||
navigate("/workflows")
|
||||
//navigate("/workflows")
|
||||
window.location.href = "/workflows"
|
||||
}
|
||||
})
|
||||
)
|
||||
@@ -264,6 +271,7 @@ const LoginDialog = (props) => {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{loginViewLoading ? (
|
||||
<div style={{ textAlign: "center", marginTop: 50 }}>
|
||||
<Typography
|
||||
@@ -466,13 +474,15 @@ const LoginDialog = (props) => {
|
||||
<div style={{ textAlign: "center", margin: 10 }}>
|
||||
<Button
|
||||
fullWidth
|
||||
id="sso_button"
|
||||
color="secondary"
|
||||
variant="outlined"
|
||||
type="button"
|
||||
style={{ flex: "1", marginTop: 5 }}
|
||||
onClick={() => {
|
||||
console.log("CLICK");
|
||||
navigate(ssoUrl)
|
||||
//console.log("CLICK SSO");
|
||||
window.location.href = ssoUrl
|
||||
//navigate(ssoUrl)
|
||||
}}
|
||||
>
|
||||
Use SSO
|
||||
@@ -488,6 +498,26 @@ const LoginDialog = (props) => {
|
||||
|
||||
const loadedCheck = isLoaded ? <div>{basedata}</div> : <div></div>;
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
if (ssoUrl !== undefined && ssoUrl !== null && ssoUrl.length > 0) {
|
||||
//id="sso_button"
|
||||
const ssoBtn = document.getElementById("sso_button");
|
||||
if (ssoBtn !== undefined && ssoBtn !== null) {
|
||||
console.log("SSO BTN: ", ssoBtn)
|
||||
const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search;
|
||||
var tmpView = new URLSearchParams(cursearch).get("autologin");
|
||||
if (tmpView !== undefined && tmpView !== null) {
|
||||
if (tmpView === "true") {
|
||||
console.log("Tmp: ", tmpView)
|
||||
ssoBtn.click()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, 200);
|
||||
}, [ssoUrl])
|
||||
|
||||
return <div>{loadedCheck}</div>;
|
||||
};
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ const Settings = (props) => {
|
||||
//Returns the value from a storage position at a given address.
|
||||
const isCloud =
|
||||
window.location.host === "localhost:3002" ||
|
||||
window.location.host === "shuffler.io";
|
||||
window.location.host === "shuffler.io"
|
||||
|
||||
const bodyDivStyle = {
|
||||
margin: "auto",
|
||||
@@ -257,6 +257,7 @@ const Settings = (props) => {
|
||||
console.log("Status not 200 for WORKFLOW EXECUTION :O!");
|
||||
}
|
||||
|
||||
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
@@ -402,11 +403,12 @@ const Settings = (props) => {
|
||||
userdata.eth_info.account.length > 0 && userdata.eth_info.parsed_balance !== undefined
|
||||
|
||||
// Random names for type & autoComplete. Didn't research :^)
|
||||
var imageData = file.length > 0 ? file : fileBase64;
|
||||
imageData =
|
||||
imageData === undefined || imageData.length === 0
|
||||
? theme.palette.defaultImage
|
||||
: imageData;
|
||||
//var imageData = file.length > 0 ? file : fileBase64;
|
||||
//imageData = imageData === undefined || imageData.length === 0
|
||||
// ? theme.palette.defaultImage
|
||||
// : imageData;
|
||||
|
||||
const imageData = userSettings.image === undefined || userSettings.image == null || userSettings.image.length === 0 ? theme.palette.defaultImage : userSettings.image
|
||||
const imageInfo = (
|
||||
<img
|
||||
src={imageData}
|
||||
@@ -729,17 +731,22 @@ const Settings = (props) => {
|
||||
<div style={{ display: runFlex ? "flex" : "", width: "100%" }}>
|
||||
<div>
|
||||
{isCloud ?
|
||||
<Button
|
||||
style={{ height: 40, marginTop: 10 }}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
fullWidth={true}
|
||||
onClick={() => {
|
||||
handleGithubConnection();
|
||||
}}
|
||||
>
|
||||
Connect to Github
|
||||
</Button>
|
||||
<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.
|
||||
</Typography>
|
||||
<Button
|
||||
style={{ height: 40, marginTop: 10 }}
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
fullWidth={true}
|
||||
onClick={() => {
|
||||
handleGithubConnection();
|
||||
}}
|
||||
>
|
||||
Connect to Github
|
||||
</Button>
|
||||
</span>
|
||||
: null}
|
||||
</div>
|
||||
<div style={{ flex: 1, display: "flex" }}>
|
||||
@@ -859,7 +866,7 @@ const Settings = (props) => {
|
||||
handleEthereumConnection();
|
||||
}}
|
||||
>
|
||||
Authenticate
|
||||
Authenticate Metamask Wallet
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -917,11 +924,11 @@ const Settings = (props) => {
|
||||
};
|
||||
|
||||
const handleGithubConnection = () => {
|
||||
console.log("GITHUB CONNECT WOO")
|
||||
console.log("GITHUB CONNECT WOO: ", isCloud)
|
||||
//result = RestClient.post('https://github.com/login/oauth/access_token',
|
||||
|
||||
console.log("HOST: ", window.location.host);
|
||||
console.log("HOST: ", window.location);
|
||||
console.log("Location: ", window.location);
|
||||
const redirectUri = isCloud
|
||||
? window.location.host === "localhost:3002"
|
||||
? "http%3A%2F%2Flocalhost:3002%2Fset_authentication"
|
||||
@@ -931,10 +938,11 @@ const Settings = (props) => {
|
||||
:
|
||||
`https%3A%2F%2F${window.location.host}%2Fset_authentication`
|
||||
|
||||
console.log("redirect: ", redirectUri)
|
||||
|
||||
const client_id = "3d272b1b782b100b1e61"
|
||||
const username = userdata.id;
|
||||
const scopes = "user:email";
|
||||
const scopes = "read:user";
|
||||
|
||||
const url = `https://github.com/login/oauth/authorize?access_type=offline&prompt=consent&client_id=${client_id}&redirect_uri=${redirectUri}&response_type=code&scope=${scopes}&state=username%3D${username}%26type%3Dgithub`
|
||||
|
||||
|
||||
+472
-136
@@ -6,12 +6,16 @@ import { Navigate } from "react-router-dom";
|
||||
|
||||
import SecurityFramework from '../components/SecurityFramework.jsx';
|
||||
import { ShepherdTour, ShepherdTourContext } from 'react-shepherd'
|
||||
import { isMobile } from "react-device-detect"
|
||||
|
||||
|
||||
import {
|
||||
Badge,
|
||||
Avatar,
|
||||
Grid,
|
||||
InputLabel,
|
||||
Select,
|
||||
ListSubheader,
|
||||
Paper,
|
||||
Tooltip,
|
||||
Divider,
|
||||
@@ -31,8 +35,15 @@ import {
|
||||
DialogTitle,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
OutlinedInput,
|
||||
Checkbox,
|
||||
ListItemText,
|
||||
} from "@material-ui/core";
|
||||
|
||||
import {
|
||||
AvatarGroup,
|
||||
} from "@mui/material"
|
||||
|
||||
import {
|
||||
GridOn as GridOnIcon,
|
||||
List as ListIcon,
|
||||
@@ -58,6 +69,8 @@ import {
|
||||
Publish as PublishIcon,
|
||||
CloudUpload as CloudUploadIcon,
|
||||
CloudDownload as CloudDownloadIcon,
|
||||
ExpandLess as ExpandLessIcon,
|
||||
ExpandMore as ExpandMoreIcon,
|
||||
} from "@material-ui/icons";
|
||||
|
||||
import NestedMenuItem from "material-ui-nested-menu-item";
|
||||
@@ -381,9 +394,27 @@ const chipStyle = {
|
||||
};
|
||||
|
||||
export const validateJson = (showResult) => {
|
||||
//showResult = showResult.split(" None").join(" \"None\"")
|
||||
showResult = showResult.split(" False").join(" false");
|
||||
showResult = showResult.split(" True").join(" true");
|
||||
//console.log("INPUT: ", showResult, typeof showResult)
|
||||
if (typeof showResult === 'string') {
|
||||
//showResult = showResult.split(" None").join(" \"None\"")
|
||||
showResult = showResult.split(" False").join(" false");
|
||||
showResult = showResult.split(" True").join(" true");
|
||||
//return {
|
||||
// valid: false,
|
||||
// result: showResult,
|
||||
//};
|
||||
}
|
||||
|
||||
//if (typeof showResult === undefined) {
|
||||
|
||||
//}
|
||||
|
||||
if (typeof showResult === "object" || typeof showResult === "array") {
|
||||
return {
|
||||
valid: true,
|
||||
result: showResult,
|
||||
};
|
||||
}
|
||||
|
||||
var jsonvalid = true;
|
||||
try {
|
||||
@@ -453,6 +484,8 @@ const Workflows = (props) => {
|
||||
var upload = "";
|
||||
|
||||
const [workflows, setWorkflows] = React.useState([]);
|
||||
const [_, setUpdate] = React.useState(""); // Used for rendering, don't remove
|
||||
const [selectedUsecases, setSelectedUsecases] = React.useState([]);
|
||||
const [filteredWorkflows, setFilteredWorkflows] = React.useState([]);
|
||||
const [selectedWorkflow, setSelectedWorkflow] = React.useState({});
|
||||
const [workflowDone, setWorkflowDone] = React.useState(false);
|
||||
@@ -488,6 +521,8 @@ const Workflows = (props) => {
|
||||
const [actionImageList, setActionImageList] = React.useState([]);
|
||||
|
||||
const [firstLoad, setFirstLoad] = React.useState(true);
|
||||
const [showMoreClicked, setShowMoreClicked] = React.useState(false);
|
||||
const [usecases, setUsecases] = React.useState([]);
|
||||
|
||||
const isCloud =
|
||||
window.location.host === "localhost:3002" ||
|
||||
@@ -834,7 +869,7 @@ const Workflows = (props) => {
|
||||
console.log("Status not 200 for workflows :O!: ", response.status);
|
||||
|
||||
if (isCloud) {
|
||||
window.location.pathname = "/login";
|
||||
window.location.pathname = "/search?tab=workflows";
|
||||
}
|
||||
|
||||
alert.info("Failed getting workflows.");
|
||||
@@ -847,8 +882,10 @@ const Workflows = (props) => {
|
||||
.then((responseJson) => {
|
||||
if (responseJson !== undefined) {
|
||||
setWorkflows(responseJson);
|
||||
fetchUsecases(responseJson)
|
||||
|
||||
if (responseJson !== undefined) {
|
||||
|
||||
var actionnamelist = [];
|
||||
var parsedactionlist = [];
|
||||
for (var key in responseJson) {
|
||||
@@ -888,6 +925,83 @@ const Workflows = (props) => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleKeysetting = (categorydata, workflows) => {
|
||||
console.log("Workflows: ", 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)
|
||||
}
|
||||
|
||||
console.log("Categories: ", newcategories)
|
||||
setUsecases(newcategories)
|
||||
} else {
|
||||
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());
|
||||
});
|
||||
};
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
useEffect(() => {
|
||||
if (workflows.length <= 0) {
|
||||
@@ -896,7 +1010,6 @@ const Workflows = (props) => {
|
||||
setView(tmpView);
|
||||
}
|
||||
|
||||
//setFirstrequest(false);
|
||||
getAvailableWorkflows();
|
||||
}
|
||||
}, [])
|
||||
@@ -905,8 +1018,8 @@ const Workflows = (props) => {
|
||||
color: "#ffffff",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
minWidth: 1024,
|
||||
maxWidth: 1024,
|
||||
minWidth: isMobile ? "100%" : 1024,
|
||||
maxWidth: isMobile ? "100%" : 1024,
|
||||
margin: "auto",
|
||||
};
|
||||
|
||||
@@ -926,6 +1039,7 @@ const Workflows = (props) => {
|
||||
flexDirection: "column",
|
||||
};
|
||||
|
||||
//flexDirection: !isMobile ? "column" : "row",
|
||||
const paperAppContainer = {
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
@@ -1270,7 +1384,7 @@ const Workflows = (props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid item xs={4} style={{ padding: "12px 10px 12px 10px" }}>
|
||||
<Grid item xs={isMobile ? 12 : 4} style={{ padding: "12px 10px 12px 10px" }}>
|
||||
<Paper
|
||||
square
|
||||
style={setupPaperStyle}
|
||||
@@ -1292,6 +1406,30 @@ const Workflows = (props) => {
|
||||
);
|
||||
};
|
||||
|
||||
const getWorkflowAppgroup = (data) => {
|
||||
if (data.actions === undefined || data.actions === null) {
|
||||
return []
|
||||
}
|
||||
|
||||
var appsFound = []
|
||||
for (var key in data.actions) {
|
||||
const parsedAction = data.actions[key]
|
||||
if (parsedAction.large_image === undefined || parsedAction.large_image === null || parsedAction.large_image === "") {
|
||||
continue
|
||||
}
|
||||
|
||||
if (parsedAction.app_name === "Shuffle Tools" || parsedAction.app_id === "bc78f35c6c6351b07a09b7aed5d29652") {
|
||||
continue
|
||||
}
|
||||
|
||||
if (appsFound.findIndex(data => data.app_name === parsedAction.app_name) < 0){
|
||||
appsFound.push(parsedAction)
|
||||
}
|
||||
}
|
||||
|
||||
return appsFound
|
||||
}
|
||||
|
||||
const WorkflowPaper = (props) => {
|
||||
const { data } = props;
|
||||
const [open, setOpen] = React.useState(false);
|
||||
@@ -1321,6 +1459,7 @@ const Workflows = (props) => {
|
||||
}
|
||||
|
||||
const actions = data.actions !== null ? data.actions.length : 0;
|
||||
const appGroup = getWorkflowAppgroup(data)
|
||||
const [triggers, subflows] = getWorkflowMeta(data);
|
||||
|
||||
const workflowMenuButtons = (
|
||||
@@ -1345,6 +1484,11 @@ const Workflows = (props) => {
|
||||
if (data.tags !== undefined && data.tags !== null) {
|
||||
setNewWorkflowTags(JSON.parse(JSON.stringify(data.tags)));
|
||||
}
|
||||
|
||||
console.log("Editing: ", data)
|
||||
if (data.usecase_ids !== undefined && data.usecase_ids !== null && data.usecase_ids.length > 0) {
|
||||
setSelectedUsecases(data.usecase_ids)
|
||||
}
|
||||
}}
|
||||
key={"change"}
|
||||
>
|
||||
@@ -1543,22 +1687,49 @@ const Workflows = (props) => {
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
<Grid item style={workflowActionStyle}>
|
||||
<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",
|
||||
}}
|
||||
>
|
||||
{actions}
|
||||
</Typography>
|
||||
</span>
|
||||
</Tooltip>
|
||||
{appGroup.length > 0 ?
|
||||
<div style={{display: "flex", marginTop: 8, }}>
|
||||
<AvatarGroup max={4} style={{marginLeft: 5, maxHeight: 24,}}>
|
||||
{appGroup.map((data, index) => {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
height: 24,
|
||||
width: 24,
|
||||
filter: "brightness(0.6)",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => {
|
||||
addFilter(data.app_name);
|
||||
}}
|
||||
>
|
||||
<Tooltip color="primary" title={data.app_name} placement="bottom">
|
||||
<Avatar alt={data.app_name} src={data.large_image} 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",
|
||||
}}
|
||||
>
|
||||
{actions}
|
||||
</Typography>
|
||||
</span>
|
||||
</Tooltip>
|
||||
}
|
||||
<Tooltip
|
||||
color="primary"
|
||||
title="Trigger amount"
|
||||
@@ -1661,9 +1832,11 @@ const Workflows = (props) => {
|
||||
justifyContent: "left",
|
||||
overflow: "hidden",
|
||||
marginTop: 5,
|
||||
maxHeight: 28,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
{data.tags !== undefined
|
||||
{data.tags !== undefined && data.tags !== null
|
||||
? data.tags.map((tag, index) => {
|
||||
if (index >= 3) {
|
||||
return null;
|
||||
@@ -1709,7 +1882,8 @@ const Workflows = (props) => {
|
||||
tags,
|
||||
defaultReturnValue,
|
||||
editingWorkflow,
|
||||
redirect
|
||||
redirect,
|
||||
currentUsecases,
|
||||
) => {
|
||||
var method = "POST";
|
||||
var extraData = "";
|
||||
@@ -1736,6 +1910,12 @@ const Workflows = (props) => {
|
||||
workflowdata["default_return_value"] = defaultReturnValue;
|
||||
}
|
||||
|
||||
if (currentUsecases !== undefined && currentUsecases !== null) {
|
||||
workflowdata["usecase_ids"] = currentUsecases
|
||||
//workflows[0].category = ["detect"]
|
||||
//workflows[0].usecase_ids = ["Correlate tickets"]
|
||||
}
|
||||
|
||||
return fetch(globalUrl + "/api/v1/workflows" + extraData, {
|
||||
method: method,
|
||||
headers: {
|
||||
@@ -1982,30 +2162,54 @@ const Workflows = (props) => {
|
||||
const data = params.row.record;
|
||||
const actions = data.actions !== null ? data.actions.length : 0;
|
||||
let [triggers, subflows] = getWorkflowMeta(data);
|
||||
const appGroup = getWorkflowAppgroup(data)
|
||||
|
||||
return (
|
||||
<Grid item>
|
||||
<div style={{ display: "flex" }}>
|
||||
<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",
|
||||
}}
|
||||
>
|
||||
{actions}
|
||||
</Typography>
|
||||
</span>
|
||||
</Tooltip>
|
||||
{appGroup.length > 0 ?
|
||||
<div style={{display: "flex", marginTop: 3, }}>
|
||||
<AvatarGroup max={4} style={{marginLeft: 5, maxHeight: 24,}}>
|
||||
{appGroup.map((data, index) => {
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
height: 24,
|
||||
width: 24,
|
||||
filter: "brightness(0.6)",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => {
|
||||
addFilter(data.app_name);
|
||||
}}
|
||||
>
|
||||
<Tooltip color="primary" title={data.app_name} placement="bottom">
|
||||
<Avatar alt={data.app_name} src={data.large_image} 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",
|
||||
}}
|
||||
>
|
||||
{actions}
|
||||
</Typography>
|
||||
</span>
|
||||
</Tooltip>
|
||||
}
|
||||
<Tooltip
|
||||
color="primary"
|
||||
title="Trigger amount"
|
||||
@@ -2189,6 +2393,7 @@ const Workflows = (props) => {
|
||||
return <div style={gridContainer}>{workflowData}</div>;
|
||||
};
|
||||
|
||||
var total_count = 0
|
||||
const modalView = modalOpen ? (
|
||||
<Dialog
|
||||
open={modalOpen}
|
||||
@@ -2199,7 +2404,8 @@ const Workflows = (props) => {
|
||||
style: {
|
||||
backgroundColor: surfaceColor,
|
||||
color: "white",
|
||||
minWidth: "800px",
|
||||
minWidth: isMobile ? "90%" : "800px",
|
||||
maxWidth: isMobile ? "90%" : "800px",
|
||||
},
|
||||
}}
|
||||
>
|
||||
@@ -2231,6 +2437,7 @@ const Workflows = (props) => {
|
||||
}}
|
||||
color="primary"
|
||||
placeholder="Name"
|
||||
required
|
||||
margin="dense"
|
||||
defaultValue={newWorkflowName}
|
||||
autoFocus
|
||||
@@ -2246,47 +2453,117 @@ const Workflows = (props) => {
|
||||
color="primary"
|
||||
defaultValue={newWorkflowDescription}
|
||||
placeholder="Description"
|
||||
rows="3"
|
||||
multiline
|
||||
margin="dense"
|
||||
fullWidth
|
||||
/>
|
||||
<ChipInput
|
||||
style={{ marginTop: 10 }}
|
||||
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);
|
||||
}}
|
||||
/>
|
||||
<div style={{display: "flex", marginTop: 10, }}>
|
||||
<ChipInput
|
||||
style={{ flex: 1}}
|
||||
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 ?
|
||||
<span>
|
||||
<TextField
|
||||
onBlur={(event) => setDefaultReturnValue(event.target.value)}
|
||||
InputProps={{
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
color="primary"
|
||||
defaultValue={defaultReturnValue}
|
||||
placeholder="Default return value (used for Subflows if the subflow fails)"
|
||||
rows="3"
|
||||
multiline
|
||||
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>
|
||||
|
||||
<TextField
|
||||
onBlur={(event) => setDefaultReturnValue(event.target.value)}
|
||||
InputProps={{
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
color="primary"
|
||||
defaultValue={defaultReturnValue}
|
||||
placeholder="Default return value (used for Subflows if the subflow fails)"
|
||||
rows="3"
|
||||
multiline
|
||||
margin="dense"
|
||||
fullWidth
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
@@ -2298,6 +2575,7 @@ const Workflows = (props) => {
|
||||
setEditingWorkflow({});
|
||||
setNewWorkflowTags([]);
|
||||
setModalOpen(false);
|
||||
setSelectedUsecases([])
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
@@ -2316,8 +2594,10 @@ const Workflows = (props) => {
|
||||
newWorkflowTags,
|
||||
defaultReturnValue,
|
||||
editingWorkflow,
|
||||
false
|
||||
false,
|
||||
selectedUsecases,
|
||||
);
|
||||
|
||||
setNewWorkflowName("");
|
||||
setDefaultReturnValue("");
|
||||
setNewWorkflowDescription("");
|
||||
@@ -2330,11 +2610,13 @@ const Workflows = (props) => {
|
||||
newWorkflowTags,
|
||||
defaultReturnValue,
|
||||
{},
|
||||
true
|
||||
true,
|
||||
selectedUsecases,
|
||||
);
|
||||
}
|
||||
|
||||
setSubmitLoading(true);
|
||||
setSelectedUsecases([])
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
@@ -2366,7 +2648,7 @@ const Workflows = (props) => {
|
||||
{view === "list" && (
|
||||
<Tooltip color="primary" title={"Grid View"} placement="top">
|
||||
<Button
|
||||
color="primary"
|
||||
color="secondary"
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
localStorage.setItem("view", "grid");
|
||||
@@ -2380,7 +2662,7 @@ const Workflows = (props) => {
|
||||
{view === "grid" && (
|
||||
<Tooltip color="primary" title={"List View"} placement="top">
|
||||
<Button
|
||||
color="primary"
|
||||
color="secondary"
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
localStorage.setItem("view", "list");
|
||||
@@ -2393,12 +2675,12 @@ const Workflows = (props) => {
|
||||
)}
|
||||
<Tooltip color="primary" title={"Import workflows"} placement="top">
|
||||
{importLoading ? (
|
||||
<Button color="primary" style={{}} variant="text" onClick={() => {}}>
|
||||
<Button color="secondary" style={{}} variant="text" onClick={() => {}}>
|
||||
<CircularProgress style={{ maxHeight: 15, maxWidth: 15 }} />
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
color="primary"
|
||||
color="secondary"
|
||||
style={{}}
|
||||
variant="text"
|
||||
onClick={() => upload.click()}
|
||||
@@ -2421,7 +2703,7 @@ const Workflows = (props) => {
|
||||
placement="top"
|
||||
>
|
||||
<Button
|
||||
color="primary"
|
||||
color="secondary"
|
||||
style={{}}
|
||||
variant="text"
|
||||
onClick={() => {
|
||||
@@ -2433,9 +2715,9 @@ const Workflows = (props) => {
|
||||
</Tooltip>
|
||||
) : null}
|
||||
{isCloud ? null : (
|
||||
<Tooltip color="primary" title={"Download workflows"} placement="top">
|
||||
<Tooltip color="primary" title={"Import workflows to Shuffle"} placement="top">
|
||||
<Button
|
||||
color="primary"
|
||||
color="secondary"
|
||||
style={{}}
|
||||
variant="text"
|
||||
onClick={() => setLoadWorkflowsModalOpen(true)}
|
||||
@@ -2606,9 +2888,54 @@ const Workflows = (props) => {
|
||||
return (
|
||||
<div style={viewStyle}>
|
||||
<div style={workflowViewStyle}>
|
||||
<div style={{ display: "flex" }}>
|
||||
<div style={{ flex: 3 }}>
|
||||
<h2>Workflows</h2>
|
||||
<div style={{ display: "flex", marginTop: 25, }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<Typography variant="h1" style={{fontSize: 30}}>
|
||||
Workflows
|
||||
</Typography>
|
||||
</div>
|
||||
{/*
|
||||
<div style={{ flex: 1 }}>
|
||||
<Typography style={{ marginTop: 7, marginBottom: "auto" }}>
|
||||
<a
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
href="https://shuffler.io/docs/workflows"
|
||||
style={{ textDecoration: "none", color: "#f85a3e" }}
|
||||
>
|
||||
Learn more about Workflows
|
||||
</a>
|
||||
</Typography>
|
||||
</div>
|
||||
*/}
|
||||
{isMobile ? null :
|
||||
<div style={{ display: "flex", margin: "0px 0px 20px 0px" }}>
|
||||
<div style={{ flex: 1, float: "right" }}>
|
||||
<ChipInput
|
||||
style={{}}
|
||||
InputProps={{
|
||||
style: {
|
||||
color: "white",
|
||||
maxWidth: 275,
|
||||
minWidth: 275,
|
||||
},
|
||||
}}
|
||||
placeholder="Add Filter"
|
||||
color="primary"
|
||||
fullWidth
|
||||
value={filters}
|
||||
onAdd={(chip) => {
|
||||
addFilter(chip);
|
||||
}}
|
||||
onDelete={(_, index) => {
|
||||
removeFilter(index);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div style={{ flex: 1, textAlign: "right" }}>
|
||||
{workflowButtons}
|
||||
</div>
|
||||
</div>
|
||||
{/*
|
||||
@@ -2658,43 +2985,52 @@ const Workflows = (props) => {
|
||||
)
|
||||
}}
|
||||
*/}
|
||||
<div style={{ display: "flex", margin: "0px 0px 20px 0px" }}>
|
||||
<div style={{ flex: 1 }}>
|
||||
<Typography style={{ marginTop: 7, marginBottom: "auto" }}>
|
||||
<a
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
href="https://shuffler.io/docs/workflows"
|
||||
style={{ textDecoration: "none", color: "#f85a3e" }}
|
||||
>
|
||||
Learn more about Workflows
|
||||
</a>
|
||||
</Typography>
|
||||
</div>
|
||||
<div style={{ flex: 1, float: "right" }}>
|
||||
<ChipInput
|
||||
style={{}}
|
||||
InputProps={{
|
||||
style: {
|
||||
color: "white",
|
||||
},
|
||||
}}
|
||||
placeholder="Add Filter"
|
||||
color="primary"
|
||||
fullWidth
|
||||
value={filters}
|
||||
onAdd={(chip) => {
|
||||
addFilter(chip);
|
||||
}}
|
||||
onDelete={(_, index) => {
|
||||
removeFilter(index);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ float: "right", flex: 1, textAlign: "right" }}>
|
||||
{workflowButtons}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{width: "100%",}}>
|
||||
{!isMobile && usecases !== null && usecases !== undefined && usecases.length > 0 ?
|
||||
<div style={{ display: "flex", }}>
|
||||
{usecases.map((usecase, index) => {
|
||||
//console.log(usecase)
|
||||
return (
|
||||
<Paper
|
||||
key={usecase.name}
|
||||
style={{
|
||||
flex: 1,
|
||||
backgroundColor: filters.includes(usecase.name.toLowerCase()) ? usecase.color : theme.palette.surfaceColor,
|
||||
borderRadius: theme.palette.borderRadius,
|
||||
marginRight: index === usecases.length-1 ? 0 : 10,
|
||||
height: 60,
|
||||
cursor: "pointer",
|
||||
border: `2px solid ${usecase.color}`,
|
||||
overflow: "hidden",
|
||||
padding: 10,
|
||||
}}
|
||||
onClick={() => {
|
||||
console.log("Clicked!")
|
||||
return
|
||||
if (filters.includes(usecase.name.toLowerCase())) {
|
||||
addFilter(usecase.name)
|
||||
} else {
|
||||
const foundIndex = filters.indexOf(usecase.name.toLowerCase())
|
||||
removeFilter(foundIndex)
|
||||
}
|
||||
|
||||
}}
|
||||
>
|
||||
<a href={`/usecases?selected=${usecase.name}`} rel="noopener noreferrer" target="_blank" style={{ textDecoration: "none", }}>
|
||||
<Typography variant="body1" color="textPrimary">
|
||||
{usecase.name}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
In use: {usecase.matches.length}/{usecase.list.length}
|
||||
</Typography>
|
||||
</a>
|
||||
</Paper>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
: null}
|
||||
</div>
|
||||
<div style={{ marginTop: 15 }} />
|
||||
{actionImageList !== undefined &&
|
||||
actionImageList !== null &&
|
||||
@@ -2805,7 +3141,7 @@ const Workflows = (props) => {
|
||||
workflowDelay += 75
|
||||
} else {
|
||||
return (
|
||||
<Grid item xs={4} style={{ padding: "12px 10px 12px 10px" }}>
|
||||
<Grid key={index} item xs={isMobile ? 12 : 4} style={{ padding: "12px 10px 12px 10px" }}>
|
||||
<WorkflowPaper key={index} data={data} />
|
||||
</Grid>
|
||||
)
|
||||
@@ -2813,7 +3149,7 @@ const Workflows = (props) => {
|
||||
|
||||
return (
|
||||
<Zoom key={index} in={true} style={{ transitionDelay: `${workflowDelay}ms` }}>
|
||||
<Grid item xs={4} style={{ padding: "12px 10px 12px 10px" }}>
|
||||
<Grid item xs={isMobile ? 12 : 4} style={{ padding: "12px 10px 12px 10px" }}>
|
||||
<WorkflowPaper key={index} data={data} />
|
||||
</Grid>
|
||||
</Zoom>
|
||||
@@ -3008,7 +3344,7 @@ const Workflows = (props) => {
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
Submit Submit
|
||||
Submit
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
@@ -3024,7 +3360,7 @@ const Workflows = (props) => {
|
||||
*/}
|
||||
<Dropzone
|
||||
style={{
|
||||
maxWidth: window.innerWidth > 1366 ? 1366 : 1200,
|
||||
maxWidth: window.innerWidth > 1366 ? 1366 : isMobile ? "100%" : 1200,
|
||||
margin: "auto",
|
||||
padding: 20,
|
||||
}}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
# AWS Lambda forwarder to Shuffle
|
||||
This function is made to forward S3 notifications to Shuffle to run a workflow when an object is made or updated.
|
||||
@@ -0,0 +1,21 @@
|
||||
import json
|
||||
import urllib.parse
|
||||
import urllib3
|
||||
import os
|
||||
|
||||
print('Loading function')
|
||||
|
||||
def lambda_handler(event, context):
|
||||
# Get the object from the event and show its content type
|
||||
bucket = event['Records'][0]['s3']['bucket']['name']
|
||||
|
||||
webhook = os.environ.get("SHUFFLE_WEBHOOK")
|
||||
if not webhook:
|
||||
return "No webhook environment defined: SHUFFLE_WEBHOOK"
|
||||
|
||||
http = urllib3.PoolManager()
|
||||
ret = http.request('POST', webhook, body=json.dumps(event["Records"][0]).encode("utf-8"))
|
||||
if ret.status != 200:
|
||||
return "Bad status code for webhook: %d" % ret.status_code
|
||||
|
||||
print("Status code: %d\nData: %s" % (ret.status, ret.data))
|
||||
@@ -1,5 +1,5 @@
|
||||
NAME=shuffle-orborus
|
||||
VERSION=0.9.50
|
||||
VERSION=0.9.61
|
||||
|
||||
echo "Running docker build with $NAME:$VERSION"
|
||||
#docker rmi frikky/shuffle:$NAME --force
|
||||
|
||||
@@ -37,9 +37,11 @@ import (
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"github.com/docker/docker/api/types/network"
|
||||
"github.com/docker/docker/api/types/swarm"
|
||||
|
||||
//"github.com/docker/docker/api/types/filters"
|
||||
dockerclient "github.com/docker/docker/client"
|
||||
"github.com/satori/go.uuid"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
|
||||
//network "github.com/docker/docker/api/types/network"
|
||||
//natting "github.com/docker/go-connections/nat"
|
||||
"github.com/mackerelio/go-osstat/cpu"
|
||||
@@ -63,7 +65,7 @@ var baseimagename = os.Getenv("SHUFFLE_BASE_IMAGE_NAME")
|
||||
var baseimageregistry = os.Getenv("SHUFFLE_BASE_IMAGE_REGISTRY")
|
||||
var baseimagetagsuffix = os.Getenv("SHUFFLE_BASE_IMAGE_TAG_SUFFIX")
|
||||
|
||||
var orgId = os.Getenv("ORG_ID")
|
||||
//var orgId = os.Getenv("ORG_ID")
|
||||
var baseUrl = os.Getenv("BASE_URL")
|
||||
var environment = os.Getenv("ENVIRONMENT_NAME")
|
||||
var dockerApiVersion = os.Getenv("DOCKER_API_VERSION")
|
||||
@@ -199,7 +201,7 @@ func deployServiceWorkers(image string) {
|
||||
// Looks for and cleans up all existing items in swarm we can't re-use (Shuffle only)
|
||||
cleanupExistingNodes(ctx)
|
||||
// frikky@debian:~/git/shuffle/functions/onprem/worker$ docker service create --replicas 5 --name shuffle-workers --env SHUFFLE_SWARM_CONFIG=run --publish published=33333,target=33333 ghcr.io/frikky/shuffle-worker:nightly
|
||||
networkName := "shuffle-executions"
|
||||
networkName := "shuffle_swarm_executions"
|
||||
if len(swarmNetworkName) > 0 {
|
||||
networkName = swarmNetworkName
|
||||
}
|
||||
@@ -538,8 +540,14 @@ func deployWorker(image string, identifier string, env []string, executionReques
|
||||
nil,
|
||||
identifier+"-2",
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed to CREATE container (2): %s", err)
|
||||
}
|
||||
|
||||
err = dockercli.ContainerStart(context.Background(), cont.ID, containerStartOptions)
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed to start container (2): %s", err)
|
||||
}
|
||||
} else {
|
||||
log.Printf("[ERROR] Failed initial container start. Quitting as this is NOT a simple network issue. Err: %s", err)
|
||||
}
|
||||
@@ -759,8 +767,12 @@ func main() {
|
||||
//baseUrl = "http://localhost:5001"
|
||||
}
|
||||
|
||||
if orgId == "" {
|
||||
log.Printf("[ERROR] Org not defined. Set variable ORG_ID based on your org")
|
||||
//if orgId == "" {
|
||||
// log.Printf("[ERROR] Org not defined. Set variable ORG_ID based on your org")
|
||||
// os.Exit(3)
|
||||
//}
|
||||
if environment == "" {
|
||||
log.Printf("[ERROR] Environment not defined. Set variable ENVIRONMENT_NAME to configure it.")
|
||||
os.Exit(3)
|
||||
}
|
||||
|
||||
@@ -797,7 +809,7 @@ func main() {
|
||||
// Run by default from now
|
||||
zombiecheck(ctx, workerTimeout)
|
||||
|
||||
log.Printf("[INFO] Running towards %s with Org %s", baseUrl, orgId)
|
||||
log.Printf("[INFO] Running towards %s (BASE_URL) with environment name %s", baseUrl, environment)
|
||||
httpProxy := os.Getenv("HTTP_PROXY")
|
||||
httpsProxy := os.Getenv("HTTPS_PROXY")
|
||||
|
||||
@@ -845,6 +857,8 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
client.Timeout = 10 * time.Second
|
||||
|
||||
fullUrl := fmt.Sprintf("%s/api/v1/workflows/queue", baseUrl)
|
||||
req, err := http.NewRequest(
|
||||
"GET",
|
||||
@@ -859,8 +873,8 @@ func main() {
|
||||
|
||||
zombiecounter := 0
|
||||
req.Header.Add("Content-Type", "application/json")
|
||||
req.Header.Add("Org-Id", orgId)
|
||||
log.Printf("[INFO] Waiting for executions at %s with Org ID %s", fullUrl, orgId)
|
||||
req.Header.Add("Org-Id", environment)
|
||||
log.Printf("[INFO] Waiting for executions at %s with Environment %s", fullUrl, environment)
|
||||
hasStarted := false
|
||||
for {
|
||||
//go getStats()
|
||||
@@ -1045,7 +1059,7 @@ func main() {
|
||||
}
|
||||
|
||||
result.Header.Add("Content-Type", "application/json")
|
||||
result.Header.Add("Org-Id", orgId)
|
||||
result.Header.Add("Org-Id", environment)
|
||||
|
||||
resultResp, err := client.Do(result)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
#
|
||||
# curl -H "Org-id: Shuffle" --proxy "http://192.168.86.45:8081" http://192.168.86.45:5001/api/v1/workflows/queue
|
||||
|
||||
import SocketServer
|
||||
import SimpleHTTPServer
|
||||
import requests
|
||||
import json
|
||||
|
||||
PORT = 8082
|
||||
|
||||
class MyProxy(SimpleHTTPServer.SimpleHTTPRequestHandler):
|
||||
def do_GET(self):
|
||||
url=self.path[:]
|
||||
allheaders = {}
|
||||
for item in ("%s" % self.headers).split("\n"):
|
||||
headersplit = item.split(":")
|
||||
if len(headersplit) == 2:
|
||||
allheaders[headersplit[0]] = (headersplit[1][:-1]).strip()
|
||||
|
||||
ret = requests.get(url, headers=allheaders)
|
||||
print("RESP (%s) - %d - %s" % (url, ret.status_code, ret.text))
|
||||
|
||||
self.send_response(ret.status_code)
|
||||
self.end_headers()
|
||||
self.wfile.write(ret.text)
|
||||
|
||||
def do_POST(self):
|
||||
url=self.path[:]
|
||||
allheaders = {}
|
||||
for item in ("%s" % self.headers).split("\n"):
|
||||
headersplit = item.split(":")
|
||||
if len(headersplit) == 2:
|
||||
allheaders[headersplit[0]] = (headersplit[1][:-1]).strip()
|
||||
|
||||
length = int(self.headers.getheader('content-length'))
|
||||
try:
|
||||
message = json.loads(self.rfile.read(length))
|
||||
print("Got message: %s" % message)
|
||||
ret = requests.post(url, headers=allheaders, json=message)
|
||||
except:
|
||||
message = self.rfile.read(length)
|
||||
print("Got message: %s" % message)
|
||||
ret = requests.post(url, headers=allheaders, data=message)
|
||||
|
||||
print("RESP (%s) - %d - %s" % (url, ret.status_code, ret.text))
|
||||
|
||||
self.send_response(ret.status_code)
|
||||
self.end_headers()
|
||||
self.wfile.write(ret.text)
|
||||
|
||||
httpd = SocketServer.ForkingTCPServer(('', PORT), MyProxy)
|
||||
print("Now serving at %d" % PORT)
|
||||
httpd.serve_forever()
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
docker run \
|
||||
--env ORG_ID=$ORG_ID \
|
||||
--env DOCKER_API_VERSION=1.40 \
|
||||
--env ENVIRONMENT_NAME="Shuffle" \
|
||||
--env BASE_URL=http://shuffle-backend:5001 \
|
||||
--env DOCKER_API_VERSION=1.42 \
|
||||
--env RUNNING_MODE="Docker" \
|
||||
--network "shuffle_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 \
|
||||
frikky/shuffle:orborus
|
||||
ghcr.io/frikky/shuffle-orborus:nightly
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
NAME=shuffle-worker
|
||||
VERSION=0.9.50
|
||||
VERSION=0.9.59
|
||||
|
||||
echo "Running docker build with $NAME:$VERSION"
|
||||
#CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o worker.bin .
|
||||
|
||||
@@ -595,6 +595,8 @@ github.com/shuffle/shuffle-shared v0.1.78 h1://YsgQ85Ep40AA3pLUXb+85BUrNz5sqGqh0
|
||||
github.com/shuffle/shuffle-shared v0.1.78/go.mod h1:cW8LBv8P24rCPyJqGV6czxqrpnrv/R1d97EOqNgIvSk=
|
||||
github.com/shuffle/shuffle-shared v0.1.81 h1:/lOt7NSuMTWlRzgOKg2e7j95eakg3MgW6i/4Fp30kd4=
|
||||
github.com/shuffle/shuffle-shared v0.1.81/go.mod h1:cW8LBv8P24rCPyJqGV6czxqrpnrv/R1d97EOqNgIvSk=
|
||||
github.com/shuffle/shuffle-shared v0.1.83 h1:xfmcqceBGXJVUyZNBmI+c6RKndrtKJyBIqdHD86v/XA=
|
||||
github.com/shuffle/shuffle-shared v0.1.83/go.mod h1:cW8LBv8P24rCPyJqGV6czxqrpnrv/R1d97EOqNgIvSk=
|
||||
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=
|
||||
|
||||
@@ -358,6 +358,7 @@ func deployApp(cli *dockerclient.Client, image string, identifier string, env []
|
||||
log.Printf("\n\n[DEBUG] Result for %s already found - returning\n\n", newExecId)
|
||||
return nil
|
||||
}
|
||||
|
||||
cacheData := []byte("1")
|
||||
err = shuffle.SetCache(ctx, newExecId, cacheData)
|
||||
if err != nil {
|
||||
@@ -371,17 +372,19 @@ func deployApp(cli *dockerclient.Client, image string, identifier string, env []
|
||||
waitTime := time.Duration(action.ExecutionDelay) * time.Second
|
||||
|
||||
time.AfterFunc(waitTime, func() {
|
||||
DeployContainer(ctx, cli, config, hostConfig, identifier, workflowExecution)
|
||||
DeployContainer(ctx, cli, config, hostConfig, identifier, workflowExecution, newExecId)
|
||||
})
|
||||
} else {
|
||||
log.Printf("[DEBUG] Running app %s in docker NORMALLY as there is no delay set", action.Name)
|
||||
return DeployContainer(ctx, cli, config, hostConfig, identifier, workflowExecution)
|
||||
log.Printf("[DEBUG] Running app %s in docker NORMALLY as there is no delay set with identifier %s", action.Name, identifier)
|
||||
returnvalue := DeployContainer(ctx, cli, config, hostConfig, identifier, workflowExecution, newExecId)
|
||||
log.Printf("[DEBUG] Normal deploy ret: %s", returnvalue)
|
||||
return returnvalue
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func DeployContainer(ctx context.Context, cli *dockerclient.Client, config *container.Config, hostConfig *container.HostConfig, identifier string, workflowExecution shuffle.WorkflowExecution) error {
|
||||
func DeployContainer(ctx context.Context, cli *dockerclient.Client, config *container.Config, hostConfig *container.HostConfig, identifier string, workflowExecution shuffle.WorkflowExecution, newExecId string) error {
|
||||
cont, err := cli.ContainerCreate(
|
||||
ctx,
|
||||
config,
|
||||
@@ -396,6 +399,11 @@ func DeployContainer(ctx context.Context, cli *dockerclient.Client, config *cont
|
||||
if !strings.Contains(err.Error(), "Conflict. The container name") {
|
||||
log.Printf("[ERROR] Container CREATE error (1): %s", err)
|
||||
|
||||
cacheErr := shuffle.DeleteCache(ctx, newExecId)
|
||||
if cacheErr != nil {
|
||||
log.Printf("[ERROR] FAILED Deleting cache for %s: %s", newExecId, cacheErr)
|
||||
}
|
||||
|
||||
return err
|
||||
} else {
|
||||
parsedUuid := uuid.NewV4()
|
||||
@@ -414,6 +422,12 @@ func DeployContainer(ctx context.Context, cli *dockerclient.Client, config *cont
|
||||
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Container create error (2): %s", err)
|
||||
|
||||
cacheErr := shuffle.DeleteCache(ctx, newExecId)
|
||||
if cacheErr != nil {
|
||||
log.Printf("[ERROR] FAILED Deleting cache for %s: %s", newExecId, cacheErr)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -445,6 +459,12 @@ func DeployContainer(ctx context.Context, cli *dockerclient.Client, config *cont
|
||||
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Container create error (3): %s", err)
|
||||
|
||||
cacheErr := shuffle.DeleteCache(ctx, newExecId)
|
||||
if cacheErr != nil {
|
||||
log.Printf("[ERROR] FAILED Deleting cache for %s: %s", newExecId, cacheErr)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -454,6 +474,12 @@ func DeployContainer(ctx context.Context, cli *dockerclient.Client, config *cont
|
||||
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed to start container in environment %s: %s", environment, err)
|
||||
|
||||
cacheErr := shuffle.DeleteCache(ctx, newExecId)
|
||||
if cacheErr != nil {
|
||||
log.Printf("[ERROR] FAILED Deleting cache for %s: %s", newExecId, cacheErr)
|
||||
}
|
||||
|
||||
//shutdown(workflowExecution, workflowExecution.Workflow.ID, true)
|
||||
return err
|
||||
}
|
||||
@@ -1163,7 +1189,6 @@ func handleExecutionResult(workflowExecution shuffle.WorkflowExecution) {
|
||||
// if everything is generated during execution
|
||||
//log.Printf("[DEBUG][%s] Deployed with CALLBACK_URL %s and BASE_URL %s", workflowExecution.ExecutionId, appCallbackUrl, baseUrl)
|
||||
env := []string{
|
||||
fmt.Sprintf("ACTION=%s", string(actionData)),
|
||||
fmt.Sprintf("EXECUTIONID=%s", workflowExecution.ExecutionId),
|
||||
fmt.Sprintf("AUTHORIZATION=%s", workflowExecution.Authorization),
|
||||
fmt.Sprintf("CALLBACK_URL=%s", baseUrl),
|
||||
@@ -1171,6 +1196,40 @@ func handleExecutionResult(workflowExecution shuffle.WorkflowExecution) {
|
||||
fmt.Sprintf("TZ=%s", timezone),
|
||||
}
|
||||
|
||||
if len(actionData) >= 100000 {
|
||||
log.Printf("[WARNING] Omitting some data from action execution. Length: %d. Fix in SDK!", len(actionData))
|
||||
newParams := []shuffle.WorkflowAppActionParameter{}
|
||||
for _, param := range action.Parameters {
|
||||
paramData, err := json.Marshal(param)
|
||||
if err != nil {
|
||||
log.Printf("[WARNING] Failed to marshal param %s: %s", param.Name, err)
|
||||
newParams = append(newParams, param)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(paramData) >= 50000 {
|
||||
log.Printf("[WARNING] Removing a lot of data from param %s with length %d", param.Name, len(paramData))
|
||||
param.Value = "SHUFFLE_AUTO_REMOVED"
|
||||
}
|
||||
|
||||
newParams = append(newParams, param)
|
||||
}
|
||||
|
||||
action.Parameters = newParams
|
||||
actionData, err = json.Marshal(action)
|
||||
if err == nil {
|
||||
log.Printf("[DEBUG] Ran data replace on action %s. new length: %d", action.Name, len(actionData))
|
||||
} else {
|
||||
log.Printf("[WARNING] Failed to marshal new actionData: %s", err)
|
||||
|
||||
}
|
||||
} else {
|
||||
log.Printf("[DEBUG] Actiondata is NOT 100000 in length. Adding as normal.")
|
||||
}
|
||||
|
||||
actionEnv := fmt.Sprintf("ACTION=%s", string(actionData))
|
||||
env = append(env, actionEnv)
|
||||
|
||||
if strings.ToLower(os.Getenv("SHUFFLE_PASS_APP_PROXY")) == "true" {
|
||||
//log.Printf("APPENDING PROXY TO THE APP!")
|
||||
env = append(env, fmt.Sprintf("HTTP_PROXY=%s", os.Getenv("HTTP_PROXY")))
|
||||
@@ -1296,6 +1355,7 @@ func handleExecutionResult(workflowExecution shuffle.WorkflowExecution) {
|
||||
} else {
|
||||
|
||||
err = deployApp(dockercli, images[0], identifier, env, workflowExecution, action)
|
||||
log.Printf("[DEBUG] Failed deploying app? %s", err)
|
||||
if err != nil && !strings.Contains(err.Error(), "Conflict. The container name") {
|
||||
if strings.Contains(err.Error(), "exited prematurely") {
|
||||
log.Printf("[DEBUG] Shutting down (9)")
|
||||
@@ -1517,7 +1577,7 @@ func executionInit(workflowExecution shuffle.WorkflowExecution) error {
|
||||
onpremApps := []string{}
|
||||
toExecuteOnprem := []string{}
|
||||
for _, action := range workflowExecution.Workflow.Actions {
|
||||
if action.Environment != environment {
|
||||
if strings.ToLower(action.Environment) != strings.ToLower(environment) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -1599,7 +1659,7 @@ func handleDefaultExecution(client *http.Client, req *http.Request, workflowExec
|
||||
|
||||
err := executionInit(workflowExecution)
|
||||
if err != nil {
|
||||
log.Printf("[INFO] Workflow setup failed: %s", workflowExecution.ExecutionId, err)
|
||||
log.Printf("[INFO] Workflow setup failed for %s: %s", workflowExecution.ExecutionId, err)
|
||||
log.Printf("[DEBUG] Shutting down (18)")
|
||||
shutdown(workflowExecution, "", "", true)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# Mindmap exporter
|
||||
Shuffle has a mindmap for Workflow use-cases. These can be changed and exported, with the most important piece being that they're explorable and editable. This has and will come in handy for us as we build it into the product.
|
||||
|
||||
https://www.mindmeister.com/map/2172644474
|
||||
|
||||
## Editing the Mindmap
|
||||
There are a few categories. To edit them, click the small plus next to the branch you want to change.
|
||||
|
||||
## Exporting the Mindmap
|
||||
Click "Export as RTF" in the top left corner of the URL. Download it there.
|
||||
|
||||
## Generating the Shuffle-comaptible mindmap
|
||||
1. Move the rtf file here
|
||||
2. Rename it categories.rtf
|
||||
3. Run the read_categories.py file (python3 read_categories.py)
|
||||
4. You now have a file called categories.json locally with all the categories in JSON format, ready to be used in graphs.
|
||||
@@ -0,0 +1,260 @@
|
||||
[
|
||||
{
|
||||
"name": "1. Collect & Distribute",
|
||||
"color": "#c51152",
|
||||
"list": [
|
||||
{
|
||||
"name": "2-way Ticket synchronization",
|
||||
"items": {}
|
||||
},
|
||||
{
|
||||
"name": "Email management",
|
||||
"items": {
|
||||
"name": "Release a quarantined message",
|
||||
"items": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "EDR to ticket",
|
||||
"items": {
|
||||
"name": "Get host information",
|
||||
"items": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "SIEM to ticket",
|
||||
"items": {}
|
||||
},
|
||||
{
|
||||
"name": "ChatOps",
|
||||
"items": {}
|
||||
},
|
||||
{
|
||||
"name": "Threat Intel received",
|
||||
"items": {}
|
||||
},
|
||||
{
|
||||
"name": "Domain investigation with LetsEncrypt",
|
||||
"items": {}
|
||||
},
|
||||
{
|
||||
"name": "Botnet tracker",
|
||||
"items": {}
|
||||
},
|
||||
{
|
||||
"name": "Get running containers",
|
||||
"items": {}
|
||||
},
|
||||
{
|
||||
"name": "Assign tickets",
|
||||
"items": {}
|
||||
},
|
||||
{
|
||||
"name": "Firewall alerts",
|
||||
"items": {
|
||||
"name": "URL filtering",
|
||||
"items": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "IDS/IPS alerts",
|
||||
"items": {
|
||||
"name": "Manage policies",
|
||||
"items": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Deduplicate information",
|
||||
"items": {}
|
||||
},
|
||||
{
|
||||
"name": "Correlate information",
|
||||
"items": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "2. Enrich",
|
||||
"color": "#f4c20d",
|
||||
"list": [
|
||||
{
|
||||
"name": "Internal Enrichment",
|
||||
"items": {
|
||||
"name": "...",
|
||||
"items": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "External historical Enrichment",
|
||||
"items": {
|
||||
"name": "...",
|
||||
"items": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Realtime",
|
||||
"items": {
|
||||
"name": "Analyze screenshots",
|
||||
"items": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Ticketing webhook verification",
|
||||
"items": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "3. Detect",
|
||||
"color": "#3cba54",
|
||||
"list": [
|
||||
{
|
||||
"name": "Search SIEM (Sigma)",
|
||||
"items": {
|
||||
"name": "Endpoint",
|
||||
"items": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Search EDR (OSQuery)",
|
||||
"items": {}
|
||||
},
|
||||
{
|
||||
"name": "Search emails (Phish)",
|
||||
"items": {
|
||||
"name": "Check headers and IOCs",
|
||||
"items": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Search IOCs (ioc-finder)",
|
||||
"items": {}
|
||||
},
|
||||
{
|
||||
"name": "Search files (Yara)",
|
||||
"items": {}
|
||||
},
|
||||
{
|
||||
"name": "Correlate tickets",
|
||||
"items": {}
|
||||
},
|
||||
{
|
||||
"name": "Honeypot access",
|
||||
"items": {
|
||||
"name": "...",
|
||||
"items": {}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "4. Respond",
|
||||
"color": "#4a148c",
|
||||
"list": [
|
||||
{
|
||||
"name": "Eradicate malware",
|
||||
"items": {}
|
||||
},
|
||||
{
|
||||
"name": "Quarantine host(s)",
|
||||
"items": {}
|
||||
},
|
||||
{
|
||||
"name": "Trigger scans",
|
||||
"items": {}
|
||||
},
|
||||
{
|
||||
"name": "Update indicators (FW, EDR, SIEM...)",
|
||||
"items": {}
|
||||
},
|
||||
{
|
||||
"name": "Autoblock activity when threat intel is received",
|
||||
"items": {}
|
||||
},
|
||||
{
|
||||
"name": "Lock/Delete/Reset account",
|
||||
"items": {}
|
||||
},
|
||||
{
|
||||
"name": "Lock vault",
|
||||
"items": {}
|
||||
},
|
||||
{
|
||||
"name": "Increase authentication",
|
||||
"items": {}
|
||||
},
|
||||
{
|
||||
"name": "Get policies from assets",
|
||||
"items": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "5. Verify",
|
||||
"color": "#4885ed",
|
||||
"list": [
|
||||
{
|
||||
"name": "Discover vulnerabilities",
|
||||
"items": {}
|
||||
},
|
||||
{
|
||||
"name": "Discover assets",
|
||||
"items": {}
|
||||
},
|
||||
{
|
||||
"name": "Ensure policies are followed",
|
||||
"items": {}
|
||||
},
|
||||
{
|
||||
"name": "Find Inactive users",
|
||||
"items": {}
|
||||
},
|
||||
{
|
||||
"name": "Ensure access rights match HR systems",
|
||||
"items": {}
|
||||
},
|
||||
{
|
||||
"name": "Ensure onboarding is followed",
|
||||
"items": {}
|
||||
},
|
||||
{
|
||||
"name": "Third party apps in SaaS",
|
||||
"items": {}
|
||||
},
|
||||
{
|
||||
"name": "Devices used for your cloud account",
|
||||
"items": {}
|
||||
},
|
||||
{
|
||||
"name": "Too much access in GCP/Azure/AWS/ other clouds",
|
||||
"items": {}
|
||||
},
|
||||
{
|
||||
"name": "Certificate validation",
|
||||
"items": {}
|
||||
},
|
||||
{
|
||||
"name": "Monitor new DNS entries for domain with passive DNS",
|
||||
"items": {}
|
||||
},
|
||||
{
|
||||
"name": "Monitor and track password dumps",
|
||||
"items": {}
|
||||
},
|
||||
{
|
||||
"name": "Monitor for mentions of domain on darknet sites",
|
||||
"items": {}
|
||||
},
|
||||
{
|
||||
"name": "Reporting",
|
||||
"items": {
|
||||
"name": "Monthly reports",
|
||||
"items": {
|
||||
"name": "...",
|
||||
"items": {}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,555 @@
|
||||
{\rtf1\ansi\deff0\deflang2057\plain\fs24\fet1
|
||||
{\fonttbl
|
||||
{\f0\froman Arial;}
|
||||
}
|
||||
{\info
|
||||
{\createim\yr2022\mo2\dy20\hr1\min15}
|
||||
}
|
||||
|
||||
\paperw11907\paperh16840\margl1800\margr1800\margt1440\margb1440
|
||||
\slmult0\ltrpar\li0
|
||||
{\b\fs28
|
||||
Shuffle categories
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li200
|
||||
{\fs24
|
||||
1. Collect & Distribute
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
2-way Ticket synchronization
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
Email management
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
Attachments
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
Manage senders
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
Manage URLs
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
Encode & Decode URLs
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
Release a quarantined message
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
EDR to ticket
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
Fetch incidents & events
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
Quarantine files
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
Quarantine host (respond)
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
Get host information
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
SIEM to ticket
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
ChatOps
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
Threat Intel received
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
Domain investigation with LetsEncrypt
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
Botnet tracker
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
Get running containers
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
Assign tickets
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
Firewall alerts
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
Block/accept policies
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
Add addresses and ports to groups
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
Support custom URL categories
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
Fetch logs for specific address
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
URL filtering
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
IDS/IPS alerts
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
Get/Fetch alerts
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
Receive alerts real-time
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
Get PCAP files
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
Get network logs
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
Manage policies
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
Deduplicate information
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
Correlate information
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li200
|
||||
{\fs24
|
||||
3. Detect
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
Search SIEM (Sigma)
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
Network
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
Endpoint
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
Search EDR (OSQuery)
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
Search emails (Phish)
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
Check malware
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
Check targeted
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
Check headers and IOCs
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
Search IOCs (ioc-finder)
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
Search files (Yara)
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
Correlate tickets
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
Honeypot access
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
S3 Honeypot
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
SSH Honeypot
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
FTP honeypot
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
Network honeypot
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
...
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li200
|
||||
{\fs24
|
||||
rich
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li200
|
||||
{\fs24
|
||||
5. Verify
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
Discover vulnerabilities
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
Discover assets
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
Ensure policies are followed
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
Find Inactive users
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
Ensure access rights match HR systems
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
Ensure onboarding is followed
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
Third party apps in SaaS
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
Devices used for your cloud account
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
Too much access in GCP/Azure/AWS/ other clouds
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
Certificate validation
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
Monitor new DNS entries for domain with passive DNS
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
Monitor and track password dumps
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
Monitor for mentions of domain on darknet sites
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
Reporting
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
Automation time saved
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
Automation money saved
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
Incident response report
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
Department cost
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
Monthly reports
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li800
|
||||
{\fs24
|
||||
EDR alerts
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li800
|
||||
{\fs24
|
||||
SIEM alerts
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li800
|
||||
{\fs24
|
||||
Emails quarantined
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li800
|
||||
{\fs24
|
||||
...
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li200
|
||||
{\fs24
|
||||
4. Respond
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
Eradicate malware
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
Quarantine host(s)
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
Trigger scans
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
Update indicators (FW, EDR, SIEM...)
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
Autoblock activity when threat intel is received
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
Lock/Delete/Reset account
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
Lock vault
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
Increase authentication
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
Get policies from assets
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li200
|
||||
{\fs24
|
||||
2. Enrich
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
Internal Enrichment
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
Users
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
Hostnames
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
IPs
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
Departments
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
Role
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
Software
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
...
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
External historical Enrichment
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
IPs
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
URLs
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
Hashes
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
Files
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
...
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
Realtime
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
File detonation
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
URL detonation
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
PCAP analysis
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li600
|
||||
{\fs24
|
||||
Analyze screenshots
|
||||
}
|
||||
\par\pard\plain
|
||||
\slmult0\ltrpar\li400
|
||||
{\fs24
|
||||
Ticketing webhook verification
|
||||
}
|
||||
\par\pard\plain
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
data = ""
|
||||
with open("categories.rtf", "r") as tmp:
|
||||
data = tmp.read()
|
||||
|
||||
fixed_json = []
|
||||
linearity = 0
|
||||
heading = ""
|
||||
subheading = ""
|
||||
subsubheading = ""
|
||||
|
||||
cnt = -1
|
||||
subcnt = -1
|
||||
|
||||
colors = ["#c51152", "#3cba54", "#4885ed", "#4a148c", "#f4c20d"]
|
||||
for line in data.split("\n"):
|
||||
if line == "rich":
|
||||
continue
|
||||
|
||||
if "li" in line:
|
||||
lisplit = line.split("\\")
|
||||
try:
|
||||
linearity = int(lisplit[-1][2])
|
||||
except:
|
||||
pass
|
||||
|
||||
#print("Linearity: %s" % linearity)
|
||||
|
||||
if line.startswith("{") or line.startswith("}"):
|
||||
continue
|
||||
|
||||
if line.startswith("\\"):
|
||||
continue
|
||||
|
||||
if linearity == 0:
|
||||
continue
|
||||
|
||||
if linearity == 2:
|
||||
#if cnt >= 0:
|
||||
# for key, value in fixed_json[cnt].items():
|
||||
# print(key, value)
|
||||
|
||||
|
||||
cnt += 1
|
||||
subcnt = -1
|
||||
fixed_json.append({"name": line, "color": colors[cnt], "list": []})
|
||||
heading = line
|
||||
elif linearity == 4:
|
||||
subheading = line
|
||||
fixed_json[cnt]["list"].append({"name": line, "items": {}})
|
||||
subcnt += 1
|
||||
elif linearity == 6:
|
||||
fixed_json[cnt]["list"][subcnt]["items"] = {"name": line, "items": {}}
|
||||
elif linearity == 8:
|
||||
fixed_json[cnt]["list"][subcnt]["items"]["items"] = {"name": line, "items": {}}
|
||||
else:
|
||||
print("No handler for %s" % line)
|
||||
|
||||
#print(line)
|
||||
#print(data)
|
||||
import json
|
||||
filename = "categories.json"
|
||||
fixed_json.sort(key=lambda x: x["name"])
|
||||
with open(filename, "w+") as tmp:
|
||||
tmp.write(json.dumps(fixed_json, indent=4))
|
||||
|
||||
print("Wrote to file %s" % filename)
|
||||
Reference in New Issue
Block a user