Merge pull request #1283 from Shuffle/1.4.0

1.3.1
This commit is contained in:
Frikky
2023-12-07 22:40:41 +01:00
committed by GitHub
35 changed files with 3726 additions and 1719 deletions
+2 -2
View File
@@ -70,7 +70,7 @@ SHUFFLE_SWARM_BRIDGE_DEFAULT_MTU=1500 # 1500 by default
# Used for auto-cleanup of containers. REALLY important at scale. Set to false to see all container info.
SHUFFLE_MEMCACHED=
SHUFFLE_CONTAINER_AUTO_CLEANUP=true
SHUFFLE_ORBORUS_EXECUTION_CONCURRENCY=3 # The amount of concurrent executions Orborus can handle. This is a soft limit, but it's recommended to keep it low.
SHUFFLE_ORBORUS_EXECUTION_CONCURRENCY=5 # The amount of concurrent executions Orborus can handle. This is a soft limit, but it's recommended to keep it low.
SHUFFLE_HEALTHCHECK_DISABLED=false
SHUFFLE_ELASTIC=true
SHUFFLE_LOGS_DISABLED=false
@@ -93,4 +93,4 @@ SHUFFLE_OPENSEARCH_PROXY=
SHUFFLE_OPENSEARCH_INDEX_PREFIX=
SHUFFLE_OPENSEARCH_SKIPSSL_VERIFY=true
DEBUG_MODE=false
DEBUG_MODE=false
+3 -3
View File
@@ -3,7 +3,7 @@ name: dockerbuild
on:
push:
branches:
- main
- 1.4.0
paths:
- "**"
- "!.github/**"
@@ -77,9 +77,9 @@ jobs:
cache-to: type=local,dest=/tmp/.buildx-cache
tags: |
ghcr.io/shuffle/shuffle-${{ matrix.app }}:${{ matrix.version }}
ghcr.io/shuffle/shuffle-${{ matrix.app }}:latest
ghcr.io/shuffle/shuffle-${{ matrix.app }}:nightly
${{ secrets.DOCKERHUB_USERNAME }}/shuffle-${{ matrix.app }}:${{ matrix.version }}
${{ secrets.DOCKERHUB_USERNAME }}/shuffle-${{ matrix.app }}:latest
${{ secrets.DOCKERHUB_USERNAME }}/shuffle-${{ matrix.app }}:nightly
- name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }}
+177 -71
View File
@@ -302,9 +302,11 @@ class AppBase:
self.authorization = os.getenv("AUTHORIZATION", "")
self.current_execution_id = os.getenv("EXECUTIONID", "")
self.full_execution = os.getenv("FULL_EXECUTION", "")
self.start_time = int(time.time())
self.result_wrapper_count = 0
# Make start time with milliseconds
self.start_time = int(time.time_ns())
self.action_result = {
"action": self.action,
"authorization": self.authorization,
@@ -312,9 +314,24 @@ class AppBase:
"result": f"",
"started_at": self.start_time,
"status": "",
"completed_at": int(time.time()),
"completed_at": int(time.time_ns()),
}
self.proxy_config = {
"http": os.getenv("HTTP_PROXY", ""),
"https": os.getenv("HTTPS_PROXY", ""),
"no_proxy": os.getenv("NO_PROXY", ""),
}
if len(os.getenv("SHUFFLE_INTERNAL_HTTP_PROXY", "")) > 0:
self.proxy_config["http"] = os.getenv("SHUFFLE_INTERNAL_HTTP_PROXY", "")
if len(os.getenv("SHUFFLE_INTERNAL_HTTPS_PROXY", "")) > 0:
self.proxy_config["https"] = os.getenv("SHUFFLE_INTERNAL_HTTP_PROXY", "")
if len(os.getenv("SHUFFLE_INTERNAL_NO_PROXY", "")) > 0:
self.proxy_config["no_proxy"] = os.getenv("SHUFFLE_INTERNAL_NO_PROXY", "")
if isinstance(self.action, str):
try:
self.action = json.loads(self.action)
@@ -468,7 +485,7 @@ class AppBase:
# Try it with some magic
action_result["completed_at"] = int(time.time())
action_result["completed_at"] = int(time.time_ns())
self.logger.info(f"""[DEBUG] Inside Send result with status {action_result["status"]}""")
#if isinstance(action_result,
@@ -512,7 +529,7 @@ class AppBase:
sleeptime = float(random.randint(0, 10) / 10)
try:
ret = requests.post(url, headers=headers, json=action_result, timeout=10, verify=False)
ret = requests.post(url, headers=headers, json=action_result, timeout=10, verify=False, proxies=self.proxy_config)
self.logger.info(f"[DEBUG] Result: {ret.status_code} (break on 200 or 201)")
if ret.status_code == 200 or ret.status_code == 201:
@@ -520,11 +537,29 @@ class AppBase:
break
else:
self.logger.info(f"[ERROR] Bad resp {ret.status_code}: {ret.text}")
time.sleep(sleeptime)
# Proxyerrror
except requests.exceptions.ProxyError as e:
self.logger.info(f"[ERROR] Proxy error: {e}")
self.proxy_config = {}
continue
except requests.exceptions.RequestException as e:
self.logger.info(f"[DEBUG] Request problem: {e}")
time.sleep(sleeptime)
# Check if we have a read timeout. If we do, exit as we most likely sent the result without getting a good result
if "Read timed out" in str(e):
self.logger.warning(f"[WARNING] Read timed out: {e}")
finished = True
break
if "Max retries exceeded with url" in str(e):
self.logger.warning(f"[WARNING] Max retries exceeded with url: {e}")
finished = True
break
#time.sleep(5)
continue
except TimeoutError as e:
@@ -560,7 +595,6 @@ class AppBase:
action_result["result"] = json.dumps({"success": False, "reason": "POST error: Failed connecting to %s over 10 retries to the backend" % url})
self.logger.info(f"[ERROR] Before typeerror stream result - NOT finished after 10 requests")
#ret = requests.post("%s%s" % (self.base_url, stream_path), headers=headers, json=action_result, verify=False)
self.send_result(action_result, {"Content-Type": "application/json", "Authorization": "Bearer %s" % self.authorization}, "/api/v1/streams")
return
@@ -572,7 +606,7 @@ class AppBase:
action_result["result"] = json.dumps({"success": False, "reason": "Typeerror when sending to backend URL %s" % url})
self.logger.info(f"[DEBUG] Before typeerror stream result: {e}")
ret = requests.post("%s%s" % (self.base_url, stream_path), headers=headers, json=action_result, verify=False)
ret = requests.post("%s%s" % (self.base_url, stream_path), headers=headers, json=action_result, verify=False, proxies=self.proxy_config)
#self.logger.info(f"[DEBUG] Result: {ret.status_code}")
#if ret.status_code != 200:
# pr
@@ -701,7 +735,7 @@ class AppBase:
#self.logger.info(f"RET: {ret.text}")
#self.logger.info(f"ID: {ret.status_code}")
url = f"{self.url}/api/v1/orgs/{org_id}/validate_app_values"
ret = requests.post(url, json=data, verify=False)
ret = requests.post(url, json=data, verify=False, proxies=self.proxy_config)
if ret.status_code == 200:
json_value = ret.json()
if len(json_value["found"]) > 0:
@@ -1011,7 +1045,7 @@ class AppBase:
"result": f"All {len(param_multiplier)} values were non-unique",
"started_at": self.start_time,
"status": "SKIPPED",
"completed_at": int(time.time()),
"completed_at": int(time.time_ns()),
}
self.send_result(self.action_result, {"Content-Type": "application/json", "Authorization": "Bearer %s" % self.authorization}, "/api/v1/streams")
@@ -1175,7 +1209,7 @@ class AppBase:
"User-Agent": "Shuffle 1.1.0",
}
ret = requests.get("%s%s" % (self.url, get_path), headers=headers, verify=False)
ret = requests.get("%s%s" % (self.url, get_path), headers=headers, verify=False, proxies=self.proxy_config)
return ret.json()
#if ret1.status_code != 200:
# return {
@@ -1201,7 +1235,7 @@ class AppBase:
"User-Agent": "Shuffle 1.1.0",
}
ret1 = requests.get("%s%s" % (self.url, get_path), headers=headers, verify=False)
ret1 = requests.get("%s%s" % (self.url, get_path), headers=headers, verify=False, proxies=self.proxy_config)
if ret1.status_code != 200:
return None
@@ -1264,7 +1298,7 @@ class AppBase:
"User-Agent": "Shuffle 1.1.0",
}
ret1 = requests.get("%s%s" % (self.url, get_path), headers=headers, verify=False)
ret1 = requests.get("%s%s" % (self.url, get_path), headers=headers, verify=False, proxies=self.proxy_config)
self.logger.info("RET1 (file get): %s" % ret1.text)
if ret1.status_code != 200:
returns.append({
@@ -1275,7 +1309,7 @@ class AppBase:
continue
content_path = "/api/v1/files/%s/content?execution_id=%s" % (item, full_execution["execution_id"])
ret2 = requests.get("%s%s" % (self.url, content_path), headers=headers, verify=False)
ret2 = requests.get("%s%s" % (self.url, content_path), headers=headers, verify=False, proxies=self.proxy_config)
self.logger.info("RET2 (file get) done")
if ret2.status_code == 200:
tmpdata = ret1.json()
@@ -1311,7 +1345,7 @@ class AppBase:
"key": key,
}
response = requests.post(url, json=data, verify=False)
response = requests.post(url, json=data, verify=False, proxies=self.proxy_config)
try:
allvalues = response.json()
return json.dumps(allvalues)
@@ -1332,7 +1366,7 @@ class AppBase:
"value": str(value),
}
response = requests.post(url, json=data, verify=False)
response = requests.post(url, json=data, verify=False, proxies=self.proxy_config)
try:
allvalues = response.json()
allvalues["key"] = key
@@ -1354,7 +1388,7 @@ class AppBase:
"key": key,
}
value = requests.post(url, json=data, verify=False)
value = requests.post(url, json=data, verify=False, proxies=self.proxy_config)
try:
allvalues = value.json()
self.logger.info("VAL1: ", allvalues)
@@ -1408,7 +1442,7 @@ class AppBase:
self.logger.info(f"KeyError in file setup: {e}")
pass
ret = requests.post("%s%s" % (self.url, create_path), headers=headers, json=data, verify=False)
ret = requests.post("%s%s" % (self.url, create_path), headers=headers, json=data, verify=False, proxies=self.proxy_config)
#self.logger.info(f"Ret CREATE: {ret.text}")
cur_id = ""
if ret.status_code == 200:
@@ -1440,7 +1474,7 @@ class AppBase:
files={"shuffle_file": (filename, curfile["data"])}
#open(filename,'rb')}
ret = requests.post("%s%s" % (self.url, upload_path), files=files, headers=new_headers, verify=False)
ret = requests.post("%s%s" % (self.url, upload_path), files=files, headers=new_headers, verify=False, proxies=self.proxy_config)
self.logger.info("Ret UPLOAD: %s" % ret.text)
self.logger.info("Ret2 UPLOAD: %d" % ret.status_code)
@@ -1457,7 +1491,7 @@ class AppBase:
"authorization": self.authorization,
"execution_id": self.current_execution_id,
"result": "",
"started_at": int(time.time()),
"started_at": int(time.time_ns()),
"status": "EXECUTING"
}
@@ -1537,7 +1571,8 @@ class AppBase:
"%s/api/v1/streams/results" % (self.base_url),
headers=headers,
json=tmpdata,
verify=False
verify=False,
proxies=self.proxy_config,
)
if ret.status_code == 200:
@@ -1964,6 +1999,8 @@ class AppBase:
# Parses JSON loops and such down to the item you're looking for
# $nodename.#.id
# $nodename.data.#min-max.info.id
# $nodename.data.#1-max.info.id
# $nodename.data.#min-1.info.id
def recurse_json(basejson, parsersplit):
match = "#([0-9a-z]+):?-?([0-9a-z]+)?#?"
try:
@@ -2080,6 +2117,8 @@ class AppBase:
if (basejson[value].endswith("}") and basejson[value].endswith("}")) or (basejson[value].startswith("[") and basejson[value].endswith("]")):
basejson = json.loads(basejson[value])
else:
# Should we sanitize here?
self.logger.info("[DEBUG] VALUE TO SANITIZE?: %s" % basejson[value])
return str(basejson[value]), False
except json.decoder.JSONDecodeError as e:
return str(basejson[value]), False
@@ -2128,7 +2167,6 @@ class AppBase:
actionname_lower = parsersplit[0][1:].lower()
#Actionname: Start_node
#print(f"\n[INFO] Actionname: {actionname_lower}")
# 1. Find the action
baseresult = ""
@@ -2466,7 +2504,7 @@ class AppBase:
self.action_result["result"] = f"Failed to parse LiquidPy: {error_msg}"
print("[WARNING] Failed to set LiquidPy result")
self.action_result["completed_at"] = int(time.time())
self.action_result["completed_at"] = int(time.time_ns())
self.send_result(self.action_result, headers, stream_path)
self.logger.info(f"[ERROR] Sent FAILURE response to backend due to : {e}")
@@ -2558,6 +2596,27 @@ class AppBase:
return data
# Makes JSON string values into valid strings in JSON
# Mainly by removing newlines and such
def fix_json_string_value(value):
try:
value = value.replace("\r\n", "\\r\\n")
value = value.replace("\n", "\\n")
value = value.replace("\r", "\\r")
# Fix quotes in the string
value = value.replace("\\\"", "\"")
value = value.replace("\"", "\\\"")
value = value.replace("\\\'", "\'")
value = value.replace("\'", "\\\'")
except Exception as e:
print(f"[WARNING] Failed to fix json string value: {e}")
return value
# Parses parameters sent to it and returns whether it did it successfully with the values found
def parse_params(action, fullexecution, parameter, self):
# Skip if it starts with $?
@@ -2621,6 +2680,20 @@ class AppBase:
value, is_loop = get_json_value(fullexecution, to_be_replaced)
#self.logger.info(f"\n\nType of value: {type(value)}")
if isinstance(value, str):
# Could we take it here?
self.logger.info(f"[DEBUG] Got value %s for parameter {paramname}" % value)
# Should check if there is are quotes infront of and after the to_be_replaced
# If there are, then we need to sanitize the value
# 1. Look for the to_be_replaced in the data
# 2. Check if there is a quote infront of it and also if there are {} in the data to validate JSON
# 3. If there are, sanitize!
#if data.find(f'"{to_be_replaced}"') != -1 and data.find("{") != -1 and data.find("}") != -1:
# print(f"[DEBUG] Found quotes infront of and after {to_be_replaced}! This probably means it's JSON and should be sanitized.")
# returnvalue = fix_json_string_value(value)
# value = returnvalue
parameter["value"] = parameter["value"].replace(to_be_replaced, value)
elif isinstance(value, dict) or isinstance(value, list):
# Changed from JSON dump to str() 28.05.2021
@@ -2633,7 +2706,7 @@ class AppBase:
# parameter["value"] = parameter["value"].replace(to_be_replaced, json.dumps(value))
# self.logger.info("Failed parsing value as string?")
else:
self.logger.info("[WARNING] Unknown type %s" % type(value))
self.logger.error("[ERROR] Unknown type %s" % type(value))
try:
parameter["value"] = parameter["value"].replace(to_be_replaced, json.dumps(value))
except json.decoder.JSONDecodeError as e:
@@ -2740,7 +2813,7 @@ class AppBase:
return "", parameter["value"], is_loop
def run_validation(sourcevalue, check, destinationvalue):
print("[DEBUG] Checking %s %s %s" % (sourcevalue, check, destinationvalue))
self.logger.info("[DEBUG] Checking %s '%s' %s" % (sourcevalue, check, destinationvalue))
if check == "=" or check.lower() == "equals":
if str(sourcevalue).lower() == str(destinationvalue).lower():
@@ -2758,15 +2831,16 @@ class AppBase:
if destinationvalue.lower() in sourcevalue.lower():
return True
elif check.lower() == "is empty":
if len(sourcevalue) == 0:
return True
elif check.lower() == "is empty" or check.lower() == "is_empty":
try:
if len(json.loads(sourcevalue)) == 0:
return True
except Exception as e:
self.logger.info(f"[WARNING] Failed to check if empty as list: {e}")
if str(sourcevalue) == 0:
if len(str(sourcevalue)) == 0:
return True
return False
elif check.lower() == "contains_any_of":
newvalue = [destinationvalue.lower()]
if "," in destinationvalue:
@@ -2782,7 +2856,6 @@ class AppBase:
print("[INFO] Found %s in %s" % (item, sourcevalue))
return True
return False
elif check.lower() == "larger than" or check.lower() == "bigger than":
try:
if str(sourcevalue).isdigit() and str(destinationvalue).isdigit():
@@ -2790,9 +2863,23 @@ class AppBase:
return True
except AttributeError as e:
print("[WARNING] Condition larger than failed with values %s and %s: %s" % (sourcevalue, destinationvalue, e))
return False
self.logger.info("[WARNING] Condition larger than failed with values %s and %s: %s" % (sourcevalue, destinationvalue, e))
try:
destinationvalue = len(json.loads(destinationvalue))
except Exception as e:
self.logger.info(f"[WARNING] Failed to convert destination to list: {e}")
try:
# Check if it's a list in autocast and if so, check the length
if len(json.loads(sourcevalue)) > int(destinationvalue):
return True
except Exception as e:
self.logger.info(f"[WARNING] Failed to check if larger than as list: {e}")
elif check.lower() == "smaller than" or check.lower() == "less than":
self.logger.info("In smaller than check: %s %s" % (sourcevalue, destinationvalue))
try:
if str(sourcevalue).isdigit() and str(destinationvalue).isdigit():
if int(sourcevalue) < int(destinationvalue):
@@ -2800,12 +2887,27 @@ class AppBase:
except AttributeError as e:
print("[WARNING] Condition smaller than failed with values %s and %s: %s" % (sourcevalue, destinationvalue, e))
return False
try:
destinationvalue = len(json.loads(destinationvalue))
except Exception as e:
self.logger.info(f"[WARNING] Failed to convert destination to list: {e}")
try:
# Check if it's a list in autocast and if so, check the length
if len(json.loads(sourcevalue)) < int(destinationvalue):
return True
except Exception as e:
self.logger.info(f"[WARNING] Failed to check if smaller than as list: {e}")
elif check.lower() == "re" or check.lower() == "matches regex":
try:
found = re.search(destinationvalue, sourcevalue)
found = re.search(str(destinationvalue), str(sourcevalue))
except re.error as e:
print("[WARNING] Regex error in condition: %s" % e)
print("[WARNING] Regex error in condition (re.error): %s" % e)
return False
except Exception as e:
print("[WARNING] Regex error in condition (catchall): %s" % e)
return False
if found == None:
@@ -2813,7 +2915,7 @@ class AppBase:
return True
else:
print("[DEBUG] Condition: can't handle %s yet. Setting to true" % check)
self.logger.error("[DEBUG] Condition: can't handle %s yet. Setting to true" % check)
return False
@@ -2826,12 +2928,40 @@ class AppBase:
return True, ""
# Startnode should always run - no need to check incoming
# Removed November 2023 due to people wanting startnode to also check
# This is to make it possible ot
try:
if action["id"] == fullexecution["start"]:
return True, ""
# Need to validate if the source is a trigger or not
# need to remove branches that are not from trigger to the startnode to make it all work
#if "workflow" in fullexecution["workflow"] and "triggers" in fullexecution["workflow"]:
# cnt = 0
# found_branch_indexes = []
# for branch in fullexecution["workflow"]["branches"]:
# if branch["destination_id"] != action["id"]:
# continue
# # Check if the source is a trigger
# # if we can't find it as trigger, remove the branch
# print("Found relevant branch: %s" % branch)
# for action in fullexecution["workflow"]["actions"]:
# if action["id"] == branch["source_id"]:
# found_branch_indexes.append(branch["source_id"])
# break
# if len(found_branch_indexes) > 0:
# for i in sorted(found_branch_indexes, reverse=True):
# fullexecution["workflow"]["branches"].pop(i)
# print("Removed %d branches" % len(found_branch_indexes))
#else:
# print("[WARNING] No branches or triggers found in fullexecution for startnode")
except Exception as error:
self.logger.info(f"[WARNING] Failed checking startnode: {error}")
return True, ""
#return True, ""
#return True, ""
available_checks = [
"=",
@@ -2850,6 +2980,8 @@ class AppBase:
"contains_any_of",
"re",
"matches regex",
"is empty",
"is_empty",
]
relevantbranches = []
@@ -2909,7 +3041,7 @@ class AppBase:
destinationvalue = parse_wrapper_start(destinationvalue, self)
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"]))
self.logger.error("[ERROR] Skipping '%s' -> %s -> '%s' because %s is invalid." % (sourcevalue, condition["condition"]["value"], destinationvalue, condition["condition"]["value"]))
continue
# Configuration = negated because of WorkflowAppActionParam..
@@ -2974,7 +3106,7 @@ class AppBase:
self.logger.info("Failed one or more branch conditions.")
self.action_result["result"] = tmpresult
self.action_result["status"] = "SKIPPED"
self.action_result["completed_at"] = int(time.time())
self.action_result["completed_at"] = int(time.time_ns())
self.send_result(self.action_result, headers, stream_path)
return
@@ -3460,7 +3592,7 @@ class AppBase:
self.logger.info("[WARNING] SHOULD STOP EXECUTION BECAUSE FIELDS AREN'T UNIQUE")
self.action_result["status"] = "SKIPPED"
self.action_result["result"] = f"A non-unique value was found"
self.action_result["completed_at"] = int(time.time())
self.action_result["completed_at"] = int(time.time_ns())
self.send_result(self.action_result, headers, stream_path)
return
@@ -3522,6 +3654,7 @@ class AppBase:
timeout_env = os.getenv("SHUFFLE_APP_SDK_TIMEOUT", timeout)
try:
timeout = int(timeout_env)
self.logger.info(f"[DEBUG] Timeout set to {timeout} seconds")
except Exception as e:
self.logger.info(f"[WARNING] Failed parsing timeout to int: {e}")
@@ -3537,7 +3670,7 @@ class AppBase:
future.cancel()
newres = json.dumps({
"success": False,
"reason": "Timeout error within %d seconds. This happens if we can't reach or use the API you're trying to use within the time limit." % timeout,
"reason": "Timeout error within %d seconds (1). This happens if we can't reach or use the API you're trying to use within the time limit. Configure SHUFFLE_APP_SDK_TIMEOUT=100 in Orborus to increase it to 100 seconds. Not changeable for cloud." % timeout,
"exception": str(e),
})
@@ -3550,40 +3683,13 @@ class AppBase:
except concurrent.futures.TimeoutError as e:
newres = json.dumps({
"success": False,
"reason": "Timeout error within %d seconds (2). This happens if we can't reach or use the API you're trying to use within the time limit" % timeout
"reason": "Timeout error within %d seconds (2). This happens if we can't reach or use the API you're trying to use within the time limit. Configure SHUFFLE_APP_SDK_TIMEOUT=100 in Orborus to increase it to 100 seconds. Not changeable for cloud." % timeout,
})
break
#thread = threading.Thread(target=func, args=(**params,))
#thread.start()
#thread.join(timeout)
#if thread.is_alive():
# # The thread is still running, so we need to stop it
# # You can handle this as needed, such as raising an exception
# timeout_handler()
#with Timeout(timeout):
# newres = func(**params)
# break
#except Timeout.Timeout as e:
# self.logger.info(f"[DEBUG] Timeout error: {e}")
# newres = json.dumps({
# "success": False,
# "reason": "Timeout error within %d seconds. This typically happens if we can't reach the API you're trying to reach." % timeout,
# "exception": str(e),
# })
# break
except TypeError as e:
newres = ""
self.logger.info(f"[DEBUG] Got exec type error: {e}")
self.logger.info(f"[ERROR] Got function exec type error: {e}")
try:
e = json.loads(f"{e}")
except:
@@ -3814,7 +3920,7 @@ class AppBase:
})
# Send the result :)
self.action_result["completed_at"] = int(time.time())
self.action_result["completed_at"] = int(time.time_ns())
self.send_result(self.action_result, headers, stream_path)
#try:
+2 -2
View File
@@ -18,8 +18,8 @@ require (
github.com/gorilla/mux v1.8.0
github.com/h2non/filetype v1.1.3
github.com/satori/go.uuid v1.2.0
github.com/shuffle/shuffle-shared v0.4.88
golang.org/x/crypto v0.9.0
github.com/shuffle/shuffle-shared v0.5.30
golang.org/x/crypto v0.14.0
google.golang.org/api v0.125.0
google.golang.org/grpc v1.55.0
gopkg.in/src-d/go-git.v4 v4.13.1
+16
View File
@@ -406,6 +406,22 @@ github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/shuffle/shuffle-shared v0.4.66 h1:Aw4qOp0VsVJrRzW1sJhEy4OY4fRGlFErUD5+93RXL6g=
github.com/shuffle/shuffle-shared v0.4.66/go.mod h1:X613gbo0dT3fnYvXDRwjQZyLC+T49T2nSQOrCV5QMlI=
github.com/shuffle/shuffle-shared v0.4.80 h1:03OL+O8prwL9zq6Gnb9SRORPWi5+ThO0jPoxk+xctOo=
github.com/shuffle/shuffle-shared v0.4.80/go.mod h1:X613gbo0dT3fnYvXDRwjQZyLC+T49T2nSQOrCV5QMlI=
github.com/shuffle/shuffle-shared v0.4.95 h1:xr92/03/uQeJiDme9S8/vgF1KWyQgJ1KQXVE7nQMKis=
github.com/shuffle/shuffle-shared v0.4.95/go.mod h1:X613gbo0dT3fnYvXDRwjQZyLC+T49T2nSQOrCV5QMlI=
github.com/shuffle/shuffle-shared v0.4.96 h1:iaIB/HP9eKpw9DMMJZhSLDbKdHJt075kFYLHg9AaiiM=
github.com/shuffle/shuffle-shared v0.4.96/go.mod h1:X613gbo0dT3fnYvXDRwjQZyLC+T49T2nSQOrCV5QMlI=
github.com/shuffle/shuffle-shared v0.4.97 h1:1c8LdNteMykKNEV97vwP63oSP2tV/Uso3O4TC+oxdFQ=
github.com/shuffle/shuffle-shared v0.4.97/go.mod h1:X613gbo0dT3fnYvXDRwjQZyLC+T49T2nSQOrCV5QMlI=
github.com/shuffle/shuffle-shared v0.4.98 h1:pgsLdWUpxZ/q+eHpAjOCH9icOsmuO5u2olmirOldy5A=
github.com/shuffle/shuffle-shared v0.4.98/go.mod h1:X613gbo0dT3fnYvXDRwjQZyLC+T49T2nSQOrCV5QMlI=
github.com/shuffle/shuffle-shared v0.5.11 h1:Eqbs9o8E49QAL5/6aV6BfFtWSjLIvgET7AL3fa4OQTg=
github.com/shuffle/shuffle-shared v0.5.11/go.mod h1:X613gbo0dT3fnYvXDRwjQZyLC+T49T2nSQOrCV5QMlI=
github.com/shuffle/shuffle-shared v0.5.14 h1:d14u1e4k+qKgnf4Insq4x2S+0MMKlDqdyTTyVP3puRA=
github.com/shuffle/shuffle-shared v0.5.14/go.mod h1:X613gbo0dT3fnYvXDRwjQZyLC+T49T2nSQOrCV5QMlI=
github.com/shuffle/shuffle-shared v0.5.29 h1:n4vThl7v3mFVXbrIW71XREFdmZZo7mOBAWxnsdiNjDk=
github.com/shuffle/shuffle-shared v0.5.29/go.mod h1:X613gbo0dT3fnYvXDRwjQZyLC+T49T2nSQOrCV5QMlI=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
+28 -10
View File
@@ -1965,6 +1965,7 @@ func executeCloudAction(action shuffle.CloudSyncJob, apikey string) error {
return err
}
defer newresp.Body.Close()
respBody, err := ioutil.ReadAll(newresp.Body)
if err != nil {
return err
@@ -3513,15 +3514,13 @@ func remoteOrgJobHandler(org shuffle.Org, interval int) error {
)
req.Header.Add("Authorization", fmt.Sprintf(`Bearer %s`, org.SyncConfig.Apikey))
//log.Printf("[INFO] Sending org sync with autho %s", org.SyncConfig.Apikey)
newresp, err := client.Do(req)
if err != nil {
//log.Printf("Failed request in org sync: %s", err)
return err
}
defer newresp.Body.Close()
respBody, err := ioutil.ReadAll(newresp.Body)
if err != nil {
log.Printf("[ERROR] Failed body read in job sync: %s", err)
@@ -3574,6 +3573,12 @@ func runInitEs(ctx context.Context) {
log.Printf("[DEBUG] Getting organizations for Elasticsearch/Opensearch")
activeOrgs, err := shuffle.GetAllOrgs(ctx)
log.Printf("[DEBUG] Got %d organizations to look into. If this is 0, we wait 10 more seconds until DB is ready and try again.", len(activeOrgs))
if len(activeOrgs) == 0 {
time.Sleep(10 * time.Second)
activeOrgs, err = shuffle.GetAllOrgs(ctx)
}
setUsers := false
_ = setUsers
if err != nil {
@@ -3655,7 +3660,6 @@ func runInitEs(ctx context.Context) {
log.Printf("Successfully updated org to have users!")
}
}
}
}
}
@@ -3688,6 +3692,10 @@ func runInitEs(ctx context.Context) {
orgId = activeOrgs[0].Id
}
if len(schedule.Org) == 36 {
orgId = schedule.Org
}
_, _, err := handleExecution(schedule.WorkflowId, shuffle.Workflow{}, request, orgId)
if err != nil {
log.Printf("[WARNING] Failed to execute %s: %s", schedule.WorkflowId, err)
@@ -3697,15 +3705,21 @@ func runInitEs(ctx context.Context) {
for _, schedule := range schedules {
if strings.ToLower(schedule.Environment) == "cloud" {
log.Printf("Skipping cloud schedule")
log.Printf("[DEBUG] Skipping cloud schedule")
continue
}
// FIXME: Add a randomized timer to avoid all schedules running at the same time
// Many are at 5 minutes / 1 hour. The point is to spread these out
// a bit instead of all of them starting at the exact same time
//log.Printf("Schedule: %#v", schedule)
//log.Printf("Schedule time: every %d seconds", schedule.Seconds)
jobret, err := newscheduler.Every(schedule.Seconds).Seconds().NotImmediately().Run(job(schedule))
if err != nil {
log.Printf("Failed to schedule workflow: %s", err)
log.Printf("[ERROR] Failed to start schedule for workflow %s: %s", schedule.WorkflowId, err)
} else {
log.Printf("[DEBUG] Successfully started schedule for workflow %s", schedule.WorkflowId)
}
scheduledJobs[schedule.Id] = jobret
@@ -3977,7 +3991,7 @@ func runInitEs(ctx context.Context) {
r, err := git.Clone(storer, fs, cloneOptions)
if err != nil {
log.Printf("[WARNING] Failed loading repo into memory (init): %s", err)
log.Printf("[ERROR] Failed loading repo into memory (init): %s", err)
}
dir, err := fs.ReadDir("")
@@ -4014,7 +4028,7 @@ func runInitEs(ctx context.Context) {
}
_, err = git.Clone(storer, fs, cloneOptions)
if err != nil {
log.Printf("[WARNING] Failed loading repo %s into memory: %s", apis, err)
log.Printf("[ERROR] Failed loading repo %s into memory: %s", apis, err)
} else if err == nil && len(workflowapps) < 10 {
log.Printf("[INFO] Finished git clone. Looking for updates to the repo.")
dir, err := fs.ReadDir("")
@@ -4030,7 +4044,7 @@ func runInitEs(ctx context.Context) {
if os.Getenv("SHUFFLE_HEALTHCHECK_DISABLED") != "true" {
healthcheckInterval := 15
healthcheckInterval := 30
log.Printf("[INFO] Starting healthcheck job every %d minute. Stats available on /api/v1/health/stats. Disable with SHUFFLE_HEALTHCHECK_DISABLED=true", healthcheckInterval)
job := func() {
// Prepare a fake http.responsewriter
@@ -4725,7 +4739,7 @@ func initHandlers() {
log.Printf("[DEBUG] Initialized Shuffle database connection. Setting up environment.")
if elasticConfig == "elasticsearch" {
time.Sleep(5 * time.Second)
time.Sleep(10 * time.Second)
go runInitEs(ctx)
} else {
//go shuffle.runInit(ctx)
@@ -4787,6 +4801,7 @@ func initHandlers() {
// App specific
// From here down isnt checked for org specific
r.HandleFunc("/api/v1/apps/{key}/execute", executeSingleAction).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/apps/{key}/run", executeSingleAction).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/apps/categories", shuffle.GetActiveCategories).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/apps/categories/run", shuffle.RunCategoryAction).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/apps/upload", handleAppZipUpload).Methods("POST", "OPTIONS")
@@ -4824,11 +4839,14 @@ func initHandlers() {
/* Everything below here increases the counters*/
r.HandleFunc("/api/v1/workflows", shuffle.GetWorkflows).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/workflows", shuffle.SetNewWorkflow).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/workflows/search", shuffle.HandleWorkflowRunSearch).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/workflows/schedules", shuffle.HandleGetSchedules).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/workflows/{key}/executions", shuffle.GetWorkflowExecutions).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/workflows/{key}/executions/{key}/rerun", checkUnfinishedExecution).Methods("GET", "POST", "OPTIONS")
r.HandleFunc("/api/v1/workflows/{key}/executions/{key}/abort", shuffle.AbortExecution).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/workflows/{key}/schedule", scheduleWorkflow).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/workflows/download_remote", loadSpecificWorkflows).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/workflows/{key}/run", executeWorkflow).Methods("GET", "POST", "OPTIONS")
r.HandleFunc("/api/v1/workflows/{key}/execute", executeWorkflow).Methods("GET", "POST", "OPTIONS")
r.HandleFunc("/api/v1/workflows/{key}/schedule/{schedule}", stopSchedule).Methods("DELETE", "OPTIONS")
r.HandleFunc("/api/v1/workflows/{key}/stream", shuffle.HandleStreamWorkflow).Methods("GET", "OPTIONS")
+188 -7
View File
@@ -705,7 +705,8 @@ func handleWorkflowQueue(resp http.ResponseWriter, request *http.Request) {
// Will make sure transactions are always ran for an execution. This is recursive if it fails. Allowed to fail up to 5 times
func runWorkflowExecutionTransaction(ctx context.Context, attempts int64, workflowExecutionId string, actionResult shuffle.ActionResult, resp http.ResponseWriter) {
log.Printf("[DEBUG] Running workflow execution transaction for %s", workflowExecutionId)
log.Printf("[DEBUG][%s] Running workflow execution update", workflowExecutionId)
// Should start a tx for the execution here
workflowExecution, err := shuffle.GetWorkflowExecution(ctx, workflowExecutionId)
@@ -1063,10 +1064,6 @@ func handleExecution(id string, workflow shuffle.Workflow, request *http.Request
}
}
err = shuffle.SetWorkflowExecution(ctx, workflowExecution, true)
if err != nil {
log.Printf("[ERROR] Failed setting workflow execution during init (2): %s", err)
}
err = imageCheckBuilder(execInfo.ImageNames)
if err != nil {
@@ -1573,6 +1570,11 @@ func handleExecution(id string, workflow shuffle.Workflow, request *http.Request
workflowExecution.ExecutionOrg = workflow.ExecutingOrg.Id
}
err = shuffle.SetWorkflowExecution(ctx, workflowExecution, true)
if err != nil {
log.Printf("[ERROR] Failed setting workflow execution during init (2): %s", err)
}
var allEnvs []shuffle.Environment
if len(workflowExecution.ExecutionOrg) > 0 {
//log.Printf("[INFO] Executing ORG: %s", workflowExecution.ExecutionOrg)
@@ -1665,7 +1667,7 @@ func handleExecution(id string, workflow shuffle.Workflow, request *http.Request
// FIXME - tmp name based on future companyname-companyId
// This leads to issues with overlaps. Should set limits and such instead
for _, environment := range execInfo.Environments {
log.Printf("[INFO] Execution: %s should execute onprem with execution environment \"%s\". Workflow: %s", workflowExecution.ExecutionId, environment, workflowExecution.Workflow.ID)
log.Printf("[INFO][%s] Execution: should execute onprem with execution environment \"%s\". Workflow: %s", workflowExecution.ExecutionId, environment, workflowExecution.Workflow.ID)
executionRequest := shuffle.ExecutionRequest{
ExecutionId: workflowExecution.ExecutionId,
@@ -3358,11 +3360,21 @@ func executeSingleAction(resp http.ResponseWriter, request *http.Request) {
return
}
workflowExecution.Priority = 10
workflowExecution.Priority = 11
environments, err := shuffle.GetEnvironments(ctx, user.ActiveOrg.Id)
environment := "Shuffle"
if len(environments) >= 1 {
// Find default one
environment = environments[0].Name
for _, env := range environments {
if env.Default {
environment = env.Name
break
}
}
} else {
log.Printf("[ERROR] No environments found for org %s. Exiting", user.ActiveOrg.Id)
resp.WriteHeader(401)
@@ -3370,6 +3382,14 @@ func executeSingleAction(resp http.ResponseWriter, request *http.Request) {
return
}
// Enforcing same env for job + run to be default
// FIXME: Should use environment that is in the source workflow if it exists
for i, _ := range workflowExecution.Workflow.Actions {
workflowExecution.Workflow.Actions[i].Environment = environment
workflowExecution.Workflow.Actions[i].Label = "TMP"
}
shuffle.SetWorkflowExecution(ctx, workflowExecution, false)
log.Printf("[INFO] Execution (single action): %s should execute onprem with execution environment \"%s\". Workflow: %s", workflowExecution.ExecutionId, environment, workflowExecution.Workflow.ID)
executionRequest := shuffle.ExecutionRequest{
@@ -3377,6 +3397,7 @@ func executeSingleAction(resp http.ResponseWriter, request *http.Request) {
WorkflowId: workflowExecution.Workflow.ID,
Authorization: workflowExecution.Authorization,
Environments: []string{environment},
Priority: 11,
}
executionRequest.Priority = workflowExecution.Priority
@@ -3397,6 +3418,9 @@ func executeSingleAction(resp http.ResponseWriter, request *http.Request) {
log.Printf("[ERROR] Failed to marshal retStruct in single execution: %s", err)
}
// Deleting as this is a single action and doesn't need to be stored
shuffle.DeleteKey(ctx, "workflowexecution", executionRequest.ExecutionId)
resp.WriteHeader(200)
resp.Write([]byte(returnBytes))
}
@@ -3993,3 +4017,160 @@ func checkWorkflowApp(workflowApp shuffle.WorkflowApp) error {
return nil
}
func checkUnfinishedExecution(resp http.ResponseWriter, request *http.Request) {
cors := shuffle.HandleCors(resp, request)
if cors {
return
}
location := strings.Split(request.URL.String(), "/")
var fileId string
if location[1] == "api" {
if len(location) <= 4 {
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false}`))
return
}
fileId = location[4]
}
if len(fileId) != 36 {
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false, "reason": "Workflow ID to abort is not valid"}`))
return
}
executionId := location[6]
if len(executionId) != 36 {
resp.WriteHeader(401)
resp.Write([]byte(`{"success": false, "reason": "ExecutionID not valid"}`))
return
}
ctx := shuffle.GetContext(request)
exec, err := shuffle.GetWorkflowExecution(ctx, executionId)
if err != nil {
log.Printf("[ERROR] Failed getting execution (rerun workflow - 1) %s: %s", executionId, err)
resp.WriteHeader(401)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed getting execution ID %s because it doesn't exist (abort)."}`, executionId)))
return
}
apikey := request.Header.Get("Authorization")
parsedKey := ""
if strings.HasPrefix(apikey, "Bearer ") {
apikeyCheck := strings.Split(apikey, " ")
if len(apikeyCheck) == 2 {
parsedKey = apikeyCheck[1]
}
}
// ONLY allowed to run automatically with the same auth (july 2022)
if exec.Authorization != parsedKey {
user, err := shuffle.HandleApiAuthentication(resp, request)
if err != nil {
log.Printf("[ERROR][%s] Bad authorization key for execution (rerun workflow - 3): %s", executionId, err)
resp.WriteHeader(403)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed because you're not authorized to see this workflow (3)."}`)))
return
}
// Check if user is in the correct org
if user.ActiveOrg.Id == exec.ExecutionOrg && user.Role != "org-reader" {
log.Printf("[AUDIT][%s] User %s (%s) is force continuing execution from org access", executionId, user.Username, user.Id)
} else if user.SupportAccess {
log.Printf("[AUDIT][%s] User %s (%s) is force continuing execution with support access", executionId, user.Username, user.Id)
} else {
log.Printf("[ERROR][%s] Bad authorization key for continue execution (rerun workflow - 2): %s", executionId, err)
resp.WriteHeader(403)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Failed because you're not authorized to see this workflow (2)."}`)))
return
}
}
// Meant as a function that periodically checks whether previous executions have finished or not.
// Should probably be based on executedIds and finishedIds
// Schedule a check in the future instead?
// Auth vs execution check!
extraInputs := 0
for _, trigger := range exec.Workflow.Triggers {
if trigger.Name == "User Input" && trigger.AppName == "User Input" {
extraInputs += 1
//exec.Workflow.Actions = append(exec.Workflow.Actions, shuffle.Action{
// ID: trigger.ID,
// Label: trigger.Label,
// Name: trigger.Name,
//})
} else if trigger.Name == "Shuffle Workflow" && trigger.AppName == "Shuffle Workflow" {
extraInputs += 1
//exec.Workflow.Actions = append(exec.Workflow.Actions, shuffle.Action{
// ID: trigger.ID,
// Label: trigger.Label,
// Name: trigger.Name,
//})
}
}
if exec.Status != "ABORTED" && exec.Status != "FINISHED" && exec.Status != "FAILURE" {
log.Printf("[DEBUG][%s] Rechecking execution and its status to send to backend IF the status is EXECUTING (%s - %d/%d finished)", exec.ExecutionId, exec.Status, len(exec.Results), len(exec.Workflow.Actions)+extraInputs)
}
// Usually caused by issue during startup
if exec.Status == "" {
resp.WriteHeader(401)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "No status for the execution"}`)))
return
}
if exec.Status != "EXECUTING" {
resp.WriteHeader(200)
resp.Write([]byte(fmt.Sprintf(`{"success": true, "reason": "Already finished"}`)))
return
}
// Force it back in the queue to be executed
if len(exec.Workflow.Actions) == 0 {
resp.WriteHeader(200)
resp.Write([]byte(fmt.Sprintf(`{"success": true, "reason": "Not a cloud env workflow. Only rerunning cloud env."}`)))
return
}
log.Printf("[DEBUG][%s] Workflow: %s (%s)", exec.ExecutionId, exec.Workflow.Name, exec.Workflow.ID)
if exec.Workflow.ID == "" || exec.Workflow.Name == "" {
log.Printf("[ERROR][%s] No workflow ID found for execution", exec.ExecutionId)
shuffle.DeleteKey(ctx, "workflowexecution", exec.ExecutionId)
resp.WriteHeader(200)
resp.Write([]byte(fmt.Sprintf(`{"success": true, "reason": "No workflow name / ID found. Can't run. Contact support@shuffler.io if this persists."}`)))
return
}
environment := exec.Workflow.Actions[0].Environment
log.Printf("[DEBUG][%s] Not a cloud env workflow. Re-adding job in queue for env %s.", exec.ExecutionId, environment)
parsedEnv := fmt.Sprintf("%s_%s", strings.ToLower(strings.ReplaceAll(strings.ReplaceAll(environment, " ", "-"), "_", "-")), exec.ExecutionOrg)
log.Printf("[DEBUG][%s] Adding new run job to env (2): %s", exec.ExecutionId, parsedEnv)
executionRequest := shuffle.ExecutionRequest{
ExecutionId: exec.ExecutionId,
WorkflowId: exec.Workflow.ID,
Authorization: exec.Authorization,
Environments: []string{environment},
}
// Increase priority on reruns to catch up
executionRequest.Priority = 11
err = shuffle.SetWorkflowQueue(ctx, executionRequest, parsedEnv)
if err != nil {
log.Printf("[ERROR] Failed adding execution to db: %s", err)
}
resp.WriteHeader(200)
resp.Write([]byte(fmt.Sprintf(`{"success": true, "reason": "Reran workflow in %s"}`, parsedEnv)))
}
+5 -4
View File
@@ -1,7 +1,7 @@
version: '3'
services:
frontend:
image: ghcr.io/shuffle/shuffle-frontend:latest
image: ghcr.io/shuffle/shuffle-frontend:nightly
container_name: shuffle-frontend
hostname: shuffle-frontend
ports:
@@ -15,7 +15,7 @@ services:
depends_on:
- backend
backend:
image: ghcr.io/shuffle/shuffle-backend:latest
image: ghcr.io/shuffle/shuffle-backend:nightly
container_name: shuffle-backend
hostname: ${BACKEND_HOSTNAME}
# Here for debugging:
@@ -34,7 +34,7 @@ services:
- SHUFFLE_FILE_LOCATION=/shuffle-files
restart: unless-stopped
orborus:
image: ghcr.io/shuffle/shuffle-orborus:latest
image: ghcr.io/shuffle/shuffle-orborus:nightly
container_name: shuffle-orborus
hostname: shuffle-orborus
networks:
@@ -42,7 +42,8 @@ services:
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- SHUFFLE_APP_SDK_TIMEOUT=300 # New SDK default timeout
- SHUFFLE_APP_SDK_TIMEOUT=300
- SHUFFLE_ORBORUS_EXECUTION_CONCURRENCY=5 # The amount of concurrent executions Orborus can handle.
#- DOCKER_HOST=tcp://docker-socket-proxy:2375
- ENVIRONMENT_NAME=${ENVIRONMENT_NAME}
- BASE_URL=http://${OUTER_HOSTNAME}:5001
+1
View File
@@ -14,6 +14,7 @@
"@mui/styles": "^5.14.0",
"@mui/x-data-grid": "^5.17.11",
"@mui/x-date-pickers": "^6.11.1",
"@uiw/codemirror-theme-vscode": "^4.21.20",
"@uiw/codemirror-themes": "^4.21.9",
"@uiw/react-codemirror": "^4.21.9",
"@use-it/interval": "^1.0.0",
+22 -11
View File
@@ -47,8 +47,6 @@ const AppSearchButtons = (props) => {
const [newSelectedApp, setNewSelectedApp] = useState(undefined)
useEffect(() => {
console.log("AppSearchButtons: newSelectedApp: " + JSON.stringify(newSelectedApp))
if (newSelectedApp !== undefined && setMissing != undefined) {
console.log("AppSearchButtons: setMissing is defined!")
@@ -131,9 +129,12 @@ const AppSearchButtons = (props) => {
//setFrameworkLoaded(true)
})
}
const icon = foundApp.large_image
console.log("index:", moreButton)
console.log("totalApps:", totalApps)
var foundAppImage = AppImage
if (foundApp.name !== undefined && foundApp.name !== null && !foundApp.name.includes(":default")) {
foundAppImage = foundApp.large_image
}
let xsValue = 12;
if (index === totalApps - 1 || index === totalApps - 2 || index === totalApps - 3 || index === totalApps - 4) {
@@ -142,6 +143,8 @@ const AppSearchButtons = (props) => {
if (index === totalApps - 5) {
xsValue = 12;
}
// This is silly huh
if (moreButton) {
switch (index) {
case totalApps - 1:
@@ -159,7 +162,6 @@ const AppSearchButtons = (props) => {
xsValue = 12;
break;
default:
// Handle other cases if needed
}
}
@@ -221,10 +223,11 @@ const AppSearchButtons = (props) => {
>
<IconButton
style={{ zIndex: 12501, position: "absolute", top: 32, right: 16 }}
disabled
onClick={(e) => {
e.preventDefault();
setLocalSearchOpen(false)
setDefaultSearch("")
const submitDeletedApp = {
"description": "",
"id": "remove",
@@ -233,15 +236,23 @@ const AppSearchButtons = (props) => {
}
setFrameworkItem(submitDeletedApp)
setNewSelectedApp({})
if (setDefaultSearch !== undefined) {
setDefaultSearch("")
}
setTimeout(() => {
setDiscoveryData({})
if (setDiscoveryData !== undefined) {
setDiscoveryData({})
}
setFrameworkItem(submitDeletedApp)
//setNewSelectedApp({})
}, 1000)
//setAppName(discoveryData.cases.name)
}}
>
<DeleteIcon style={{ color: "white", height: 15, width: 15, }} />
<DeleteIcon style={{ height: 15, width: 15, }} />
</IconButton>
</Tooltip>
</div>
@@ -283,12 +294,12 @@ const AppSearchButtons = (props) => {
}}
>
<div style={{ display: "flex", textAlign: "center", justifyContent: "center", alignItems: "center", marginRight: "auto" }}>
{AppImage === undefined || AppImage === null || AppImage.length === 0 ?
<div style={{ width: 40, height: 40, borderRadius: 9999, backgroundColor: "#2F2F2F", textAlign: "center" }}>
{foundAppImage === undefined || foundAppImage === null || foundAppImage.length === 0 ?
<div style={{ width: 40, height: 40, borderRadius: 40, backgroundColor: "#2F2F2F", textAlign: "center" }}>
<img style={{ paddingLeft: 11, paddingTop: 11, width: 40, height: 40, flexShrink: 0, }} src={icon} />
</div>
:
<img style={{ marginRight: 8, width: 35, height: 35, flexShrink: 0, borderRadius: 40, }} src={AppImage} />
<img style={{ marginRight: 8, width: 35, height: 35, flexShrink: 0, borderRadius: 40, }} src={foundAppImage} />
}
<div style={{ marginLeft: 8, }}>
<Typography style={{
+30 -29
View File
@@ -269,35 +269,8 @@ const EditWorkflow = (props) => {
/>
</div>
<div style={{display: "flex", marginTop: 10, }}>
<MuiChipsInput
style={{ flex: 1, maxHeight: 40, }}
InputProps={{
style: {
color: "white",
},
}}
placeholder="Tags"
color="primary"
fullWidth
value={newWorkflowTags}
onChange={(chip) => {
console.log("Chip: ", chip)
//newWorkflowTags.push(chip);
setNewWorkflowTags(chip);
}}
onAdd={(chip) => {
newWorkflowTags.push(chip);
setNewWorkflowTags(newWorkflowTags);
}}
onDelete={(chip, index) => {
console.log("Deleting: ", chip, index)
newWorkflowTags.splice(index, 1);
setNewWorkflowTags(newWorkflowTags);
setUpdate(Math.random());
}}
/>
{usecases !== null && usecases !== undefined && usecases.length > 0 ?
<FormControl style={{flex: 1, marginLeft: 5, }}>
<FormControl style={{flex: 1, marginRight: 5,}}>
<InputLabel htmlFor="grouped-select-usecase">Usecases</InputLabel>
<Select
defaultValue=""
@@ -350,6 +323,33 @@ const EditWorkflow = (props) => {
</Select>
</FormControl>
: null}
<MuiChipsInput
style={{ flex: 1, maxHeight: 120, overflow: "auto",}}
InputProps={{
style: {
color: "white",
},
}}
placeholder="Tags"
color="primary"
fullWidth
value={newWorkflowTags}
onChange={(chip) => {
console.log("Chip: ", chip)
//newWorkflowTags.push(chip);
setNewWorkflowTags(chip);
}}
onAdd={(chip) => {
newWorkflowTags.push(chip);
setNewWorkflowTags(newWorkflowTags);
}}
onDelete={(chip, index) => {
console.log("Deleting: ", chip, index)
newWorkflowTags.splice(index, 1);
setNewWorkflowTags(newWorkflowTags);
setUpdate(Math.random());
}}
/>
</div>
{showMoreClicked === true ?
@@ -365,7 +365,8 @@ const EditWorkflow = (props) => {
onChange={(e) => {
console.log("Data: ", e.target.value)
innerWorkflow.workflow_type = e.target.value
//innerWorkflow.workflow_type = e.target.value
innerWorkflow.status = e.target.value
setInnerWorkflow(innerWorkflow)
}}
>
+36 -36
View File
@@ -216,44 +216,44 @@ const { globalUrl, setNotifications, notifications, isLoggedIn, removeCookie, ho
const NotificationItem = (props) => {
const {data} = props
var image = "";
var orgName = "";
var orgId = "";
if (userdata.orgs !== undefined) {
const foundOrg = userdata.orgs.find((org) => org.id === data["org_id"]);
if (foundOrg !== undefined && foundOrg !== null) {
//position: "absolute", bottom: 5, right: -5,
const imageStyle = {
width: imagesize,
height: imagesize,
pointerEvents: "none",
marginLeft: data.creator_org !== undefined && data.creator_org.length > 0 ? 20 : 0,
borderRadius: 10,
border: foundOrg.id === userdata.active_org.id ? `3px solid ${boxColor}` : null,
cursor: "pointer",
marginRight: 10,
};
var image = "";
var orgName = "";
var orgId = "";
if (userdata.orgs !== undefined) {
const foundOrg = userdata.orgs.find((org) => org.id === data["org_id"]);
if (foundOrg !== undefined && foundOrg !== null) {
//position: "absolute", bottom: 5, right: -5,
const imageStyle = {
width: imagesize,
height: imagesize,
pointerEvents: "none",
marginLeft: data.creator_org !== undefined && data.creator_org.length > 0 ? 20 : 0,
borderRadius: 10,
border: foundOrg.id === userdata.active_org.id ? `3px solid ${boxColor}` : null,
cursor: "pointer",
marginRight: 10,
};
image =
foundOrg.image === "" ? (
<img
alt={foundOrg.name}
src={theme.palette.defaultImage}
style={imageStyle}
/>
) : (
<img
alt={foundOrg.name}
src={foundOrg.image}
style={imageStyle}
onClick={() => {}}
/>
);
image =
foundOrg.image === "" ? (
<img
alt={foundOrg.name}
src={theme.palette.defaultImage}
style={imageStyle}
/>
) : (
<img
alt={foundOrg.name}
src={foundOrg.image}
style={imageStyle}
onClick={() => {}}
/>
);
orgName = foundOrg.name;
orgId = foundOrg.id;
}
}
orgName = foundOrg.name;
orgId = foundOrg.id;
}
}
return (
<Paper style={{backgroundColor: theme.palette.surfaceColor, width: notificationWidth, padding: 25, borderBottom: "1px solid rgba(255,255,255,0.4)"}}>
+41 -49
View File
@@ -106,6 +106,8 @@ const Header = (props) => {
const clearNotifications = () => {
// Don't really care about the logout
toast("Clearing notifications")
fetch(`${globalUrl}/api/v1/notifications/clear`, {
credentials: "include",
method: "GET",
@@ -294,49 +296,26 @@ const Header = (props) => {
borderBottom: "1px solid rgba(255,255,255,0.4)",
}}
>
{/*<Typography variant="h6">
{new Date(data.updated_at).toISOString()}
</Typography >*/}
{data.reference_url !== undefined &&
data.reference_url !== null &&
data.reference_url.length > 0 ? (
<Link
to={data.reference_url}
style={{ color: "#f86a3e", textDecoration: "none" }}
>
<Typography variant="body1">{data.title}</Typography>
</Link>
) : (
<Typography variant="body1" color="textSecondary">
{data.title}
</Typography>
)}
{data.reference_url !== undefined && data.reference_url !== null && data.reference_url.length > 0 ?
<Link to={data.reference_url} style={{color: "#f86a3e", textDecoration: "none",}}>
<Typography variant="body1">
{data.title} ({data.amount})
</Typography >
</Link>
:
<Typography variant="body1" color="textSecondary">
{data.title}
</Typography >
}
{data.image !== undefined &&
data.image !== null &&
data.image.length > 0 ? (
<img
alt={data.title}
src={data.image}
style={{ height: 100, width: 100 }}
/>
) : null}
<Typography variant="body2">{data.description}</Typography>
{/*data.tags !== undefined && data.tags !== null && data.tags.length > 0 ?
data.tags.map((tag, index) => {
return (
<Chip
key={index}
style={chipStyle}
label={tag}
onClick={() => {
}}
variant="outlined"
color="primary"
/>
)
})
: null */}
{data.image !== undefined && data.image !== null && data.image.length > 0 ?
<img alt={data.title} src={data.image} style={{height: 100, width: 100, }} />
:
null
}
<Typography variant="body2" style={{marginTop: 10, maxHeight: 200, overflowX: "hidden", overflowY: "auto", }}>
{data.description}
</Typography >
<div style={{ display: "flex" }}>
{data.read === false ? (
<Button
@@ -374,7 +353,7 @@ const Header = (props) => {
setAnchorEl(event.currentTarget);
}}
>
<Badge badgeContent={notifications.length} color="primary">
<Badge badgeContent={notifications.filter((n) => n.read === false).length} color="primary">
<NotificationsIcon
color="secondary"
style={{ height: 30, width: 30 }}
@@ -413,7 +392,7 @@ const Header = (props) => {
>
<div style={{ display: "flex", marginBottom: 5 }}>
<Typography variant="body1">
Your Notifications ({notifications.length})
Your Notifications ({notifications.filter((data) => !data.read).length})
</Typography>
{notifications.length > 1 ? (
<Button
@@ -429,12 +408,17 @@ const Header = (props) => {
) : null}
</div>
<Typography variant="body2">
Notifications are made by Shuffle to help you discover issues or
improvements.
Notifications generated made by Shuffle to help you discover issues or
improvements. <a href="/docs/organizations#notifications" target="_blank" rel="noopener noreferrer" style={{color: "#f86a3e", textDecoration: "none", }}>
Learn more</a>
</Typography>
</Paper>
{notifications.map((data, index) => {
return <NotificationItem data={data} key={index} />;
if (data.read) {
return null
}
return <NotificationItem data={data} key={index} />;
})}
</Menu>
</span>
@@ -626,6 +610,11 @@ const Header = (props) => {
>
<MeetingRoomIcon style={{ marginRight: 5 }} /> &nbsp;Logout
</MenuItem>
<Divider style={{marginBottom: 10, }}/>
<Typography variant="body2" color="textSecondary" align="center" style={{marginTop: 5, marginBottom: 5,}}>
Version: 1.3.1
</Typography>
</Menu>
</span>
);
@@ -1352,14 +1341,17 @@ const Header = (props) => {
);
// <Divider style={{height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/>
return !isMobile ?
isLoggedIn ?
//
/*
!isLoggedIn ?
<div style={{minHeight: 68, maxHeight: 68, backgroundColor: theme.palette.backgroundColor, }}>
<BrowserView style={{position: "sticky", top: 0, }}>
{loginTextBrowser}
</BrowserView>
</div>
:
*/
return !isMobile ?
<AppBar
position="sticky"
color="transparent"
+124 -36
View File
@@ -112,20 +112,19 @@ const AuthenticationOauth2 = (props) => {
authenticationType.client_secret.length > 0
);
const [clientId, setClientId] = React.useState(
defaultConfigSet ? authenticationType.client_id : ""
);
const [clientSecret, setClientSecret] = React.useState(
defaultConfigSet ? authenticationType.client_secret : ""
);
const [clientId, setClientId] = React.useState(defaultConfigSet ? authenticationType.client_id : "");
const [clientSecret, setClientSecret] = React.useState(defaultConfigSet ? authenticationType.client_secret : "");
const [username, setUsername] = React.useState("");
const [password, setPassword] = React.useState("");
const [oauthUrl, setOauthUrl] = React.useState("");
const [buttonClicked, setButtonClicked] = React.useState(false);
const [offlineAccess, setOfflineAccess] = React.useState(true);
const allscopes = authenticationType.scope !== undefined ? authenticationType.scope : [];
const allscopes = authenticationType.scope !== undefined && authenticationType.scope !== null ? authenticationType.scope : [];
const [selectedScopes, setSelectedScopes] = React.useState(allscopes !== null && allscopes !== undefined ? allscopes.length > 0 && allscopes.length <= 3 ? allscopes : [] : [])
const [selectedScopes, setSelectedScopes] = React.useState(allscopes.length > 0 && allscopes.length <= 3 ? [allscopes[0]] : [])
const [manuallyConfigure, setManuallyConfigure] = React.useState(
defaultConfigSet ? false : true
);
@@ -158,6 +157,7 @@ const AuthenticationOauth2 = (props) => {
return null;
}
const startOauth2Request = (admin_consent) => {
// Admin consent also means to add refresh tokens
console.log("Inside oauth2 request for app: ", selectedApp.name)
@@ -301,6 +301,23 @@ const AuthenticationOauth2 = (props) => {
if ((authenticationType.redirect_uri === undefined || authenticationType.redirect_uri === null || authenticationType.redirect_uri.length === 0) && (authenticationType.token_uri !== undefined && authenticationType.token_uri !== null && authenticationType.token_uri.length > 0)) {
console.log("No redirect URI found, and token URI found. Assuming client credentials flow and saving directly in the database")
var tokenUri = authenticationType.token_uri;
if (oauthUrl !== undefined && oauthUrl !== null && oauthUrl.length > 0 && selectedApp !== undefined && selectedApp !== null) {
var same = false
for (var i = 0; i < selectedApp.authentication.parameters.length; i++) {
const param = selectedApp.authentication.parameters[i];
if (param.name === "url" && (param.value === oauthUrl || param.example === oauthUrl)) {
same = true
break
}
}
if (!same) {
tokenUri = oauthUrl
}
}
// Find app.configuration=true fields in the app.paramters
var parsedFields = [{
"key": "client_id",
@@ -316,9 +333,35 @@ const AuthenticationOauth2 = (props) => {
},
{
"key": "token_uri",
"value": authenticationType.token_uri,
"value": tokenUri,
}]
if (authenticationType.grant_type !== undefined && authenticationType.grant_type !== null && authenticationType.grant_type.length > 0) {
if (authenticationType.grant_type === "client_credentials") {
parsedFields.push({
"key": "grant_type",
"value": authenticationType.grant_type,
})
} else if (authenticationType.grant_type === "password") {
parsedFields.push({
"key": "grant_type",
"value": authenticationType.grant_type,
})
parsedFields.push({
"key": "username",
"value": username,
})
parsedFields.push({
"key": "password",
"value": password,
})
} else {
toast("Unknown grant type: " + authenticationType.grant_type)
}
}
const appAuthData = {
"label": "OAuth2 for " + selectedApp.name,
"app": {
@@ -332,14 +375,18 @@ const AuthenticationOauth2 = (props) => {
"reference_workflow": workflowId,
}
setNewAppAuth(appAuthData)
if (setNewAppAuth !== undefined) {
setNewAppAuth(appAuthData, true)
} else {
console.log("setNewAppAuth is undefined")
}
// Wait 1 second, then get app auth with update
//
if (getAppAuthentication !== undefined) {
setTimeout(() => {
getAppAuthentication(true, true, true);
}, 1000)
}
//if (getAppAuthentication !== undefined) {
// setTimeout(() => {
// getAppAuthentication(true, true, true);
// }, 1000)
//}
return
}
@@ -369,8 +416,6 @@ const AuthenticationOauth2 = (props) => {
}
const authentication_url = authenticationType.token_uri;
//console.log("AUTH: ", authenticationType)
//console.log("SCOPES2: ", resources)
const redirectUri = `${window.location.protocol}//${window.location.host}/set_authentication`;
const workflowId = workflow !== undefined ? workflow.id : "";
var state = `workflow_id%3D${workflowId}%26reference_action_id%3d${selectedAction.app_id}%26app_name%3d${selectedAction.app_name}%26app_id%3d${selectedAction.app_id}%26app_version%3d${selectedAction.app_version}%26authentication_url%3d${authentication_url}%26scope%3d${resources}%26client_id%3d${client_id}%26client_secret%3d${client_secret}`;
@@ -478,8 +523,6 @@ const AuthenticationOauth2 = (props) => {
}
return;
//do {
//} while (
};
authenticationOption.app.actions = [];
@@ -497,7 +540,6 @@ const AuthenticationOauth2 = (props) => {
}
const handleSubmitCheck = () => {
console.log("NEW AUTH: ", authenticationOption);
if (authenticationOption.label.length === 0) {
authenticationOption.label = `Auth for ${selectedApp.name}`;
//toast("Label can't be empty")
@@ -564,7 +606,10 @@ const AuthenticationOauth2 = (props) => {
console.log("FIELDS: ", newFields);
newAuthOption.fields = newFields;
setNewAppAuth(newAuthOption);
if (setNewAppAuth !== undefined) {
setNewAppAuth(newAuthOption);
}
//appAuthentication.push(newAuthOption)
//setAppAuthentication(appAuthentication)
//
@@ -647,7 +692,7 @@ const AuthenticationOauth2 = (props) => {
)}
</Button>
if (authButtonOnly === true) {
if (authButtonOnly === true && (authenticationType.redirect_uri !== undefined && authenticationType.redirect_uri !== null && authenticationType.redirect_uri.length > 0) && (authenticationType.token_uri !== undefined && authenticationType.token_uri !== null && authenticationType.token_uri.length > 0)) {
return autoAuthButton
}
@@ -751,10 +796,14 @@ const AuthenticationOauth2 = (props) => {
setOauthUrl(data.value);
}
const defaultValue = data.name === "url" && authenticationType.token_uri !== undefined && authenticationType.token_uri !== null && authenticationType.token_uri.length > 0 && (authenticationType.authorizationUrl === undefined || authenticationType.authorizationUrl === null || authenticationType.authorizationUrl.length === 0) ? authenticationType.token_uri : data.value === undefined || data.value === null ? "" : data.value
const fieldname = data.name === "url" && authenticationType.grant_type !== undefined && authenticationType.grant_type !== null && authenticationType.grant_type.length > 0 ? "Token URL" : data.name
return (
<div key={index} style={{ marginTop: 10 }}>
<LockOpenIcon style={{ marginRight: 10 }} />
<b>{data.name}</b>
<b>{fieldname}</b>
{data.schema !== undefined &&
data.schema !== null &&
@@ -767,6 +816,7 @@ const AuthenticationOauth2 = (props) => {
}}
defaultValue={"false"}
fullWidth
label={fieldname}
onChange={(e) => {
console.log("Value: ", e.target.value);
authenticationOption.fields[data.name] = e.target.value;
@@ -816,16 +866,11 @@ const AuthenticationOauth2 = (props) => {
: "text"
}
color="primary"
defaultValue={
data.value !== undefined && data.value !== null
? data.value
: ""
}
defaultValue={defaultValue}
placeholder={data.example}
onChange={(event) => {
authenticationOption.fields[data.name] =
event.target.value;
console.log("Setting oauth url");
authenticationOption.fields[data.name] = event.target.value;
console.log("Setting oauth url: ", event.target.value);
setOauthUrl(event.target.value);
//const [oauthUrl, setOauthUrl] = React.useState("")
}}
@@ -872,8 +917,51 @@ const AuthenticationOauth2 = (props) => {
//authenticationOption.label = event.target.value
}}
/>
{allscopes.length === 0 ? null : "Scopes (access rights)"}
{allscopes.length === 0 ? null : (
{authenticationType.grant_type !== "password" ? null :
<div>
<TextField
style={{
backgroundColor: theme.palette.inputColor,
borderRadius: theme.palette.borderRadius,
}}
InputProps={{
style: {
},
}}
fullWidth
color="primary"
label={"Username"}
placeholder={"Username"}
onChange={(event) => {
setUsername(event.target.value);
//authenticationOption.label = event.target.value
}}
/>
<TextField
style={{
backgroundColor: theme.palette.inputColor,
borderRadius: theme.palette.borderRadius,
marginBottom: 10,
}}
InputProps={{
style: {
},
}}
fullWidth
color="primary"
label={"Password"}
placeholder={"Password"}
onChange={(event) => {
setPassword(event.target.value);
//authenticationOption.label = event.target.value
}}
/>
</div>
}
{allscopes === undefined || allscopes === null || allscopes.length === 0 ? null : "Scopes (access rights)"}
{allscopes === undefined || allscopes === null || allscopes.length === 0 ? null : (
<div style={{width: "100%", marginTop: 10, display: "flex"}}>
<span>
<Select
@@ -931,7 +1019,7 @@ const AuthenticationOauth2 = (props) => {
borderRadius: theme.palette.borderRadius,
}}
disabled={
clientSecret.length === 0 || clientId.length === 0 || buttonClicked || selectedScopes.length === 0
clientSecret.length === 0 || clientId.length === 0 || buttonClicked || (allscopes.length !== 0 && selectedScopes.length === 0)
}
variant="contained"
fullWidth
+21 -4
View File
@@ -170,6 +170,7 @@ const ParsedAction = (props) => {
setEditorData,
setcodedata,
setAiQueryModalOpen,
} = props;
const classes = useStyles();
@@ -181,6 +182,8 @@ const ParsedAction = (props) => {
const [fieldCount, setFieldCount] = React.useState(0);
const [hiddenDescription, setHiddenDescription] = React.useState(true);
const [autoCompleting, setAutocompleting] = React.useState(false);
useEffect(() => {
if (setLastSaved !== undefined) {
@@ -796,7 +799,6 @@ const ParsedAction = (props) => {
selectedActionParameters[count].value = splitparsed[0]
selectedAction.parameters[count].value = splitparsed[0]
//changeActionParameter({target: {value: splitparsed[1]}},
selectedActionParameters[1].value = splitparsed[1]
selectedAction.parameters[1].value = splitparsed[1]
forceUpdate = true
@@ -2909,17 +2911,26 @@ const ParsedAction = (props) => {
marginLeft: 15,
paddingRight: 0,
}}
disabled={autoCompleting}
onClick={() => {
// aiSubmit(aiMsg, undefined, undefined, newSelectedAction)
aiSubmit("Fill based on previous values", undefined, undefined, selectedAction)
//if (setAiQueryModalOpen !== undefined) {
// setAiQueryModalOpen(true)
//} else {
aiSubmit("Fill based on previous values", undefined, undefined, selectedAction)
//}
setAutocompleting(true)
}}
>
<Tooltip
color="primary"
title={"Autocompletes fields. Uses NAME of the action and previous values' results."}
title={"Autocomplete fields. Will show a popup so that you can query how you would like to fill it in"}
placement="top"
>
{autoCompleting ?
<CircularProgress style={{height: 20, width: 20, }} />
:
<AutoFixHighIcon style={{ color: "rgba(255,255,255,0.7)", height: 24, }} />
}
</Tooltip>
</IconButton>
</div>
@@ -3409,6 +3420,12 @@ const ParsedAction = (props) => {
setSelectedActionEnvironment(env);
selectedAction.environment = env.Name;
setSelectedAction(selectedAction);
for (let actionkey in workflow.actions) {
workflow.actions[actionkey].environment = env.Name
}
setWorkflow(workflow)
toast("Set environment for ALL actions to " + env.Name)
}}
style={{
backgroundColor: theme.palette.inputColor,
+199 -3
View File
@@ -1,24 +1,30 @@
import React, { useState, useEffect } from "react";
import { toast } from "react-toastify";
import theme from "../theme.jsx";
import {
Paper,
Typography,
Tooltip,
Typography,
Divider,
Button,
ButtonGroup,
Grid,
Card,
Switch,
Chip,
Switch,
} from "@mui/material";
import { useNavigate, Link } from "react-router-dom";
import Priority from "../components/Priority.jsx";
//import { useAlert
const Priorities = (props) => {
const { globalUrl, userdata, serverside, billingInfo, stripeKey, checkLogin, setAdminTab, setCurTab, } = props;
const { globalUrl, userdata, serverside, billingInfo, stripeKey, checkLogin, setAdminTab, setCurTab, notifications, setNotifications, } = props;
const [showDismissed, setShowDismissed] = React.useState(false);
const [showRead, setShowRead] = React.useState(false);
const [appFramework, setAppFramework] = React.useState({});
let navigate = useNavigate();
useEffect(() => {
getFramework()
@@ -61,6 +67,182 @@ const Priorities = (props) => {
})
}
const dismissNotification = (alert_id) => {
// Don't really care about the logout
fetch(`${globalUrl}/api/v1/notifications/${alert_id}/markasread`, {
credentials: "include",
method: "GET",
headers: {
"Content-Type": "application/json",
},
})
.then(function (response) {
if (response.status !== 200) {
console.log("Error in response");
}
return response.json();
})
.then(function (responseJson) {
if (responseJson.success === true) {
const newNotifications = notifications.filter(
(data) => data.id !== alert_id
);
console.log("NEW NOTIFICATIONS: ", newNotifications);
if (setNotifications !== undefined) {
setNotifications(newNotifications)
}
} else {
toast("Failed dismissing notification. Please try again later.");
}
})
.catch((error) => {
console.log("error in notification dismissal: ", error);
//removeCookie("session_token", {path: "/"})
})
}
const notificationWidth = "100%"
const imagesize = 22
const boxColor = "#86c142"
const NotificationItem = (props) => {
const {data} = props
var image = "";
var orgName = "";
var orgId = "";
if (userdata.orgs !== undefined) {
const foundOrg = userdata.orgs.find((org) => org.id === data["org_id"]);
if (foundOrg !== undefined && foundOrg !== null) {
//position: "absolute", bottom: 5, right: -5,
const imageStyle = {
width: imagesize,
height: imagesize,
pointerEvents: "none",
marginLeft:
data.creator_org !== undefined && data.creator_org.length > 0
? 20
: 0,
borderRadius: 10,
border:
foundOrg.id === userdata.active_org.id
? `3px solid ${boxColor}`
: null,
cursor: "pointer",
marginRight: 10,
};
image =
foundOrg.image === "" ? (
<img
alt={foundOrg.name}
src={theme.palette.defaultImage}
style={imageStyle}
/>
) : (
<img
alt={foundOrg.name}
src={foundOrg.image}
style={imageStyle}
onClick={() => {}}
/>
);
orgName = foundOrg.name;
orgId = foundOrg.id;
}
}
return (
<Paper
style={{
backgroundColor: theme.palette.platformColor,
width: notificationWidth,
padding: 30,
borderBottom: "1px solid rgba(255,255,255,0.4)",
marginBottom: 20,
}}
>
<div style={{display: "flex", }}>
{data.amount === 1 && data.read === false ?
<Chip
label={"First seen"}
variant="contained"
color="primary"
style={{marginRight: 15, height: 25, }}
/>
: null}
{data.read === false ?
<Chip
label={"Unread"}
variant="outlined"
color="primary"
style={{marginRight: 15, height: 25, }}
/>
:
<Chip
label={"Read"}
variant="outlined"
color="secondary"
style={{marginRight: 15, height: 25, }}
/>
}
<Typography variant="body1" color="textPrimary">
{data.title}
</Typography >
</div>
{data.image !== undefined && data.image !== null && data.image.length > 0 ?
<img alt={data.title} src={data.image} style={{height: 100, width: 100, }} />
:
null
}
<Typography variant="body2" color="textSecondary" style={{marginTop: 10, maxHeight: 200, overflowX: "hidden", overflowY: "auto", }}>
{data.description}
</Typography >
<div style={{ display: "flex" }}>
<ButtonGroup>
<Button
color="secondary"
variant="outlined"
style={{ marginTop: 15 }}
disabled={data.reference_url === undefined || data.reference_url === null || data.reference_url.length === 0}
onClick={() => {
window.open(data.reference_url, "_blank")
}}
>
Explore
</Button>
{data.read === false ? (
<Button
color="secondary"
variant="outlined"
style={{ marginTop: 15 }}
onClick={() => {
dismissNotification(data.id);
}}
>
Dismiss
</Button>
) : null}
</ButtonGroup>
<Typography variant="body2" color="textSecondary" style={{marginLeft: 20, marginTop: 20, }}>
<b>First seen</b>: {new Date(data.created_at * 1000).toISOString().slice(0, 19)}
</Typography >
<Typography variant="body2" color="textSecondary" style={{marginLeft: 20, marginTop: 20, }}>
<b>Last seen</b>: {new Date(data.updated_at * 1000).toISOString().slice(0, 19)}
</Typography >
<Typography variant="body2" color="textSecondary" style={{marginLeft: 20, marginTop: 20, }}>
<b>Times seen</b>: {data.amount}
</Typography >
</div>
</Paper>
);
}
return (
<div style={{maxWidth: 1000, }}>
<h2 style={{ display: "inline" }}>Suggestions</h2>
@@ -125,6 +307,20 @@ const Priorities = (props) => {
setShowRead(!showRead);
}}
/>&nbsp; Show read
{notifications === null || notifications === undefined || notifications.length === 0 ? null :
<div>
{notifications.map((notification, index) => {
if (showRead === false && notification.read === true) {
return null
}
return (
<NotificationItem data={notification} key={index} />
)
})}
</div>
}
</div>
)
}
+448 -20
View File
@@ -4,6 +4,7 @@ import {
TextField,
Link,
Button,
ButtonGroup,
CircularProgress,
Select,
MenuList,
@@ -13,6 +14,7 @@ import {
Autocomplete,
Tooltip,
Typography,
IconButton,
} from '@mui/material';
import { toast } from "react-toastify"
@@ -21,6 +23,7 @@ import theme from '../theme.jsx';
import dayjs from 'dayjs';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'
import Pagination from '@mui/material/Pagination';
import { triggers as alltriggers } from "../views/AngularWorkflow.jsx"
import {
DatePicker,
DateTimePicker,
@@ -29,6 +32,9 @@ import {
import {
OpenInNew as OpenInNewIcon,
PlayArrow as PlayArrowIcon,
Insights as InsightsIcon,
Replay as ReplayIcon,
} from '@mui/icons-material';
import { DataGrid, GridColDef, GridValueGetterParams } from '@mui/x-data-grid'
@@ -44,21 +50,18 @@ const RuntimeDebugger = (props) => {
const classes = useStyles();
//const [workflowId, setWorkflowId] = useState("");
//const [status, setStatus] = useState("FINISHED");
//const [endTime, setEndTime] = useState(dayjs().subtract(0, 'day'))
//const [startTime, setStartTime] = useState(dayjs().subtract(30, 'day'))
const [workflowId, setWorkflowId] = useState("")
const [status, setStatus] = useState("")
const [endTime, setEndTime] = useState("")
const [startTime, setStartTime] = useState("")
const [workflow, setWorkflow] = useState({})
const [ignoreOrg, setIgnoreOrg] = useState(false)
const [searchLoading, setSearchLoading] = useState(false)
const [rowCursor, setCursor] = useState("")
const [rowsPerPage, setRowsPerPage] = useState(10)
const [resultRows, setResultRows] = useState([])
const [selectedWorkflowExecutions, setSelectedWorkflowExecutions] = useState([])
const [workflows, setWorkflows] = useState([
{"id": "", "name": "All Workflows",}
])
@@ -73,7 +76,7 @@ const RuntimeDebugger = (props) => {
const submitSearch = (workflowId, status, startTime, endTime, cursor, limit) => {
setResultRows([])
//setResultRows([])
setSearchLoading(true)
const fetchData = {
workflow_id: workflowId,
@@ -83,6 +86,7 @@ const RuntimeDebugger = (props) => {
status: status.toUpperCase(),
start_time: startTime,
end_time: endTime,
ignore_org: ignoreOrg,
}
fetch(`${globalUrl}/api/v1/workflows/search`, {
@@ -105,22 +109,29 @@ const RuntimeDebugger = (props) => {
//data.runs[key].endTimestamp = data.runs[key].ended_at.toISOString().slice(0, 19).replace('T', ' ')
const startTimestamp = new Date(data.runs[key].started_at*1000)
data.runs[key].startTimestamp = startTimestamp.toISOString().slice(0, 19).replace('T', ' ')
const endTimestamp = new Date(data.runs[key].completed_at*1000)
data.runs[key].endTimestamp = endTimestamp.toISOString().slice(0, 19).replace('T', ' ')
if (data.runs[key].completed_at === 0 || data.runs[key].completed_at === null) {
data.runs[key].endTimestamp = ""
}
}
// Add 20 empty rows to the end of the resultRows array
// This is to make sure that the scrollbar is always visible
setResultRows(data.runs)
} else {
toast("No results found. Keeping old runs")
}
} else {
console.error("Search error: ", data.reason)
toast("Failed to search for runs. Please try again.")
}
})
.catch((error) => {
setSearchLoading(false)
console.error("Error:", error);
toast("Failed to search for runs. Please try again (2)")
})
}
@@ -176,11 +187,78 @@ const RuntimeDebugger = (props) => {
}
}, [])
const forceContinue = (execution) => {
console.log(`FORCE CONTINUE execution ${execution.execution_id} for workflow ${execution.workflow.id}`)
fetch(`${globalUrl}/api/v1/workflows/${execution.workflow.id}/executions/${execution.execution_id}/rerun`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
credentials: "include",
})
.then((response) => response.json())
.then((data) => {
if (data.success) {
if (data.reason !== undefined && data.reason !== null && data.reason !== "") {
toast("Successful response: " + data.reason)
} else {
toast("Successfully forced continue")
}
} else {
if (data.reason !== undefined && data.reason !== null && data.reason !== "") {
toast(`Failed to force continue: ${data.reason}`)
} else {
toast("Failed to force continue")
}
}
})
.catch((error) => {
console.error("Error:", error);
toast(`Failed to force continue: ${error}`)
})
}
const imageSize = 30
const timenowUnix = Math.floor(Date.now() / 1000)
const columns: GridColDef[] = [
{
field: 'execution_source',
headerName: 'Source',
width: 75,
renderCell: (params) => {
var foundSource = <PlayArrowIcon style={{color: theme.palette.primary.main, height: imageSize, width: imageSize, }} />
var source = params.row.execution_source
if (source === "schedule") {
foundSource = <img src={alltriggers[1].large_image} alt="schedule" style={{borderRadius: theme.palette.borderRadius, height: imageSize, width: imageSize, }} />
} else if (source === "webhook") {
foundSource = <img src={alltriggers[0].large_image} alt="webhook" style={{borderRadius: theme.palette.borderRadius, height: imageSize, width: imageSize, }} />
} else if (source === "subflow" || source.length === 36) {
foundSource = <img src={alltriggers[4].large_image} alt="subflow" style={{borderRadius: theme.palette.borderRadius, height: imageSize, width: imageSize, }} />
source = "subflow"
} else if (source === "rerun" || source.length === 36) {
foundSource = <ReplayIcon style={{color: theme.palette.primary.secondary, height: imageSize, width: imageSize, }} />
source = "rerun of a previous run"
} else {
source = "manual"
}
return (
<span style={{}} onClick={() => {
//setStatus(params.row.status)
}}>
<Tooltip title={source} placement="top">
{foundSource}
</Tooltip>
</span>
)
},
},
{
field: 'status',
headerName: 'Status',
width: 150,
width: 100,
renderCell: (params) => (
<span style={{cursor: "pointer", }} onClick={() => {
setStatus(params.row.status)
@@ -208,6 +286,7 @@ const RuntimeDebugger = (props) => {
</span>
),
},
{
field: 'workflow results',
headerName: 'Results',
@@ -235,17 +314,212 @@ const RuntimeDebugger = (props) => {
)
},
},
{ field: 'startTimestamp', headerName: 'Start time', width: 160, },
{ field: 'endTimestamp', headerName: 'End time', width: 160, },
{
field: 'finished',
headerName: 'Finished',
width: 75,
renderCell: (params) => {
var foundItems = 0
var foundSkipped = 0
if (params.row.results !== null && params.row.results !== undefined) {
for (let key in params.row.results) {
if (params.row.results[key].status === "SUCCESS") {
foundItems += 1
}
if (params.row.results[key].status === "SKIPPED") {
foundSkipped += 1
}
}
}
var foundError = ""
if (foundItems + foundSkipped < params.row.workflow.actions.length && params.row.status === "FINISHED") {
foundError = "Workflow is done, but all nodes are not finished. This most likely indicates a problem with the workflow"
}
return (
<Tooltip title={foundError} placement="top">
<span style={{backgroundColor: foundError.length > 0 ? "rgba(244,0,0,0.6)" : "inherit"}} onClick={() => {
}}>
{foundItems}
</span>
</Tooltip>
)
},
},
{
field: 'skipped',
headerName: 'Skipped',
width: 75,
renderCell: (params) => {
var foundItems = 0
if (params.row.results !== null && params.row.results !== undefined) {
for (let key in params.row.results) {
if (params.row.results[key].status === "SKIPPED") {
foundItems += 1
}
}
}
return (
<span style={{}} onClick={() => {
}}>
{foundItems}
</span>
)
},
},
{ field: 'startTimestamp', headerName: 'Start time (UTC)', width: 160,
renderCell: (params) => {
const comparisonTimestamp = params.row.completed_at === 0 ? timenowUnix : params.row.completed_at
const hasError = comparisonTimestamp-params.row.started_at > 300
return (
<Tooltip title={hasError ? "More than 5 minutes from start to finish" : ""} placement="top">
<span style={{cursor: "pointer", backgroundColor: !hasError ? "inherit" : "rgba(244,0,0,0.4)",}} onClick={() => {
console.log("Zoom in on end timestamp is this one: ", params.row.endTimestamp)
//setEndTimestamp(params.row.endTimestamp)
// Make a new Date() from params.row.startTimestamp and set it in the endTime
const newEndTime = new Date(params.row.startTimestamp)
if (newEndTime !== null && newEndTime !== undefined && newEndTime !== "" && newEndTime !== "Invalid Date") {
// Translate newEndTime to UTC no matter what timezone we are in. Based it on local()
// Plus 1 minute to make sure it comes in
setEndTime(dayjs(newEndTime.setMinutes(newEndTime.getMinutes()+1)))
// Use dayjs to translate it into something useful
// Remove 5 minutes from it and set startTime
//newEndTime.setMinutes(newEndTime.getMinutes()-5)
//setStartTime(dayjs(newEndTime))
}
}}>
{params.row.startTimestamp}
</span>
</Tooltip>
)
}
},
{ field: 'endTimestamp', headerName: 'End time (UTC)', width: 160, },
{
field: 'id',
headerName: 'Explore',
width: 65,
renderCell: (params) => (
<Link href={`/workflows/${params.row.workflow.id}?execution_id=${params.row.id}`} target="_blank" rel="noopener noreferrer">
<OpenInNewIcon fontSize="small" />
</Link>
),
width: 120,
renderCell: (params) => {
const parsedResult = params.row.result === null || params.row.result === undefined || params.row.result === "" ? "" : params.row.result
var errorReason = ""
var hasError = parsedResult !== null && parsedResult !== undefined && parsedResult !== "" ? parsedResult.includes("{%") && parsedResult.includes("%}") : false
if (hasError) {
errorReason = "Liquid parsing error"
}
// if success: false
// if node == FAILURE or ABORTED
if (parsedResult.includes(`\"success\": false`)) {
errorReason = "success: false in last result"
hasError = true
}
if (!hasError && parsedResult.includes(`\"status\":`)) {
// Look for any status that is 300 or higher
const statusSplit = parsedResult.split(`\"status\":`)
if (statusSplit.length > 1) {
var foundStatus = statusSplit[1].trim()
// Check if pattern is \d,
if (foundStatus.includes(",")) {
const foundStatusSplit = foundStatus.split(",")
if (foundStatusSplit.length > 1) {
foundStatus = foundStatusSplit[0].trim()
// Check if it's a number
}
} else {
foundStatus = ""
}
if (!isNaN(foundStatus) && foundStatus >= 300) {
errorReason = "Status code: "+foundStatus
hasError = true
}
}
}
if (!hasError) {
// Find last node that isn't skipped and check status
var lastresult = {}
for (var key in params.row.results) {
const result = params.row.results[key]
if (result.status === "SKIPPED") {
continue
}
if (result.completed_at === undefined || result.completed_at === null) {
continue
}
if (result.completed_at >= lastresult.completed_at) {
lastresult = result
}
}
if (lastresult.id !== undefined && lastresult.status !== "SUCCESS" && lastresult.status !== "SKIPPED") {
errorReason = "Bad status for last node: "+lastresult.status
hasError = true
}
}
if (!hasError && params.row.notifications_created !== null && params.row.notifications_created !== undefined && params.row.notifications_created !== 0) {
hasError = true
errorReason = "Generated notifications: "+params.row.notifications_created
}
return (
<div style={{display: "flex", }}>
<Tooltip arrow placement="left" title={
<Typography variant="body2" style={{whiteSpace: "pre-line", padding: 10, }}>
Workflow result: {errorReason}<br/><br/>
{params.row.result !== null && params.row.result !== undefined && params.row.result !== "" ?
params.row.result
:
null
}
</Typography>
}>
<span style={{backgroundColor: !hasError ? "inherit" : "rgba(244,0,0,0.45)", display: "flex", }}>
<Link href={`/workflows/${params.row.workflow.id}?execution_id=${params.row.id}`} target="_blank" rel="noopener noreferrer">
<OpenInNewIcon fontSize="small" style={{marginTop: 7, }} />
</Link>
</span>
</Tooltip>
<Tooltip arrow title="Force continue workflow. Only workflows for workflows in EXECUTING state. This is NOT a rerun, but way for Shuffle to figure out the next steps automatically. If the execution doesn't finish even after trying this, please contact support@shuffler.io">
<IconButton
style={{marginLeft: 5, }}
disabled={params.row.status !== "EXECUTING"}
onClick={() => {
forceContinue(params.row)
}}
>
<PlayArrowIcon fontSize="small" />
</IconButton>
</Tooltip>
<Tooltip arrow title="Explore workflow run logs">
<IconButton
style={{marginLeft: 5, }}
onClick={() => {
window.open(`${globalUrl}/api/v1/workflows/search/${params.row.id}`, "_blank")
}}
disabled={!userdata.support}
>
<InsightsIcon fontSize="small" />
</IconButton>
</Tooltip>
</div>
)
}
},
]
@@ -253,7 +527,6 @@ const RuntimeDebugger = (props) => {
// Check if the user is currently focusing a texxtfield or not
// If they are, don't submit the search
if (document.activeElement.tagName === "INPUT") {
console.log("User is focusing a textfield, not submitting search")
return
}
@@ -273,6 +546,29 @@ const RuntimeDebugger = (props) => {
setEndTime(date)
}
const abortExecution = (workflowId, executionId) => {
fetch(`${globalUrl}/api/v1/workflows/${workflowId}/executions/${executionId}/abort`,
{
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
}
)
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for ABORT EXECUTION :O!");
}
return response.json();
})
.catch((error) => {
toast.error("Error aborting execution: "+error.toString())
});
};
const handleWorkflowSelectionUpdate = (e, isUserinput) => {
if (e.target.value === undefined || e.target.value === null || e.target.value.id === undefined) {
console.log("Returning as there's no id")
@@ -287,9 +583,119 @@ const RuntimeDebugger = (props) => {
submitSearch(e.target.value.id, status, startTime, endTime, rowCursor, rowsPerPage)
}
const executeWorkflow = (execution) => {
const data = {
execution_argument: execution.execution_argument,
start: execution.start,
execution_source: "rerun",
};
fetch(`${globalUrl}/api/v1/workflows/${execution.workflow.id}/execute?start=${execution.start}`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
body: JSON.stringify(data),
}
)
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for WORKFLOW EXECUTION :O!");
}
return response.json();
})
.then((responseJson) => {
if (!responseJson.success) {
toast("Error executing workflow: "+responseJson.error)
} else {
console.log("Executed workflow: ", responseJson)
}
})
.catch((error) => {
toast("Failed to execute workflow: "+error.toString())
});
}
return (
<div style={{minWidth: 1000, maxWidth: 1000, margin: "auto", }}>
<h1>Workflow Run Debugger</h1>
<div style={{minWidth: 1150, maxWidth: 1150, margin: "auto", }}>
<div style={{display: "flex", }}>
<h1 style={{flex: 3, }}>Workflow Run Debugger</h1>
{selectedWorkflowExecutions.length > 0 ?
<ButtonGroup>
<Tooltip title="Reruns ALL selected workflows. This will make a new execution for them, and not continue the existing.">
<Button
variant="outlined"
color="secondary"
style={{maxHeight: 40, marginTop: 25, }}
onClick={() => {
for (var i = 0; i < selectedWorkflowExecutions.length; i++) {
const selected = selectedWorkflowExecutions[i]
executeWorkflow(selected)
}
toast("Reran "+selectedWorkflowExecutions.length+" workflow run!")
setSelectedWorkflowExecutions([])
}}
>
Rerun Selected ({selectedWorkflowExecutions.length})
</Button>
</Tooltip>
<Tooltip title="Aborts ALL selected workflows in EXECUTING state">
<Button
variant="contained"
color="primary"
style={{maxHeight: 40, marginTop: 25, }}
onClick={() => {
toast("Attempting to abort "+selectedWorkflowExecutions.length+" workflow runs...")
var aborted = 0
for (var i = 0; i < selectedWorkflowExecutions.length; i++) {
const selected = selectedWorkflowExecutions[i]
if (selected.status === "EXECUTING") {
abortExecution(selected.workflow.id, selected.execution_id)
aborted += 1
}
}
if (aborted === 0) {
toast("No workflows were aborted as they are not executing.")
} else {
toast("Aborted "+aborted+" workflows.")
// Research
submitSearch(workflowId, status, startTime, endTime, rowCursor, rowsPerPage)
setSelectedWorkflowExecutions([])
}
}}
>
Abort Selected ({selectedWorkflowExecutions.length})
</Button>
</Tooltip>
</ButtonGroup>
: null}
{userdata.support === true ?
<Button
variant={ignoreOrg ? "contained" : "outlined"}
color="primary"
style={{maxHeight: 40, marginTop: 25, }}
onClick={() => {
setIgnoreOrg(!ignoreOrg)
}}
>
{ignoreOrg ? "Ignoring Org" : "Ignore Org"}
</Button>
: null}
</div>
<form onSubmit={(e) => {
submitSearch(workflowId, status, startTime, endTime, rowCursor, rowsPerPage)
}} style={{display: "flex", }}>
@@ -432,6 +838,7 @@ const RuntimeDebugger = (props) => {
minWidth: 240,
maxWidth: 240,
}}
ampm={false}
label="Search from"
format="YYYY-MM-DD HH:mm:ss"
value={startTime}
@@ -445,6 +852,7 @@ const RuntimeDebugger = (props) => {
minWidth: 240,
maxWidth: 240,
}}
ampm={false}
label="Search until"
format="YYYY-MM-DD HH:mm:ss"
value={endTime}
@@ -454,7 +862,7 @@ const RuntimeDebugger = (props) => {
</LocalizationProvider>
<Button
variant="contained"
variant="outlined"
color="primary"
onClick={() => {
submitSearch(workflowId, status, startTime, endTime, rowCursor, rowsPerPage)
@@ -482,6 +890,26 @@ const RuntimeDebugger = (props) => {
onPageChange={(params) => {
console.log("params: ", params)
}}
onSelectionModelChange={(newSelection) => {
//console.log("newSelection: ", newSelection)
//setSelectedWorkflowExecutionsIndexes(newSelection)
var found = []
for (var i = 0; i < newSelection.length; i++) {
// Find the workflow in the resultRows
var selected = resultRows.find((workflow) => {
return workflow.id === newSelection[i]
})
if (selected === undefined || selected === null) {
continue
}
found.push(selected)
}
setSelectedWorkflowExecutions(found)
}}
// Track which items are selected
/>
</div>
</div>
+53 -85
View File
@@ -50,6 +50,7 @@ const SearchData = props => {
const [value, setValue] = useState("");
const [userTyped, setUserTyped] = useState(false)
if (serverside === true) {
return null
}
@@ -59,25 +60,16 @@ const SearchData = props => {
//}
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
//if (window.location.pathname !== oldPath) {
// setSearchOpen(false)
// setOldPath(window.location.pathname)
//}
//if (window.location.pathname === "/search") {
// setModalOpen(true)
//}
// if (window.location.pathname === "/docs" || window.location.pathname === "/apps" || window.location.pathname === "/usecases" ) {
// setModalOpen(false)
// }
//useEffect(() => {
// if (searchOpen) {
// var tarfield = document.getElementById("shuffle_search_field")
// tarfield.focus()
// }
//}, searchOpen)
// useEffect(() => {
// if (searchOpen) {
// var tarfield = document.getElementById("shuffle_search_field")
// tarfield.focus()
// }
// }, searchOpen)
const SearchBox = ({ currentRefinement, refine, isSearchStalled, }) => {
const keyPressHandler = (e) => {
@@ -85,9 +77,11 @@ const SearchData = props => {
if (e.which === 13) {
// alert("You pressed enter!");
navigate("/search?q=" + currentRefinement, { state: value, replace: true });
setSearchOpen(false)
setModalOpen(false)
return
setSearchOpen(false)
setModalOpen(false)
return
}
};
/*
@@ -104,10 +98,11 @@ const SearchData = props => {
return (
<form id="search_form" noValidate type="searchbox" action="" role="search" onClick={() => {
}}>
}}
>
<TextField
fullWidth
style={{ backgroundColor: theme.palette.surfaceColor, borderRadius: borderRadius, minWidth: "100%", maxWidth: "100%", }}
style={{ zIndex: 1100, marginTop:-20,marginBottom: 200, position:"fixed", backgroundColor: theme.palette.inputColor, borderRadius: borderRadius, width: 685, }}
InputProps={{
style: {
color: "white",
@@ -120,7 +115,7 @@ const SearchData = props => {
disableUnderline: true,
startAdornment: (
<InputAdornment position="start">
<SearchIcon style={{ marginLeft: 5, marginRight: 10, color: "#f86a3e", }} />
<SearchIcon style={{ marginLeft: 5, marginRight: 10, color: "#f86a3e", }} />
</InputAdornment>
),
endAdornment: (
@@ -188,8 +183,8 @@ const SearchData = props => {
const baseImage = <CodeIcon />
return (
<Card elevation={0} style={{ marginRight: 10, color: "white", zIndex: 1002, backgroundColor: theme.palette.inputColor, width: "100%", left: 75, boxShadows: "none", }}>
<Typography variant="h6" style={{ margin: "10px 10px 0px 20px", }}>
<Card elevation={0} style={{ marginRight: 10,marginTop:50, color: "white", zIndex: 1002, backgroundColor: theme.palette.inputColor, width: "100%", left: 75, boxShadows: "none", }}>
<Typography variant="h6" style={{ margin: "10px 10px 0px 20px", color:"#FF8444", borderBottom: "1px solid", width: 105 }}>
Workflows
</Typography>
@@ -349,13 +344,13 @@ const SearchData = props => {
const baseImage = <LibraryBooksIcon />
return (
<Card elevation={0} style={{ marginRight: 10, color: "white", zIndex: 1001, backgroundColor: theme.palette.inputColor, width: "100%", left: -30, boxShadows: "none", }}>
<Card elevation={0} style={{ marginRight: 10, color: "white", zIndex: 999, backgroundColor: theme.palette.inputColor, width: 685, boxShadows: "none", }}>
{/* <IconButton style={{ zIndex: 5000, position: "absolute", right: 14, color: "grey" }} onClick={() => {
setSearchOpen(false)
}}>
<CloseIcon />
</IconButton> */}
<Typography variant="h6" style={{ margin: "10px 10px 0px 20px", }}>
<Typography variant="h6" style={{ margin: "40px 10px 0px 20px", color:"#FF8444", borderBottom: "1px solid", width: 50 }}>
Apps
</Typography>
@@ -392,7 +387,6 @@ const SearchData = props => {
const name = hit.name === undefined ?
hit.filename.charAt(0).toUpperCase() + hit.filename.slice(1).replaceAll("_", " ") + " - " + hit.title :
(hit.name.charAt(0).toUpperCase() + hit.name.slice(1)).replaceAll("_", " ")
var secondaryText = hit.data !== undefined ? hit.data.slice(0, 40) + "..." : ""
const avatar = hit.image_url === undefined ?
baseImage
@@ -401,7 +395,6 @@ const SearchData = props => {
src={hit.image_url}
variant="rounded"
/>
//console.log(hit)
if (hit.categories !== undefined && hit.categories !== null && hit.categories.length > 0) {
secondaryText = hit.categories.slice(0, 3).map((data, index) => {
@@ -520,13 +513,13 @@ const SearchData = props => {
//console.log(type, hits.length, hits)
return (
<Card elevation={0} style={{ marginRight: 10, color: "white", zIndex: 1002, backgroundColor: theme.palette.inputColor, width: "100%", left: 470, boxShadows: "none", }}>
<Card elevation={0} style={{ marginRight: 10,marginTop:50, color: "white", zIndex: 1002, backgroundColor: theme.palette.inputColor, width: "100%", left: 470, boxShadows: "none", }}>
{/* <IconButton style={{ zIndex: 5000, position: "absolute", right: 14, color: "grey" }} onClick={() => {
setSearchOpen(false)
}}>
<CloseIcon />
</IconButton> */}
<Typography variant="h6" style={{ margin: "10px 10px 0px 20px", }}>
<Typography variant="h6" style={{ margin: "10px 10px 0px 20px", color:"#FF8444", borderBottom: "1px solid", width: 152}}>
Documentation
</Typography>
{/*
@@ -643,42 +636,14 @@ const SearchData = props => {
</Card>
)
}
const CustomSearchBox = connectSearchBox(SearchBox)
const CustomAppHits = connectHits(AppHits)
const CustomWorkflowHits = connectHits(WorkflowHits)
const CustomDocHits = connectHits(DocHits)
const modalView = (
<div>
<Grid container style={{ display: "contents" }}>
<Grid item xs="auto" style={{ marginTop: 12 }}>
<Index indexName="appsearch">
<CustomAppHits />
</Index>
</Grid>
<Grid item xs="auto" style={{ marginTop: 12 }}>
<Index indexName="workflows">
<CustomWorkflowHits />
</Index>
</Grid>
<Grid item xs="auto" style={{ marginTop: 12 }}>
<Index indexName="documentation">
<CustomDocHits />
</Index>
</Grid>
</Grid>
</div>
)
const gettingStartData = (
const gettingStartData = !searchOpen ? (
<Grid
container
direction="row"
alignItems="center"
// justify="space-evenly"
>
<Grid item xs={6} style={{ alignItems: "center", flexDirection: "row" }}>
<Grid item xs={6} style={{ alignItems: "center", flexDirection: "row", marginTop: 70, }}>
<List style={{ width: "100%", marginLeft: 10, color: "var(--Paragraph-text, #C8C8C8)" }}>
<ListItem>
<ArticleIcon style={{ marginRight: 10, display: "flex", width: 22 }} />
@@ -715,7 +680,7 @@ const SearchData = props => {
</div>
</List>
</Grid>
<Grid item xs={6} style={{ alignItems: "center", flexDirection: "row", width: 22 }}>
<Grid item xs={6} style={{ alignItems: "center", flexDirection: "row", width: 22, marginTop: 70 }}>
<List style={{ width: "100%", marginLeft: 10, color: "var(--Paragraph-text, #C8C8C8)", }}>
<ListItem>
<ManageSearchIcon style={{ marginRight: 10, display: "flex" }} />
@@ -749,29 +714,6 @@ const SearchData = props => {
</div>
</List>
</Grid>
{/* <Grid item xs={12} style={{ alignItems: "center", flexDirection: "row", width: 22 }}>
<List style={{ width: "100%", marginLeft: 10, color: "var(--Paragraph-text, #C8C8C8)", }}>
<ListItem>
<ManageSearchIcon style={{ marginRight: 10, display: "flex" }} />
<Typography variant="body1" style={{ display: "flex", fontSize: 16 }}>Popular searches</Typography>
</ListItem>
<div style={{ marginLeft: 35 }}>
<ListItem>
<Typography variant="body1" style={{ fontSize: 16, }}>Apps</Typography>
<KeyboardArrowRightIcon />
</ListItem>
<ListItem>
<Typography variant="body1" style={{ fontSize: 16, }}>Workflows</Typography>
<KeyboardArrowRightIcon />
</ListItem>
<ListItem>
<Typography variant="body1" style={{ fontSize: 16, }}>Creator</Typography>
<KeyboardArrowRightIcon />
</ListItem>
</div>
</List>
</Grid> */}
<Grid style={{ textAlign: "end", width: "100%", textTransform: 'capitalize', }}>
<Button style={{ textAlign: "center", textTransform: 'capitalize' }}
onClick={() => { window.location = "/search"; }} >
@@ -779,12 +721,38 @@ const SearchData = props => {
</Button>
</Grid>
</Grid>
): null
const CustomSearchBox = connectSearchBox(SearchBox)
const CustomAppHits = connectHits(AppHits)
const CustomWorkflowHits = connectHits(WorkflowHits)
const CustomDocHits = connectHits(DocHits)
const modalView = (
<div>
<Grid container style={{ display: "contents", }}>
<Grid item xs="auto" style={{ }}>
<Index indexName="appsearch">
<CustomAppHits />
</Index>
</Grid>
<Grid item xs="auto" style={{ }}>
<Index indexName="workflows">
<CustomWorkflowHits />
</Index>
</Grid>
<Grid item xs="auto" style={{ }}>
<Index indexName="documentation">
<CustomDocHits />
</Index>
</Grid>
</Grid>
</div>
)
return (
<div ref={node} style={{ width: "100%", maxWidth: "100%", margin: "auto", position: "relative", }}>
<div ref={node} style={{ width: "100%", maxWidth: "100%", margin: "auto", }}>
<InstantSearch searchClient={searchClient} indexName="appsearch" onClick={() => {
console.log("CLICKED 1")
}}>
<Configure clickAnalytics />
<CustomSearchBox onClick={() => {
+8 -8
View File
@@ -90,8 +90,8 @@ const SearchField = props => {
},
}}
>
{isHeader ? <div style={{ display: "flex" }}>
<DialogTitle style={{ marginTop: 15, marginLeft: 5, color: "var(--Paragraph-text, #C8C8C8)" }} >Search Shuffle</DialogTitle>
{isHeader ? <div style={{ display: "flex"}}>
<DialogTitle style={{ marginTop: 15, marginLeft: 5, color: "var(--Paragraph-text, #C8C8C8)" }} >Search for docs, apps, workflows and more</DialogTitle>
<Button fullWidth style={{ marginLeft: 470 }} onClick={() => {
setModalOpen(false);
}}><CloseIcon /></Button>
@@ -101,12 +101,11 @@ const SearchField = props => {
<SearchBox setModalOpen={setModalOpen} modalOpen={modalOpen} serverside={serverside} userdata={userdata} />
</DialogContent>
<Divider style={{overflow: "hidden"}}/>
<span style={{display:"flex", width:"100%"}}>
<div style={{display: "flex", marginTop: 6, marginBottom: 6, marginRight: 100, marginLeft: 20, alignItems: "center"}}>
{/* <Typography variant="body2" style={{ fontSize: 16, fontWidth: 550, color: "var(--Paragraph-text, #C8C8C8)"}}>
<span style={{display:"flex", width:"100%", height:30}}>
{/* <div style={{display: "flex", marginTop: 6, marginBottom: 6, marginRight: 100, marginLeft: 20, alignItems: "center"}}>
<Typography variant="body2" style={{ fontSize: 16, fontWidth: 550, color: "var(--Paragraph-text, #C8C8C8)"}}>
Discord
</Typography> */}
{/*
<a rel="noopener noreferrer" href="https://discord.com/invite/B2CBzUm" target="_blank" style={{ textDecoration: "none", color: "white" }}>
<img src={"/images/social/discode.svg"} alt="Algolia logo" style={{ height: 22, marginLeft: 5, marginTop: 3, }} />
</a>
@@ -118,8 +117,9 @@ const SearchField = props => {
<a rel="noopener noreferrer" href="https://www.algolia.com/" target="_blank" style={{ textDecoration: "none", color: "white" }}>
<img src={"/images/logo-algolia-nebula-blue-full.svg"} alt="Algolia logo" style={{ height: 17, marginLeft: 5, marginTop: 3, }} />
</a>
*/}
</div>
*/}
</span>
</Dialog>
);
+186 -138
View File
@@ -22,6 +22,7 @@ import { isMobile } from "react-device-detect"
import { NestedMenuItem } from "mui-nested-menu"
import { GetParsedPaths, FindJsonPath } from "../views/Apps.jsx";
import { SetJsonDotnotation } from "../views/AngularWorkflow.jsx";
import { vscodeDark, vscodeDarkInit } from '@uiw/codemirror-theme-vscode';
import {
FullscreenExit as FullscreenExitIcon,
@@ -82,6 +83,7 @@ const pythonFilters = [
{"name": "Handle JSON", "value": `{% python %}\nimport json\njsondata = json.loads(r"""$nodename""")\n{% endpython %}`, "example": ``},
]
/*
const shuffleTheme = createTheme({
theme: 'dark',
settings: {
@@ -110,6 +112,7 @@ const shuffleTheme = createTheme({
{ tag: t.attributeName, color: '#5c6166' },
],
});
*/
const CodeEditor = (props) => {
const {
@@ -547,7 +550,6 @@ const CodeEditor = (props) => {
var code_lines = localcodedata.split('\n')
for (var i = 0; i < code_lines.length; i++){
var current_code_line = code_lines[i]
// console.log(current_code_line)
var variable_occurence = current_code_line.match(/[\\]{0,1}[$]{1}([a-zA-Z0-9_-]+\.?){1}([a-zA-Z0-9#_-]+\.?){0,}/g)
@@ -610,8 +612,7 @@ const CodeEditor = (props) => {
var correctVariable = availableVariables.includes(fixedVariable)
if(!correctVariable) {
value.markText({line:i, ch:dollar_occurence[occ]}, {line:i, ch:dollar_occurence_len[occ]+dollar_occurence[occ]}, {"css": "background-color: rgb(248, 106, 62, 0.9); padding-top: 2px; padding-bottom: 2px; color: white"})
}
else{
} else {
value.markText({line:i, ch:dollar_occurence[occ]}, {line:i, ch:dollar_occurence_len[occ]+dollar_occurence[occ]}, {"css": "background-color: #8b8e26; padding-top: 2px; padding-bottom: 2px; color: white"})
}
// console.log(correctVariables)
@@ -660,6 +661,23 @@ const CodeEditor = (props) => {
setlocalcodedata(updatedCode)
}
const fixStringInput = (new_input) => {
// Newline fixes
new_input = new_input.replace(/\r\n/g, "\\n")
new_input = new_input.replace(/\n/g, "\\n")
// Quote fixes
new_input = new_input.replace(/\\"/g, '"')
new_input = new_input.replace(/"/g, '\\"')
new_input = new_input.replace(/\\'/g, "'")
new_input = new_input.replace(/'/g, "\\'")
return new_input
}
const expectedOutput = (input) => {
//const found = input.match(/[$]{1}([a-zA-Z0-9_-]+\.?){1}([a-zA-Z0-9#_-]+\.?){0,}/g)
@@ -674,36 +692,39 @@ const CodeEditor = (props) => {
try {
for (var i = 0; i < found.length; i++) {
try {
// For found specifically, should replace .#\d with .# with regex
//found[i] = found[i].toLowerCase()
const fixedVariable = fixVariable(found[i])
//var correctVariable = availableVariables.includes(fixedVariable)
//
var valuefound = false
for (var j = 0; j < actionlist.length; j++) {
if(fixedVariable.slice(1,).toLowerCase() === actionlist[j].autocomplete.toLowerCase()){
valuefound = true
if(fixedVariable.slice(1,).toLowerCase() !== actionlist[j].autocomplete.toLowerCase()){
continue
}
try {
if (typeof actionlist[j].example === "object") {
input = input.replace(found[i], JSON.stringify(actionlist[j].example), -1);
valuefound = true
} else if (actionlist[j].example.trim().startsWith("{") || actionlist[j].example.trim().startsWith("[")) {
input = input.replace(found[i], JSON.stringify(actionlist[j].example), -1);
} else {
input = input.replace(found[i], actionlist[j].example, -1)
}
} catch (e) {
input = input.replace(found[i], actionlist[j].example, -1)
console.log("Here. Checking if we got an example?")
try {
if (typeof actionlist[j].example === "object") {
input = input.replace(found[i], JSON.stringify(actionlist[j].example), -1);
} else if (actionlist[j].example.trim().startsWith("{") || actionlist[j].example.trim().startsWith("[")) {
input = input.replace(found[i], JSON.stringify(actionlist[j].example), -1);
} else {
console.log("This?")
const newExample = fixStringInput(actionlist[j].example)
input = input.replace(found[i], newExample, -1)
}
} else {
// Couldn't find the correct example value
} catch (e) {
input = input.replace(found[i], actionlist[j].example, -1)
}
}
//if (!valuefound) {
// console.log("Couldn't find value "+fixedVariable)
//}
if (!valuefound && availableVariables.includes(fixedVariable)) {
var shouldbreak = false
for (var k=0; k < actionlist.length; k++){
@@ -714,46 +735,50 @@ const CodeEditor = (props) => {
for (var key in parsedPaths) {
const fullpath = "$"+actionlist[k].autocomplete.toLowerCase()+parsedPaths[key].autocomplete
if (fullpath === fixedVariable) {
//if (actionlist[k].example === undefined) {
// actionlist[k].example = "TMP"
//}
if (fullpath !== fixedVariable) {
continue
}
var new_input = ""
try {
new_input = FindJsonPath(fullpath, actionlist[k].example)
} catch (e) {
console.log("ERR IN INPUT: ", e)
}
//if (actionlist[k].example === undefined) {
// actionlist[k].example = "TMP"
//}
//console.log("Got output for: ", fullpath, new_input, actionlist[k].example, typeof new_input)
var new_input = ""
try {
new_input = FindJsonPath(fullpath, actionlist[k].example)
} catch (e) {
console.log("ERR IN INPUT: ", e)
}
if (typeof new_input === "object") {
new_input = JSON.stringify(new_input)
console.log("Got output for: ", fullpath, new_input, actionlist[k].example, typeof new_input)
if (typeof new_input === "object") {
new_input = JSON.stringify(new_input)
} else {
if (typeof new_input === "string") {
// Check if it contains any newlines, and replace them with raw newlines
new_input = fixStringInput(new_input)
// Replace quotes with nothing
} else {
if (typeof new_input === "string") {
new_input = new_input
} else {
console.log("NO TYPE? ", typeof new_input)
try {
new_input = new_input.toString()
} catch (e) {
new_input = ""
}
console.log("NO TYPE? ", typeof new_input)
try {
new_input = new_input.toString()
} catch (e) {
new_input = ""
}
}
//console.log("FOUND2: ", fixedVariable, actionlist[j].example)
input = input.replace(fixedVariable, new_input, -1)
input = input.replace(found[i], new_input, -1)
//} catch (e) {
// input = input.replace(found[i], actionlist[k].example)
//}
shouldbreak = true
break
}
input = input.replace(fixedVariable, new_input, -1)
input = input.replace(found[i], new_input, -1)
//} catch (e) {
// input = input.replace(found[i], actionlist[k].example)
//}
shouldbreak = true
break
}
if (shouldbreak) {
@@ -766,7 +791,7 @@ const CodeEditor = (props) => {
}
}
} catch (e) {
//console.log("Outer replace error: ", e)
console.log("Outer replace error: ", e)
}
}
@@ -895,7 +920,7 @@ const CodeEditor = (props) => {
aria-labelledby="draggable-code-modal"
disableBackdropClick={true}
disableEnforceFocus={true}
//style={{ pointerEvents: "none" }}
//style={{ pointerEvents: "none" }}
hideBackdrop={true}
open={expansionModalOpen}
onClose={() => {
@@ -964,6 +989,7 @@ const CodeEditor = (props) => {
}}
>
<div style={{display: "flex"}}>
{/*
<DialogTitle
id="draggable-dialog-title"
style={{
@@ -974,59 +1000,9 @@ const CodeEditor = (props) => {
>
Code Editor
</DialogTitle>
<IconButton
style={{
marginLeft: isMobile ? "80%" : 350,
height: 50,
width: 50,
}}
onClick={() => {
}}
>
<Tooltip
color="primary"
title={"Test Liquid in the playground"}
placement="top"
>
<a
href="https://pwwang.github.io/liquidpy/playground/"
rel="norefferer"
target="_blank"
>
<ExtensionIcon style={{color: "rgba(255,255,255,0.7)"}}/>
</a>
</Tooltip>
</IconButton>
<IconButton
style={{
height: 50,
width: 50,
}}
disabled={isAiLoading}
onClick={() => {
autoFormat(localcodedata)
}}
>
<Tooltip
color="primary"
title={"Auto format data"}
placement="top"
>
{isAiLoading ?
<CircularProgress style={{height: 20, width: 20, color: "rgba(255,255,255,0.7)"}}/>
:
<AutoFixHighIcon style={{color: "rgba(255,255,255,0.7)"}}/>
}
</Tooltip>
</IconButton>
</div>
</div>
}
*/}
{ isFileEditor ? null :
<div style={{display: "flex"}}>
<div style={{display: "flex", maxHeight: 40, }}>
<Button
id="basic-button"
aria-haspopup="true"
@@ -1035,7 +1011,7 @@ const CodeEditor = (props) => {
variant="outlined"
color="secondary"
style={{
textTransform: "none",
textTransform: "none",
width: 100,
}}
onClick={(event) => {
@@ -1143,9 +1119,9 @@ const CodeEditor = (props) => {
variant="outlined"
color="secondary"
style={{
textTransform: "none",
textTransform: "none",
width: 130,
marginLeft: 170,
marginLeft: 20,
}}
onClick={(event) => {
setMenuPosition({
@@ -1393,49 +1369,105 @@ const CodeEditor = (props) => {
</Menu>
</div>
}
<span style={{
<IconButton
style={{
marginLeft: isMobile ? "80%" : 30,
height: 50,
width: 50,
}}
onClick={() => {
}}
>
<Tooltip
color="primary"
title={"Test Liquid in the playground"}
placement="top"
>
<a
href="https://pwwang.github.io/liquidpy/playground/"
rel="norefferer"
target="_blank"
>
<ExtensionIcon style={{color: "rgba(255,255,255,0.7)"}}/>
</a>
</Tooltip>
</IconButton>
<IconButton
style={{
height: 50,
width: 50,
}}
disabled={isAiLoading}
onClick={() => {
autoFormat(localcodedata)
}}
>
<Tooltip
color="primary"
title={"Auto format data"}
placement="top"
>
{isAiLoading ?
<CircularProgress style={{height: 20, width: 20, color: "rgba(255,255,255,0.7)"}}/>
:
<AutoFixHighIcon style={{color: "rgba(255,255,255,0.7)"}}/>
}
</Tooltip>
</IconButton>
</div>
</div>
}
<div style={{
borderRadius: theme.palette.borderRadius,
position: "relative",
paddingTop: 0,
minHeight: 548,
overflow: "hidden",
}}>
<CodeMirror
theme={vscodeDark}
value={localcodedata}
height={isFileEditor ? 450 : 525}
height={isFileEditor ? 450 : 500}
width={isFileEditor ? 650 : 600}
style={{
maxWidth: isFileEditor ? 450 : 500,
maxWidth: isFileEditor ? 450 : 600,
maxHeight: 548,
minHeight: 548,
wordBreak: "break-word",
marginTop: 0,
paddingTop: 0,
backgroundColor: "rgba(40,40,40,1)",
minHeight: 470,
paddingBottom: 10,
overflow: "hidden",
}}
onCursorActivity = {(value) => {
// console.log(value.getCursor())
console.log("CURSOR: ", value.getCursor())
setCurrentCharacter(value.getCursor().ch)
setCurrentLine(value.getCursor().line)
// console.log(value.getCursor().ch, value.getCursor().line)
findIndex(value.getCursor().line, value.getCursor().ch)
highlight_variables(value)
}}
onChange={(value, viewUpdate) => {
console.log("Value: ", value, viewUpdate)
setlocalcodedata(value)
expectedOutput(value)
highlight_variables(value)
//if(value.display.input.prevInput.startsWith('$') || value.display.input.prevInput.endsWith('$')){
// setEditorPopupOpen(true)
//}
}}
extensions={[]}//indentWithTab]}
theme={shuffleTheme}
options={{
styleSelectedText: true,
keyMap: 'sublime',
mode: validation === true ? "json" : "python",
lineWrapping: linewrap,
theme: vscodeDark,
}}
/>
</span>
</div>
{/*editorPopupOpen ?
<Paper
@@ -1550,13 +1582,14 @@ const CodeEditor = (props) => {
</div>
</div>
<div style={{flex: 1, marginLeft: 25, }}>
<div style={{flex: 1, marginLeft: 5, borderLeft: "1px solid rgba(255,255,255,0.3)", paddingLeft: 5, }}>
{isFileEditor ? null :
<div>
{isMobile ? null :
<DialogTitle
style={{
paddingLeft: 10,
paddingTop: 0,
display: "flex",
}}
>
@@ -1564,14 +1597,29 @@ const CodeEditor = (props) => {
Expected Output
</span>
<IconButton disabled={executing} color="primary" style={{border: `1px solid ${theme.palette.primary.main}`, marginLeft: 100, padding: 8}} variant="contained" onClick={() => {
executeSingleAction(expOutput)
}}>
<Tooltip title="Try it! This runs the Shuffle Tools 'repeat back to me' or 'execute python' action with what you see in the expected output window. Commonly used to test your Python scripts or Liquid filters, not requiring the full workflow to run again." placement="top">
{executing ? <CircularProgress style={{height: 18, width: 18, }} /> : <PlayArrowIcon style={{height: 18, width: 18, }} /> }
</Tooltip>
</IconButton>
<Tooltip title="Try it! This runs the Shuffle Tools 'repeat back to me' or 'execute python' action with what you see in the expected output window. Commonly used to test your Python scripts or Liquid filters, not requiring the full workflow to run again." placement="top">
<Button
variant="outlined"
disabled={executing}
color="primary"
style={{
border: `1px solid ${theme.palette.primary.main}`,
marginLeft: 200,
maxHeight: 35,
minWidth: 70,
}}
variant="contained"
onClick={() => {
executeSingleAction(expOutput)
}}
>
{executing ?
<CircularProgress style={{height: 18, width: 18, }} />
:
<span>Try it <PlayArrowIcon style={{height: 18, width: 18, marginBottom: -4, marginLeft: 5, }} /> </span>
}
</Button>
</Tooltip>
</DialogTitle>
}
@@ -1613,8 +1661,8 @@ const CodeEditor = (props) => {
borderRadius: theme.palette.borderRadius,
maxHeight: 500,
minHeight: 500,
minWidth: 500,
maxWidth: 500,
minWidth: 580,
maxWidth: 580,
overflow: "auto",
whiteSpace: "pre-wrap",
}}
@@ -44,7 +44,6 @@ const WorkflowTemplatePopup = (props) => {
const [missingDestination, setMissingDestination] = React.useState(undefined);
useEffect(() => {
console.log("Source & Dest check:", missingSource, missingDestination)
}, [missingSource, missingDestination])
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
@@ -392,7 +391,7 @@ const WorkflowTemplatePopup = (props) => {
appFramework={appFramework}
appType={missingSource.type}
appImage={missingSource.image}
AppImage={missingSource.image}
setMissing={setMissingSource}
/>
@@ -406,7 +405,7 @@ const WorkflowTemplatePopup = (props) => {
appFramework={appFramework}
appType={missingDestination.type}
appImage={missingDestination.image}
AppImage={missingDestination.image}
setMissing={setMissingDestination}
/>
@@ -443,7 +442,7 @@ const WorkflowTemplatePopup = (props) => {
if (title.length > maxlength) {
parsedTitle = title.substring(0, maxlength) + "..."
}
console.log("isHomePage", isHomePage)
parsedTitle = parsedTitle.replaceAll("_", " ")
const parsedDescription = description !== undefined && description !== null ? description.replaceAll("_", " ") : ""
+2 -3
View File
@@ -4,11 +4,10 @@ const data = [
css: {
label: "data(label)",
"text-valign": "center",
"font-family":
"Segoe UI, Tahoma, Geneva, Verdana, sans-serif, sans-serif",
"font-family": "Segoe UI, Tahoma, Geneva, Verdana, sans-serif, sans-serif",
"font-weight": "lighter",
"margin-right": "10px",
"font-size": "18px",
"margin-right": "10px",
width: "80px",
height: "80px",
color: "white",
+200 -140
View File
@@ -16,6 +16,7 @@ import {
OutlinedInput,
Checkbox,
Card,
Chip,
Tooltip,
FormControlLabel,
Typography,
@@ -133,7 +134,7 @@ const FileCategoryInput = (props) => {
const Admin = (props) => {
const { globalUrl, userdata, serverside, checkLogin } = props;
const { globalUrl, userdata, serverside, checkLogin, notifications, setNotifications, } = props;
var to_be_copied = "";
const classes = useStyles();
@@ -485,6 +486,14 @@ If you're interested, please let me know a time that works for you, or set up a
return `mailto:${admins}?bcc=frikky@shuffler.io,binu@shuffler.io&subject=${subject}&body=${body}`
}
const changeDistribution = (data) => {
//changeDistributed(data, !isDistributed)
console.log("Should change distribution to be shared among suborgs")
editAuthenticationConfig(data.id, "suborg_distribute")
}
const deleteAuthentication = (data) => {
toast("Deleting auth " + data.label);
@@ -800,10 +809,10 @@ If you're interested, please let me know a time that works for you, or set up a
});
};
const editAuthenticationConfig = (id) => {
const editAuthenticationConfig = (id, parentAction) => {
const data = {
id: id,
action: "assign_everywhere",
action: parentAction !== undefined && parentAction !== null ? parentAction : "assign_everywhere",
};
const url = globalUrl + "/api/v1/apps/authentication/" + id + "/config";
@@ -821,9 +830,9 @@ If you're interested, please let me know a time that works for you, or set up a
.then((response) =>
response.json().then((responseJson) => {
if (responseJson["success"] === false) {
toast("Failed overwriting appauth in workflows");
toast("Failed overwriting appauth");
} else {
toast("Successfully updated auth everywhere!");
toast("Successfully updated auth!");
setSelectedUserModalOpen(false);
setTimeout(() => {
getAppAuthentication();
@@ -1732,7 +1741,7 @@ If you're interested, please let me know a time that works for you, or set up a
const userId = user.id;
const data = { user_id: userId };
console.log(user, userdata)
toast("Generating new API key")
var fetchdata = {
method: "POST",
@@ -2827,6 +2836,8 @@ If you're interested, please let me know a time that works for you, or set up a
checkLogin={checkLogin}
setAdminTab={setAdminTab}
setCurTab={setCurTab}
notifications={notifications}
setNotifications={setNotifications}
/>
: adminTab === 3 ?
<Billing
@@ -3440,6 +3451,7 @@ If you're interested, please let me know a time that works for you, or set up a
style={{ minWidth: 300, maxWidth: 300, overflow: "hidden" }}
/>
<ListItemText primary="Actions" />
<ListItemText primary="Delegation" />
</ListItem>
{schedules === undefined || schedules === null
? null
@@ -3658,10 +3670,13 @@ If you're interested, please let me know a time that works for you, or set up a
style={{ minWidth: 125, maxWidth: 125, overflow: "hidden" }}
/>
<ListItemText
primary="Created"
primary="Edited"
style={{ minWidth: 230, maxWidth: 230, overflow: "hidden" }}
/>
<ListItemText primary="Actions" />
<ListItemText primary="Actions"
style={{ minWidth: 150, maxWidth: 150, }}
/>
<ListItemText primary="Distribution" />
</ListItem>
{authentication === undefined || authentication === null
? null
@@ -3693,6 +3708,8 @@ If you're interested, please let me know a time that works for you, or set up a
];
}
const isDistributed = data.suborg_distributed === true ? true : false;
return (
<ListItem key={index} style={{ backgroundColor: bgColor }}>
<ListItemText
@@ -3768,31 +3785,32 @@ If you're interested, please let me know a time that works for you, or set up a
minWidth: 230,
overflow: "hidden",
}}
primary={new Date(data.created * 1000).toISOString()}
primary={new Date(data.edited * 1000).toISOString()}
/>
<ListItemText>
<IconButton
onClick={() => {
updateAppAuthentication(data);
}}
disabled={data.org_id !== selectedOrganization.id ? true : false}
>
<EditIcon color="primary" />
<EditIcon color="secondary" />
</IconButton>
{data.defined ? (
<Tooltip
color="primary"
title="Set in EVERY workflow"
title="Set in EVERY workflow in the organization"
placement="top"
>
<IconButton
style={{ marginRight: 10 }}
disabled={data.defined === false}
disabled={data.defined === false || data.org_id !== selectedOrganization.id ? true : false}
onClick={() => {
editAuthenticationConfig(data.id);
}}
>
<SelectAllIcon
color={data.defined ? "primary" : "secondary"}
color={"secondary"}
/>
</IconButton>
</Tooltip>
@@ -3803,23 +3821,54 @@ If you're interested, please let me know a time that works for you, or set up a
placement="top"
>
<IconButton
style={{ marginRight: 10 }}
style={{}}
onClick={() => {}}
disabled={data.org_id !== selectedOrganization.id ? true : false}
>
<SelectAllIcon
color={data.defined ? "primary" : "secondary"}
color="secondary"
/>
</IconButton>
</Tooltip>
)}
<IconButton
style={{marginLeft: 0, }}
disabled={data.org_id !== selectedOrganization.id ? true : false}
onClick={() => {
deleteAuthentication(data);
}}
>
<DeleteIcon color="primary" />
<DeleteIcon color="secondary" />
</IconButton>
</ListItemText>
<ListItemText>
{selectedOrganization.id !== undefined && data.org_id !== selectedOrganization.id ?
<Tooltip
title="Parent organization controlled auth. You can use, but not modify this auth. Contact an admin of your parent organization if you need changes to this."
placement="top"
>
<Chip
label={"Parent"}
variant="contained"
color="secondary"
/>
</Tooltip>
:
<Tooltip
title="Distributed to sub-organizations. This means the sub organizations can use this authentication, but not modify it."
placement="top"
>
<Checkbox
disabled={selectedOrganization.creator_org !== undefined && selectedOrganization.creator_org !== null && selectedOrganization.creator_org !== "" ? true : false}
checked={isDistributed}
color="secondary"
onClick={() => {
changeDistribution(data, !isDistributed)
}}
/>
</Tooltip>
}
</ListItemText>
</ListItem>
);
})}
@@ -3937,21 +3986,21 @@ If you're interested, please let me know a time that works for you, or set up a
style={{ minWidth: 125, maxWidth: 125 }}
/>
<ListItemText
primary="Default"
style={{ minWidth: 125, maxWidth: 125 }}
primary={"In Queue"}
style={{ minWidth: 100, maxWidth: 100 }}
/>
<ListItemText
primary="Disabled"
style={{ minWidth: 100, maxWidth: 100 }}
primary="Default"
style={{ minWidth: 150, maxWidth: 150 }}
/>
<ListItemText
primary="Actions"
style={{ minWidth: 200, maxWidth: 200}}
/>
<ListItemText
primary="Last Edited"
style={{ minWidth: 170, maxWidth: 170 }}
/>
<ListItemText
primary="Actions"
style={{ minWidth: 150, maxWidth: 150 }}
/>
</ListItem>
{environments === undefined || environments === null
? null
@@ -3969,18 +4018,20 @@ If you're interested, please let me know a time that works for you, or set up a
bgColor = "#1f2023";
}
// Check if there's a notification for it in userdata.priorities
var showCPUAlert = false
var foundIndex = -1
if (userdata !== undefined && userdata !== null && userdata.priorities !== undefined && userdata.priorities !== null && userdata.priorities.length > 0) {
foundIndex = userdata.priorities.findIndex(prio => prio.name.includes("CPU") && prio.active === true)
// Check if there's a notification for it in userdata.priorities
var showCPUAlert = false
var foundIndex = -1
if (userdata !== undefined && userdata !== null && userdata.priorities !== undefined && userdata.priorities !== null && userdata.priorities.length > 0) {
foundIndex = userdata.priorities.findIndex(prio => prio.name.includes("CPU") && prio.active === true)
if (foundIndex >= 0 && userdata.priorities[foundIndex].name.endsWith(environment.Name)) {
showCPUAlert = true
}
}
if (foundIndex >= 0 && userdata.priorities[foundIndex].name.endsWith(environment.Name)) {
showCPUAlert = true
}
}
console.log("Show CPU alert: ", showCPUAlert)
//console.log("Show CPU alert: ", showCPUAlert)
const queueSize = environment.queue !== undefined && environment.queue !== null ? environment.queue < 0 ? 0 : environment.queue > 99 ? ">99" : environment.queue : 0
return (
<span key={index}>
@@ -4016,49 +4067,56 @@ If you're interested, please let me know a time that works for you, or set up a
<ListItemText
style={{ minWidth: 100, maxWidth: 100 }}
primary={
<Tooltip
title={"Copy Orborus command"}
style={{}}
aria-label={"Copy orborus command"}
>
<IconButton
style={{}}
disabled={environment.Type === "cloud"}
onClick={() => {
if (environment.Type === "cloud") {
toast("No Orborus necessary for environment cloud. Create and use a different environment to run executions on-premises.")
return
}
<Tooltip
title={"Copy Orborus command"}
style={{}}
aria-label={"Copy orborus command"}
>
<IconButton
style={{}}
disabled={environment.Type === "cloud"}
onClick={() => {
if (environment.Type === "cloud") {
toast("No Orborus necessary for environment cloud. Create and use a different environment to run executions on-premises.")
return
}
const elementName = "copy_element_shuffle";
const auth = environment.auth === "" ? 'cb5st3d3Z!3X3zaJ*Pc' : environment.auth
const commandData = `docker run --volume "/var/run/docker.sock:/var/run/docker.sock" -e ENVIRONMENT_NAME="${environment.Name}" -e 'AUTH=${auth}' -e ORG="${props.userdata.active_org.id}" -e DOCKER_API_VERSION=1.40 -e BASE_URL="${globalUrl}" --name="shuffle-orborus" -d ghcr.io/shuffle/shuffle-orborus:latest`
var copyText = document.getElementById(elementName);
if (copyText !== null && copyText !== undefined) {
const clipboard = navigator.clipboard;
if (clipboard === undefined) {
toast("Can only copy over HTTPS (port 3443)");
return;
}
if (props.userdata.active_org === undefined || props.userdata.active_org === null) {
toast("No active organization yet. Are you logged in?")
return
}
navigator.clipboard.writeText(commandData);
copyText.select();
copyText.setSelectionRange(
0,
99999
); /* For mobile devices */
const elementName = "copy_element_shuffle";
const auth = environment.auth === "" ? 'cb5st3d3Z!3X3zaJ*Pc' : environment.auth
const newUrl = globalUrl === "https://shuffler.io" ? "https://shuffle-backend-stbuwivzoq-nw.a.run.app" : globalUrl
/* Copy the text inside the text field */
document.execCommand("copy");
const commandData = `docker run --volume "/var/run/docker.sock:/var/run/docker.sock" -e ENVIRONMENT_NAME="${environment.Name}" -e 'AUTH=${auth}' -e ORG="${props.userdata.active_org.id}" -e DOCKER_API_VERSION=1.40 -e BASE_URL="${newUrl}" --name="shuffle-orborus" -d ghcr.io/shuffle/shuffle-orborus:latest`
var copyText = document.getElementById(elementName);
if (copyText !== null && copyText !== undefined) {
const clipboard = navigator.clipboard;
if (clipboard === undefined) {
toast("Can only copy over HTTPS (port 3443)");
return;
}
toast("Orborus command copied to clipboard");
}
}}
>
<FileCopyIcon disabled={environment.Type === "cloud"} style={{ color: environment.Type === "cloud" ? "rgba(255,255,255,0.2)" : "rgba(255,255,255,0.8)" }} />
</IconButton>
</Tooltip>
}
navigator.clipboard.writeText(commandData);
copyText.select();
copyText.setSelectionRange(
0,
99999
); /* For mobile devices */
/* Copy the text inside the text field */
document.execCommand("copy");
toast("Orborus command copied to clipboard");
}
}}
>
<FileCopyIcon disabled={environment.Type === "cloud"} style={{ color: environment.Type === "cloud" ? "rgba(255,255,255,0.2)" : "rgba(255,255,255,0.8)" }} />
</IconButton>
</Tooltip>
}
/>
<ListItemText
@@ -4067,8 +4125,18 @@ If you're interested, please let me know a time that works for you, or set up a
/>
<ListItemText
style={{
minWidth: 125,
maxWidth: 125,
minWidth: 100,
maxWidth: 100,
overflow: "hidden",
marginLeft: 10,
}}
primary={queueSize}
/>
<ListItemText
style={{
minWidth: 140,
maxWidth: 140,
overflow: "hidden",
}}
primary={environment.default ? "true" : null}
@@ -4080,19 +4148,49 @@ If you're interested, please let me know a time that works for you, or set up a
onClick={() => setDefaultEnvironment(environment)}
color="primary"
>
Make default
Set Default
</Button>
)}
</ListItemText>
<ListItemText
style={{
minWidth: 100,
maxWidth: 100,
minWidth: 200,
maxWidth: 200,
overflow: "hidden",
marginLeft: 10,
}}
primary={environment.archived.toString()}
/>
>
<div style={{ display: "flex" }}>
<ButtonGroup style={{borderRadius: "5px 5px 5px 5px",}}>
<Button
variant={environment.archived ? "contained" : "outlined"}
style={{ }}
onClick={() => deleteEnvironment(environment)}
color="primary"
>
{environment.archived ? "Activate" : "Disable"}
</Button>
<Button
variant={"outlined"}
style={{ }}
disabled={isCloud && environment.Name.toLowerCase() !== "cloud"}
onClick={() => {
console.log("Should clear executions for: ", environment);
if (isCloud && environment.Name.toLowerCase() === "cloud") {
rerunCloudWorkflows(environment);
} else {
abortEnvironmentWorkflows(environment);
}
}}
color="primary"
>
{isCloud && environment.Name.toLowerCase() === "cloud" ? "Rerun" : "Clear"}
</Button>
</ButtonGroup>
</div>
</ListItemText>
<ListItemText
style={{
minWidth: 150,
@@ -4107,68 +4205,30 @@ If you're interested, please let me know a time that works for you, or set up a
: 0
}
/>
<ListItemText
style={{
minWidth: 300,
maxWidth: 300,
overflow: "hidden",
marginLeft: 10,
}}
>
<div style={{ display: "flex" }}>
<ButtonGroup style={{borderRadius: "5px 5px 5px 5px",}}>
<Button
variant={environment.archived ? "contained" : "outlined"}
style={{ }}
onClick={() => deleteEnvironment(environment)}
color="primary"
>
{environment.archived ? "Activate" : "Disable"}
</Button>
<Button
variant={"outlined"}
style={{ }}
disabled={isCloud && environment.Name.toLowerCase() !== "cloud"}
onClick={() => {
console.log("Should clear executions for: ", environment);
if (isCloud && environment.Name.toLowerCase() === "cloud") {
rerunCloudWorkflows(environment);
} else {
abortEnvironmentWorkflows(environment);
}
}}
color="primary"
>
{isCloud && environment.Name.toLowerCase() === "cloud" ? "Rerun" : "Clear"}
</Button>
</ButtonGroup>
</div>
</ListItemText>
</ListItem>
{showCPUAlert === false ? null :
{showCPUAlert === false ? null :
<ListItem key={index+"_cpu"} style={{ backgroundColor: bgColor }}>
<div style={{border: "1px solid #f85a3e", borderRadius: theme.palette.borderRadius, marginTop: 10, marginBottom: 10, padding: 15, textAlign: "center", height: 70, textAlign: "left", backgroundColor: theme.palette.surfaceColor, display: "flex", }}>
<div style={{flex: 2, overflow: "hidden",}}>
<Typography variant="body1" >
90% CPU the server(s) hosting the Shuffle App Runner (Orborus) was found.
</Typography>
<Typography variant="body2" color="textSecondary">
Need help with High Availability and Scale? <a href="/docs/configuration#scale" target="_blank" rel="noopener noreferrer" style={{ textDecoration: "none", color: "#f85a3e" }}>Read documentation</a> and <a href="https://shuffler.io/contact" target="_blank" rel="noopener noreferrer" style={{ textDecoration: "none", color: "#f85a3e" }}>Get in touch</a>.
</Typography>
</div>
<div style={{flex: 1, display: "flex", marginLeft: 30, }}>
<Button style={{borderRadius: 25, width: 200, height: 50, marginTop: 8, }} variant="outlined" color="secondary" onClick={() => {
// dismiss -> get envs
changeRecommendation(userdata.priorities[foundIndex], "dismiss")
}}>
Dismiss
</Button>
</div>
</div>
</ListItem>
}
</span>
<div style={{border: "1px solid #f85a3e", borderRadius: theme.palette.borderRadius, marginTop: 10, marginBottom: 10, padding: 15, textAlign: "center", height: 70, textAlign: "left", backgroundColor: theme.palette.surfaceColor, display: "flex", }}>
<div style={{flex: 2, overflow: "hidden",}}>
<Typography variant="body1" >
90% CPU the server(s) hosting the Shuffle App Runner (Orborus) was found.
</Typography>
<Typography variant="body2" color="textSecondary">
Need help with High Availability and Scale? <a href="/docs/configuration#scale" target="_blank" rel="noopener noreferrer" style={{ textDecoration: "none", color: "#f85a3e" }}>Read documentation</a> and <a href="https://shuffler.io/contact" target="_blank" rel="noopener noreferrer" style={{ textDecoration: "none", color: "#f85a3e" }}>Get in touch</a>.
</Typography>
</div>
<div style={{flex: 1, display: "flex", marginLeft: 30, }}>
<Button style={{borderRadius: 25, width: 200, height: 50, marginTop: 8, }} variant="outlined" color="secondary" onClick={() => {
// dismiss -> get envs
changeRecommendation(userdata.priorities[foundIndex], "dismiss")
}}>
Dismiss
</Button>
</div>
</div>
</ListItem>
}
</span>
);
})}
</List>
+354 -138
View File
@@ -11,7 +11,7 @@ import { useNavigate, Link, useParams } from "react-router-dom";
import { useBeforeunload } from "react-beforeunload";
import ReactJson from "react-json-view";
import { NestedMenuItem } from 'mui-nested-menu';
import ReactMarkdown from "react-markdown";
import Markdown from "react-markdown";
//import { useAlert
import { ToastContainer, toast } from "react-toastify"
import { isMobile } from "react-device-detect"
@@ -71,6 +71,7 @@ import {
import {
Folder as FolderIcon,
Insights as InsightsIcon,
LibraryBooks as LibraryBooksIcon,
OpenInNew as OpenInNewIcon,
Undo as UndoIcon,
@@ -112,6 +113,7 @@ import {
AutoFixHigh as AutoFixHighIcon,
Polyline as PolylineIcon,
QueryStats as QueryStatsIcon,
AutoAwesome as AutoAwesomeIcon,
} from "@mui/icons-material";
@@ -395,9 +397,10 @@ const AngularWorkflow = (defaultprops) => {
var to_be_copied = "";
const [firstrequest, setFirstrequest] = React.useState(true);
const [cystyle] = useState(cytoscapestyle);
const [cy, setCy] = React.useState();
const [toolsApp, setToolsApp] = React.useState({});
const [toolsApp, setToolsApp] = React.useState({});
const [currentView, setCurrentView] = React.useState(0);
const [triggerAuthentication, setTriggerAuthentication] = React.useState({});
const [triggerFolders, setTriggerFolders] = React.useState([]);
@@ -437,6 +440,7 @@ const AngularWorkflow = (defaultprops) => {
const [appAuthentication, setAppAuthentication] = React.useState(undefined);
const [variablesModalOpen, setVariablesModalOpen] = React.useState(false);
const [aiQueryModalOpen, setAiQueryModalOpen] = React.useState(false)
const [executionVariablesModalOpen, setExecutionVariablesModalOpen] =
React.useState(false);
const [authenticationModalOpen, setAuthenticationModalOpen] = React.useState(false);
@@ -500,6 +504,7 @@ const AngularWorkflow = (defaultprops) => {
const [selectedAction, setSelectedAction] = React.useState({});
const [selectedActionEnvironment, setSelectedActionEnvironment] = React.useState({});
const [streamDisabled, setStreamDisabled] = React.useState(false);
const [executionRequest, setExecutionRequest] = React.useState({});
const [executionRunning, setExecutionRunning] = React.useState(false);
@@ -550,10 +555,18 @@ const AngularWorkflow = (defaultprops) => {
props.userdata.active_org !== undefined
? props.userdata.active_org.cloud_sync === true
: false;
const isCloud =
window.location.host === "localhost:3002" ||
window.location.host === "shuffler.io";
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
useEffect(() => {
return () => {
console.log("UNMOUNTING USER!")
sendStreamRequest({
"item": "workflow",
"type": "leave",
"id": workflow.id,
})
}
}, [])
/*
useEffect(() => {
console.log("In useeffect for workflow: ", workflow)
@@ -582,17 +595,22 @@ const AngularWorkflow = (defaultprops) => {
const [elements, setElements] = useState([]);
const [loopRunning, setLoopRunning] = useState(false)
var loopRunning2 = loopRunning
const stop = () => {
setLoopRunning(false)
loopRunning2 = false
}
const start = () => {
setLoopRunning(true)
loopRunning2 = true
}
useEffect(() => {
//console.log("In useeffect for loopRunning: ", loopRunning)
if (loopRunning) {
// Current variable + future state controlled
// This is so that the loop can stop itself as well
console.log("In useeffect for loopRunning: ", loopRunning, loopRunning2)
if (loopRunning && loopRunning2) {
const intervalId = setInterval(() => {
if (!loopRunning) {
clearInterval(intervalId);
@@ -634,7 +652,11 @@ const AngularWorkflow = (defaultprops) => {
if (responseJson.success === true) {
if (responseJson.reason !== undefined && responseJson.reason !== undefined && responseJson.reason.length > 0) {
if (!responseJson.reason.includes("404: Not Found") && responseJson.reason.length > 25) {
selectedApp.documentation = responseJson.reason
// Translate <img> into markdown ![]()
const imgRegex = /<img.*?src="(.*?)"/g;
const newdata = responseJson.reason.replace(imgRegex, '![]($1)');
selectedApp.documentation = newdata
setSelectedApp(selectedApp)
setUpdate(Math.random())
}
@@ -913,7 +935,7 @@ const AngularWorkflow = (defaultprops) => {
});
};
const setNewAppAuth = (appAuthData) => {
const setNewAppAuth = (appAuthData, refresh) => {
fetch(globalUrl + "/api/v1/apps/authentication", {
method: "PUT",
headers: {
@@ -932,9 +954,14 @@ const AngularWorkflow = (defaultprops) => {
})
.then((responseJson) => {
if (!responseJson.success) {
toast("Failed to set app auth: " + responseJson.reason);
toast("Error: " + responseJson.reason);
} else {
getAppAuthentication(true, false);
if (refresh === true) {
getAppAuthentication(true, true, true);
} else {
getAppAuthentication(true, false);
}
setAuthenticationModalOpen(false);
// Needs a refresh with the new authentication..
@@ -964,20 +991,20 @@ const AngularWorkflow = (defaultprops) => {
return response.json();
})
.then((responseJson) => {
if (responseJson !== undefined && responseJson !== null && responseJson.executions !== undefined && responseJson.executions !== null && responseJson.executions.length > 0) {
console.log("GOT A RESPONSE??")
if (responseJson !== undefined && responseJson !== null && responseJson.executions !== undefined && responseJson.executions !== null) {
// - means it's opposite
const newkeys = sortByKey(responseJson.executions, "-started_at");
setWorkflowExecutions(newkeys);
const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search;
var tmpView = new URLSearchParams(cursearch).get("execution_id");
if (execution_id !== undefined && execution_id !== null && execution_id.length > 0 && (tmpView === undefined || tmpView === null || tmpView.length === 0)) {
tmpView = execution_id;
}
console.log("TMPVIEW: ", tmpView);
console.log("EXECUTION ID: ", tmpView)
// Compare with currently selected item
if (tmpView !== undefined && tmpView !== null && tmpView.length > 0) {
@@ -1000,16 +1027,15 @@ const AngularWorkflow = (defaultprops) => {
}
setExecutionModalView(1);
start();
setExecutionRequest({
execution_id: execution.execution_id,
authorization: execution.authorization,
});
const newitem = removeParam("execution_id", cursearch);
navigate(curpath + newitem)
//props.history.push(curpath + newitem);
start();
//const newitem = removeParam("execution_id", cursearch);
//navigate(curpath + newitem)
} else {
console.log("Couldn't find execution for execution ID. Retrying as user to get ", tmpView)
@@ -1018,19 +1044,33 @@ const AngularWorkflow = (defaultprops) => {
execution_id: tmpView,
//authorization: data.authorization,
}
setExecutionRunning(true);
setExecutionModalView(1);
setExecutionRequest(cur_execution);
start();
const newitem = removeParam("execution_id", cursearch);
navigate(curpath + newitem)
setTimeout(() => {
stop()
}, 5000);
//const newitem = removeParam("execution_id", cursearch);
//navigate(curpath + newitem)
//setTimeout(() => {
// stop()
//}, 5000);
}
}
}
} else {
const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search;
var tmpView = new URLSearchParams(cursearch).get("execution_id");
console.log("Alertnative execution id check: ", tmpView)
if (tmpView === undefined || tmpView === null || tmpView.length === 0) {
const execution_id = tmpView;
setExecutionModalView(1);
setExecutionRequest({
execution_id: execution_id,
});
start()
}
}
})
.catch((error) => {
//toast(error.toString());
@@ -1051,8 +1091,14 @@ const AngularWorkflow = (defaultprops) => {
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for stream results :O!");
stop();
setExecutionModalView(0);
toast("Failed loading the workflow run")
console.log("Status not 200 for stream results :O!");
//const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search;
//const newitem = removeParam("execution_id", cursearch);
//navigate(curpath + newitem)
}
return response.json();
@@ -1231,10 +1277,11 @@ const AngularWorkflow = (defaultprops) => {
// Controls the colors and direction of execution results.
// Style is in defaultCytoscapeStyle.js
const handleUpdateResults = (responseJson, executionRequest) => {
if (responseJson === undefined || responseJson === null || responseJson.success === false) {
return
}
//console.log(responseJson)
if (responseJson === undefined || responseJson === null || responseJson.success === false) {
stop()
return
}
//console.log(responseJson)
// Loop nodes and find results
// Update on every interval? idk
@@ -1247,7 +1294,8 @@ const AngularWorkflow = (defaultprops) => {
//console.log("Updating data!")
setExecutionData(responseJson)
} else {
if (responseJson.status === "ABORTED" || responseJson.status === "STOPPED" || responseJson.status === "FAILURE" || responseJson.status === "WAITING") {
if (responseJson.status === "ABORTED" || responseJson.status === "STOPPED" || responseJson.status === "FAILURE" || responseJson.status === "WAITING" || responseJson.status === "FINISHED") {
console.log("DONE!")
stop()
}
@@ -1305,14 +1353,31 @@ const AngularWorkflow = (defaultprops) => {
})
};
var streamDisabled2 = false
const sendStreamRequest = (body) => {
//console.log("Stream not activated yet.")
return
if (!isCloud) {
console.log("Stream not activated yet for onprem")
return
}
if (streamDisabled) {
console.log("Stream disabled")
return
}
// Session may be important here huh
body.user_id = userdata.id
fetch(`${globalUrl}/api/v1/workflows/${props.match.params.key}/stream`, {
//const url = ${globalUrl}/api/v1/workflows/${props.match.params.key}/stream
//const streamUrl = "http://localhost:5002"
console.log("Stream request: ", body)
const streamUrl = "https://stream.shuffler.io"
const url = `${streamUrl}/api/v1/workflows/${props.match.params.key}/stream`
fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -1324,6 +1389,9 @@ const AngularWorkflow = (defaultprops) => {
.then((response) => {
setSavingState(0);
if (response.status !== 200) {
setStreamDisabled(true)
streamDisabled2 = true
//console.log("Status not 200 for stream :O!");
}
@@ -1334,7 +1402,8 @@ const AngularWorkflow = (defaultprops) => {
})
.catch((error) => {
console.log("Stream send error: ", error.toString())
//toast(error.toString());
setStreamDisabled(true)
streamDisabled2 = true
})
}
@@ -1668,7 +1737,7 @@ const AngularWorkflow = (defaultprops) => {
if (hasSaved === false) {
setExecutionRequestStarted(true);
saveWorkflow(workflow, executionArgument, startNode);
console.log("FIXME: Might have forgotten to save before executing.");
//console.log("FIXME: Might have forgotten to save before executing.");
return;
}
@@ -1869,6 +1938,7 @@ const AngularWorkflow = (defaultprops) => {
setSelectedAction(selectedAction);
setWorkflow(workflow);
saveWorkflow(workflow);
toast("Added and updated authentication!");
shouldClose = true
} else {
@@ -2081,7 +2151,9 @@ const AngularWorkflow = (defaultprops) => {
}
const onChunkedResponseError = (err) => {
console.error(err)
if (streamDisabled) {
return
}
}
@@ -2502,6 +2574,17 @@ const AngularWorkflow = (defaultprops) => {
try {
var chunkJson = JSON.parse(chunk)
if (chunkJson.success === false) {
console.log("Chunk failed: ", chunkJson)
if (!streamDisabled) {
setStreamDisabled(true)
streamDisabled2 = true
}
return
}
if (chunkJson.item !== undefined && chunkJson.item !== null && chunkJson.item !== "") {
if (chunkJson.item === "node") {
if (chunkJson.type === "move") {
@@ -2526,6 +2609,13 @@ const AngularWorkflow = (defaultprops) => {
}
} catch (e) {
console.log("Chunk JSON error: ", e)
if (!streamDisabled) {
setStreamDisabled(true)
streamDisabled2 = true
}
return
}
//data.push(chunk)
@@ -2567,14 +2657,30 @@ const AngularWorkflow = (defaultprops) => {
}
const startWorkflowStream = async (workflowId) => {
const timeout = 60000
if (!isCloud) {
console.log("Not cloud, not starting workflow stream")
return
}
return
if (streamDisabled) {
console.log("Stream disabled")
return
}
const timeout = 60000
//const url = `${globalUrl}/api/v1/workflows/${workflowId}/stream`
//const streamUrl = "https://shuffle-streaming-backend-stbuwivzoq-ew.a.run.app"
//
const streamUrl = "https://stream.shuffler.io"
const url = `${streamUrl}/api/v1/workflows/${workflowId}/stream`
while (true) {
if (streamDisabled === true || streamDisabled2 === true) {
break
}
// Wait 1 second before next request just in case of timeouts
await new Promise(r => setTimeout(r, 1000));
await fetchWithTimeout(`${globalUrl}/api/v1/workflows/${workflowId}/stream`, {
await fetchWithTimeout(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
@@ -2970,7 +3076,7 @@ const AngularWorkflow = (defaultprops) => {
sendStreamRequest({
"item": "node",
"type": "unselect",
"userid": userdata.id,
"id": workflow.id,
})
//}, 150)
};
@@ -3782,6 +3888,8 @@ const AngularWorkflow = (defaultprops) => {
//}
curaction.app_id = curapp.id
console.log("CURAPP: ", curapp.authentication)
setAuthenticationType(
curapp.authentication.type === "oauth2" && curapp.authentication.redirect_uri !== undefined && curapp.authentication.redirect_uri !== null ? {
type: "oauth2",
@@ -3791,6 +3899,7 @@ const AngularWorkflow = (defaultprops) => {
scope: curapp.authentication.scope,
client_id: curapp.authentication.client_id,
client_secret: curapp.authentication.client_secret,
grant_type: curapp.authentication.grant_type,
} : {
type: "",
}
@@ -4017,7 +4126,6 @@ const AngularWorkflow = (defaultprops) => {
"item": "node",
"type": "select",
"id": data.id,
"userid": userdata.id,
"location": {
"x": event.target.position("x"),
"y": event.target.position("y"),
@@ -5931,10 +6039,10 @@ const AngularWorkflow = (defaultprops) => {
const xParsed = destinationnodePosition.x - sourcenodePosition.x
const yParsed = destinationnodePosition.y - sourcenodePosition.y
const z = Math.sqrt(xParsed * xParsed + yParsed * yParsed);
const costheta = xParsed / z;
const alpha = 0.25;
var controlPointDistance = [-alpha * yParsed * costheta, alpha * yParsed * costheta];
const z = Math.sqrt(xParsed * xParsed + yParsed * yParsed)
const costheta = xParsed / z
const alpha = 0.3
var controlPointDistance = [-alpha * yParsed * costheta, alpha * yParsed * costheta]
var controlPointWeight = [alpha, 1 - alpha]
//'control-point-weight': ['0.33', '0.66'],
@@ -6097,8 +6205,18 @@ const AngularWorkflow = (defaultprops) => {
const foundtriggers = inputworkflow.triggers.map((trigger) => {
const node = {};
node.position = trigger.position;
node.data = trigger;
if (trigger.large_image === undefined || trigger.large_image === null || trigger.large_image.length === 0) {
// Search triggers array for it where the name is matching and set image
var foundTrigger = triggers.find((t) => t.name === trigger.name)
if (foundTrigger !== undefined && foundTrigger !== null) {
console.log("Autofilled missing trigger image")
trigger.large_image = foundTrigger.large_image
}
}
node.data = trigger;
node.data._id = trigger["id"];
node.data.id = trigger["id"];
node.data.type = "TRIGGER";
@@ -6255,7 +6373,6 @@ const AngularWorkflow = (defaultprops) => {
}
insertedNodes = insertedNodes.concat(newedges);
setWorkflow(inputworkflow);
// Reset view for cytoscape
@@ -6265,6 +6382,8 @@ const AngularWorkflow = (defaultprops) => {
} else {
setElements(insertedNodes);
}
console.log("Setupgraph done 2!")
};
const removeNode = (nodeId) => {
@@ -6339,11 +6458,14 @@ const AngularWorkflow = (defaultprops) => {
sendStreamRequest({
"item": "workflow",
"type": "enter",
"userid": userdata.id,
"id": workflow.id,
})
}
const fetchRecommendations = (inputWorkflow) => {
console.log("Disabled recommendations as they were too inaccurate")
return
const parsedWorkflow = JSON.parse(JSON.stringify(inputWorkflow))
fetch(globalUrl + "/api/v1/workflows/recommend", {
@@ -6442,6 +6564,11 @@ const AngularWorkflow = (defaultprops) => {
return response.json();
})
.then((responseJson) => {
if (responseJson === null) {
console.log("No revisions found")
return
}
if (responseJson.success === false) {
console.log("Error getting workflow revisions: ", responseJson)
return
@@ -6495,13 +6622,16 @@ const AngularWorkflow = (defaultprops) => {
}
// App length necessary cus of cy initialization
if (elements.length === 0 && workflow.actions !== undefined && !graphSetup && Object.getOwnPropertyNames(workflow).length > 0 && workflowRecommendations !== undefined) {
// Not using recommendations, so skipping this for now
//if (elements.length === 0 && workflow.actions !== undefined && !graphSetup && Object.getOwnPropertyNames(workflow).length > 0 && workflowRecommendations !== undefined) {
if (elements.length === 0 && workflow.actions !== undefined && !graphSetup && Object.getOwnPropertyNames(workflow).length > 0) {
setGraphSetup(true);
setupGraph(workflow);
console.log("In graph setup")
// 2nd load - configures cytoscape
} else if (!established && cy !== undefined && ((apps !== null && apps !== undefined && apps.length > 0) || workflow.public === true) && Object.getOwnPropertyNames(workflow).length > 0 && appAuthentication !== undefined && workflowRecommendations !== undefined) {
//} else if (!established && cy !== undefined && ((apps !== null && apps !== undefined && apps.length > 0) || workflow.public === true) && Object.getOwnPropertyNames(workflow).length > 0 && appAuthentication !== undefined && workflowRecommendations !== undefined) {
} else if (!established && cy !== undefined && ((apps !== null && apps !== undefined && apps.length > 0) || workflow.public === true) && Object.getOwnPropertyNames(workflow).length > 0 && appAuthentication !== undefined) {
console.log("In POST graph setup!")
@@ -6567,6 +6697,7 @@ const AngularWorkflow = (defaultprops) => {
}
// preview: true,
console.log("In POST graph setup 2")
cy.fit(null, 200);
cy.on("boxselect", "node", (e) => {
@@ -6615,6 +6746,7 @@ const AngularWorkflow = (defaultprops) => {
document.title = "Workflow - " + workflow.name;
console.log("In POST graph setup 3")
startWorkflowStream(props.match.params.key);
registerKeys();
@@ -7507,14 +7639,30 @@ const AngularWorkflow = (defaultprops) => {
description = app.actions[actionIndex].description
}
const parsedEnvironments =
var parsedEnvironments =
environments === null || environments === []
? "cloud"
: environments[defaultEnvironmentIndex] === undefined
? "cloud"
: environments[defaultEnvironmentIndex].Name;
// activated: app.generated === true ? app.activated === false ? false : true : true,
// List other nodes in the workflow and see if they have an environment set. If they do, use that as the default
if (cy !== undefined && cy !== null) {
const foundnodes = cy.nodes().jsons()
if (foundnodes !== undefined && foundnodes !== null && foundnodes.length > 0) {
// As they should all be the same, this is just an override
for (let nodekey in foundnodes) {
const curnode = foundnodes[nodekey]
if (curnode.data.environment !== undefined && curnode.data.environment !== null && curnode.data.environment.length > 0) {
console.log("Found environment: ", curnode.data.environment)
parsedEnvironments = curnode.data.environment
break
}
}
}
}
console.log("Discovered environment: ", parsedEnvironments)
const newAppData = {
name: app.actions[actionIndex].name,
label: actionLabel,
@@ -8130,6 +8278,25 @@ const AngularWorkflow = (defaultprops) => {
</div>
)
})}
{visibleApps.length <= 4 ? (
<div
style={{ textAlign: "center", width: leftBarSize, marginTop: 40, maxWidth: 340, overflow: "hidden", }}
onLoad={() => {
}}
>
<Typography variant="body1" color="textSecondary">
Click one of the relevant public apps below to Activate it for your organization.
</Typography>
<InstantSearch searchClient={searchClient} indexName="appsearch" onClick={() => {
console.log("CLICKED")
}}>
<CustomSearchBox />
<Index indexName="appsearch">
<CustomAppHits />
</Index>
</InstantSearch>
</div>
) : null}
</div>
) : apps.length > 0 ? (
<div
@@ -8139,7 +8306,7 @@ const AngularWorkflow = (defaultprops) => {
}}
>
<Typography variant="body1" color="textSecondary">
Couldn't find the app you're looking for? Searching unactivated apps. Click one of the below apps to Activate it for your organization.
Couldn't find the apps you were looking for? Searching unactivated apps. Click one of the below apps to Activate it for your organization.
</Typography>
<InstantSearch searchClient={searchClient} indexName="appsearch" onClick={() => {
console.log("CLICKED")
@@ -8355,7 +8522,6 @@ const AngularWorkflow = (defaultprops) => {
}
}
console.log("NEW ACTION: ", newSelectedAction);
setSelectedAction(newSelectedAction);
setUpdate(Math.random());
@@ -8992,6 +9158,33 @@ const AngularWorkflow = (defaultprops) => {
backgroundColor: theme.palette.inputColor,
};
const aiQueryModal =
<Dialog
PaperComponent={PaperComponent}
disableEnforceFocus={true}
hideBackdrop={true}
disableBackdropClick={true}
style={{ pointerEvents: "none" }}
PaperComponent={PaperComponent}
aria-labelledby="draggable-dialog-title"
open={aiQueryModalOpen}
PaperProps={{
style: {
pointerEvents: "auto",
color: "white",
minWidth: isMobile ? "90%" : 800,
border: theme.palette.defaultBorder,
},
}}
onClose={() => {
}}
>
<DialogTitle id="draggable-dialog-title" style={{ cursor: "move", }}>
<span style={{ color: "white" }}>Condition</span>
</DialogTitle>
</Dialog>
const conditionsModal = (
<Dialog
PaperComponent={PaperComponent}
@@ -10958,15 +11151,7 @@ const AngularWorkflow = (defaultprops) => {
style={{ marginTop: 10 }}
label={<div style={{ color: "white" }}>Wait for results</div>}
/>
<Divider
style={{
marginTop: "20px",
height: "1px",
width: "100%",
backgroundColor: "rgb(91, 96, 100)",
}}
/>
<div style={{ flex: "6", marginTop: "20px" }}>
<div style={{ flex: "6", marginTop: 10, }}>
<div>
<div style={{ display: "flex" }}>
<div
@@ -11651,7 +11836,7 @@ const AngularWorkflow = (defaultprops) => {
}}
onChange={(event, newValue) => {
// Workaround with event lol
console.log(event, newValue)
console.log("CHANGE: ", event, newValue)
if (newValue !== undefined && newValue !== null) {
var parsedvalue = JSON.parse(JSON.stringify(newValue))
parsedvalue.actions = []
@@ -11678,6 +11863,7 @@ const AngularWorkflow = (defaultprops) => {
>
<MenuItem
onClick={() => {
console.log("CLICK: ", app)
const newValue = app
if (newValue !== undefined && newValue !== null) {
@@ -11881,7 +12067,7 @@ const AngularWorkflow = (defaultprops) => {
workflow.triggers[selectedTriggerIndex].parameters[0].value
}
color="primary"
placeholder="defaultValue"
placeholder="10"
onBlur={(e) => {
setTriggerCronWrapper(e.target.value);
}}
@@ -12349,7 +12535,7 @@ const AngularWorkflow = (defaultprops) => {
// email,sms,app ...
workflow.triggers[selectedTriggerIndex].parameters[2] = {
name: "type",
value: "email",
value: "subflow",
};
workflow.triggers[selectedTriggerIndex].parameters[3] = {
@@ -12430,16 +12616,7 @@ const AngularWorkflow = (defaultprops) => {
/>
</div>
*/}
<Divider
style={{
marginTop: "20px",
height: "1px",
width: "100%",
backgroundColor: "rgb(91, 96, 100)",
}}
/>
<div style={{ flex: "6", marginTop: "20px" }}>
<b>Parameters</b>
<div style={{ flex: "6", marginTop: 10, }}>
<div
style={{
marginTop: "20px",
@@ -12447,17 +12624,11 @@ const AngularWorkflow = (defaultprops) => {
display: "flex",
}}
>
<div
style={{
width: "17px",
height: "17px",
borderRadius: 17 / 2,
backgroundColor: "#f85a3e",
marginRight: "10px",
}}
/>
<div style={{ flex: "10" }}>
<b>Information</b>
<Typography variant="body2" color="textSecondary">
The information you want to show the user. Supports variables.
</Typography>
</div>
</div>
<TextField
@@ -12488,17 +12659,11 @@ const AngularWorkflow = (defaultprops) => {
display: "flex",
}}
>
<div
style={{
width: "17px",
height: "17px",
borderRadius: 17 / 2,
backgroundColor: "#f85a3e",
marginRight: "10px",
}}
/>
<div style={{ flex: "10" }}>
<b>Contact options</b>
<b>Input options</b>
<Typography variant="body2" color="textSecondary">
Use subflows to connect to any app you want, or use the default email and sms options
</Typography>
</div>
</div>
<FormGroup
@@ -12508,6 +12673,20 @@ const AngularWorkflow = (defaultprops) => {
<FormControlLabel
control={
<Checkbox
checked={workflow.triggers[selectedTriggerIndex].parameters[2] !== undefined && workflow.triggers[selectedTriggerIndex].parameters[2].value.includes("subflow")}
onChange={() => {
setTriggerOptionsWrapper("subflow");
}}
color="primary"
value="subflow"
/>
}
label={<div style={{ color: "white" }}>Subflow</div>}
/>
<FormControlLabel
control={
<Checkbox
disabled={!isCloud}
checked={
workflow.triggers[selectedTriggerIndex].parameters[2] !== undefined && workflow.triggers[selectedTriggerIndex].parameters[2].value.includes("email")
}
@@ -12523,6 +12702,7 @@ const AngularWorkflow = (defaultprops) => {
<FormControlLabel
control={
<Checkbox
disabled={!isCloud}
checked={workflow.triggers !== undefined && workflow.triggers !== null && workflow.triggers[selectedTriggerIndex].parameters !== undefined && workflow.triggers[selectedTriggerIndex].parameters.length > 0 && workflow.triggers[selectedTriggerIndex].parameters[2] !== undefined && workflow.triggers[selectedTriggerIndex].parameters[2].value !== undefined ? workflow.triggers[selectedTriggerIndex].parameters[2].value.includes("sms") : false}
onChange={() => {
setTriggerOptionsWrapper("sms");
@@ -12534,21 +12714,8 @@ const AngularWorkflow = (defaultprops) => {
}
label={<div style={{ color: "white" }}>SMS</div>}
/>
<FormControlLabel
control={
<Checkbox
checked={workflow.triggers[selectedTriggerIndex].parameters[2] !== undefined && workflow.triggers[selectedTriggerIndex].parameters[2].value.includes("subflow")}
onChange={() => {
setTriggerOptionsWrapper("subflow");
}}
color="primary"
value="subflow"
/>
}
label={<div style={{ color: "white" }}>Subflow</div>}
/>
</FormGroup>
{workflow.triggers[selectedTriggerIndex].parameters[2] !== undefined && workflow.triggers[selectedTriggerIndex].parameters[2].value.includes("subflow") ? (
{workflow.triggers[selectedTriggerIndex].parameters[2] !== undefined && workflow.triggers[selectedTriggerIndex].parameters[2].value.includes("subflow") ? (
<div style={{ }}>
{workflows === undefined ||
workflows === null ||
@@ -12654,7 +12821,7 @@ const AngularWorkflow = (defaultprops) => {
},
}}
fullWidth
label="Email"
label="Email"
color="primary"
required
placeholder={"mail1@company.com,mail2@company.com"}
@@ -13250,10 +13417,6 @@ const AngularWorkflow = (defaultprops) => {
"user": "Anonymous",
"user_id": "user_id",
"color": "blue",
}, {
"user": "frikky",
"user_id": "user_id",
"color": "red",
}]
@@ -13309,18 +13472,18 @@ const AngularWorkflow = (defaultprops) => {
const showErrors = !isMobile && !workflow.public && workflow.errors !== undefined && workflow.errors !== null && workflow.errors.length > 0 ?
<div
style={{
border: "1px solid rgba(255,255,255,0.3)",
border: "1px solid rgba(255,255,255,0.1)",
position: "absolute",
bottom: 130,
left: leftBarSize+20,
color: "white",
padding: 5,
padding: 10,
borderRadius: theme.palette.borderRadius,
}}
>
<Typography variant="body22">
<WarningIcon style={{color: "yellow", marginRight: 5, height: 15, width: 15, }} />
<b>{workflow.errors.length} Potential Workflow Issue{workflow.errors.length > 1 ? "s" : ""}</b>
<WarningIcon style={{marginRight: 5, height: 15, width: 15, }} />
<b>{workflow.errors.length} Workflow Issue{workflow.errors.length > 1 ? "s" : ""}</b>
</Typography>
<Typography
variant="body2"
@@ -13766,6 +13929,7 @@ const AngularWorkflow = (defaultprops) => {
expansionModalOpen={codeEditorModalOpen}
setExpansionModalOpen={setCodeEditorModalOpen}
setEditorData={setEditorData}
setAiQueryModalOpen={setAiQueryModalOpen}
/>
} else if (Object.getOwnPropertyNames(selectedComment).length > 0) {
@@ -14313,6 +14477,13 @@ const AngularWorkflow = (defaultprops) => {
style={{ width: size, height: size }}
/>
);
} else if (execution.execution_source === "ShuffleGPT") {
return (
<AutoAwesomeIcon
color="secondary"
style={{paddingTop: 8, paddingLeft: 4, height: 25, width: 25, }}
/>
);
}
if (
@@ -14917,7 +15088,10 @@ const AngularWorkflow = (defaultprops) => {
marginTop: "auto",
marginBottom: "auto",
}}
onClick={() => { }}
onClick={() => {
setExecutionRunning(false);
stop()
}}
>
<ArrowBackIcon style={{ color: "rgba(255,255,255,0.5)" }} />
</IconButton>
@@ -14927,6 +15101,8 @@ const AngularWorkflow = (defaultprops) => {
const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search;
const newitem = removeParam("execution_id", cursearch);
navigate(curpath + newitem)
setExecutionRunning(false);
stop()
}}
>
See more runs
@@ -14991,7 +15167,40 @@ const AngularWorkflow = (defaultprops) => {
</span>
</Tooltip>
) : null}
{isCloud ? (
<Tooltip
color="primary"
title="Explore logs for the workflow"
placement="top"
style={{ zIndex: 50000 }}
>
<span style={{}}>
<Button
color="primary"
style={{ float: "right", marginTop: 20, marginLeft: 10 }}
disabled={!userdata.support}
onClick={() => {
window.open(`/api/v1/workflows/search/${executionData.execution_id}`, "_blank")
}}
>
<InsightsIcon color="secondary" />
</Button>
</span>
</Tooltip>
) : null}
</div>
{executionData.workflow !== undefined && executionData.workflow !== null && executionData.workflow.actions !== undefined && executionData.workflow.actions !== null && executionData.workflow.actions.length > 0 && executionData.workflow.actions[0].environment !== "Cloud" ?
<div style={{ display: "flex", marginLeft: 10, }}>
<Typography variant="body1">
<b>Env &nbsp;&nbsp;&nbsp;&nbsp;</b>
</Typography>
<Typography variant="body1" color="textSecondary" style={{color: "#f85a3e", cursor: "pointer", }} onClick={() => {
window.open("/admin?tab=environments", "_blank")
}}>
{executionData.workflow.actions[0].environment}
</Typography>
</div>
: null}
{executionData.status !== undefined &&
executionData.status.length > 0 ? (
<div style={{ display: "flex", marginLeft: 10, }}>
@@ -15382,19 +15591,23 @@ const AngularWorkflow = (defaultprops) => {
width: 30,
}}
onClick={() => {
const oldstartnode = cy.getElementById(data.action.id);
//console.log("FOUND NODe: ", oldstartnode)
if (oldstartnode !== undefined && oldstartnode !== null) {
const foundname = oldstartnode.data("label")
if (foundname !== undefined && foundname !== null) {
data.action.label = foundname
}
}
if (cy !== undefined) {
const oldstartnode = cy.getElementById(data.action.id);
//console.log("FOUND NODe: ", oldstartnode)
if (oldstartnode !== undefined && oldstartnode !== null) {
const foundname = oldstartnode.data("label")
if (foundname !== undefined && foundname !== null) {
data.action.label = foundname
}
}
//console.log("Click data: ", data)
//data.action.label = ""
setSelectedResult(data);
setCodeModalOpen(true);
//console.log("Click data: ", data)
//data.action.label = ""
setSelectedResult(data);
setCodeModalOpen(true);
} else {
toast("Please wait until the workflow is loaded and try again")
}
}}
>
<Tooltip
@@ -15415,7 +15628,8 @@ const AngularWorkflow = (defaultprops) => {
marginBottom: "auto",
}}
>
<b>{data.action.label}</b>
<b>{data.action.label === undefined || data.action.label === null || data.action.label === "" ? data.action.label : data.action.label.replaceAll("_", " ")}</b>
</div>
<div style={{ fontSize: 14 }}>
<Typography variant="body2" color="textSecondary">
@@ -15973,8 +16187,9 @@ const AngularWorkflow = (defaultprops) => {
cy={(incy) => {
// FIXME: There's something specific loading when
// you do the first hover of a node. Why is this different?
//console.log("CY: ", incy)
setCy(incy);
setCy(incy);
}}
/>
</span>
@@ -16938,7 +17153,7 @@ const AngularWorkflow = (defaultprops) => {
)}
</span>
) : (
<ReactMarkdown
<Markdown
components={{
img: Img,
code: CodeHandler,
@@ -16957,7 +17172,7 @@ const AngularWorkflow = (defaultprops) => {
}}
>
{selectedApp.documentation}
</ReactMarkdown>
</Markdown>
)}
</div>
</div>
@@ -17694,6 +17909,7 @@ const AngularWorkflow = (defaultprops) => {
{newView}
<VariablesModal variableInfo={variableInfo} setVariableInfo={setVariableInfo} />
<ExecutionVariableModal variableInfo={variableInfo} setVariableInfo={setVariableInfo} />
{aiQueryModal}
{conditionsModal}
{authenticationModal}
{codePopoutModal}
+95 -34
View File
@@ -398,7 +398,6 @@ const AppCreator = (defaultprops) => {
apikeySelection.length > 0 ? apikeySelection[0] : ""
);
const [refreshUrl, setRefreshUrl] = useState("");
const [oauth2Scopes, setOauth2Scopes] = useState([]);
const [projectCategories, setProjectCategories] = useState([]);
const [selectedCategory, setSelectedCategory] = useState("");
@@ -411,7 +410,12 @@ const AppCreator = (defaultprops) => {
const [appBuilding, setAppBuilding] = useState(false);
const [fileDownloadEnabled, setFileDownloadEnabled] = useState(false);
const [actionAmount, setActionAmount] = useState(increaseAmount);
const [oauth2Type, setOauth2Type] = useState("application");
const [oauth2Scopes, setOauth2Scopes] = useState([]);
const [oauth2Type, setOauth2Type] = useState("delegated");
//client_credentials
const [oauth2GrantType, setOauth2GrantType] = useState("");
const defaultAuth = {
name: "",
type: "header",
@@ -1748,17 +1752,25 @@ const AppCreator = (defaultprops) => {
//console.log("FLOW-1: ", value)
const flowkey = value.flow === undefined ? "flows" : "flow";
//console.log("FLOW: ", value[flowkey])
const basekey = value[flowkey].authorizationCode !== undefined
? "authorizationCode"
: "implicit";
// Doesn't seem to be used for now
const basekey = value[flowkey].authorizationCode !== undefined ? "authorizationCode" : "implicit";
// Kind of fucked up, but it works for now?
if (value["x-grant-type"] !== undefined && value["x-grant-type"] !== null && value["x-grant-type"].length !== 0) {
setOauth2GrantType(value["x-grant-type"])
}
//console.log("FLOW2: ", value[flowkey][basekey])
if (value[flowkey] !== undefined && value[flowkey][basekey] !== undefined
) {
if (value[flowkey][basekey].authorizationUrl !== undefined && parameterName.length === 0) {
var newparamname = parameterName
if (value[flowkey][basekey].authorizationUrl !== undefined && value[flowkey][basekey].authorizationUrl !== null && value[flowkey][basekey].authorizationUrl.length !== 0 && parameterName.length === 0) {
setParameterName(value[flowkey][basekey].authorizationUrl);
// } else {
// setOauth2Type("application")
} else {
setOauth2Type("application")
}
var tokenUrl = "";
@@ -2499,10 +2511,16 @@ const AppCreator = (defaultprops) => {
scheme: "basic",
};
} else if (authenticationOption === "Oauth2") {
console.log("oauth2: ", parameterName)
console.log("oauth2: ", parameterName)
var newparamName = parameterName.replaceAll('"', "");
newparamName = newparamName.replaceAll("'", "");
// FIXME - this is a hack to get around the fact that the oauth2
// flow is not properly defined
if (oauth2Type === "application") {
newparamName = ""
}
//parameterName, parameterValue, revocationUrl
data.components.securitySchemes["Oauth2"] = {
type: "oauth2",
@@ -2519,6 +2537,14 @@ const AppCreator = (defaultprops) => {
},
},
};
//if (value[flowkey][basekey]["x-grant-type"] !== undefined && value[flowkey][basekey]["x-grant-type"] !== null && value[flowkey][basekey]["x-grant-type"].length !== 0) {
if (oauth2GrantType.length > 0) {
data.components.securitySchemes["Oauth2"]["x-grant-type"] = oauth2GrantType;
}
console.log("SECURITYSCHEMES: ", data.components);
}
if (setExtraAuth.length > 0) {
@@ -2925,7 +2951,7 @@ const AppCreator = (defaultprops) => {
color="textSecondary"
style={{ marginTop: 10 }}
>
Base Authorization URL for Oauth2
Authorization URL for Oauth2
</Typography>
<TextField
required
@@ -3082,7 +3108,7 @@ const AppCreator = (defaultprops) => {
},
}}
style={{ maxHeight: 80, overflowX: "hidden", overflowY: "auto" }}
placeholder="Available Oauth2 Scopes (enter to add)"
placeholder="Available Oauth2 Scopes"
color="primary"
fullWidth
value={oauth2Scopes}
@@ -5957,36 +5983,71 @@ const AppCreator = (defaultprops) => {
</Select>
</FormControl>
{authenticationOption === "Oauth2" ?
<FormControl style={{ marginLeft: 350, maxWidth: 200, }} variant="outlined">
<FormControl style={{ marginLeft: 310, maxWidth: 240, }} variant="outlined">
{/*
<Typography variant="body2">
- Delegated: The user will get a popup for access their personal data.
- Application: Permissions are set by the app creator in the 3rd party platform.
</Typography>
*/}
<Select
fullWidth
label="Oauth2 type"
onChange={(e) => {
setOauth2Type(e.target.value);
}}
value={oauth2Type}
style={{
backgroundColor: inputColor,
color: "white",
height: "50px",
}}
>
{["delegated", "application"].map((data, index) => (
<MenuItem
key={index}
style={{ backgroundColor: inputColor, color: "white" }}
value={data}
<div style={{display: "flex", flexDirection: "row", alignItems: "center", justifyContent: "space-between", }}>
<Typography variant="body2" style={{ marginTop: 10, marginRight: 10, }} color="textSecondary">Oauth2 type</Typography>
<Select
fullWidth
onChange={(e) => {
setOauth2Type(e.target.value);
if (e.target.value === "application" && oauth2GrantType === "") {
setOauth2GrantType("client_credentials")
}
}}
value={oauth2Type}
style={{
backgroundColor: inputColor,
color: "white",
height: "50px",
}}
>
{["delegated", "application"].map((data, index) => (
<MenuItem
key={index}
style={{ backgroundColor: inputColor, color: "white" }}
value={data}
>
{data}
</MenuItem>
))}
</Select>
</div>
{oauth2Type === "application" ?
<div style={{display: "flex", }}>
<Typography variant="body2" style={{ marginTop: 10, marginRight: 10, }} color="textSecondary">Grant Type</Typography>
<Select
fullWidth
label="Grant Type"
onChange={(e) => {
setOauth2GrantType(e.target.value);
}}
value={oauth2GrantType}
style={{
backgroundColor: inputColor,
color: "white",
height: "50px",
}}
>
{data}
</MenuItem>
))}
</Select>
{["client_credentials", "password"].map((data, index) => (
<MenuItem
key={index}
style={{ backgroundColor: inputColor, color: "white" }}
value={data}
>
{data}
</MenuItem>
))}
</Select>
</div>
: null}
</FormControl>
: null}
</span>
+1 -1
View File
@@ -403,7 +403,7 @@ const Apps = (props) => {
return response.json();
})
.then((responseJson) => {
//console.log("Apps: ", responseJson)
console.log("Apps: ", responseJson)
//responseJson = sortByKey(responseJson, "large_image")
//responseJson = sortByKey(responseJson, "is_valid")
//setFilteredApps(responseJson.filter(app => !internalIds.includes(app.name) && !(!app.activated && app.generated)))
+72 -40
View File
@@ -102,6 +102,8 @@ const UsecaseListComponent = (props) => {
const [expandedItem, setExpandedItem] = useState(-1);
const [inputUsecase, setInputUsecase] = useState({});
const [prevSubcase, setPrevSubcase] = useState({})
const [editing, setEditing] = useState(false);
const [description, setDescription] = useState("");
const [video, setVideo] = useState("");
@@ -117,6 +119,42 @@ const UsecaseListComponent = (props) => {
const [mitreTags, setMitreTags] = useState([]);
const parseUsecase = (subcase) => {
const srcdata = findSpecificApp(frameworkData, subcase.type)
const dstdata = findSpecificApp(frameworkData, subcase.last)
if (srcdata !== undefined && srcdata !== null) {
subcase.srcimg = srcdata.large_image
subcase.srcapp = srcdata.name
}
if (dstdata !== undefined && dstdata !== null) {
subcase.dstimg = dstdata.large_image
subcase.dstapp = dstdata.name
}
return subcase
}
useEffect(() => {
console.log("In frameworkData useEffect: frameworkData: ", frameworkData)
if (frameworkData === undefined || prevSubcase === undefined) {
return
}
console.log("PAST!")
var parsedUsecase = inputUsecase
const subcase = parseUsecase(prevSubcase)
parsedUsecase.srcimg = subcase.srcimg
parsedUsecase.srcapp = subcase.srcapp
parsedUsecase.dstimg = subcase.dstimg
parsedUsecase.dstapp = subcase.dstapp
setInputUsecase(parsedUsecase)
}, [frameworkData])
const loadApps = () => {
fetch(`${globalUrl}/api/v1/apps`, {
method: "GET",
@@ -163,35 +201,14 @@ const UsecaseListComponent = (props) => {
if (keys === undefined || keys === null || keys.length === 0) {
return null
}
const parseUsecase = (subcase) => {
//console.log("parseUsecase: ", subcase)
const srcdata = findSpecificApp(frameworkData, subcase.type)
const dstdata = findSpecificApp(frameworkData, subcase.last)
if (srcdata !== undefined && srcdata !== null) {
subcase.srcimg = srcdata.large_image
subcase.srcapp = srcdata.name
}
if (dstdata !== undefined && dstdata !== null) {
subcase.dstimg = dstdata.large_image
subcase.dstapp = dstdata.name
}
return subcase
}
}
// Timeout 50ms to delay it slightly
const getUsecase = (subcase, index, subindex) => {
subcase = parseUsecase(subcase)
// Timeout 50ms to delay it slightly
//setTimeout(() => {
// setInputUsecase(subcase)
//}, 50)
setPrevSubcase(subcase)
fetch(`${globalUrl}/api/v1/workflows/usecases/${escape(subcase.name.replaceAll(" ", "_"))}`, {
method: "GET",
@@ -214,8 +231,6 @@ const UsecaseListComponent = (props) => {
if (responseJson.success === false) {
parsedUsecase = subcase
} else {
console.log("FOUND: ", JSON.parse(JSON.stringify(responseJson)))
parsedUsecase = responseJson
parsedUsecase.srcimg = subcase.srcimg
@@ -314,7 +329,7 @@ const UsecaseListComponent = (props) => {
})
.catch((error) => {
//toast(error.toString());
//setFrameworkLoaded(true)
//setFrameworkLoaded(true)
})
}
@@ -416,15 +431,16 @@ const UsecaseListComponent = (props) => {
return (
<Grid id={fixedName} item xs={selectedItem ? 12 : 4} key={subindex} style={{minHeight: 110,}} onClick={() => {
if (fixedName === "increase authentication") {
getUsecase(subcase, index, subindex)
return
}
//setSelectedWorkflows([])
if (selectedItem) {
} else {
getUsecase(subcase, index, subindex)
navigate(`/usecases?selected_object=${fixedName}`)
//const newitem = removeParam("selected_object", cursearch);
//navigate(curpath + newitem)
}
}}>
<Paper style={{padding: 25, minHeight: isCloud ? 75 : 122, cursor: !selectedItem ? "pointer" : "default", border: itemBorder, backgroundColor: backgroundColor,}} onClick={() => {
@@ -594,11 +610,12 @@ const UsecaseListComponent = (props) => {
>
<IconButton
style={{}}
index="close_selection"
onClick={(e) => {
setExpandedItem(-1)
setExpandedIndex(-1)
setEditing(false)
setInputUsecase({})
setExpandedItem(-1)
setExpandedIndex(-1)
setEditing(false)
setInputUsecase({})
}}
>
<CloseIcon style={{ color: "white" }} />
@@ -1172,13 +1189,28 @@ const Dashboard = (props) => {
if (foundQuery !== null && foundQuery !== undefined) {
setSelectedUsecaseCategory(foundQuery)
const newitem = removeParam("selected", cursearch);
const newitem = removeParam("selected", cursearch);
navigate(curpath + newitem)
}
const baseItem = document.getElementById("increase authentication")
if (baseItem !== undefined && baseItem !== null) {
baseItem.click()
// Find close window button -> go to top
const foundButton = document.getElementById("close_selection")
if (foundButton !== undefined && foundButton !== null) {
foundButton.click()
}
// Scroll back to top
window.scrollTo(0, 0)
}
const foundQuery2 = params["selected_object"]
if (foundQuery2 !== null && foundQuery2 !== undefined) {
//console.log("Got selected_object: ", foundQuery2)
// Take a random object, quickly click it, then go to this one
// Something is weird with loading apps without it
const queryName = foundQuery2.toLowerCase().replaceAll("_", " ")
// Waiting a bit for it to render
@@ -1198,7 +1230,7 @@ const Dashboard = (props) => {
} else {
//console.log("Couldn't find item with name ", queryName)
}
}, 100);
}, 1000);
}
}
@@ -1251,6 +1283,7 @@ const Dashboard = (props) => {
})
}
const getAvailableWorkflows = () => {
fetch(globalUrl + "/api/v1/workflows", {
method: "GET",
@@ -1385,8 +1418,7 @@ const Dashboard = (props) => {
useEffect(() => {
getAvailableWorkflows()
getFramework()
//fetchUsecases()
getFramework()
}, []);
const fetchdata = (stats_id) => {
+42 -102
View File
@@ -1,13 +1,12 @@
import React, { useEffect, useState } from "react";
import ReactMarkdown from "react-markdown";
import { toast } from 'react-toastify';
import Markdown from 'react-markdown'
import { BrowserView, MobileView } from "react-device-detect";
import { useParams, useNavigate, Link } from "react-router-dom";
import { isMobile } from "react-device-detect";
import theme from '../theme.jsx';
import remarkGfm from 'remark-gfm'
import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import {
Grid,
@@ -29,6 +28,8 @@ import {
import {
Link as LinkIcon,
Edit as EditIcon,
KeyboardArrowRight as KeyboardArrowRightIcon,
ExpandMore as ExpandMoreIcon,
} from "@mui/icons-material";
const Body = {
@@ -135,7 +136,7 @@ const Docs = (defaultprops) => {
};
const fetchDocList = () => {
fetch(globalUrl + "/api/v1/docs", {
fetch(`${globalUrl}/api/v1/docs`, {
method: "GET",
headers: {
"Content-Type": "application/json",
@@ -147,9 +148,8 @@ const Docs = (defaultprops) => {
if (responseJson.success) {
setList(responseJson.list);
} else {
setList([
"# Error loading documentation. Please contact us if this persists.",
]);
setList(["# Error loading documentation. Please contact us if this persists.",]);
toast("Failed loading documentation. Please reload the window")
}
setListLoaded(true);
})
@@ -157,7 +157,7 @@ const Docs = (defaultprops) => {
};
const fetchDocs = (docId) => {
fetch(globalUrl + "/api/v1/docs/" + docId, {
fetch(`${globalUrl}/api/v1/docs/${docId}`, {
method: "GET",
headers: {
"Content-Type": "application/json",
@@ -166,8 +166,15 @@ const Docs = (defaultprops) => {
})
.then((response) => response.json())
.then((responseJson) => {
if (responseJson.success) {
setData(responseJson.reason);
if (responseJson.success === false) {
//toast("Failed loading documentation. Please reload the UI")
}
if (responseJson.success && responseJson.reason !== undefined) {
// Find <img> tags and translate them into ![]() format
const imgRegex = /<img.*?src="(.*?)"/g;
const newdata = responseJson.reason.replace(imgRegex, '![]($1)');
setData(newdata)
if (docId === undefined) {
document.title = "Shuffle documentation introduction";
} else {
@@ -365,18 +372,17 @@ const Docs = (defaultprops) => {
}
const markdownStyle = {
color: "rgba(255, 255, 255, 0.65)",
color: "rgba(255, 255, 255, 0.90)",
overflow: "hidden",
paddingBottom: 100,
margin: "auto",
maxWidth: "100%",
minWidth: "100%",
overflow: "hidden",
fontSize: isMobile ? "1.3rem" : "1.0rem",
fontSize: isMobile ? "1.3rem" : "1.1rem",
};
function OuterLink(props) {
console.log("Link: ", props.href)
if (props.href.includes("http") || props.href.includes("mailto")) {
return (
<a
@@ -402,7 +408,7 @@ const Docs = (defaultprops) => {
}
function CodeHandler(props) {
console.log("PROPS: ", props)
//console.log("Codehandler PROPS: ", props)
const propvalue = props.value !== undefined && props.value !== null ? props.value : props.children !== undefined && props.children !== null && props.children.length > 0 ? props.children[0] : ""
@@ -603,7 +609,6 @@ const Docs = (defaultprops) => {
const [hover, setHover] = useState(false);
console.log("Link: ", link)
if (link === undefined || link === null) {
return null
}
@@ -681,60 +686,20 @@ const Docs = (defaultprops) => {
<b>Organize.</b> Whether an organization of 1000 or 1, management tools are necessary. In Shuffle we offer full user management, MFA and single-signon options, multi-tenancy and a lot more - for free!
</Typography>
</div>
{/*
<Grid container spacing={2} style={{marginTop: 50, }}>
{list.map((data, index) => {
const item = data.name;
if (item === undefined) {
return null;
}
const path = "/docs/" + item;
const newname =
item.charAt(0).toUpperCase() +
item.substring(1).split("_").join(" ").split("-").join(" ");
const itemMatching = props.match.params.key === undefined ? false :
props.match.params.key.toLowerCase() === item.toLowerCase();
return (
<Grid key={index} item xs={4}>
<DocumentationButton key={index} item={newname} link={"/docs/"+data.name} />
</Grid>
)
})}
</Grid>
*/}
{/*
<TextField
required
style={{
flex: "1",
backgroundColor: theme.palette.inputColor,
height: 50,
}}
InputProps={{
style:{
color: "white",
height: 50,
},
}}
placeholder={"Search Knowledgebase"}
color="primary"
fullWidth={true}
type="firstname"
id={"Searchfield"}
margin="normal"
variant="outlined"
onChange={(event) => {
console.log("Change: ", event.target.value)
}}
/>
*/}
</div>
const markdownComponents = {
img: Img,
code: CodeHandler,
h1: Heading,
h2: Heading,
h3: Heading,
h4: Heading,
h5: Heading,
h6: Heading,
a: OuterLink,
}
// PostDataBrowser Section
const postDataBrowser =
list === undefined || list === null ? null : (
@@ -812,32 +777,22 @@ const Docs = (defaultprops) => {
mainpageInfo
:
<div id="markdown_wrapper_outer" style={markdownStyle}>
<ReactMarkdown
components={{
img: Img,
code: CodeHandler,
h1: Heading,
h2: Heading,
h3: Heading,
h4: Heading,
h5: Heading,
h6: Heading,
a: OuterLink,
}}
<Markdown
components={markdownComponents}
id="markdown_wrapper"
escapeHtml={false}
skipHtml={false}
style={{
maxWidth: "100%", minWidth: "100%",
}}
>
{data}
</ReactMarkdown>
</Markdown>
</div>
}
</div>
</div>
);
// remarkPlugins={[remarkGfm]}
const mobileStyle = {
color: "white",
@@ -849,6 +804,7 @@ const Docs = (defaultprops) => {
flexDirection: "column",
};
const postDataMobile =
list === undefined || list === null ? null : (
<div style={mobileStyle}>
@@ -899,18 +855,8 @@ const Docs = (defaultprops) => {
mainpageInfo
:
<div id="markdown_wrapper_outer" style={markdownStyle}>
<ReactMarkdown
components={{
img: Img,
code: CodeHandler,
h1: Heading,
h2: Heading,
h3: Heading,
h4: Heading,
h5: Heading,
h6: Heading,
a: OuterLink,
}}
<Markdown
components={markdownComponents}
id="markdown_wrapper"
escapeHtml={false}
style={{
@@ -918,7 +864,7 @@ const Docs = (defaultprops) => {
}}
>
{data}
</ReactMarkdown>
</Markdown>
</div>
}
<Divider
@@ -941,15 +887,9 @@ const Docs = (defaultprops) => {
</div>
);
//const imageModal =
// <Dialog modal
// open={imageModalOpen}
// </Dialog>
// {imageModal}
// Padding and zIndex etc set because of footer in cloud.
const loadedCheck = (
<div style={{ minHeight: 1000, paddingBottom: 100, zIndex: 50000, }}>
<div style={{ minHeight: 1000, paddingBottom: 100, zIndex: 50000, maxWidth: 1920, minWidth: 1366, margin: "auto", }}>
<BrowserView>{postDataBrowser}</BrowserView>
<MobileView>{postDataMobile}</MobileView>
</div>
+11 -11
View File
@@ -1344,7 +1344,7 @@ const Workflows = (props) => {
}, i * 200);
}
toast(`exporting and keeping original for all ${allWorkflows.length} workflows`);
toast(`Exporting and keeping original for all ${allWorkflows.length} workflows`);
};
const deduplicateIds = (data, skip_sanitize) => {
@@ -1982,16 +1982,16 @@ const Workflows = (props) => {
</div>
</Tooltip>
<Tooltip arrow title={
<span style={{}}>
{data.image !== undefined && data.image !== null && data.image.length > 0 ?
<img src={data.image} alt={data.name} style={{backgroundColor: theme.palette.surfaceColor, maxHeight: 200, minHeigth: 200, borderRadius: theme.palette.borderRadius, }} />
: null}
<Typography>
Edit {data.name}
</Typography>
</span>
} placement="bottom">
<Typography
<div style={{width: "100%", minWidth: 250, maxWidth: 310, }}>
{data.image !== undefined && data.image !== null && data.image.length > 0 ?
<img src={data.image} alt={data.name} style={{backgroundColor: theme.palette.surfaceColor, maxWidth: 300, minWidth: 250, borderRadius: theme.palette.borderRadius, }} />
: null}
<Typography>
Edit {data.name}
</Typography>
</div>
} placement="left">
<Typography
variant="body1"
style={{
marginBottom: 0,
+1 -1
View File
@@ -9,7 +9,7 @@ require (
github.com/mackerelio/go-osstat v0.2.3
github.com/satori/go.uuid v1.2.0
github.com/shirou/gopsutil v3.21.11+incompatible
github.com/shuffle/shuffle-shared v0.4.62
github.com/shuffle/shuffle-shared v0.5.29
k8s.io/api v0.28.1
k8s.io/apimachinery v0.28.1
k8s.io/client-go v0.28.1
+8
View File
@@ -272,6 +272,14 @@ github.com/shuffle/shuffle-shared v0.4.59 h1:5Sv8aorgQJFZr3cCKltfycdXzp9v5zlF2l3
github.com/shuffle/shuffle-shared v0.4.59/go.mod h1:X613gbo0dT3fnYvXDRwjQZyLC+T49T2nSQOrCV5QMlI=
github.com/shuffle/shuffle-shared v0.4.62 h1:L76zWCD/7gIBuhr3feWZwzT4I8VCiLRd8ZAub/3EiO0=
github.com/shuffle/shuffle-shared v0.4.62/go.mod h1:X613gbo0dT3fnYvXDRwjQZyLC+T49T2nSQOrCV5QMlI=
github.com/shuffle/shuffle-shared v0.4.86 h1:QrFx3j+maUgeU/dP48WMx+NBcWdwe3Ov2yKlLUUZRHw=
github.com/shuffle/shuffle-shared v0.4.86/go.mod h1:X613gbo0dT3fnYvXDRwjQZyLC+T49T2nSQOrCV5QMlI=
github.com/shuffle/shuffle-shared v0.4.95 h1:xr92/03/uQeJiDme9S8/vgF1KWyQgJ1KQXVE7nQMKis=
github.com/shuffle/shuffle-shared v0.4.95/go.mod h1:X613gbo0dT3fnYvXDRwjQZyLC+T49T2nSQOrCV5QMlI=
github.com/shuffle/shuffle-shared v0.4.96 h1:iaIB/HP9eKpw9DMMJZhSLDbKdHJt075kFYLHg9AaiiM=
github.com/shuffle/shuffle-shared v0.4.96/go.mod h1:X613gbo0dT3fnYvXDRwjQZyLC+T49T2nSQOrCV5QMlI=
github.com/shuffle/shuffle-shared v0.5.29 h1:n4vThl7v3mFVXbrIW71XREFdmZZo7mOBAWxnsdiNjDk=
github.com/shuffle/shuffle-shared v0.5.29/go.mod h1:X613gbo0dT3fnYvXDRwjQZyLC+T49T2nSQOrCV5QMlI=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+57 -11
View File
@@ -63,7 +63,7 @@ var sleepTime = 2
// Making it work on low-end machines even during busy times :)
// May cause some things to run slowly
var maxConcurrency = 3
var maxConcurrency = 7
// Timeout if something rashes
var workerTimeoutEnv = os.Getenv("SHUFFLE_ORBORUS_EXECUTION_TIMEOUT")
@@ -556,6 +556,23 @@ func deployServiceWorkers(image string) {
}
}
// Look for SHUFFLE_VOLUME_BINDS
if len(os.Getenv("SHUFFLE_VOLUME_BINDS")) > 0 {
serviceSpec.TaskTemplate.ContainerSpec.Env = append(serviceSpec.TaskTemplate.ContainerSpec.Env, fmt.Sprintf("SHUFFLE_VOLUME_BINDS=%s", os.Getenv("SHUFFLE_VOLUME_BINDS")))
}
overrideHttpProxy := os.Getenv("SHUFFLE_INTERNAL_HTTP_PROXY")
overrideHttpsProxy := os.Getenv("SHUFFLE_INTERNAL_HTTPS_PROXY")
if len(overrideHttpProxy) > 0 {
log.Printf("[DEBUG] Added internal proxy: %s", overrideHttpProxy)
serviceSpec.TaskTemplate.ContainerSpec.Env = append(serviceSpec.TaskTemplate.ContainerSpec.Env, fmt.Sprintf("SHUFFLE_INTERNAL_HTTP_PROXY=%s", overrideHttpProxy))
}
if len(overrideHttpsProxy) > 0 {
log.Printf("[DEBUG] Added internal proxy: %s", overrideHttpsProxy)
serviceSpec.TaskTemplate.ContainerSpec.Env = append(serviceSpec.TaskTemplate.ContainerSpec.Env, fmt.Sprintf("SHUFFLE_INTERNAL_HTTPS_PROXY=%s", overrideHttpsProxy))
}
serviceOptions := types.ServiceCreateOptions{}
_, err = dockercli.ServiceCreate(
ctx,
@@ -788,7 +805,7 @@ func deployWorker(image string, identifier string, env []string, executionReques
log.Printf("[ERROR] Failed to start worker container in environment %s: %s", environment, err)
return err
} else {
log.Printf("[INFO] Worker Container %s was created under environment %s for execution %s: docker logs %s", cont.ID, environment, executionRequest.ExecutionId, cont.ID)
log.Printf("[INFO][%s] Worker Container created. Environment %s: docker logs %s", executionRequest.ExecutionId, environment, cont.ID)
}
//stats, err := cli.ContainerInspect(context.Background(), containerName)
@@ -813,7 +830,7 @@ func deployWorker(image string, identifier string, env []string, executionReques
// }
//}
} else {
log.Printf("[INFO] Worker Container %s was created under environment %s: docker logs %s", cont.ID, environment, cont.ID)
log.Printf("[INFO][%s] New Worker created. Environment %s: docker logs %s", executionRequest.ExecutionId, environment, cont.ID)
}
return nil
@@ -1298,7 +1315,6 @@ func main() {
ctx := context.Background()
// Run by default from now
//commenting for now as its stoppoing minikube
// zombiecheck(ctx, workerTimeout)
log.Printf("[INFO] Running towards %s (BASE_URL) with environment name %s", baseUrl, environment)
@@ -1333,6 +1349,8 @@ func main() {
//deployServiceWorkers(workerImage)
}
zombiecheck(ctx, workerTimeout)
client := shuffle.GetExternalClient(baseUrl)
fullUrl := fmt.Sprintf("%s/api/v1/workflows/queue", baseUrl)
log.Printf("[INFO] Finished configuring docker environment. Connecting to %s", fullUrl)
@@ -1389,7 +1407,7 @@ func main() {
// Should find data to send (memory etc.)
// Create timeout of max 4 seconds just in case
ctx, cancel := context.WithTimeout(context.Background(), 4*time.Second)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Marshal and set body
@@ -1524,9 +1542,12 @@ func main() {
log.Printf("[INFO] Execution already handled (rerun of old executions?): %s", execution.ExecutionId)
toBeRemoved.Data = append(toBeRemoved.Data, execution)
// Should check when last this was ran, and if it's more than 10 minutes ago and it's not finished, we should run it again?
/*
if swarmConfig != "run" && swarmConfig != "swarm" {
continue
}
*/
}
// Now, how do I execute this one?
@@ -1574,6 +1595,30 @@ func main() {
env = append(env, fmt.Sprintf("SHUFFLE_DEBUG_MEMORY=%s", os.Getenv("SHUFFLE_DEBUG_MEMORY")))
}
// Look for volume binds
if len(os.Getenv("SHUFFLE_VOLUME_BINDS")) > 0 {
log.Printf("[DEBUG] Added volume binds: %s", os.Getenv("SHUFFLE_VOLUME_BINDS"))
env = append(env, fmt.Sprintf("SHUFFLE_VOLUME_BINDS=%s", os.Getenv("SHUFFLE_VOLUME_BINDS")))
}
if len(os.Getenv("SHUFFLE_APP_SDK_TIMEOUT")) > 0 {
env = append(env, fmt.Sprintf("SHUFFLE_APP_SDK_TIMEOUT=%s", os.Getenv("SHUFFLE_APP_SDK_TIMEOUT")))
}
// Setting up internal proxy config for Shuffle -> shuffle comms
overrideHttpProxy := os.Getenv("SHUFFLE_INTERNAL_HTTP_PROXY")
overrideHttpsProxy := os.Getenv("SHUFFLE_INTERNAL_HTTPS_PROXY")
if len(overrideHttpProxy) > 0 {
log.Printf("[DEBUG] Added internal proxy: %s", overrideHttpProxy)
env = append(env, fmt.Sprintf("HTTP_PROXY=%s", overrideHttpProxy))
}
if len(overrideHttpsProxy) > 0 {
log.Printf("[DEBUG] Added internal proxy: %s", overrideHttpsProxy)
env = append(env, fmt.Sprintf("HTTPS_PROXY=%s", overrideHttpsProxy))
}
err = deployWorker(workerImage, containerName, env, execution)
zombiecounter += 1
if err == nil {
@@ -1754,7 +1799,7 @@ func zombiecheck(ctx context.Context, workerTimeout int) error {
return nil
}
log.Println("[INFO] Looking for old containers (zombies)")
log.Println("[INFO] Looking for old containers to remove")
containers, err := dockercli.ContainerList(ctx, types.ContainerListOptions{
All: true,
})
@@ -1771,10 +1816,11 @@ func zombiecheck(ctx context.Context, workerTimeout int) error {
stopContainers := []string{}
removeContainers := []string{}
log.Printf("[INFO] Baseimage: %s, Workertimeout: %d", baseimagename, int64(workerTimeout))
baseString := `/bin/sh -c 'python app.py --log-level DEBUG'`
//baseString := `/bin/sh -c 'python app.py --log-level DEBUG'`
baseString := `python app.py`
for _, container := range containers {
// Skip random containers. Only handle things related to Shuffle.
if !strings.Contains(container.Image, baseimagename) && container.Command != baseString && container.Command != "./worker" {
if !strings.Contains(container.Image, baseimagename) && !strings.Contains(container.Command, baseString) && !strings.Contains(container.Command, "walkoff") && container.Command != "./worker" {
shuffleFound := false
for _, item := range container.Labels {
if item == "shuffle" {
@@ -1785,7 +1831,7 @@ func zombiecheck(ctx context.Context, workerTimeout int) error {
// Check image name
if !shuffleFound {
//log.Printf("[WARNING] Zombie container skip: %#v, %s", container.Labels, container.Image)
log.Printf("[WARNING] Zombie container skip: %#v, %s", container.Labels, container.Image)
continue
}
//} else {
@@ -1820,7 +1866,7 @@ func zombiecheck(ctx context.Context, workerTimeout int) error {
}
// FIXME - add killing of apps with same execution ID too
log.Printf("[INFO] Should STOP %d containers.", len(stopContainers))
log.Printf("[INFO] Should STOP and remove %d containers.", len(stopContainers))
var options container.StopOptions
for _, containername := range stopContainers {
log.Printf("[INFO] Stopping and removing container %s", containerNames[containername])
@@ -1878,7 +1924,7 @@ func sendWorkerRequest(workflowExecution shuffle.ExecutionRequest) error {
streamUrl = fmt.Sprintf("%s:33333/api/v1/execute", workerServerUrl)
}
if strings.Contains(streamUrl, "localhost") || strings.Contains(streamUrl, "shuffle-backend") {
if strings.Contains(streamUrl, "shuffler.io") || strings.Contains(streamUrl, "localhost") || strings.Contains(streamUrl, "shuffle-backend") {
log.Printf("[INFO] Using default worker server url as previous is invalid: %s", streamUrl)
streamUrl = fmt.Sprintf("http://shuffle-workers:33333/api/v1/execute")
}
+1 -1
View File
@@ -11,7 +11,7 @@ require (
github.com/gorilla/mux v1.8.0
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/satori/go.uuid v1.2.0
github.com/shuffle/shuffle-shared v0.4.57
github.com/shuffle/shuffle-shared v0.5.29
k8s.io/api v0.28.3
k8s.io/apimachinery v0.28.3
k8s.io/client-go v0.28.3
+2
View File
@@ -284,6 +284,8 @@ github.com/shuffle/shuffle-shared v0.4.50 h1:fJLfhWIJ5mYap4JwHnD/B5aaLyIULwylFSl
github.com/shuffle/shuffle-shared v0.4.50/go.mod h1:X613gbo0dT3fnYvXDRwjQZyLC+T49T2nSQOrCV5QMlI=
github.com/shuffle/shuffle-shared v0.4.57 h1:o+mMPRY4ourkE3R0qdi80jg6RlCtvAJ/VVrPk4y75Hk=
github.com/shuffle/shuffle-shared v0.4.57/go.mod h1:X613gbo0dT3fnYvXDRwjQZyLC+T49T2nSQOrCV5QMlI=
github.com/shuffle/shuffle-shared v0.5.29 h1:n4vThl7v3mFVXbrIW71XREFdmZZo7mOBAWxnsdiNjDk=
github.com/shuffle/shuffle-shared v0.5.29/go.mod h1:X613gbo0dT3fnYvXDRwjQZyLC+T49T2nSQOrCV5QMlI=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
File diff suppressed because it is too large Load Diff