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. # Used for auto-cleanup of containers. REALLY important at scale. Set to false to see all container info.
SHUFFLE_MEMCACHED= SHUFFLE_MEMCACHED=
SHUFFLE_CONTAINER_AUTO_CLEANUP=true 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_HEALTHCHECK_DISABLED=false
SHUFFLE_ELASTIC=true SHUFFLE_ELASTIC=true
SHUFFLE_LOGS_DISABLED=false SHUFFLE_LOGS_DISABLED=false
@@ -93,4 +93,4 @@ SHUFFLE_OPENSEARCH_PROXY=
SHUFFLE_OPENSEARCH_INDEX_PREFIX= SHUFFLE_OPENSEARCH_INDEX_PREFIX=
SHUFFLE_OPENSEARCH_SKIPSSL_VERIFY=true SHUFFLE_OPENSEARCH_SKIPSSL_VERIFY=true
DEBUG_MODE=false DEBUG_MODE=false
+3 -3
View File
@@ -3,7 +3,7 @@ name: dockerbuild
on: on:
push: push:
branches: branches:
- main - 1.4.0
paths: paths:
- "**" - "**"
- "!.github/**" - "!.github/**"
@@ -77,9 +77,9 @@ jobs:
cache-to: type=local,dest=/tmp/.buildx-cache cache-to: type=local,dest=/tmp/.buildx-cache
tags: | tags: |
ghcr.io/shuffle/shuffle-${{ matrix.app }}:${{ matrix.version }} 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 }}:${{ matrix.version }}
${{ secrets.DOCKERHUB_USERNAME }}/shuffle-${{ matrix.app }}:latest ${{ secrets.DOCKERHUB_USERNAME }}/shuffle-${{ matrix.app }}:nightly
- name: Image digest - name: Image digest
run: echo ${{ steps.docker_build.outputs.digest }} run: echo ${{ steps.docker_build.outputs.digest }}
+177 -71
View File
@@ -302,9 +302,11 @@ class AppBase:
self.authorization = os.getenv("AUTHORIZATION", "") self.authorization = os.getenv("AUTHORIZATION", "")
self.current_execution_id = os.getenv("EXECUTIONID", "") self.current_execution_id = os.getenv("EXECUTIONID", "")
self.full_execution = os.getenv("FULL_EXECUTION", "") self.full_execution = os.getenv("FULL_EXECUTION", "")
self.start_time = int(time.time())
self.result_wrapper_count = 0 self.result_wrapper_count = 0
# Make start time with milliseconds
self.start_time = int(time.time_ns())
self.action_result = { self.action_result = {
"action": self.action, "action": self.action,
"authorization": self.authorization, "authorization": self.authorization,
@@ -312,9 +314,24 @@ class AppBase:
"result": f"", "result": f"",
"started_at": self.start_time, "started_at": self.start_time,
"status": "", "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): if isinstance(self.action, str):
try: try:
self.action = json.loads(self.action) self.action = json.loads(self.action)
@@ -468,7 +485,7 @@ class AppBase:
# Try it with some magic # 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"]}""") self.logger.info(f"""[DEBUG] Inside Send result with status {action_result["status"]}""")
#if isinstance(action_result, #if isinstance(action_result,
@@ -512,7 +529,7 @@ class AppBase:
sleeptime = float(random.randint(0, 10) / 10) sleeptime = float(random.randint(0, 10) / 10)
try: 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)") self.logger.info(f"[DEBUG] Result: {ret.status_code} (break on 200 or 201)")
if ret.status_code == 200 or ret.status_code == 201: if ret.status_code == 200 or ret.status_code == 201:
@@ -520,11 +537,29 @@ class AppBase:
break break
else: else:
self.logger.info(f"[ERROR] Bad resp {ret.status_code}: {ret.text}") 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: except requests.exceptions.RequestException as e:
self.logger.info(f"[DEBUG] Request problem: {e}") self.logger.info(f"[DEBUG] Request problem: {e}")
time.sleep(sleeptime) 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) #time.sleep(5)
continue continue
except TimeoutError as e: 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}) 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") 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") self.send_result(action_result, {"Content-Type": "application/json", "Authorization": "Bearer %s" % self.authorization}, "/api/v1/streams")
return return
@@ -572,7 +606,7 @@ class AppBase:
action_result["result"] = json.dumps({"success": False, "reason": "Typeerror when sending to backend URL %s" % url}) 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}") 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}") #self.logger.info(f"[DEBUG] Result: {ret.status_code}")
#if ret.status_code != 200: #if ret.status_code != 200:
# pr # pr
@@ -701,7 +735,7 @@ class AppBase:
#self.logger.info(f"RET: {ret.text}") #self.logger.info(f"RET: {ret.text}")
#self.logger.info(f"ID: {ret.status_code}") #self.logger.info(f"ID: {ret.status_code}")
url = f"{self.url}/api/v1/orgs/{org_id}/validate_app_values" 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: if ret.status_code == 200:
json_value = ret.json() json_value = ret.json()
if len(json_value["found"]) > 0: if len(json_value["found"]) > 0:
@@ -1011,7 +1045,7 @@ class AppBase:
"result": f"All {len(param_multiplier)} values were non-unique", "result": f"All {len(param_multiplier)} values were non-unique",
"started_at": self.start_time, "started_at": self.start_time,
"status": "SKIPPED", "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") 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", "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() return ret.json()
#if ret1.status_code != 200: #if ret1.status_code != 200:
# return { # return {
@@ -1201,7 +1235,7 @@ class AppBase:
"User-Agent": "Shuffle 1.1.0", "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: if ret1.status_code != 200:
return None return None
@@ -1264,7 +1298,7 @@ class AppBase:
"User-Agent": "Shuffle 1.1.0", "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) self.logger.info("RET1 (file get): %s" % ret1.text)
if ret1.status_code != 200: if ret1.status_code != 200:
returns.append({ returns.append({
@@ -1275,7 +1309,7 @@ class AppBase:
continue continue
content_path = "/api/v1/files/%s/content?execution_id=%s" % (item, full_execution["execution_id"]) 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") self.logger.info("RET2 (file get) done")
if ret2.status_code == 200: if ret2.status_code == 200:
tmpdata = ret1.json() tmpdata = ret1.json()
@@ -1311,7 +1345,7 @@ class AppBase:
"key": key, "key": key,
} }
response = requests.post(url, json=data, verify=False) response = requests.post(url, json=data, verify=False, proxies=self.proxy_config)
try: try:
allvalues = response.json() allvalues = response.json()
return json.dumps(allvalues) return json.dumps(allvalues)
@@ -1332,7 +1366,7 @@ class AppBase:
"value": str(value), "value": str(value),
} }
response = requests.post(url, json=data, verify=False) response = requests.post(url, json=data, verify=False, proxies=self.proxy_config)
try: try:
allvalues = response.json() allvalues = response.json()
allvalues["key"] = key allvalues["key"] = key
@@ -1354,7 +1388,7 @@ class AppBase:
"key": key, "key": key,
} }
value = requests.post(url, json=data, verify=False) value = requests.post(url, json=data, verify=False, proxies=self.proxy_config)
try: try:
allvalues = value.json() allvalues = value.json()
self.logger.info("VAL1: ", allvalues) self.logger.info("VAL1: ", allvalues)
@@ -1408,7 +1442,7 @@ class AppBase:
self.logger.info(f"KeyError in file setup: {e}") self.logger.info(f"KeyError in file setup: {e}")
pass 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}") #self.logger.info(f"Ret CREATE: {ret.text}")
cur_id = "" cur_id = ""
if ret.status_code == 200: if ret.status_code == 200:
@@ -1440,7 +1474,7 @@ class AppBase:
files={"shuffle_file": (filename, curfile["data"])} files={"shuffle_file": (filename, curfile["data"])}
#open(filename,'rb')} #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("Ret UPLOAD: %s" % ret.text)
self.logger.info("Ret2 UPLOAD: %d" % ret.status_code) self.logger.info("Ret2 UPLOAD: %d" % ret.status_code)
@@ -1457,7 +1491,7 @@ class AppBase:
"authorization": self.authorization, "authorization": self.authorization,
"execution_id": self.current_execution_id, "execution_id": self.current_execution_id,
"result": "", "result": "",
"started_at": int(time.time()), "started_at": int(time.time_ns()),
"status": "EXECUTING" "status": "EXECUTING"
} }
@@ -1537,7 +1571,8 @@ class AppBase:
"%s/api/v1/streams/results" % (self.base_url), "%s/api/v1/streams/results" % (self.base_url),
headers=headers, headers=headers,
json=tmpdata, json=tmpdata,
verify=False verify=False,
proxies=self.proxy_config,
) )
if ret.status_code == 200: if ret.status_code == 200:
@@ -1964,6 +1999,8 @@ class AppBase:
# Parses JSON loops and such down to the item you're looking for # Parses JSON loops and such down to the item you're looking for
# $nodename.#.id # $nodename.#.id
# $nodename.data.#min-max.info.id # $nodename.data.#min-max.info.id
# $nodename.data.#1-max.info.id
# $nodename.data.#min-1.info.id
def recurse_json(basejson, parsersplit): def recurse_json(basejson, parsersplit):
match = "#([0-9a-z]+):?-?([0-9a-z]+)?#?" match = "#([0-9a-z]+):?-?([0-9a-z]+)?#?"
try: try:
@@ -2080,6 +2117,8 @@ class AppBase:
if (basejson[value].endswith("}") and basejson[value].endswith("}")) or (basejson[value].startswith("[") and basejson[value].endswith("]")): if (basejson[value].endswith("}") and basejson[value].endswith("}")) or (basejson[value].startswith("[") and basejson[value].endswith("]")):
basejson = json.loads(basejson[value]) basejson = json.loads(basejson[value])
else: else:
# Should we sanitize here?
self.logger.info("[DEBUG] VALUE TO SANITIZE?: %s" % basejson[value])
return str(basejson[value]), False return str(basejson[value]), False
except json.decoder.JSONDecodeError as e: except json.decoder.JSONDecodeError as e:
return str(basejson[value]), False return str(basejson[value]), False
@@ -2128,7 +2167,6 @@ class AppBase:
actionname_lower = parsersplit[0][1:].lower() actionname_lower = parsersplit[0][1:].lower()
#Actionname: Start_node #Actionname: Start_node
#print(f"\n[INFO] Actionname: {actionname_lower}")
# 1. Find the action # 1. Find the action
baseresult = "" baseresult = ""
@@ -2466,7 +2504,7 @@ class AppBase:
self.action_result["result"] = f"Failed to parse LiquidPy: {error_msg}" self.action_result["result"] = f"Failed to parse LiquidPy: {error_msg}"
print("[WARNING] Failed to set LiquidPy result") 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.send_result(self.action_result, headers, stream_path)
self.logger.info(f"[ERROR] Sent FAILURE response to backend due to : {e}") self.logger.info(f"[ERROR] Sent FAILURE response to backend due to : {e}")
@@ -2558,6 +2596,27 @@ class AppBase:
return data 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 # Parses parameters sent to it and returns whether it did it successfully with the values found
def parse_params(action, fullexecution, parameter, self): def parse_params(action, fullexecution, parameter, self):
# Skip if it starts with $? # Skip if it starts with $?
@@ -2621,6 +2680,20 @@ class AppBase:
value, is_loop = get_json_value(fullexecution, to_be_replaced) value, is_loop = get_json_value(fullexecution, to_be_replaced)
#self.logger.info(f"\n\nType of value: {type(value)}") #self.logger.info(f"\n\nType of value: {type(value)}")
if isinstance(value, str): 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) parameter["value"] = parameter["value"].replace(to_be_replaced, value)
elif isinstance(value, dict) or isinstance(value, list): elif isinstance(value, dict) or isinstance(value, list):
# Changed from JSON dump to str() 28.05.2021 # 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)) # parameter["value"] = parameter["value"].replace(to_be_replaced, json.dumps(value))
# self.logger.info("Failed parsing value as string?") # self.logger.info("Failed parsing value as string?")
else: else:
self.logger.info("[WARNING] Unknown type %s" % type(value)) self.logger.error("[ERROR] Unknown type %s" % type(value))
try: try:
parameter["value"] = parameter["value"].replace(to_be_replaced, json.dumps(value)) parameter["value"] = parameter["value"].replace(to_be_replaced, json.dumps(value))
except json.decoder.JSONDecodeError as e: except json.decoder.JSONDecodeError as e:
@@ -2740,7 +2813,7 @@ class AppBase:
return "", parameter["value"], is_loop return "", parameter["value"], is_loop
def run_validation(sourcevalue, check, destinationvalue): 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 check == "=" or check.lower() == "equals":
if str(sourcevalue).lower() == str(destinationvalue).lower(): if str(sourcevalue).lower() == str(destinationvalue).lower():
@@ -2758,15 +2831,16 @@ class AppBase:
if destinationvalue.lower() in sourcevalue.lower(): if destinationvalue.lower() in sourcevalue.lower():
return True return True
elif check.lower() == "is empty": elif check.lower() == "is empty" or check.lower() == "is_empty":
if len(sourcevalue) == 0: try:
return True 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 True
return False
elif check.lower() == "contains_any_of": elif check.lower() == "contains_any_of":
newvalue = [destinationvalue.lower()] newvalue = [destinationvalue.lower()]
if "," in destinationvalue: if "," in destinationvalue:
@@ -2782,7 +2856,6 @@ class AppBase:
print("[INFO] Found %s in %s" % (item, sourcevalue)) print("[INFO] Found %s in %s" % (item, sourcevalue))
return True return True
return False
elif check.lower() == "larger than" or check.lower() == "bigger than": elif check.lower() == "larger than" or check.lower() == "bigger than":
try: try:
if str(sourcevalue).isdigit() and str(destinationvalue).isdigit(): if str(sourcevalue).isdigit() and str(destinationvalue).isdigit():
@@ -2790,9 +2863,23 @@ class AppBase:
return True return True
except AttributeError as e: except AttributeError as e:
print("[WARNING] Condition larger than failed with values %s and %s: %s" % (sourcevalue, destinationvalue, e)) self.logger.info("[WARNING] Condition larger 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 larger than as list: {e}")
elif check.lower() == "smaller than" or check.lower() == "less than": elif check.lower() == "smaller than" or check.lower() == "less than":
self.logger.info("In smaller than check: %s %s" % (sourcevalue, destinationvalue))
try: try:
if str(sourcevalue).isdigit() and str(destinationvalue).isdigit(): if str(sourcevalue).isdigit() and str(destinationvalue).isdigit():
if int(sourcevalue) < int(destinationvalue): if int(sourcevalue) < int(destinationvalue):
@@ -2800,12 +2887,27 @@ class AppBase:
except AttributeError as e: except AttributeError as e:
print("[WARNING] Condition smaller than failed with values %s and %s: %s" % (sourcevalue, destinationvalue, 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": elif check.lower() == "re" or check.lower() == "matches regex":
try: try:
found = re.search(destinationvalue, sourcevalue) found = re.search(str(destinationvalue), str(sourcevalue))
except re.error as e: 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 return False
if found == None: if found == None:
@@ -2813,7 +2915,7 @@ class AppBase:
return True return True
else: 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 return False
@@ -2826,12 +2928,40 @@ class AppBase:
return True, "" return True, ""
# Startnode should always run - no need to check incoming # 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: try:
if action["id"] == fullexecution["start"]: if action["id"] == fullexecution["start"]:
return True, "" 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: except Exception as error:
self.logger.info(f"[WARNING] Failed checking startnode: {error}") self.logger.info(f"[WARNING] Failed checking startnode: {error}")
return True, "" #return True, ""
#return True, ""
available_checks = [ available_checks = [
"=", "=",
@@ -2850,6 +2980,8 @@ class AppBase:
"contains_any_of", "contains_any_of",
"re", "re",
"matches regex", "matches regex",
"is empty",
"is_empty",
] ]
relevantbranches = [] relevantbranches = []
@@ -2909,7 +3041,7 @@ class AppBase:
destinationvalue = parse_wrapper_start(destinationvalue, self) destinationvalue = parse_wrapper_start(destinationvalue, self)
if not condition["condition"]["value"] in available_checks: 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 continue
# Configuration = negated because of WorkflowAppActionParam.. # Configuration = negated because of WorkflowAppActionParam..
@@ -2974,7 +3106,7 @@ class AppBase:
self.logger.info("Failed one or more branch conditions.") self.logger.info("Failed one or more branch conditions.")
self.action_result["result"] = tmpresult self.action_result["result"] = tmpresult
self.action_result["status"] = "SKIPPED" 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) self.send_result(self.action_result, headers, stream_path)
return return
@@ -3460,7 +3592,7 @@ class AppBase:
self.logger.info("[WARNING] SHOULD STOP EXECUTION BECAUSE FIELDS AREN'T UNIQUE") self.logger.info("[WARNING] SHOULD STOP EXECUTION BECAUSE FIELDS AREN'T UNIQUE")
self.action_result["status"] = "SKIPPED" self.action_result["status"] = "SKIPPED"
self.action_result["result"] = f"A non-unique value was found" 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) self.send_result(self.action_result, headers, stream_path)
return return
@@ -3522,6 +3654,7 @@ class AppBase:
timeout_env = os.getenv("SHUFFLE_APP_SDK_TIMEOUT", timeout) timeout_env = os.getenv("SHUFFLE_APP_SDK_TIMEOUT", timeout)
try: try:
timeout = int(timeout_env) timeout = int(timeout_env)
self.logger.info(f"[DEBUG] Timeout set to {timeout} seconds")
except Exception as e: except Exception as e:
self.logger.info(f"[WARNING] Failed parsing timeout to int: {e}") self.logger.info(f"[WARNING] Failed parsing timeout to int: {e}")
@@ -3537,7 +3670,7 @@ class AppBase:
future.cancel() future.cancel()
newres = json.dumps({ newres = json.dumps({
"success": False, "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), "exception": str(e),
}) })
@@ -3550,40 +3683,13 @@ class AppBase:
except concurrent.futures.TimeoutError as e: except concurrent.futures.TimeoutError as e:
newres = json.dumps({ newres = json.dumps({
"success": False, "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 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: except TypeError as e:
newres = "" newres = ""
self.logger.info(f"[DEBUG] Got exec type error: {e}") self.logger.info(f"[ERROR] Got function exec type error: {e}")
try: try:
e = json.loads(f"{e}") e = json.loads(f"{e}")
except: except:
@@ -3814,7 +3920,7 @@ class AppBase:
}) })
# Send the result :) # 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) self.send_result(self.action_result, headers, stream_path)
#try: #try:
+2 -2
View File
@@ -18,8 +18,8 @@ require (
github.com/gorilla/mux v1.8.0 github.com/gorilla/mux v1.8.0
github.com/h2non/filetype v1.1.3 github.com/h2non/filetype v1.1.3
github.com/satori/go.uuid v1.2.0 github.com/satori/go.uuid v1.2.0
github.com/shuffle/shuffle-shared v0.4.88 github.com/shuffle/shuffle-shared v0.5.30
golang.org/x/crypto v0.9.0 golang.org/x/crypto v0.14.0
google.golang.org/api v0.125.0 google.golang.org/api v0.125.0
google.golang.org/grpc v1.55.0 google.golang.org/grpc v1.55.0
gopkg.in/src-d/go-git.v4 v4.13.1 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/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 h1:Aw4qOp0VsVJrRzW1sJhEy4OY4fRGlFErUD5+93RXL6g=
github.com/shuffle/shuffle-shared v0.4.66/go.mod h1:X613gbo0dT3fnYvXDRwjQZyLC+T49T2nSQOrCV5QMlI= 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/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.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= 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 return err
} }
defer newresp.Body.Close()
respBody, err := ioutil.ReadAll(newresp.Body) respBody, err := ioutil.ReadAll(newresp.Body)
if err != nil { if err != nil {
return err 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)) 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) newresp, err := client.Do(req)
if err != nil { if err != nil {
//log.Printf("Failed request in org sync: %s", err) //log.Printf("Failed request in org sync: %s", err)
return err return err
} }
defer newresp.Body.Close()
respBody, err := ioutil.ReadAll(newresp.Body) respBody, err := ioutil.ReadAll(newresp.Body)
if err != nil { if err != nil {
log.Printf("[ERROR] Failed body read in job sync: %s", err) 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") log.Printf("[DEBUG] Getting organizations for Elasticsearch/Opensearch")
activeOrgs, err := shuffle.GetAllOrgs(ctx) 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 := false
_ = setUsers _ = setUsers
if err != nil { if err != nil {
@@ -3655,7 +3660,6 @@ func runInitEs(ctx context.Context) {
log.Printf("Successfully updated org to have users!") log.Printf("Successfully updated org to have users!")
} }
} }
} }
} }
} }
@@ -3688,6 +3692,10 @@ func runInitEs(ctx context.Context) {
orgId = activeOrgs[0].Id orgId = activeOrgs[0].Id
} }
if len(schedule.Org) == 36 {
orgId = schedule.Org
}
_, _, err := handleExecution(schedule.WorkflowId, shuffle.Workflow{}, request, orgId) _, _, err := handleExecution(schedule.WorkflowId, shuffle.Workflow{}, request, orgId)
if err != nil { if err != nil {
log.Printf("[WARNING] Failed to execute %s: %s", schedule.WorkflowId, err) log.Printf("[WARNING] Failed to execute %s: %s", schedule.WorkflowId, err)
@@ -3697,15 +3705,21 @@ func runInitEs(ctx context.Context) {
for _, schedule := range schedules { for _, schedule := range schedules {
if strings.ToLower(schedule.Environment) == "cloud" { if strings.ToLower(schedule.Environment) == "cloud" {
log.Printf("Skipping cloud schedule") log.Printf("[DEBUG] Skipping cloud schedule")
continue 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: %#v", schedule)
//log.Printf("Schedule time: every %d seconds", schedule.Seconds) //log.Printf("Schedule time: every %d seconds", schedule.Seconds)
jobret, err := newscheduler.Every(schedule.Seconds).Seconds().NotImmediately().Run(job(schedule)) jobret, err := newscheduler.Every(schedule.Seconds).Seconds().NotImmediately().Run(job(schedule))
if err != nil { 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 scheduledJobs[schedule.Id] = jobret
@@ -3977,7 +3991,7 @@ func runInitEs(ctx context.Context) {
r, err := git.Clone(storer, fs, cloneOptions) r, err := git.Clone(storer, fs, cloneOptions)
if err != nil { 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("") dir, err := fs.ReadDir("")
@@ -4014,7 +4028,7 @@ func runInitEs(ctx context.Context) {
} }
_, err = git.Clone(storer, fs, cloneOptions) _, err = git.Clone(storer, fs, cloneOptions)
if err != nil { 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 { } else if err == nil && len(workflowapps) < 10 {
log.Printf("[INFO] Finished git clone. Looking for updates to the repo.") log.Printf("[INFO] Finished git clone. Looking for updates to the repo.")
dir, err := fs.ReadDir("") dir, err := fs.ReadDir("")
@@ -4030,7 +4044,7 @@ func runInitEs(ctx context.Context) {
if os.Getenv("SHUFFLE_HEALTHCHECK_DISABLED") != "true" { 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) 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() { job := func() {
// Prepare a fake http.responsewriter // Prepare a fake http.responsewriter
@@ -4725,7 +4739,7 @@ func initHandlers() {
log.Printf("[DEBUG] Initialized Shuffle database connection. Setting up environment.") log.Printf("[DEBUG] Initialized Shuffle database connection. Setting up environment.")
if elasticConfig == "elasticsearch" { if elasticConfig == "elasticsearch" {
time.Sleep(5 * time.Second) time.Sleep(10 * time.Second)
go runInitEs(ctx) go runInitEs(ctx)
} else { } else {
//go shuffle.runInit(ctx) //go shuffle.runInit(ctx)
@@ -4787,6 +4801,7 @@ func initHandlers() {
// App specific // App specific
// From here down isnt checked for org 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}/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", shuffle.GetActiveCategories).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/apps/categories/run", shuffle.RunCategoryAction).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/apps/categories/run", shuffle.RunCategoryAction).Methods("POST", "OPTIONS")
r.HandleFunc("/api/v1/apps/upload", handleAppZipUpload).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*/ /* Everything below here increases the counters*/
r.HandleFunc("/api/v1/workflows", shuffle.GetWorkflows).Methods("GET", "OPTIONS") 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", 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/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", 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}/executions/{key}/abort", shuffle.AbortExecution).Methods("GET", "OPTIONS")
r.HandleFunc("/api/v1/workflows/{key}/schedule", scheduleWorkflow).Methods("POST", "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/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}/execute", executeWorkflow).Methods("GET", "POST", "OPTIONS")
r.HandleFunc("/api/v1/workflows/{key}/schedule/{schedule}", stopSchedule).Methods("DELETE", "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") 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 // 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) { 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 // Should start a tx for the execution here
workflowExecution, err := shuffle.GetWorkflowExecution(ctx, workflowExecutionId) 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) err = imageCheckBuilder(execInfo.ImageNames)
if err != nil { if err != nil {
@@ -1573,6 +1570,11 @@ func handleExecution(id string, workflow shuffle.Workflow, request *http.Request
workflowExecution.ExecutionOrg = workflow.ExecutingOrg.Id 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 var allEnvs []shuffle.Environment
if len(workflowExecution.ExecutionOrg) > 0 { if len(workflowExecution.ExecutionOrg) > 0 {
//log.Printf("[INFO] Executing ORG: %s", workflowExecution.ExecutionOrg) //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 // FIXME - tmp name based on future companyname-companyId
// This leads to issues with overlaps. Should set limits and such instead // This leads to issues with overlaps. Should set limits and such instead
for _, environment := range execInfo.Environments { 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{ executionRequest := shuffle.ExecutionRequest{
ExecutionId: workflowExecution.ExecutionId, ExecutionId: workflowExecution.ExecutionId,
@@ -3358,11 +3360,21 @@ func executeSingleAction(resp http.ResponseWriter, request *http.Request) {
return return
} }
workflowExecution.Priority = 10
workflowExecution.Priority = 11
environments, err := shuffle.GetEnvironments(ctx, user.ActiveOrg.Id) environments, err := shuffle.GetEnvironments(ctx, user.ActiveOrg.Id)
environment := "Shuffle" environment := "Shuffle"
if len(environments) >= 1 { if len(environments) >= 1 {
// Find default one
environment = environments[0].Name environment = environments[0].Name
for _, env := range environments {
if env.Default {
environment = env.Name
break
}
}
} else { } else {
log.Printf("[ERROR] No environments found for org %s. Exiting", user.ActiveOrg.Id) log.Printf("[ERROR] No environments found for org %s. Exiting", user.ActiveOrg.Id)
resp.WriteHeader(401) resp.WriteHeader(401)
@@ -3370,6 +3382,14 @@ func executeSingleAction(resp http.ResponseWriter, request *http.Request) {
return 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) 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{ executionRequest := shuffle.ExecutionRequest{
@@ -3377,6 +3397,7 @@ func executeSingleAction(resp http.ResponseWriter, request *http.Request) {
WorkflowId: workflowExecution.Workflow.ID, WorkflowId: workflowExecution.Workflow.ID,
Authorization: workflowExecution.Authorization, Authorization: workflowExecution.Authorization,
Environments: []string{environment}, Environments: []string{environment},
Priority: 11,
} }
executionRequest.Priority = workflowExecution.Priority 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) 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.WriteHeader(200)
resp.Write([]byte(returnBytes)) resp.Write([]byte(returnBytes))
} }
@@ -3993,3 +4017,160 @@ func checkWorkflowApp(workflowApp shuffle.WorkflowApp) error {
return nil 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' version: '3'
services: services:
frontend: frontend:
image: ghcr.io/shuffle/shuffle-frontend:latest image: ghcr.io/shuffle/shuffle-frontend:nightly
container_name: shuffle-frontend container_name: shuffle-frontend
hostname: shuffle-frontend hostname: shuffle-frontend
ports: ports:
@@ -15,7 +15,7 @@ services:
depends_on: depends_on:
- backend - backend
backend: backend:
image: ghcr.io/shuffle/shuffle-backend:latest image: ghcr.io/shuffle/shuffle-backend:nightly
container_name: shuffle-backend container_name: shuffle-backend
hostname: ${BACKEND_HOSTNAME} hostname: ${BACKEND_HOSTNAME}
# Here for debugging: # Here for debugging:
@@ -34,7 +34,7 @@ services:
- SHUFFLE_FILE_LOCATION=/shuffle-files - SHUFFLE_FILE_LOCATION=/shuffle-files
restart: unless-stopped restart: unless-stopped
orborus: orborus:
image: ghcr.io/shuffle/shuffle-orborus:latest image: ghcr.io/shuffle/shuffle-orborus:nightly
container_name: shuffle-orborus container_name: shuffle-orborus
hostname: shuffle-orborus hostname: shuffle-orborus
networks: networks:
@@ -42,7 +42,8 @@ services:
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
environment: 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 #- DOCKER_HOST=tcp://docker-socket-proxy:2375
- ENVIRONMENT_NAME=${ENVIRONMENT_NAME} - ENVIRONMENT_NAME=${ENVIRONMENT_NAME}
- BASE_URL=http://${OUTER_HOSTNAME}:5001 - BASE_URL=http://${OUTER_HOSTNAME}:5001
+1
View File
@@ -14,6 +14,7 @@
"@mui/styles": "^5.14.0", "@mui/styles": "^5.14.0",
"@mui/x-data-grid": "^5.17.11", "@mui/x-data-grid": "^5.17.11",
"@mui/x-date-pickers": "^6.11.1", "@mui/x-date-pickers": "^6.11.1",
"@uiw/codemirror-theme-vscode": "^4.21.20",
"@uiw/codemirror-themes": "^4.21.9", "@uiw/codemirror-themes": "^4.21.9",
"@uiw/react-codemirror": "^4.21.9", "@uiw/react-codemirror": "^4.21.9",
"@use-it/interval": "^1.0.0", "@use-it/interval": "^1.0.0",
+22 -11
View File
@@ -47,8 +47,6 @@ const AppSearchButtons = (props) => {
const [newSelectedApp, setNewSelectedApp] = useState(undefined) const [newSelectedApp, setNewSelectedApp] = useState(undefined)
useEffect(() => { useEffect(() => {
console.log("AppSearchButtons: newSelectedApp: " + JSON.stringify(newSelectedApp))
if (newSelectedApp !== undefined && setMissing != undefined) { if (newSelectedApp !== undefined && setMissing != undefined) {
console.log("AppSearchButtons: setMissing is defined!") console.log("AppSearchButtons: setMissing is defined!")
@@ -131,9 +129,12 @@ const AppSearchButtons = (props) => {
//setFrameworkLoaded(true) //setFrameworkLoaded(true)
}) })
} }
const icon = foundApp.large_image const icon = foundApp.large_image
console.log("index:", moreButton) var foundAppImage = AppImage
console.log("totalApps:", totalApps) if (foundApp.name !== undefined && foundApp.name !== null && !foundApp.name.includes(":default")) {
foundAppImage = foundApp.large_image
}
let xsValue = 12; let xsValue = 12;
if (index === totalApps - 1 || index === totalApps - 2 || index === totalApps - 3 || index === totalApps - 4) { if (index === totalApps - 1 || index === totalApps - 2 || index === totalApps - 3 || index === totalApps - 4) {
@@ -142,6 +143,8 @@ const AppSearchButtons = (props) => {
if (index === totalApps - 5) { if (index === totalApps - 5) {
xsValue = 12; xsValue = 12;
} }
// This is silly huh
if (moreButton) { if (moreButton) {
switch (index) { switch (index) {
case totalApps - 1: case totalApps - 1:
@@ -159,7 +162,6 @@ const AppSearchButtons = (props) => {
xsValue = 12; xsValue = 12;
break; break;
default: default:
// Handle other cases if needed
} }
} }
@@ -221,10 +223,11 @@ const AppSearchButtons = (props) => {
> >
<IconButton <IconButton
style={{ zIndex: 12501, position: "absolute", top: 32, right: 16 }} style={{ zIndex: 12501, position: "absolute", top: 32, right: 16 }}
disabled
onClick={(e) => { onClick={(e) => {
e.preventDefault(); e.preventDefault();
setLocalSearchOpen(false) setLocalSearchOpen(false)
setDefaultSearch("")
const submitDeletedApp = { const submitDeletedApp = {
"description": "", "description": "",
"id": "remove", "id": "remove",
@@ -233,15 +236,23 @@ const AppSearchButtons = (props) => {
} }
setFrameworkItem(submitDeletedApp) setFrameworkItem(submitDeletedApp)
setNewSelectedApp({}) setNewSelectedApp({})
if (setDefaultSearch !== undefined) {
setDefaultSearch("")
}
setTimeout(() => { setTimeout(() => {
setDiscoveryData({}) if (setDiscoveryData !== undefined) {
setDiscoveryData({})
}
setFrameworkItem(submitDeletedApp) setFrameworkItem(submitDeletedApp)
//setNewSelectedApp({}) //setNewSelectedApp({})
}, 1000) }, 1000)
//setAppName(discoveryData.cases.name) //setAppName(discoveryData.cases.name)
}} }}
> >
<DeleteIcon style={{ color: "white", height: 15, width: 15, }} /> <DeleteIcon style={{ height: 15, width: 15, }} />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
</div> </div>
@@ -283,12 +294,12 @@ const AppSearchButtons = (props) => {
}} }}
> >
<div style={{ display: "flex", textAlign: "center", justifyContent: "center", alignItems: "center", marginRight: "auto" }}> <div style={{ display: "flex", textAlign: "center", justifyContent: "center", alignItems: "center", marginRight: "auto" }}>
{AppImage === undefined || AppImage === null || AppImage.length === 0 ? {foundAppImage === undefined || foundAppImage === null || foundAppImage.length === 0 ?
<div style={{ width: 40, height: 40, borderRadius: 9999, backgroundColor: "#2F2F2F", textAlign: "center" }}> <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} /> <img style={{ paddingLeft: 11, paddingTop: 11, width: 40, height: 40, flexShrink: 0, }} src={icon} />
</div> </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, }}> <div style={{ marginLeft: 8, }}>
<Typography style={{ <Typography style={{
+30 -29
View File
@@ -269,35 +269,8 @@ const EditWorkflow = (props) => {
/> />
</div> </div>
<div style={{display: "flex", marginTop: 10, }}> <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 ? {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> <InputLabel htmlFor="grouped-select-usecase">Usecases</InputLabel>
<Select <Select
defaultValue="" defaultValue=""
@@ -350,6 +323,33 @@ const EditWorkflow = (props) => {
</Select> </Select>
</FormControl> </FormControl>
: null} : 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> </div>
{showMoreClicked === true ? {showMoreClicked === true ?
@@ -365,7 +365,8 @@ const EditWorkflow = (props) => {
onChange={(e) => { onChange={(e) => {
console.log("Data: ", e.target.value) 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) setInnerWorkflow(innerWorkflow)
}} }}
> >
+36 -36
View File
@@ -216,44 +216,44 @@ const { globalUrl, setNotifications, notifications, isLoggedIn, removeCookie, ho
const NotificationItem = (props) => { const NotificationItem = (props) => {
const {data} = props const {data} = props
var image = ""; var image = "";
var orgName = ""; var orgName = "";
var orgId = ""; var orgId = "";
if (userdata.orgs !== undefined) { if (userdata.orgs !== undefined) {
const foundOrg = userdata.orgs.find((org) => org.id === data["org_id"]); const foundOrg = userdata.orgs.find((org) => org.id === data["org_id"]);
if (foundOrg !== undefined && foundOrg !== null) { if (foundOrg !== undefined && foundOrg !== null) {
//position: "absolute", bottom: 5, right: -5, //position: "absolute", bottom: 5, right: -5,
const imageStyle = { const imageStyle = {
width: imagesize, width: imagesize,
height: imagesize, height: imagesize,
pointerEvents: "none", pointerEvents: "none",
marginLeft: data.creator_org !== undefined && data.creator_org.length > 0 ? 20 : 0, marginLeft: data.creator_org !== undefined && data.creator_org.length > 0 ? 20 : 0,
borderRadius: 10, borderRadius: 10,
border: foundOrg.id === userdata.active_org.id ? `3px solid ${boxColor}` : null, border: foundOrg.id === userdata.active_org.id ? `3px solid ${boxColor}` : null,
cursor: "pointer", cursor: "pointer",
marginRight: 10, marginRight: 10,
}; };
image = image =
foundOrg.image === "" ? ( foundOrg.image === "" ? (
<img <img
alt={foundOrg.name} alt={foundOrg.name}
src={theme.palette.defaultImage} src={theme.palette.defaultImage}
style={imageStyle} style={imageStyle}
/> />
) : ( ) : (
<img <img
alt={foundOrg.name} alt={foundOrg.name}
src={foundOrg.image} src={foundOrg.image}
style={imageStyle} style={imageStyle}
onClick={() => {}} onClick={() => {}}
/> />
); );
orgName = foundOrg.name; orgName = foundOrg.name;
orgId = foundOrg.id; orgId = foundOrg.id;
} }
} }
return ( return (
<Paper style={{backgroundColor: theme.palette.surfaceColor, width: notificationWidth, padding: 25, borderBottom: "1px solid rgba(255,255,255,0.4)"}}> <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 = () => { const clearNotifications = () => {
// Don't really care about the logout // Don't really care about the logout
toast("Clearing notifications")
fetch(`${globalUrl}/api/v1/notifications/clear`, { fetch(`${globalUrl}/api/v1/notifications/clear`, {
credentials: "include", credentials: "include",
method: "GET", method: "GET",
@@ -294,49 +296,26 @@ const Header = (props) => {
borderBottom: "1px solid rgba(255,255,255,0.4)", borderBottom: "1px solid rgba(255,255,255,0.4)",
}} }}
> >
{/*<Typography variant="h6"> {data.reference_url !== undefined && data.reference_url !== null && data.reference_url.length > 0 ?
{new Date(data.updated_at).toISOString()} <Link to={data.reference_url} style={{color: "#f86a3e", textDecoration: "none",}}>
</Typography >*/} <Typography variant="body1">
{data.reference_url !== undefined && {data.title} ({data.amount})
data.reference_url !== null && </Typography >
data.reference_url.length > 0 ? ( </Link>
<Link :
to={data.reference_url} <Typography variant="body1" color="textSecondary">
style={{ color: "#f86a3e", textDecoration: "none" }} {data.title}
> </Typography >
<Typography variant="body1">{data.title}</Typography> }
</Link>
) : (
<Typography variant="body1" color="textSecondary">
{data.title}
</Typography>
)}
{data.image !== undefined && {data.image !== undefined && data.image !== null && data.image.length > 0 ?
data.image !== null && <img alt={data.title} src={data.image} style={{height: 100, width: 100, }} />
data.image.length > 0 ? ( :
<img null
alt={data.title} }
src={data.image} <Typography variant="body2" style={{marginTop: 10, maxHeight: 200, overflowX: "hidden", overflowY: "auto", }}>
style={{ height: 100, width: 100 }} {data.description}
/> </Typography >
) : 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 */}
<div style={{ display: "flex" }}> <div style={{ display: "flex" }}>
{data.read === false ? ( {data.read === false ? (
<Button <Button
@@ -374,7 +353,7 @@ const Header = (props) => {
setAnchorEl(event.currentTarget); setAnchorEl(event.currentTarget);
}} }}
> >
<Badge badgeContent={notifications.length} color="primary"> <Badge badgeContent={notifications.filter((n) => n.read === false).length} color="primary">
<NotificationsIcon <NotificationsIcon
color="secondary" color="secondary"
style={{ height: 30, width: 30 }} style={{ height: 30, width: 30 }}
@@ -413,7 +392,7 @@ const Header = (props) => {
> >
<div style={{ display: "flex", marginBottom: 5 }}> <div style={{ display: "flex", marginBottom: 5 }}>
<Typography variant="body1"> <Typography variant="body1">
Your Notifications ({notifications.length}) Your Notifications ({notifications.filter((data) => !data.read).length})
</Typography> </Typography>
{notifications.length > 1 ? ( {notifications.length > 1 ? (
<Button <Button
@@ -429,12 +408,17 @@ const Header = (props) => {
) : null} ) : null}
</div> </div>
<Typography variant="body2"> <Typography variant="body2">
Notifications are made by Shuffle to help you discover issues or Notifications generated made by Shuffle to help you discover issues or
improvements. improvements. <a href="/docs/organizations#notifications" target="_blank" rel="noopener noreferrer" style={{color: "#f86a3e", textDecoration: "none", }}>
Learn more</a>
</Typography> </Typography>
</Paper> </Paper>
{notifications.map((data, index) => { {notifications.map((data, index) => {
return <NotificationItem data={data} key={index} />; if (data.read) {
return null
}
return <NotificationItem data={data} key={index} />;
})} })}
</Menu> </Menu>
</span> </span>
@@ -626,6 +610,11 @@ const Header = (props) => {
> >
<MeetingRoomIcon style={{ marginRight: 5 }} /> &nbsp;Logout <MeetingRoomIcon style={{ marginRight: 5 }} /> &nbsp;Logout
</MenuItem> </MenuItem>
<Divider style={{marginBottom: 10, }}/>
<Typography variant="body2" color="textSecondary" align="center" style={{marginTop: 5, marginBottom: 5,}}>
Version: 1.3.1
</Typography>
</Menu> </Menu>
</span> </span>
); );
@@ -1352,14 +1341,17 @@ const Header = (props) => {
); );
// <Divider style={{height: "1px", width: "100%", backgroundColor: "rgb(91, 96, 100)"}}/> // <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, }}> <div style={{minHeight: 68, maxHeight: 68, backgroundColor: theme.palette.backgroundColor, }}>
<BrowserView style={{position: "sticky", top: 0, }}> <BrowserView style={{position: "sticky", top: 0, }}>
{loginTextBrowser} {loginTextBrowser}
</BrowserView> </BrowserView>
</div> </div>
: :
*/
return !isMobile ?
<AppBar <AppBar
position="sticky" position="sticky"
color="transparent" color="transparent"
+124 -36
View File
@@ -112,20 +112,19 @@ const AuthenticationOauth2 = (props) => {
authenticationType.client_secret.length > 0 authenticationType.client_secret.length > 0
); );
const [clientId, setClientId] = React.useState( const [clientId, setClientId] = React.useState(defaultConfigSet ? authenticationType.client_id : "");
defaultConfigSet ? authenticationType.client_id : "" const [clientSecret, setClientSecret] = React.useState(defaultConfigSet ? authenticationType.client_secret : "");
);
const [clientSecret, setClientSecret] = React.useState( const [username, setUsername] = React.useState("");
defaultConfigSet ? authenticationType.client_secret : "" const [password, setPassword] = React.useState("");
);
const [oauthUrl, setOauthUrl] = React.useState(""); const [oauthUrl, setOauthUrl] = React.useState("");
const [buttonClicked, setButtonClicked] = React.useState(false); const [buttonClicked, setButtonClicked] = React.useState(false);
const [offlineAccess, setOfflineAccess] = React.useState(true); 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( const [manuallyConfigure, setManuallyConfigure] = React.useState(
defaultConfigSet ? false : true defaultConfigSet ? false : true
); );
@@ -158,6 +157,7 @@ const AuthenticationOauth2 = (props) => {
return null; return null;
} }
const startOauth2Request = (admin_consent) => { const startOauth2Request = (admin_consent) => {
// Admin consent also means to add refresh tokens // Admin consent also means to add refresh tokens
console.log("Inside oauth2 request for app: ", selectedApp.name) 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)) { 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") 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 // Find app.configuration=true fields in the app.paramters
var parsedFields = [{ var parsedFields = [{
"key": "client_id", "key": "client_id",
@@ -316,9 +333,35 @@ const AuthenticationOauth2 = (props) => {
}, },
{ {
"key": "token_uri", "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 = { const appAuthData = {
"label": "OAuth2 for " + selectedApp.name, "label": "OAuth2 for " + selectedApp.name,
"app": { "app": {
@@ -332,14 +375,18 @@ const AuthenticationOauth2 = (props) => {
"reference_workflow": workflowId, "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 // Wait 1 second, then get app auth with update
// //if (getAppAuthentication !== undefined) {
if (getAppAuthentication !== undefined) { // setTimeout(() => {
setTimeout(() => { // getAppAuthentication(true, true, true);
getAppAuthentication(true, true, true); // }, 1000)
}, 1000) //}
}
return return
} }
@@ -369,8 +416,6 @@ const AuthenticationOauth2 = (props) => {
} }
const authentication_url = authenticationType.token_uri; 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 redirectUri = `${window.location.protocol}//${window.location.host}/set_authentication`;
const workflowId = workflow !== undefined ? workflow.id : ""; 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}`; 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; return;
//do {
//} while (
}; };
authenticationOption.app.actions = []; authenticationOption.app.actions = [];
@@ -497,7 +540,6 @@ const AuthenticationOauth2 = (props) => {
} }
const handleSubmitCheck = () => { const handleSubmitCheck = () => {
console.log("NEW AUTH: ", authenticationOption);
if (authenticationOption.label.length === 0) { if (authenticationOption.label.length === 0) {
authenticationOption.label = `Auth for ${selectedApp.name}`; authenticationOption.label = `Auth for ${selectedApp.name}`;
//toast("Label can't be empty") //toast("Label can't be empty")
@@ -564,7 +606,10 @@ const AuthenticationOauth2 = (props) => {
console.log("FIELDS: ", newFields); console.log("FIELDS: ", newFields);
newAuthOption.fields = newFields; newAuthOption.fields = newFields;
setNewAppAuth(newAuthOption);
if (setNewAppAuth !== undefined) {
setNewAppAuth(newAuthOption);
}
//appAuthentication.push(newAuthOption) //appAuthentication.push(newAuthOption)
//setAppAuthentication(appAuthentication) //setAppAuthentication(appAuthentication)
// //
@@ -647,7 +692,7 @@ const AuthenticationOauth2 = (props) => {
)} )}
</Button> </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 return autoAuthButton
} }
@@ -751,10 +796,14 @@ const AuthenticationOauth2 = (props) => {
setOauthUrl(data.value); 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 ( return (
<div key={index} style={{ marginTop: 10 }}> <div key={index} style={{ marginTop: 10 }}>
<LockOpenIcon style={{ marginRight: 10 }} /> <LockOpenIcon style={{ marginRight: 10 }} />
<b>{data.name}</b>
<b>{fieldname}</b>
{data.schema !== undefined && {data.schema !== undefined &&
data.schema !== null && data.schema !== null &&
@@ -767,6 +816,7 @@ const AuthenticationOauth2 = (props) => {
}} }}
defaultValue={"false"} defaultValue={"false"}
fullWidth fullWidth
label={fieldname}
onChange={(e) => { onChange={(e) => {
console.log("Value: ", e.target.value); console.log("Value: ", e.target.value);
authenticationOption.fields[data.name] = e.target.value; authenticationOption.fields[data.name] = e.target.value;
@@ -816,16 +866,11 @@ const AuthenticationOauth2 = (props) => {
: "text" : "text"
} }
color="primary" color="primary"
defaultValue={ defaultValue={defaultValue}
data.value !== undefined && data.value !== null
? data.value
: ""
}
placeholder={data.example} placeholder={data.example}
onChange={(event) => { onChange={(event) => {
authenticationOption.fields[data.name] = authenticationOption.fields[data.name] = event.target.value;
event.target.value; console.log("Setting oauth url: ", event.target.value);
console.log("Setting oauth url");
setOauthUrl(event.target.value); setOauthUrl(event.target.value);
//const [oauthUrl, setOauthUrl] = React.useState("") //const [oauthUrl, setOauthUrl] = React.useState("")
}} }}
@@ -872,8 +917,51 @@ const AuthenticationOauth2 = (props) => {
//authenticationOption.label = event.target.value //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"}}> <div style={{width: "100%", marginTop: 10, display: "flex"}}>
<span> <span>
<Select <Select
@@ -931,7 +1019,7 @@ const AuthenticationOauth2 = (props) => {
borderRadius: theme.palette.borderRadius, borderRadius: theme.palette.borderRadius,
}} }}
disabled={ 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" variant="contained"
fullWidth fullWidth
+21 -4
View File
@@ -170,6 +170,7 @@ const ParsedAction = (props) => {
setEditorData, setEditorData,
setcodedata, setcodedata,
setAiQueryModalOpen,
} = props; } = props;
const classes = useStyles(); const classes = useStyles();
@@ -181,6 +182,8 @@ const ParsedAction = (props) => {
const [fieldCount, setFieldCount] = React.useState(0); const [fieldCount, setFieldCount] = React.useState(0);
const [hiddenDescription, setHiddenDescription] = React.useState(true); const [hiddenDescription, setHiddenDescription] = React.useState(true);
const [autoCompleting, setAutocompleting] = React.useState(false);
useEffect(() => { useEffect(() => {
if (setLastSaved !== undefined) { if (setLastSaved !== undefined) {
@@ -796,7 +799,6 @@ const ParsedAction = (props) => {
selectedActionParameters[count].value = splitparsed[0] selectedActionParameters[count].value = splitparsed[0]
selectedAction.parameters[count].value = splitparsed[0] selectedAction.parameters[count].value = splitparsed[0]
//changeActionParameter({target: {value: splitparsed[1]}},
selectedActionParameters[1].value = splitparsed[1] selectedActionParameters[1].value = splitparsed[1]
selectedAction.parameters[1].value = splitparsed[1] selectedAction.parameters[1].value = splitparsed[1]
forceUpdate = true forceUpdate = true
@@ -2909,17 +2911,26 @@ const ParsedAction = (props) => {
marginLeft: 15, marginLeft: 15,
paddingRight: 0, paddingRight: 0,
}} }}
disabled={autoCompleting}
onClick={() => { onClick={() => {
// aiSubmit(aiMsg, undefined, undefined, newSelectedAction) //if (setAiQueryModalOpen !== undefined) {
aiSubmit("Fill based on previous values", undefined, undefined, selectedAction) // setAiQueryModalOpen(true)
//} else {
aiSubmit("Fill based on previous values", undefined, undefined, selectedAction)
//}
setAutocompleting(true)
}} }}
> >
<Tooltip <Tooltip
color="primary" 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" placement="top"
> >
{autoCompleting ?
<CircularProgress style={{height: 20, width: 20, }} />
:
<AutoFixHighIcon style={{ color: "rgba(255,255,255,0.7)", height: 24, }} /> <AutoFixHighIcon style={{ color: "rgba(255,255,255,0.7)", height: 24, }} />
}
</Tooltip> </Tooltip>
</IconButton> </IconButton>
</div> </div>
@@ -3409,6 +3420,12 @@ const ParsedAction = (props) => {
setSelectedActionEnvironment(env); setSelectedActionEnvironment(env);
selectedAction.environment = env.Name; selectedAction.environment = env.Name;
setSelectedAction(selectedAction); 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={{ style={{
backgroundColor: theme.palette.inputColor, backgroundColor: theme.palette.inputColor,
+199 -3
View File
@@ -1,24 +1,30 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import { toast } from "react-toastify";
import theme from "../theme.jsx"; import theme from "../theme.jsx";
import { import {
Paper, Paper,
Typography, Tooltip,
Typography,
Divider, Divider,
Button, Button,
ButtonGroup,
Grid, Grid,
Card, Card,
Switch, Chip,
Switch,
} from "@mui/material"; } from "@mui/material";
import { useNavigate, Link } from "react-router-dom";
import Priority from "../components/Priority.jsx"; import Priority from "../components/Priority.jsx";
//import { useAlert //import { useAlert
const Priorities = (props) => { 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 [showDismissed, setShowDismissed] = React.useState(false);
const [showRead, setShowRead] = React.useState(false); const [showRead, setShowRead] = React.useState(false);
const [appFramework, setAppFramework] = React.useState({}); const [appFramework, setAppFramework] = React.useState({});
let navigate = useNavigate();
useEffect(() => { useEffect(() => {
getFramework() 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 ( return (
<div style={{maxWidth: 1000, }}> <div style={{maxWidth: 1000, }}>
<h2 style={{ display: "inline" }}>Suggestions</h2> <h2 style={{ display: "inline" }}>Suggestions</h2>
@@ -125,6 +307,20 @@ const Priorities = (props) => {
setShowRead(!showRead); setShowRead(!showRead);
}} }}
/>&nbsp; Show read />&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> </div>
) )
} }
+448 -20
View File
@@ -4,6 +4,7 @@ import {
TextField, TextField,
Link, Link,
Button, Button,
ButtonGroup,
CircularProgress, CircularProgress,
Select, Select,
MenuList, MenuList,
@@ -13,6 +14,7 @@ import {
Autocomplete, Autocomplete,
Tooltip, Tooltip,
Typography, Typography,
IconButton,
} from '@mui/material'; } from '@mui/material';
import { toast } from "react-toastify" import { toast } from "react-toastify"
@@ -21,6 +23,7 @@ import theme from '../theme.jsx';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs' import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'
import Pagination from '@mui/material/Pagination'; import Pagination from '@mui/material/Pagination';
import { triggers as alltriggers } from "../views/AngularWorkflow.jsx"
import { import {
DatePicker, DatePicker,
DateTimePicker, DateTimePicker,
@@ -29,6 +32,9 @@ import {
import { import {
OpenInNew as OpenInNewIcon, OpenInNew as OpenInNewIcon,
PlayArrow as PlayArrowIcon,
Insights as InsightsIcon,
Replay as ReplayIcon,
} from '@mui/icons-material'; } from '@mui/icons-material';
import { DataGrid, GridColDef, GridValueGetterParams } from '@mui/x-data-grid' import { DataGrid, GridColDef, GridValueGetterParams } from '@mui/x-data-grid'
@@ -44,21 +50,18 @@ const RuntimeDebugger = (props) => {
const classes = useStyles(); 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 [workflowId, setWorkflowId] = useState("")
const [status, setStatus] = useState("") const [status, setStatus] = useState("")
const [endTime, setEndTime] = useState("") const [endTime, setEndTime] = useState("")
const [startTime, setStartTime] = useState("") const [startTime, setStartTime] = useState("")
const [workflow, setWorkflow] = useState({}) const [workflow, setWorkflow] = useState({})
const [ignoreOrg, setIgnoreOrg] = useState(false)
const [searchLoading, setSearchLoading] = useState(false) const [searchLoading, setSearchLoading] = useState(false)
const [rowCursor, setCursor] = useState("") const [rowCursor, setCursor] = useState("")
const [rowsPerPage, setRowsPerPage] = useState(10) const [rowsPerPage, setRowsPerPage] = useState(10)
const [resultRows, setResultRows] = useState([]) const [resultRows, setResultRows] = useState([])
const [selectedWorkflowExecutions, setSelectedWorkflowExecutions] = useState([])
const [workflows, setWorkflows] = useState([ const [workflows, setWorkflows] = useState([
{"id": "", "name": "All Workflows",} {"id": "", "name": "All Workflows",}
]) ])
@@ -73,7 +76,7 @@ const RuntimeDebugger = (props) => {
const submitSearch = (workflowId, status, startTime, endTime, cursor, limit) => { const submitSearch = (workflowId, status, startTime, endTime, cursor, limit) => {
setResultRows([]) //setResultRows([])
setSearchLoading(true) setSearchLoading(true)
const fetchData = { const fetchData = {
workflow_id: workflowId, workflow_id: workflowId,
@@ -83,6 +86,7 @@ const RuntimeDebugger = (props) => {
status: status.toUpperCase(), status: status.toUpperCase(),
start_time: startTime, start_time: startTime,
end_time: endTime, end_time: endTime,
ignore_org: ignoreOrg,
} }
fetch(`${globalUrl}/api/v1/workflows/search`, { 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', ' ') //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) const startTimestamp = new Date(data.runs[key].started_at*1000)
data.runs[key].startTimestamp = startTimestamp.toISOString().slice(0, 19).replace('T', ' ') data.runs[key].startTimestamp = startTimestamp.toISOString().slice(0, 19).replace('T', ' ')
const endTimestamp = new Date(data.runs[key].completed_at*1000) const endTimestamp = new Date(data.runs[key].completed_at*1000)
data.runs[key].endTimestamp = endTimestamp.toISOString().slice(0, 19).replace('T', ' ') 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 // Add 20 empty rows to the end of the resultRows array
// This is to make sure that the scrollbar is always visible // This is to make sure that the scrollbar is always visible
setResultRows(data.runs) setResultRows(data.runs)
} else {
toast("No results found. Keeping old runs")
} }
} else { } else {
console.error("Search error: ", data.reason) toast("Failed to search for runs. Please try again.")
} }
}) })
.catch((error) => { .catch((error) => {
setSearchLoading(false) setSearchLoading(false)
console.error("Error:", error); 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[] = [ 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', field: 'status',
headerName: 'Status', headerName: 'Status',
width: 150, width: 100,
renderCell: (params) => ( renderCell: (params) => (
<span style={{cursor: "pointer", }} onClick={() => { <span style={{cursor: "pointer", }} onClick={() => {
setStatus(params.row.status) setStatus(params.row.status)
@@ -208,6 +286,7 @@ const RuntimeDebugger = (props) => {
</span> </span>
), ),
}, },
{ {
field: 'workflow results', field: 'workflow results',
headerName: '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', field: 'id',
headerName: 'Explore', headerName: 'Explore',
width: 65, width: 120,
renderCell: (params) => ( renderCell: (params) => {
<Link href={`/workflows/${params.row.workflow.id}?execution_id=${params.row.id}`} target="_blank" rel="noopener noreferrer"> const parsedResult = params.row.result === null || params.row.result === undefined || params.row.result === "" ? "" : params.row.result
<OpenInNewIcon fontSize="small" />
</Link> 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 // Check if the user is currently focusing a texxtfield or not
// If they are, don't submit the search // If they are, don't submit the search
if (document.activeElement.tagName === "INPUT") { if (document.activeElement.tagName === "INPUT") {
console.log("User is focusing a textfield, not submitting search")
return return
} }
@@ -273,6 +546,29 @@ const RuntimeDebugger = (props) => {
setEndTime(date) 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) => { const handleWorkflowSelectionUpdate = (e, isUserinput) => {
if (e.target.value === undefined || e.target.value === null || e.target.value.id === undefined) { if (e.target.value === undefined || e.target.value === null || e.target.value.id === undefined) {
console.log("Returning as there's no id") 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) 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 ( return (
<div style={{minWidth: 1000, maxWidth: 1000, margin: "auto", }}> <div style={{minWidth: 1150, maxWidth: 1150, margin: "auto", }}>
<h1>Workflow Run Debugger</h1>
<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) => { <form onSubmit={(e) => {
submitSearch(workflowId, status, startTime, endTime, rowCursor, rowsPerPage) submitSearch(workflowId, status, startTime, endTime, rowCursor, rowsPerPage)
}} style={{display: "flex", }}> }} style={{display: "flex", }}>
@@ -432,6 +838,7 @@ const RuntimeDebugger = (props) => {
minWidth: 240, minWidth: 240,
maxWidth: 240, maxWidth: 240,
}} }}
ampm={false}
label="Search from" label="Search from"
format="YYYY-MM-DD HH:mm:ss" format="YYYY-MM-DD HH:mm:ss"
value={startTime} value={startTime}
@@ -445,6 +852,7 @@ const RuntimeDebugger = (props) => {
minWidth: 240, minWidth: 240,
maxWidth: 240, maxWidth: 240,
}} }}
ampm={false}
label="Search until" label="Search until"
format="YYYY-MM-DD HH:mm:ss" format="YYYY-MM-DD HH:mm:ss"
value={endTime} value={endTime}
@@ -454,7 +862,7 @@ const RuntimeDebugger = (props) => {
</LocalizationProvider> </LocalizationProvider>
<Button <Button
variant="contained" variant="outlined"
color="primary" color="primary"
onClick={() => { onClick={() => {
submitSearch(workflowId, status, startTime, endTime, rowCursor, rowsPerPage) submitSearch(workflowId, status, startTime, endTime, rowCursor, rowsPerPage)
@@ -482,6 +890,26 @@ const RuntimeDebugger = (props) => {
onPageChange={(params) => { onPageChange={(params) => {
console.log("params: ", 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>
</div> </div>
+53 -85
View File
@@ -50,6 +50,7 @@ const SearchData = props => {
const [value, setValue] = useState(""); const [value, setValue] = useState("");
const [userTyped, setUserTyped] = useState(false) const [userTyped, setUserTyped] = useState(false)
if (serverside === true) { if (serverside === true) {
return null return null
} }
@@ -59,25 +60,16 @@ const SearchData = props => {
//} //}
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
//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" ) { // if (window.location.pathname === "/docs" || window.location.pathname === "/apps" || window.location.pathname === "/usecases" ) {
// setModalOpen(false) // setModalOpen(false)
// } // }
//useEffect(() => { // useEffect(() => {
// if (searchOpen) { // if (searchOpen) {
// var tarfield = document.getElementById("shuffle_search_field") // var tarfield = document.getElementById("shuffle_search_field")
// tarfield.focus() // tarfield.focus()
// } // }
//}, searchOpen) // }, searchOpen)
const SearchBox = ({ currentRefinement, refine, isSearchStalled, }) => { const SearchBox = ({ currentRefinement, refine, isSearchStalled, }) => {
const keyPressHandler = (e) => { const keyPressHandler = (e) => {
@@ -85,9 +77,11 @@ const SearchData = props => {
if (e.which === 13) { if (e.which === 13) {
// alert("You pressed enter!"); // alert("You pressed enter!");
navigate("/search?q=" + currentRefinement, { state: value, replace: true }); navigate("/search?q=" + currentRefinement, { state: value, replace: true });
setSearchOpen(false)
setModalOpen(false) setSearchOpen(false)
return setModalOpen(false)
return
} }
}; };
/* /*
@@ -104,10 +98,11 @@ const SearchData = props => {
return ( return (
<form id="search_form" noValidate type="searchbox" action="" role="search" onClick={() => { <form id="search_form" noValidate type="searchbox" action="" role="search" onClick={() => {
}}> }}
>
<TextField <TextField
fullWidth 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={{ InputProps={{
style: { style: {
color: "white", color: "white",
@@ -120,7 +115,7 @@ const SearchData = props => {
disableUnderline: true, disableUnderline: true,
startAdornment: ( startAdornment: (
<InputAdornment position="start"> <InputAdornment position="start">
<SearchIcon style={{ marginLeft: 5, marginRight: 10, color: "#f86a3e", }} /> <SearchIcon style={{ marginLeft: 5, marginRight: 10, color: "#f86a3e", }} />
</InputAdornment> </InputAdornment>
), ),
endAdornment: ( endAdornment: (
@@ -188,8 +183,8 @@ const SearchData = props => {
const baseImage = <CodeIcon /> const baseImage = <CodeIcon />
return ( return (
<Card elevation={0} style={{ marginRight: 10, color: "white", zIndex: 1002, backgroundColor: theme.palette.inputColor, width: "100%", left: 75, boxShadows: "none", }}> <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", }}> <Typography variant="h6" style={{ margin: "10px 10px 0px 20px", color:"#FF8444", borderBottom: "1px solid", width: 105 }}>
Workflows Workflows
</Typography> </Typography>
@@ -349,13 +344,13 @@ const SearchData = props => {
const baseImage = <LibraryBooksIcon /> const baseImage = <LibraryBooksIcon />
return ( 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={() => { {/* <IconButton style={{ zIndex: 5000, position: "absolute", right: 14, color: "grey" }} onClick={() => {
setSearchOpen(false) setSearchOpen(false)
}}> }}>
<CloseIcon /> <CloseIcon />
</IconButton> */} </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 Apps
</Typography> </Typography>
@@ -392,7 +387,6 @@ const SearchData = props => {
const name = hit.name === undefined ? const name = hit.name === undefined ?
hit.filename.charAt(0).toUpperCase() + hit.filename.slice(1).replaceAll("_", " ") + " - " + hit.title : hit.filename.charAt(0).toUpperCase() + hit.filename.slice(1).replaceAll("_", " ") + " - " + hit.title :
(hit.name.charAt(0).toUpperCase() + hit.name.slice(1)).replaceAll("_", " ") (hit.name.charAt(0).toUpperCase() + hit.name.slice(1)).replaceAll("_", " ")
var secondaryText = hit.data !== undefined ? hit.data.slice(0, 40) + "..." : "" var secondaryText = hit.data !== undefined ? hit.data.slice(0, 40) + "..." : ""
const avatar = hit.image_url === undefined ? const avatar = hit.image_url === undefined ?
baseImage baseImage
@@ -401,7 +395,6 @@ const SearchData = props => {
src={hit.image_url} src={hit.image_url}
variant="rounded" variant="rounded"
/> />
//console.log(hit) //console.log(hit)
if (hit.categories !== undefined && hit.categories !== null && hit.categories.length > 0) { if (hit.categories !== undefined && hit.categories !== null && hit.categories.length > 0) {
secondaryText = hit.categories.slice(0, 3).map((data, index) => { secondaryText = hit.categories.slice(0, 3).map((data, index) => {
@@ -520,13 +513,13 @@ const SearchData = props => {
//console.log(type, hits.length, hits) //console.log(type, hits.length, hits)
return ( 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={() => { {/* <IconButton style={{ zIndex: 5000, position: "absolute", right: 14, color: "grey" }} onClick={() => {
setSearchOpen(false) setSearchOpen(false)
}}> }}>
<CloseIcon /> <CloseIcon />
</IconButton> */} </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 Documentation
</Typography> </Typography>
{/* {/*
@@ -643,42 +636,14 @@ const SearchData = props => {
</Card> </Card>
) )
} }
const gettingStartData = !searchOpen ? (
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 = (
<Grid <Grid
container container
direction="row" direction="row"
alignItems="center" alignItems="center"
// justify="space-evenly" // 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)" }}> <List style={{ width: "100%", marginLeft: 10, color: "var(--Paragraph-text, #C8C8C8)" }}>
<ListItem> <ListItem>
<ArticleIcon style={{ marginRight: 10, display: "flex", width: 22 }} /> <ArticleIcon style={{ marginRight: 10, display: "flex", width: 22 }} />
@@ -715,7 +680,7 @@ const SearchData = props => {
</div> </div>
</List> </List>
</Grid> </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)", }}> <List style={{ width: "100%", marginLeft: 10, color: "var(--Paragraph-text, #C8C8C8)", }}>
<ListItem> <ListItem>
<ManageSearchIcon style={{ marginRight: 10, display: "flex" }} /> <ManageSearchIcon style={{ marginRight: 10, display: "flex" }} />
@@ -749,29 +714,6 @@ const SearchData = props => {
</div> </div>
</List> </List>
</Grid> </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', }}> <Grid style={{ textAlign: "end", width: "100%", textTransform: 'capitalize', }}>
<Button style={{ textAlign: "center", textTransform: 'capitalize' }} <Button style={{ textAlign: "center", textTransform: 'capitalize' }}
onClick={() => { window.location = "/search"; }} > onClick={() => { window.location = "/search"; }} >
@@ -779,12 +721,38 @@ const SearchData = props => {
</Button> </Button>
</Grid> </Grid>
</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 ( 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={() => { <InstantSearch searchClient={searchClient} indexName="appsearch" onClick={() => {
console.log("CLICKED 1")
}}> }}>
<Configure clickAnalytics /> <Configure clickAnalytics />
<CustomSearchBox onClick={() => { <CustomSearchBox onClick={() => {
+8 -8
View File
@@ -90,8 +90,8 @@ const SearchField = props => {
}, },
}} }}
> >
{isHeader ? <div style={{ display: "flex" }}> {isHeader ? <div style={{ display: "flex"}}>
<DialogTitle style={{ marginTop: 15, marginLeft: 5, color: "var(--Paragraph-text, #C8C8C8)" }} >Search Shuffle</DialogTitle> <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={() => { <Button fullWidth style={{ marginLeft: 470 }} onClick={() => {
setModalOpen(false); setModalOpen(false);
}}><CloseIcon /></Button> }}><CloseIcon /></Button>
@@ -101,12 +101,11 @@ const SearchField = props => {
<SearchBox setModalOpen={setModalOpen} modalOpen={modalOpen} serverside={serverside} userdata={userdata} /> <SearchBox setModalOpen={setModalOpen} modalOpen={modalOpen} serverside={serverside} userdata={userdata} />
</DialogContent> </DialogContent>
<Divider style={{overflow: "hidden"}}/> <Divider style={{overflow: "hidden"}}/>
<span style={{display:"flex", width:"100%"}}> <span style={{display:"flex", width:"100%", height:30}}>
<div style={{display: "flex", marginTop: 6, marginBottom: 6, marginRight: 100, marginLeft: 20, alignItems: "center"}}> {/* <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)"}}> <Typography variant="body2" style={{ fontSize: 16, fontWidth: 550, color: "var(--Paragraph-text, #C8C8C8)"}}>
Discord Discord
</Typography> */}
{/*
<a rel="noopener noreferrer" href="https://discord.com/invite/B2CBzUm" target="_blank" style={{ textDecoration: "none", color: "white" }}> <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, }} /> <img src={"/images/social/discode.svg"} alt="Algolia logo" style={{ height: 22, marginLeft: 5, marginTop: 3, }} />
</a> </a>
@@ -118,8 +117,9 @@ const SearchField = props => {
<a rel="noopener noreferrer" href="https://www.algolia.com/" target="_blank" style={{ textDecoration: "none", color: "white" }}> <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, }} /> <img src={"/images/logo-algolia-nebula-blue-full.svg"} alt="Algolia logo" style={{ height: 17, marginLeft: 5, marginTop: 3, }} />
</a> </a>
*/}
</div> </div>
*/}
</span> </span>
</Dialog> </Dialog>
); );
+186 -138
View File
@@ -22,6 +22,7 @@ import { isMobile } from "react-device-detect"
import { NestedMenuItem } from "mui-nested-menu" import { NestedMenuItem } from "mui-nested-menu"
import { GetParsedPaths, FindJsonPath } from "../views/Apps.jsx"; import { GetParsedPaths, FindJsonPath } from "../views/Apps.jsx";
import { SetJsonDotnotation } from "../views/AngularWorkflow.jsx"; import { SetJsonDotnotation } from "../views/AngularWorkflow.jsx";
import { vscodeDark, vscodeDarkInit } from '@uiw/codemirror-theme-vscode';
import { import {
FullscreenExit as FullscreenExitIcon, FullscreenExit as FullscreenExitIcon,
@@ -82,6 +83,7 @@ const pythonFilters = [
{"name": "Handle JSON", "value": `{% python %}\nimport json\njsondata = json.loads(r"""$nodename""")\n{% endpython %}`, "example": ``}, {"name": "Handle JSON", "value": `{% python %}\nimport json\njsondata = json.loads(r"""$nodename""")\n{% endpython %}`, "example": ``},
] ]
/*
const shuffleTheme = createTheme({ const shuffleTheme = createTheme({
theme: 'dark', theme: 'dark',
settings: { settings: {
@@ -110,6 +112,7 @@ const shuffleTheme = createTheme({
{ tag: t.attributeName, color: '#5c6166' }, { tag: t.attributeName, color: '#5c6166' },
], ],
}); });
*/
const CodeEditor = (props) => { const CodeEditor = (props) => {
const { const {
@@ -547,7 +550,6 @@ const CodeEditor = (props) => {
var code_lines = localcodedata.split('\n') var code_lines = localcodedata.split('\n')
for (var i = 0; i < code_lines.length; i++){ for (var i = 0; i < code_lines.length; i++){
var current_code_line = code_lines[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) 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) var correctVariable = availableVariables.includes(fixedVariable)
if(!correctVariable) { 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"}) 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"}) 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) // console.log(correctVariables)
@@ -660,6 +661,23 @@ const CodeEditor = (props) => {
setlocalcodedata(updatedCode) 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 expectedOutput = (input) => {
//const found = input.match(/[$]{1}([a-zA-Z0-9_-]+\.?){1}([a-zA-Z0-9#_-]+\.?){0,}/g) //const found = input.match(/[$]{1}([a-zA-Z0-9_-]+\.?){1}([a-zA-Z0-9#_-]+\.?){0,}/g)
@@ -674,36 +692,39 @@ const CodeEditor = (props) => {
try { try {
for (var i = 0; i < found.length; i++) { for (var i = 0; i < found.length; i++) {
try { try {
// For found specifically, should replace .#\d with .# with regex
//found[i] = found[i].toLowerCase()
const fixedVariable = fixVariable(found[i]) const fixedVariable = fixVariable(found[i])
//var correctVariable = availableVariables.includes(fixedVariable)
//
var valuefound = false var valuefound = false
for (var j = 0; j < actionlist.length; j++) { for (var j = 0; j < actionlist.length; j++) {
if(fixedVariable.slice(1,).toLowerCase() === actionlist[j].autocomplete.toLowerCase()){ if(fixedVariable.slice(1,).toLowerCase() !== actionlist[j].autocomplete.toLowerCase()){
valuefound = true continue
}
try { valuefound = true
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("[")) { console.log("Here. Checking if we got an example?")
input = input.replace(found[i], JSON.stringify(actionlist[j].example), -1); try {
} else { if (typeof actionlist[j].example === "object") {
input = input.replace(found[i], actionlist[j].example, -1)
} input = input.replace(found[i], JSON.stringify(actionlist[j].example), -1);
} catch (e) {
input = input.replace(found[i], 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 { } catch (e) {
// Couldn't find the correct example value input = input.replace(found[i], actionlist[j].example, -1)
} }
} }
//if (!valuefound) {
// console.log("Couldn't find value "+fixedVariable)
//}
if (!valuefound && availableVariables.includes(fixedVariable)) { if (!valuefound && availableVariables.includes(fixedVariable)) {
var shouldbreak = false var shouldbreak = false
for (var k=0; k < actionlist.length; k++){ for (var k=0; k < actionlist.length; k++){
@@ -714,46 +735,50 @@ const CodeEditor = (props) => {
for (var key in parsedPaths) { for (var key in parsedPaths) {
const fullpath = "$"+actionlist[k].autocomplete.toLowerCase()+parsedPaths[key].autocomplete const fullpath = "$"+actionlist[k].autocomplete.toLowerCase()+parsedPaths[key].autocomplete
if (fullpath === fixedVariable) { if (fullpath !== fixedVariable) {
//if (actionlist[k].example === undefined) { continue
// actionlist[k].example = "TMP" }
//}
var new_input = "" //if (actionlist[k].example === undefined) {
try { // actionlist[k].example = "TMP"
new_input = FindJsonPath(fullpath, actionlist[k].example) //}
} catch (e) {
console.log("ERR IN INPUT: ", e)
}
//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") { console.log("Got output for: ", fullpath, new_input, actionlist[k].example, typeof new_input)
new_input = JSON.stringify(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 { } else {
if (typeof new_input === "string") { console.log("NO TYPE? ", typeof new_input)
new_input = new_input try {
} else { new_input = new_input.toString()
console.log("NO TYPE? ", typeof new_input) } catch (e) {
try { new_input = ""
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) { if (shouldbreak) {
@@ -766,7 +791,7 @@ const CodeEditor = (props) => {
} }
} }
} catch (e) { } 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" aria-labelledby="draggable-code-modal"
disableBackdropClick={true} disableBackdropClick={true}
disableEnforceFocus={true} disableEnforceFocus={true}
//style={{ pointerEvents: "none" }} //style={{ pointerEvents: "none" }}
hideBackdrop={true} hideBackdrop={true}
open={expansionModalOpen} open={expansionModalOpen}
onClose={() => { onClose={() => {
@@ -964,6 +989,7 @@ const CodeEditor = (props) => {
}} }}
> >
<div style={{display: "flex"}}> <div style={{display: "flex"}}>
{/*
<DialogTitle <DialogTitle
id="draggable-dialog-title" id="draggable-dialog-title"
style={{ style={{
@@ -974,59 +1000,9 @@ const CodeEditor = (props) => {
> >
Code Editor Code Editor
</DialogTitle> </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 : { isFileEditor ? null :
<div style={{display: "flex"}}> <div style={{display: "flex", maxHeight: 40, }}>
<Button <Button
id="basic-button" id="basic-button"
aria-haspopup="true" aria-haspopup="true"
@@ -1035,7 +1011,7 @@ const CodeEditor = (props) => {
variant="outlined" variant="outlined"
color="secondary" color="secondary"
style={{ style={{
textTransform: "none", textTransform: "none",
width: 100, width: 100,
}} }}
onClick={(event) => { onClick={(event) => {
@@ -1143,9 +1119,9 @@ const CodeEditor = (props) => {
variant="outlined" variant="outlined"
color="secondary" color="secondary"
style={{ style={{
textTransform: "none", textTransform: "none",
width: 130, width: 130,
marginLeft: 170, marginLeft: 20,
}} }}
onClick={(event) => { onClick={(event) => {
setMenuPosition({ setMenuPosition({
@@ -1393,49 +1369,105 @@ const CodeEditor = (props) => {
</Menu> </Menu>
</div> </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, borderRadius: theme.palette.borderRadius,
position: "relative", position: "relative",
paddingTop: 0,
minHeight: 548,
overflow: "hidden",
}}> }}>
<CodeMirror <CodeMirror
theme={vscodeDark}
value={localcodedata} value={localcodedata}
height={isFileEditor ? 450 : 525} height={isFileEditor ? 450 : 500}
width={isFileEditor ? 650 : 600} width={isFileEditor ? 650 : 600}
style={{ style={{
maxWidth: isFileEditor ? 450 : 500, maxWidth: isFileEditor ? 450 : 600,
maxHeight: 548,
minHeight: 548,
wordBreak: "break-word", wordBreak: "break-word",
marginTop: 0, marginTop: 0,
paddingTop: 0, paddingBottom: 10,
backgroundColor: "rgba(40,40,40,1)", overflow: "hidden",
minHeight: 470,
}} }}
onCursorActivity = {(value) => { onCursorActivity = {(value) => {
// console.log(value.getCursor()) console.log("CURSOR: ", value.getCursor())
setCurrentCharacter(value.getCursor().ch) setCurrentCharacter(value.getCursor().ch)
setCurrentLine(value.getCursor().line) setCurrentLine(value.getCursor().line)
// console.log(value.getCursor().ch, value.getCursor().line) // console.log(value.getCursor().ch, value.getCursor().line)
findIndex(value.getCursor().line, value.getCursor().ch) findIndex(value.getCursor().line, value.getCursor().ch)
highlight_variables(value) highlight_variables(value)
}} }}
onChange={(value, viewUpdate) => { onChange={(value, viewUpdate) => {
console.log("Value: ", value, viewUpdate)
setlocalcodedata(value) setlocalcodedata(value)
expectedOutput(value) expectedOutput(value)
highlight_variables(value)
//if(value.display.input.prevInput.startsWith('$') || value.display.input.prevInput.endsWith('$')){ //if(value.display.input.prevInput.startsWith('$') || value.display.input.prevInput.endsWith('$')){
// setEditorPopupOpen(true) // setEditorPopupOpen(true)
//} //}
}} }}
extensions={[]}//indentWithTab]}
theme={shuffleTheme}
options={{ options={{
styleSelectedText: true,
keyMap: 'sublime',
mode: validation === true ? "json" : "python", mode: validation === true ? "json" : "python",
lineWrapping: linewrap, lineWrapping: linewrap,
theme: vscodeDark,
}} }}
/> />
</span> </div>
{/*editorPopupOpen ? {/*editorPopupOpen ?
<Paper <Paper
@@ -1550,13 +1582,14 @@ const CodeEditor = (props) => {
</div> </div>
</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 : {isFileEditor ? null :
<div> <div>
{isMobile ? null : {isMobile ? null :
<DialogTitle <DialogTitle
style={{ style={{
paddingLeft: 10, paddingLeft: 10,
paddingTop: 0,
display: "flex", display: "flex",
}} }}
> >
@@ -1564,14 +1597,29 @@ const CodeEditor = (props) => {
Expected Output Expected Output
</span> </span>
<IconButton disabled={executing} color="primary" style={{border: `1px solid ${theme.palette.primary.main}`, marginLeft: 100, padding: 8}} variant="contained" onClick={() => { <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">
executeSingleAction(expOutput) <Button
}}> variant="outlined"
<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"> disabled={executing}
{executing ? <CircularProgress style={{height: 18, width: 18, }} /> : <PlayArrowIcon style={{height: 18, width: 18, }} /> } color="primary"
style={{
</Tooltip> border: `1px solid ${theme.palette.primary.main}`,
</IconButton> 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> </DialogTitle>
} }
@@ -1613,8 +1661,8 @@ const CodeEditor = (props) => {
borderRadius: theme.palette.borderRadius, borderRadius: theme.palette.borderRadius,
maxHeight: 500, maxHeight: 500,
minHeight: 500, minHeight: 500,
minWidth: 500, minWidth: 580,
maxWidth: 500, maxWidth: 580,
overflow: "auto", overflow: "auto",
whiteSpace: "pre-wrap", whiteSpace: "pre-wrap",
}} }}
@@ -44,7 +44,6 @@ const WorkflowTemplatePopup = (props) => {
const [missingDestination, setMissingDestination] = React.useState(undefined); const [missingDestination, setMissingDestination] = React.useState(undefined);
useEffect(() => { useEffect(() => {
console.log("Source & Dest check:", missingSource, missingDestination)
}, [missingSource, missingDestination]) }, [missingSource, missingDestination])
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
@@ -392,7 +391,7 @@ const WorkflowTemplatePopup = (props) => {
appFramework={appFramework} appFramework={appFramework}
appType={missingSource.type} appType={missingSource.type}
appImage={missingSource.image} AppImage={missingSource.image}
setMissing={setMissingSource} setMissing={setMissingSource}
/> />
@@ -406,7 +405,7 @@ const WorkflowTemplatePopup = (props) => {
appFramework={appFramework} appFramework={appFramework}
appType={missingDestination.type} appType={missingDestination.type}
appImage={missingDestination.image} AppImage={missingDestination.image}
setMissing={setMissingDestination} setMissing={setMissingDestination}
/> />
@@ -443,7 +442,7 @@ const WorkflowTemplatePopup = (props) => {
if (title.length > maxlength) { if (title.length > maxlength) {
parsedTitle = title.substring(0, maxlength) + "..." parsedTitle = title.substring(0, maxlength) + "..."
} }
console.log("isHomePage", isHomePage)
parsedTitle = parsedTitle.replaceAll("_", " ") parsedTitle = parsedTitle.replaceAll("_", " ")
const parsedDescription = description !== undefined && description !== null ? description.replaceAll("_", " ") : "" const parsedDescription = description !== undefined && description !== null ? description.replaceAll("_", " ") : ""
+2 -3
View File
@@ -4,11 +4,10 @@ const data = [
css: { css: {
label: "data(label)", label: "data(label)",
"text-valign": "center", "text-valign": "center",
"font-family": "font-family": "Segoe UI, Tahoma, Geneva, Verdana, sans-serif, sans-serif",
"Segoe UI, Tahoma, Geneva, Verdana, sans-serif, sans-serif",
"font-weight": "lighter", "font-weight": "lighter",
"margin-right": "10px",
"font-size": "18px", "font-size": "18px",
"margin-right": "10px",
width: "80px", width: "80px",
height: "80px", height: "80px",
color: "white", color: "white",
+200 -140
View File
@@ -16,6 +16,7 @@ import {
OutlinedInput, OutlinedInput,
Checkbox, Checkbox,
Card, Card,
Chip,
Tooltip, Tooltip,
FormControlLabel, FormControlLabel,
Typography, Typography,
@@ -133,7 +134,7 @@ const FileCategoryInput = (props) => {
const Admin = (props) => { const Admin = (props) => {
const { globalUrl, userdata, serverside, checkLogin } = props; const { globalUrl, userdata, serverside, checkLogin, notifications, setNotifications, } = props;
var to_be_copied = ""; var to_be_copied = "";
const classes = useStyles(); 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}` 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) => { const deleteAuthentication = (data) => {
toast("Deleting auth " + data.label); 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 = { const data = {
id: id, id: id,
action: "assign_everywhere", action: parentAction !== undefined && parentAction !== null ? parentAction : "assign_everywhere",
}; };
const url = globalUrl + "/api/v1/apps/authentication/" + id + "/config"; 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) => .then((response) =>
response.json().then((responseJson) => { response.json().then((responseJson) => {
if (responseJson["success"] === false) { if (responseJson["success"] === false) {
toast("Failed overwriting appauth in workflows"); toast("Failed overwriting appauth");
} else { } else {
toast("Successfully updated auth everywhere!"); toast("Successfully updated auth!");
setSelectedUserModalOpen(false); setSelectedUserModalOpen(false);
setTimeout(() => { setTimeout(() => {
getAppAuthentication(); 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 userId = user.id;
const data = { user_id: userId }; const data = { user_id: userId };
console.log(user, userdata) toast("Generating new API key")
var fetchdata = { var fetchdata = {
method: "POST", 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} checkLogin={checkLogin}
setAdminTab={setAdminTab} setAdminTab={setAdminTab}
setCurTab={setCurTab} setCurTab={setCurTab}
notifications={notifications}
setNotifications={setNotifications}
/> />
: adminTab === 3 ? : adminTab === 3 ?
<Billing <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" }} style={{ minWidth: 300, maxWidth: 300, overflow: "hidden" }}
/> />
<ListItemText primary="Actions" /> <ListItemText primary="Actions" />
<ListItemText primary="Delegation" />
</ListItem> </ListItem>
{schedules === undefined || schedules === null {schedules === undefined || schedules === null
? 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" }} style={{ minWidth: 125, maxWidth: 125, overflow: "hidden" }}
/> />
<ListItemText <ListItemText
primary="Created" primary="Edited"
style={{ minWidth: 230, maxWidth: 230, overflow: "hidden" }} style={{ minWidth: 230, maxWidth: 230, overflow: "hidden" }}
/> />
<ListItemText primary="Actions" /> <ListItemText primary="Actions"
style={{ minWidth: 150, maxWidth: 150, }}
/>
<ListItemText primary="Distribution" />
</ListItem> </ListItem>
{authentication === undefined || authentication === null {authentication === undefined || authentication === null
? 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 ( return (
<ListItem key={index} style={{ backgroundColor: bgColor }}> <ListItem key={index} style={{ backgroundColor: bgColor }}>
<ListItemText <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, minWidth: 230,
overflow: "hidden", overflow: "hidden",
}} }}
primary={new Date(data.created * 1000).toISOString()} primary={new Date(data.edited * 1000).toISOString()}
/> />
<ListItemText> <ListItemText>
<IconButton <IconButton
onClick={() => { onClick={() => {
updateAppAuthentication(data); updateAppAuthentication(data);
}} }}
disabled={data.org_id !== selectedOrganization.id ? true : false}
> >
<EditIcon color="primary" /> <EditIcon color="secondary" />
</IconButton> </IconButton>
{data.defined ? ( {data.defined ? (
<Tooltip <Tooltip
color="primary" color="primary"
title="Set in EVERY workflow" title="Set in EVERY workflow in the organization"
placement="top" placement="top"
> >
<IconButton <IconButton
style={{ marginRight: 10 }} style={{ marginRight: 10 }}
disabled={data.defined === false} disabled={data.defined === false || data.org_id !== selectedOrganization.id ? true : false}
onClick={() => { onClick={() => {
editAuthenticationConfig(data.id); editAuthenticationConfig(data.id);
}} }}
> >
<SelectAllIcon <SelectAllIcon
color={data.defined ? "primary" : "secondary"} color={"secondary"}
/> />
</IconButton> </IconButton>
</Tooltip> </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" placement="top"
> >
<IconButton <IconButton
style={{ marginRight: 10 }} style={{}}
onClick={() => {}} onClick={() => {}}
disabled={data.org_id !== selectedOrganization.id ? true : false}
> >
<SelectAllIcon <SelectAllIcon
color={data.defined ? "primary" : "secondary"} color="secondary"
/> />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
)} )}
<IconButton <IconButton
style={{marginLeft: 0, }}
disabled={data.org_id !== selectedOrganization.id ? true : false}
onClick={() => { onClick={() => {
deleteAuthentication(data); deleteAuthentication(data);
}} }}
> >
<DeleteIcon color="primary" /> <DeleteIcon color="secondary" />
</IconButton> </IconButton>
</ListItemText> </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> </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 }} style={{ minWidth: 125, maxWidth: 125 }}
/> />
<ListItemText <ListItemText
primary="Default" primary={"In Queue"}
style={{ minWidth: 125, maxWidth: 125 }} style={{ minWidth: 100, maxWidth: 100 }}
/> />
<ListItemText <ListItemText
primary="Disabled" primary="Default"
style={{ minWidth: 100, maxWidth: 100 }} style={{ minWidth: 150, maxWidth: 150 }}
/>
<ListItemText
primary="Actions"
style={{ minWidth: 200, maxWidth: 200}}
/> />
<ListItemText <ListItemText
primary="Last Edited" primary="Last Edited"
style={{ minWidth: 170, maxWidth: 170 }} style={{ minWidth: 170, maxWidth: 170 }}
/> />
<ListItemText
primary="Actions"
style={{ minWidth: 150, maxWidth: 150 }}
/>
</ListItem> </ListItem>
{environments === undefined || environments === null {environments === undefined || environments === null
? 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"; bgColor = "#1f2023";
} }
// Check if there's a notification for it in userdata.priorities // Check if there's a notification for it in userdata.priorities
var showCPUAlert = false var showCPUAlert = false
var foundIndex = -1 var foundIndex = -1
if (userdata !== undefined && userdata !== null && userdata.priorities !== undefined && userdata.priorities !== null && userdata.priorities.length > 0) { 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) foundIndex = userdata.priorities.findIndex(prio => prio.name.includes("CPU") && prio.active === true)
if (foundIndex >= 0 && userdata.priorities[foundIndex].name.endsWith(environment.Name)) { if (foundIndex >= 0 && userdata.priorities[foundIndex].name.endsWith(environment.Name)) {
showCPUAlert = true 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 ( return (
<span key={index}> <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 <ListItemText
style={{ minWidth: 100, maxWidth: 100 }} style={{ minWidth: 100, maxWidth: 100 }}
primary={ primary={
<Tooltip <Tooltip
title={"Copy Orborus command"} title={"Copy Orborus command"}
style={{}} style={{}}
aria-label={"Copy orborus command"} aria-label={"Copy orborus command"}
> >
<IconButton <IconButton
style={{}} style={{}}
disabled={environment.Type === "cloud"} disabled={environment.Type === "cloud"}
onClick={() => { onClick={() => {
if (environment.Type === "cloud") { if (environment.Type === "cloud") {
toast("No Orborus necessary for environment cloud. Create and use a different environment to run executions on-premises.") toast("No Orborus necessary for environment cloud. Create and use a different environment to run executions on-premises.")
return return
} }
const elementName = "copy_element_shuffle"; if (props.userdata.active_org === undefined || props.userdata.active_org === null) {
const auth = environment.auth === "" ? 'cb5st3d3Z!3X3zaJ*Pc' : environment.auth toast("No active organization yet. Are you logged in?")
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` return
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;
}
navigator.clipboard.writeText(commandData); const elementName = "copy_element_shuffle";
copyText.select(); const auth = environment.auth === "" ? 'cb5st3d3Z!3X3zaJ*Pc' : environment.auth
copyText.setSelectionRange( const newUrl = globalUrl === "https://shuffler.io" ? "https://shuffle-backend-stbuwivzoq-nw.a.run.app" : globalUrl
0,
99999
); /* For mobile devices */
/* Copy the text inside the text field */ 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`
document.execCommand("copy"); 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"); navigator.clipboard.writeText(commandData);
} copyText.select();
}} copyText.setSelectionRange(
> 0,
<FileCopyIcon disabled={environment.Type === "cloud"} style={{ color: environment.Type === "cloud" ? "rgba(255,255,255,0.2)" : "rgba(255,255,255,0.8)" }} /> 99999
</IconButton> ); /* For mobile devices */
</Tooltip>
} /* 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 <ListItemText
@@ -4067,8 +4125,18 @@ If you're interested, please let me know a time that works for you, or set up a
/> />
<ListItemText <ListItemText
style={{ style={{
minWidth: 125, minWidth: 100,
maxWidth: 125, maxWidth: 100,
overflow: "hidden",
marginLeft: 10,
}}
primary={queueSize}
/>
<ListItemText
style={{
minWidth: 140,
maxWidth: 140,
overflow: "hidden", overflow: "hidden",
}} }}
primary={environment.default ? "true" : null} 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)} onClick={() => setDefaultEnvironment(environment)}
color="primary" color="primary"
> >
Make default Set Default
</Button> </Button>
)} )}
</ListItemText> </ListItemText>
<ListItemText <ListItemText
style={{ style={{
minWidth: 100, minWidth: 200,
maxWidth: 100, maxWidth: 200,
overflow: "hidden", overflow: "hidden",
marginLeft: 10, 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 <ListItemText
style={{ style={{
minWidth: 150, 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 : 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> </ListItem>
{showCPUAlert === false ? null : {showCPUAlert === false ? null :
<ListItem key={index+"_cpu"} style={{ backgroundColor: bgColor }}> <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={{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",}}> <div style={{flex: 2, overflow: "hidden",}}>
<Typography variant="body1" > <Typography variant="body1" >
90% CPU the server(s) hosting the Shuffle App Runner (Orborus) was found. 90% CPU the server(s) hosting the Shuffle App Runner (Orborus) was found.
</Typography> </Typography>
<Typography variant="body2" color="textSecondary"> <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>. 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> </Typography>
</div> </div>
<div style={{flex: 1, display: "flex", marginLeft: 30, }}> <div style={{flex: 1, display: "flex", marginLeft: 30, }}>
<Button style={{borderRadius: 25, width: 200, height: 50, marginTop: 8, }} variant="outlined" color="secondary" onClick={() => { <Button style={{borderRadius: 25, width: 200, height: 50, marginTop: 8, }} variant="outlined" color="secondary" onClick={() => {
// dismiss -> get envs // dismiss -> get envs
changeRecommendation(userdata.priorities[foundIndex], "dismiss") changeRecommendation(userdata.priorities[foundIndex], "dismiss")
}}> }}>
Dismiss Dismiss
</Button> </Button>
</div> </div>
</div> </div>
</ListItem> </ListItem>
} }
</span> </span>
); );
})} })}
</List> </List>
+354 -138
View File
@@ -11,7 +11,7 @@ import { useNavigate, Link, useParams } from "react-router-dom";
import { useBeforeunload } from "react-beforeunload"; import { useBeforeunload } from "react-beforeunload";
import ReactJson from "react-json-view"; import ReactJson from "react-json-view";
import { NestedMenuItem } from 'mui-nested-menu'; import { NestedMenuItem } from 'mui-nested-menu';
import ReactMarkdown from "react-markdown"; import Markdown from "react-markdown";
//import { useAlert //import { useAlert
import { ToastContainer, toast } from "react-toastify" import { ToastContainer, toast } from "react-toastify"
import { isMobile } from "react-device-detect" import { isMobile } from "react-device-detect"
@@ -71,6 +71,7 @@ import {
import { import {
Folder as FolderIcon, Folder as FolderIcon,
Insights as InsightsIcon,
LibraryBooks as LibraryBooksIcon, LibraryBooks as LibraryBooksIcon,
OpenInNew as OpenInNewIcon, OpenInNew as OpenInNewIcon,
Undo as UndoIcon, Undo as UndoIcon,
@@ -112,6 +113,7 @@ import {
AutoFixHigh as AutoFixHighIcon, AutoFixHigh as AutoFixHighIcon,
Polyline as PolylineIcon, Polyline as PolylineIcon,
QueryStats as QueryStatsIcon, QueryStats as QueryStatsIcon,
AutoAwesome as AutoAwesomeIcon,
} from "@mui/icons-material"; } from "@mui/icons-material";
@@ -395,9 +397,10 @@ const AngularWorkflow = (defaultprops) => {
var to_be_copied = ""; var to_be_copied = "";
const [firstrequest, setFirstrequest] = React.useState(true); const [firstrequest, setFirstrequest] = React.useState(true);
const [cystyle] = useState(cytoscapestyle); const [cystyle] = useState(cytoscapestyle);
const [cy, setCy] = React.useState(); const [cy, setCy] = React.useState();
const [toolsApp, setToolsApp] = React.useState({}); const [toolsApp, setToolsApp] = React.useState({});
const [currentView, setCurrentView] = React.useState(0); const [currentView, setCurrentView] = React.useState(0);
const [triggerAuthentication, setTriggerAuthentication] = React.useState({}); const [triggerAuthentication, setTriggerAuthentication] = React.useState({});
const [triggerFolders, setTriggerFolders] = React.useState([]); const [triggerFolders, setTriggerFolders] = React.useState([]);
@@ -437,6 +440,7 @@ const AngularWorkflow = (defaultprops) => {
const [appAuthentication, setAppAuthentication] = React.useState(undefined); const [appAuthentication, setAppAuthentication] = React.useState(undefined);
const [variablesModalOpen, setVariablesModalOpen] = React.useState(false); const [variablesModalOpen, setVariablesModalOpen] = React.useState(false);
const [aiQueryModalOpen, setAiQueryModalOpen] = React.useState(false)
const [executionVariablesModalOpen, setExecutionVariablesModalOpen] = const [executionVariablesModalOpen, setExecutionVariablesModalOpen] =
React.useState(false); React.useState(false);
const [authenticationModalOpen, setAuthenticationModalOpen] = React.useState(false); const [authenticationModalOpen, setAuthenticationModalOpen] = React.useState(false);
@@ -500,6 +504,7 @@ const AngularWorkflow = (defaultprops) => {
const [selectedAction, setSelectedAction] = React.useState({}); const [selectedAction, setSelectedAction] = React.useState({});
const [selectedActionEnvironment, setSelectedActionEnvironment] = React.useState({}); const [selectedActionEnvironment, setSelectedActionEnvironment] = React.useState({});
const [streamDisabled, setStreamDisabled] = React.useState(false);
const [executionRequest, setExecutionRequest] = React.useState({}); const [executionRequest, setExecutionRequest] = React.useState({});
const [executionRunning, setExecutionRunning] = React.useState(false); const [executionRunning, setExecutionRunning] = React.useState(false);
@@ -550,10 +555,18 @@ const AngularWorkflow = (defaultprops) => {
props.userdata.active_org !== undefined props.userdata.active_org !== undefined
? props.userdata.active_org.cloud_sync === true ? props.userdata.active_org.cloud_sync === true
: false; : false;
const isCloud = const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
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(() => { useEffect(() => {
console.log("In useeffect for workflow: ", workflow) console.log("In useeffect for workflow: ", workflow)
@@ -582,17 +595,22 @@ const AngularWorkflow = (defaultprops) => {
const [elements, setElements] = useState([]); const [elements, setElements] = useState([]);
const [loopRunning, setLoopRunning] = useState(false) const [loopRunning, setLoopRunning] = useState(false)
var loopRunning2 = loopRunning
const stop = () => { const stop = () => {
setLoopRunning(false) setLoopRunning(false)
loopRunning2 = false
} }
const start = () => { const start = () => {
setLoopRunning(true) setLoopRunning(true)
loopRunning2 = true
} }
useEffect(() => { useEffect(() => {
//console.log("In useeffect for loopRunning: ", loopRunning) // Current variable + future state controlled
if (loopRunning) { // 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(() => { const intervalId = setInterval(() => {
if (!loopRunning) { if (!loopRunning) {
clearInterval(intervalId); clearInterval(intervalId);
@@ -634,7 +652,11 @@ const AngularWorkflow = (defaultprops) => {
if (responseJson.success === true) { if (responseJson.success === true) {
if (responseJson.reason !== undefined && responseJson.reason !== undefined && responseJson.reason.length > 0) { if (responseJson.reason !== undefined && responseJson.reason !== undefined && responseJson.reason.length > 0) {
if (!responseJson.reason.includes("404: Not Found") && responseJson.reason.length > 25) { 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) setSelectedApp(selectedApp)
setUpdate(Math.random()) setUpdate(Math.random())
} }
@@ -913,7 +935,7 @@ const AngularWorkflow = (defaultprops) => {
}); });
}; };
const setNewAppAuth = (appAuthData) => { const setNewAppAuth = (appAuthData, refresh) => {
fetch(globalUrl + "/api/v1/apps/authentication", { fetch(globalUrl + "/api/v1/apps/authentication", {
method: "PUT", method: "PUT",
headers: { headers: {
@@ -932,9 +954,14 @@ const AngularWorkflow = (defaultprops) => {
}) })
.then((responseJson) => { .then((responseJson) => {
if (!responseJson.success) { if (!responseJson.success) {
toast("Failed to set app auth: " + responseJson.reason); toast("Error: " + responseJson.reason);
} else { } else {
getAppAuthentication(true, false); if (refresh === true) {
getAppAuthentication(true, true, true);
} else {
getAppAuthentication(true, false);
}
setAuthenticationModalOpen(false); setAuthenticationModalOpen(false);
// Needs a refresh with the new authentication.. // Needs a refresh with the new authentication..
@@ -964,20 +991,20 @@ const AngularWorkflow = (defaultprops) => {
return response.json(); return response.json();
}) })
.then((responseJson) => { .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 // - means it's opposite
const newkeys = sortByKey(responseJson.executions, "-started_at"); const newkeys = sortByKey(responseJson.executions, "-started_at");
setWorkflowExecutions(newkeys); setWorkflowExecutions(newkeys);
const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search;
var tmpView = new URLSearchParams(cursearch).get("execution_id"); 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)) { if (execution_id !== undefined && execution_id !== null && execution_id.length > 0 && (tmpView === undefined || tmpView === null || tmpView.length === 0)) {
tmpView = execution_id; tmpView = execution_id;
} }
console.log("TMPVIEW: ", tmpView); console.log("EXECUTION ID: ", tmpView)
// Compare with currently selected item // Compare with currently selected item
if (tmpView !== undefined && tmpView !== null && tmpView.length > 0) { if (tmpView !== undefined && tmpView !== null && tmpView.length > 0) {
@@ -1000,16 +1027,15 @@ const AngularWorkflow = (defaultprops) => {
} }
setExecutionModalView(1); setExecutionModalView(1);
start();
setExecutionRequest({ setExecutionRequest({
execution_id: execution.execution_id, execution_id: execution.execution_id,
authorization: execution.authorization, authorization: execution.authorization,
}); });
const newitem = removeParam("execution_id", cursearch); start();
navigate(curpath + newitem)
//props.history.push(curpath + newitem); //const newitem = removeParam("execution_id", cursearch);
//navigate(curpath + newitem)
} else { } else {
console.log("Couldn't find execution for execution ID. Retrying as user to get ", tmpView) 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, execution_id: tmpView,
//authorization: data.authorization, //authorization: data.authorization,
} }
setExecutionRunning(true);
setExecutionModalView(1); setExecutionModalView(1);
setExecutionRequest(cur_execution); setExecutionRequest(cur_execution);
start(); start();
const newitem = removeParam("execution_id", cursearch); //const newitem = removeParam("execution_id", cursearch);
navigate(curpath + newitem) //navigate(curpath + newitem)
//setTimeout(() => {
setTimeout(() => { // stop()
stop() //}, 5000);
}, 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) => { .catch((error) => {
//toast(error.toString()); //toast(error.toString());
@@ -1051,8 +1091,14 @@ const AngularWorkflow = (defaultprops) => {
}) })
.then((response) => { .then((response) => {
if (response.status !== 200) { if (response.status !== 200) {
console.log("Status not 200 for stream results :O!");
stop(); 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(); return response.json();
@@ -1231,10 +1277,11 @@ const AngularWorkflow = (defaultprops) => {
// Controls the colors and direction of execution results. // Controls the colors and direction of execution results.
// Style is in defaultCytoscapeStyle.js // Style is in defaultCytoscapeStyle.js
const handleUpdateResults = (responseJson, executionRequest) => { const handleUpdateResults = (responseJson, executionRequest) => {
if (responseJson === undefined || responseJson === null || responseJson.success === false) { if (responseJson === undefined || responseJson === null || responseJson.success === false) {
return stop()
} return
//console.log(responseJson) }
//console.log(responseJson)
// Loop nodes and find results // Loop nodes and find results
// Update on every interval? idk // Update on every interval? idk
@@ -1247,7 +1294,8 @@ const AngularWorkflow = (defaultprops) => {
//console.log("Updating data!") //console.log("Updating data!")
setExecutionData(responseJson) setExecutionData(responseJson)
} else { } 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() stop()
} }
@@ -1305,14 +1353,31 @@ const AngularWorkflow = (defaultprops) => {
}) })
}; };
var streamDisabled2 = false
const sendStreamRequest = (body) => { const sendStreamRequest = (body) => {
//console.log("Stream not activated yet.") //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 // Session may be important here huh
body.user_id = userdata.id 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", method: "POST",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -1324,6 +1389,9 @@ const AngularWorkflow = (defaultprops) => {
.then((response) => { .then((response) => {
setSavingState(0); setSavingState(0);
if (response.status !== 200) { if (response.status !== 200) {
setStreamDisabled(true)
streamDisabled2 = true
//console.log("Status not 200 for stream :O!"); //console.log("Status not 200 for stream :O!");
} }
@@ -1334,7 +1402,8 @@ const AngularWorkflow = (defaultprops) => {
}) })
.catch((error) => { .catch((error) => {
console.log("Stream send error: ", error.toString()) console.log("Stream send error: ", error.toString())
//toast(error.toString()); setStreamDisabled(true)
streamDisabled2 = true
}) })
} }
@@ -1668,7 +1737,7 @@ const AngularWorkflow = (defaultprops) => {
if (hasSaved === false) { if (hasSaved === false) {
setExecutionRequestStarted(true); setExecutionRequestStarted(true);
saveWorkflow(workflow, executionArgument, startNode); 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; return;
} }
@@ -1869,6 +1938,7 @@ const AngularWorkflow = (defaultprops) => {
setSelectedAction(selectedAction); setSelectedAction(selectedAction);
setWorkflow(workflow); setWorkflow(workflow);
saveWorkflow(workflow); saveWorkflow(workflow);
toast("Added and updated authentication!"); toast("Added and updated authentication!");
shouldClose = true shouldClose = true
} else { } else {
@@ -2081,7 +2151,9 @@ const AngularWorkflow = (defaultprops) => {
} }
const onChunkedResponseError = (err) => { const onChunkedResponseError = (err) => {
console.error(err) if (streamDisabled) {
return
}
} }
@@ -2502,6 +2574,17 @@ const AngularWorkflow = (defaultprops) => {
try { try {
var chunkJson = JSON.parse(chunk) 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 !== undefined && chunkJson.item !== null && chunkJson.item !== "") {
if (chunkJson.item === "node") { if (chunkJson.item === "node") {
if (chunkJson.type === "move") { if (chunkJson.type === "move") {
@@ -2526,6 +2609,13 @@ const AngularWorkflow = (defaultprops) => {
} }
} catch (e) { } catch (e) {
console.log("Chunk JSON error: ", e) console.log("Chunk JSON error: ", e)
if (!streamDisabled) {
setStreamDisabled(true)
streamDisabled2 = true
}
return
} }
//data.push(chunk) //data.push(chunk)
@@ -2567,14 +2657,30 @@ const AngularWorkflow = (defaultprops) => {
} }
const startWorkflowStream = async (workflowId) => { 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) { while (true) {
if (streamDisabled === true || streamDisabled2 === true) {
break
}
// Wait 1 second before next request just in case of timeouts // Wait 1 second before next request just in case of timeouts
await new Promise(r => setTimeout(r, 1000)); await new Promise(r => setTimeout(r, 1000));
await fetchWithTimeout(`${globalUrl}/api/v1/workflows/${workflowId}/stream`, { await fetchWithTimeout(url, {
method: "GET", method: "GET",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -2970,7 +3076,7 @@ const AngularWorkflow = (defaultprops) => {
sendStreamRequest({ sendStreamRequest({
"item": "node", "item": "node",
"type": "unselect", "type": "unselect",
"userid": userdata.id, "id": workflow.id,
}) })
//}, 150) //}, 150)
}; };
@@ -3782,6 +3888,8 @@ const AngularWorkflow = (defaultprops) => {
//} //}
curaction.app_id = curapp.id curaction.app_id = curapp.id
console.log("CURAPP: ", curapp.authentication)
setAuthenticationType( setAuthenticationType(
curapp.authentication.type === "oauth2" && curapp.authentication.redirect_uri !== undefined && curapp.authentication.redirect_uri !== null ? { curapp.authentication.type === "oauth2" && curapp.authentication.redirect_uri !== undefined && curapp.authentication.redirect_uri !== null ? {
type: "oauth2", type: "oauth2",
@@ -3791,6 +3899,7 @@ const AngularWorkflow = (defaultprops) => {
scope: curapp.authentication.scope, scope: curapp.authentication.scope,
client_id: curapp.authentication.client_id, client_id: curapp.authentication.client_id,
client_secret: curapp.authentication.client_secret, client_secret: curapp.authentication.client_secret,
grant_type: curapp.authentication.grant_type,
} : { } : {
type: "", type: "",
} }
@@ -4017,7 +4126,6 @@ const AngularWorkflow = (defaultprops) => {
"item": "node", "item": "node",
"type": "select", "type": "select",
"id": data.id, "id": data.id,
"userid": userdata.id,
"location": { "location": {
"x": event.target.position("x"), "x": event.target.position("x"),
"y": event.target.position("y"), "y": event.target.position("y"),
@@ -5931,10 +6039,10 @@ const AngularWorkflow = (defaultprops) => {
const xParsed = destinationnodePosition.x - sourcenodePosition.x const xParsed = destinationnodePosition.x - sourcenodePosition.x
const yParsed = destinationnodePosition.y - sourcenodePosition.y const yParsed = destinationnodePosition.y - sourcenodePosition.y
const z = Math.sqrt(xParsed * xParsed + yParsed * yParsed); const z = Math.sqrt(xParsed * xParsed + yParsed * yParsed)
const costheta = xParsed / z; const costheta = xParsed / z
const alpha = 0.25; const alpha = 0.3
var controlPointDistance = [-alpha * yParsed * costheta, alpha * yParsed * costheta]; var controlPointDistance = [-alpha * yParsed * costheta, alpha * yParsed * costheta]
var controlPointWeight = [alpha, 1 - alpha] var controlPointWeight = [alpha, 1 - alpha]
//'control-point-weight': ['0.33', '0.66'], //'control-point-weight': ['0.33', '0.66'],
@@ -6097,8 +6205,18 @@ const AngularWorkflow = (defaultprops) => {
const foundtriggers = inputworkflow.triggers.map((trigger) => { const foundtriggers = inputworkflow.triggers.map((trigger) => {
const node = {}; const node = {};
node.position = trigger.position; 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.id = trigger["id"]; node.data.id = trigger["id"];
node.data.type = "TRIGGER"; node.data.type = "TRIGGER";
@@ -6255,7 +6373,6 @@ const AngularWorkflow = (defaultprops) => {
} }
insertedNodes = insertedNodes.concat(newedges); insertedNodes = insertedNodes.concat(newedges);
setWorkflow(inputworkflow); setWorkflow(inputworkflow);
// Reset view for cytoscape // Reset view for cytoscape
@@ -6265,6 +6382,8 @@ const AngularWorkflow = (defaultprops) => {
} else { } else {
setElements(insertedNodes); setElements(insertedNodes);
} }
console.log("Setupgraph done 2!")
}; };
const removeNode = (nodeId) => { const removeNode = (nodeId) => {
@@ -6339,11 +6458,14 @@ const AngularWorkflow = (defaultprops) => {
sendStreamRequest({ sendStreamRequest({
"item": "workflow", "item": "workflow",
"type": "enter", "type": "enter",
"userid": userdata.id, "id": workflow.id,
}) })
} }
const fetchRecommendations = (inputWorkflow) => { const fetchRecommendations = (inputWorkflow) => {
console.log("Disabled recommendations as they were too inaccurate")
return
const parsedWorkflow = JSON.parse(JSON.stringify(inputWorkflow)) const parsedWorkflow = JSON.parse(JSON.stringify(inputWorkflow))
fetch(globalUrl + "/api/v1/workflows/recommend", { fetch(globalUrl + "/api/v1/workflows/recommend", {
@@ -6442,6 +6564,11 @@ const AngularWorkflow = (defaultprops) => {
return response.json(); return response.json();
}) })
.then((responseJson) => { .then((responseJson) => {
if (responseJson === null) {
console.log("No revisions found")
return
}
if (responseJson.success === false) { if (responseJson.success === false) {
console.log("Error getting workflow revisions: ", responseJson) console.log("Error getting workflow revisions: ", responseJson)
return return
@@ -6495,13 +6622,16 @@ const AngularWorkflow = (defaultprops) => {
} }
// App length necessary cus of cy initialization // 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); setGraphSetup(true);
setupGraph(workflow); setupGraph(workflow);
console.log("In graph setup") console.log("In graph setup")
// 2nd load - configures cytoscape // 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!") console.log("In POST graph setup!")
@@ -6567,6 +6697,7 @@ const AngularWorkflow = (defaultprops) => {
} }
// preview: true, // preview: true,
console.log("In POST graph setup 2")
cy.fit(null, 200); cy.fit(null, 200);
cy.on("boxselect", "node", (e) => { cy.on("boxselect", "node", (e) => {
@@ -6615,6 +6746,7 @@ const AngularWorkflow = (defaultprops) => {
document.title = "Workflow - " + workflow.name; document.title = "Workflow - " + workflow.name;
console.log("In POST graph setup 3")
startWorkflowStream(props.match.params.key); startWorkflowStream(props.match.params.key);
registerKeys(); registerKeys();
@@ -7507,14 +7639,30 @@ const AngularWorkflow = (defaultprops) => {
description = app.actions[actionIndex].description description = app.actions[actionIndex].description
} }
const parsedEnvironments = var parsedEnvironments =
environments === null || environments === [] environments === null || environments === []
? "cloud" ? "cloud"
: environments[defaultEnvironmentIndex] === undefined : environments[defaultEnvironmentIndex] === undefined
? "cloud" ? "cloud"
: environments[defaultEnvironmentIndex].Name; : 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 = { const newAppData = {
name: app.actions[actionIndex].name, name: app.actions[actionIndex].name,
label: actionLabel, label: actionLabel,
@@ -8130,6 +8278,25 @@ const AngularWorkflow = (defaultprops) => {
</div> </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> </div>
) : apps.length > 0 ? ( ) : apps.length > 0 ? (
<div <div
@@ -8139,7 +8306,7 @@ const AngularWorkflow = (defaultprops) => {
}} }}
> >
<Typography variant="body1" color="textSecondary"> <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> </Typography>
<InstantSearch searchClient={searchClient} indexName="appsearch" onClick={() => { <InstantSearch searchClient={searchClient} indexName="appsearch" onClick={() => {
console.log("CLICKED") console.log("CLICKED")
@@ -8355,7 +8522,6 @@ const AngularWorkflow = (defaultprops) => {
} }
} }
console.log("NEW ACTION: ", newSelectedAction);
setSelectedAction(newSelectedAction); setSelectedAction(newSelectedAction);
setUpdate(Math.random()); setUpdate(Math.random());
@@ -8992,6 +9158,33 @@ const AngularWorkflow = (defaultprops) => {
backgroundColor: theme.palette.inputColor, 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 = ( const conditionsModal = (
<Dialog <Dialog
PaperComponent={PaperComponent} PaperComponent={PaperComponent}
@@ -10958,15 +11151,7 @@ const AngularWorkflow = (defaultprops) => {
style={{ marginTop: 10 }} style={{ marginTop: 10 }}
label={<div style={{ color: "white" }}>Wait for results</div>} label={<div style={{ color: "white" }}>Wait for results</div>}
/> />
<Divider <div style={{ flex: "6", marginTop: 10, }}>
style={{
marginTop: "20px",
height: "1px",
width: "100%",
backgroundColor: "rgb(91, 96, 100)",
}}
/>
<div style={{ flex: "6", marginTop: "20px" }}>
<div> <div>
<div style={{ display: "flex" }}> <div style={{ display: "flex" }}>
<div <div
@@ -11651,7 +11836,7 @@ const AngularWorkflow = (defaultprops) => {
}} }}
onChange={(event, newValue) => { onChange={(event, newValue) => {
// Workaround with event lol // Workaround with event lol
console.log(event, newValue) console.log("CHANGE: ", event, newValue)
if (newValue !== undefined && newValue !== null) { if (newValue !== undefined && newValue !== null) {
var parsedvalue = JSON.parse(JSON.stringify(newValue)) var parsedvalue = JSON.parse(JSON.stringify(newValue))
parsedvalue.actions = [] parsedvalue.actions = []
@@ -11678,6 +11863,7 @@ const AngularWorkflow = (defaultprops) => {
> >
<MenuItem <MenuItem
onClick={() => { onClick={() => {
console.log("CLICK: ", app)
const newValue = app const newValue = app
if (newValue !== undefined && newValue !== null) { if (newValue !== undefined && newValue !== null) {
@@ -11881,7 +12067,7 @@ const AngularWorkflow = (defaultprops) => {
workflow.triggers[selectedTriggerIndex].parameters[0].value workflow.triggers[selectedTriggerIndex].parameters[0].value
} }
color="primary" color="primary"
placeholder="defaultValue" placeholder="10"
onBlur={(e) => { onBlur={(e) => {
setTriggerCronWrapper(e.target.value); setTriggerCronWrapper(e.target.value);
}} }}
@@ -12349,7 +12535,7 @@ const AngularWorkflow = (defaultprops) => {
// email,sms,app ... // email,sms,app ...
workflow.triggers[selectedTriggerIndex].parameters[2] = { workflow.triggers[selectedTriggerIndex].parameters[2] = {
name: "type", name: "type",
value: "email", value: "subflow",
}; };
workflow.triggers[selectedTriggerIndex].parameters[3] = { workflow.triggers[selectedTriggerIndex].parameters[3] = {
@@ -12430,16 +12616,7 @@ const AngularWorkflow = (defaultprops) => {
/> />
</div> </div>
*/} */}
<Divider <div style={{ flex: "6", marginTop: 10, }}>
style={{
marginTop: "20px",
height: "1px",
width: "100%",
backgroundColor: "rgb(91, 96, 100)",
}}
/>
<div style={{ flex: "6", marginTop: "20px" }}>
<b>Parameters</b>
<div <div
style={{ style={{
marginTop: "20px", marginTop: "20px",
@@ -12447,17 +12624,11 @@ const AngularWorkflow = (defaultprops) => {
display: "flex", display: "flex",
}} }}
> >
<div
style={{
width: "17px",
height: "17px",
borderRadius: 17 / 2,
backgroundColor: "#f85a3e",
marginRight: "10px",
}}
/>
<div style={{ flex: "10" }}> <div style={{ flex: "10" }}>
<b>Information</b> <b>Information</b>
<Typography variant="body2" color="textSecondary">
The information you want to show the user. Supports variables.
</Typography>
</div> </div>
</div> </div>
<TextField <TextField
@@ -12488,17 +12659,11 @@ const AngularWorkflow = (defaultprops) => {
display: "flex", display: "flex",
}} }}
> >
<div
style={{
width: "17px",
height: "17px",
borderRadius: 17 / 2,
backgroundColor: "#f85a3e",
marginRight: "10px",
}}
/>
<div style={{ flex: "10" }}> <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>
</div> </div>
<FormGroup <FormGroup
@@ -12508,6 +12673,20 @@ const AngularWorkflow = (defaultprops) => {
<FormControlLabel <FormControlLabel
control={ control={
<Checkbox <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={ checked={
workflow.triggers[selectedTriggerIndex].parameters[2] !== undefined && workflow.triggers[selectedTriggerIndex].parameters[2].value.includes("email") workflow.triggers[selectedTriggerIndex].parameters[2] !== undefined && workflow.triggers[selectedTriggerIndex].parameters[2].value.includes("email")
} }
@@ -12523,6 +12702,7 @@ const AngularWorkflow = (defaultprops) => {
<FormControlLabel <FormControlLabel
control={ control={
<Checkbox <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} 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={() => { onChange={() => {
setTriggerOptionsWrapper("sms"); setTriggerOptionsWrapper("sms");
@@ -12534,21 +12714,8 @@ const AngularWorkflow = (defaultprops) => {
} }
label={<div style={{ color: "white" }}>SMS</div>} 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> </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={{ }}> <div style={{ }}>
{workflows === undefined || {workflows === undefined ||
workflows === null || workflows === null ||
@@ -12654,7 +12821,7 @@ const AngularWorkflow = (defaultprops) => {
}, },
}} }}
fullWidth fullWidth
label="Email" label="Email"
color="primary" color="primary"
required required
placeholder={"mail1@company.com,mail2@company.com"} placeholder={"mail1@company.com,mail2@company.com"}
@@ -13250,10 +13417,6 @@ const AngularWorkflow = (defaultprops) => {
"user": "Anonymous", "user": "Anonymous",
"user_id": "user_id", "user_id": "user_id",
"color": "blue", "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 ? const showErrors = !isMobile && !workflow.public && workflow.errors !== undefined && workflow.errors !== null && workflow.errors.length > 0 ?
<div <div
style={{ style={{
border: "1px solid rgba(255,255,255,0.3)", border: "1px solid rgba(255,255,255,0.1)",
position: "absolute", position: "absolute",
bottom: 130, bottom: 130,
left: leftBarSize+20, left: leftBarSize+20,
color: "white", color: "white",
padding: 5, padding: 10,
borderRadius: theme.palette.borderRadius, borderRadius: theme.palette.borderRadius,
}} }}
> >
<Typography variant="body22"> <Typography variant="body22">
<WarningIcon style={{color: "yellow", marginRight: 5, height: 15, width: 15, }} /> <WarningIcon style={{marginRight: 5, height: 15, width: 15, }} />
<b>{workflow.errors.length} Potential Workflow Issue{workflow.errors.length > 1 ? "s" : ""}</b> <b>{workflow.errors.length} Workflow Issue{workflow.errors.length > 1 ? "s" : ""}</b>
</Typography> </Typography>
<Typography <Typography
variant="body2" variant="body2"
@@ -13766,6 +13929,7 @@ const AngularWorkflow = (defaultprops) => {
expansionModalOpen={codeEditorModalOpen} expansionModalOpen={codeEditorModalOpen}
setExpansionModalOpen={setCodeEditorModalOpen} setExpansionModalOpen={setCodeEditorModalOpen}
setEditorData={setEditorData} setEditorData={setEditorData}
setAiQueryModalOpen={setAiQueryModalOpen}
/> />
} else if (Object.getOwnPropertyNames(selectedComment).length > 0) { } else if (Object.getOwnPropertyNames(selectedComment).length > 0) {
@@ -14313,6 +14477,13 @@ const AngularWorkflow = (defaultprops) => {
style={{ width: size, height: size }} 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 ( if (
@@ -14917,7 +15088,10 @@ const AngularWorkflow = (defaultprops) => {
marginTop: "auto", marginTop: "auto",
marginBottom: "auto", marginBottom: "auto",
}} }}
onClick={() => { }} onClick={() => {
setExecutionRunning(false);
stop()
}}
> >
<ArrowBackIcon style={{ color: "rgba(255,255,255,0.5)" }} /> <ArrowBackIcon style={{ color: "rgba(255,255,255,0.5)" }} />
</IconButton> </IconButton>
@@ -14927,6 +15101,8 @@ const AngularWorkflow = (defaultprops) => {
const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search;
const newitem = removeParam("execution_id", cursearch); const newitem = removeParam("execution_id", cursearch);
navigate(curpath + newitem) navigate(curpath + newitem)
setExecutionRunning(false);
stop()
}} }}
> >
See more runs See more runs
@@ -14991,7 +15167,40 @@ const AngularWorkflow = (defaultprops) => {
</span> </span>
</Tooltip> </Tooltip>
) : null} ) : 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> </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 !== undefined &&
executionData.status.length > 0 ? ( executionData.status.length > 0 ? (
<div style={{ display: "flex", marginLeft: 10, }}> <div style={{ display: "flex", marginLeft: 10, }}>
@@ -15382,19 +15591,23 @@ const AngularWorkflow = (defaultprops) => {
width: 30, width: 30,
}} }}
onClick={() => { onClick={() => {
const oldstartnode = cy.getElementById(data.action.id); if (cy !== undefined) {
//console.log("FOUND NODe: ", oldstartnode) const oldstartnode = cy.getElementById(data.action.id);
if (oldstartnode !== undefined && oldstartnode !== null) { //console.log("FOUND NODe: ", oldstartnode)
const foundname = oldstartnode.data("label") if (oldstartnode !== undefined && oldstartnode !== null) {
if (foundname !== undefined && foundname !== null) { const foundname = oldstartnode.data("label")
data.action.label = foundname if (foundname !== undefined && foundname !== null) {
} data.action.label = foundname
} }
}
//console.log("Click data: ", data) //console.log("Click data: ", data)
//data.action.label = "" //data.action.label = ""
setSelectedResult(data); setSelectedResult(data);
setCodeModalOpen(true); setCodeModalOpen(true);
} else {
toast("Please wait until the workflow is loaded and try again")
}
}} }}
> >
<Tooltip <Tooltip
@@ -15415,7 +15628,8 @@ const AngularWorkflow = (defaultprops) => {
marginBottom: "auto", 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>
<div style={{ fontSize: 14 }}> <div style={{ fontSize: 14 }}>
<Typography variant="body2" color="textSecondary"> <Typography variant="body2" color="textSecondary">
@@ -15973,8 +16187,9 @@ const AngularWorkflow = (defaultprops) => {
cy={(incy) => { cy={(incy) => {
// FIXME: There's something specific loading when // FIXME: There's something specific loading when
// you do the first hover of a node. Why is this different? // you do the first hover of a node. Why is this different?
//console.log("CY: ", incy)
setCy(incy);
setCy(incy);
}} }}
/> />
</span> </span>
@@ -16938,7 +17153,7 @@ const AngularWorkflow = (defaultprops) => {
)} )}
</span> </span>
) : ( ) : (
<ReactMarkdown <Markdown
components={{ components={{
img: Img, img: Img,
code: CodeHandler, code: CodeHandler,
@@ -16957,7 +17172,7 @@ const AngularWorkflow = (defaultprops) => {
}} }}
> >
{selectedApp.documentation} {selectedApp.documentation}
</ReactMarkdown> </Markdown>
)} )}
</div> </div>
</div> </div>
@@ -17694,6 +17909,7 @@ const AngularWorkflow = (defaultprops) => {
{newView} {newView}
<VariablesModal variableInfo={variableInfo} setVariableInfo={setVariableInfo} /> <VariablesModal variableInfo={variableInfo} setVariableInfo={setVariableInfo} />
<ExecutionVariableModal variableInfo={variableInfo} setVariableInfo={setVariableInfo} /> <ExecutionVariableModal variableInfo={variableInfo} setVariableInfo={setVariableInfo} />
{aiQueryModal}
{conditionsModal} {conditionsModal}
{authenticationModal} {authenticationModal}
{codePopoutModal} {codePopoutModal}
+95 -34
View File
@@ -398,7 +398,6 @@ const AppCreator = (defaultprops) => {
apikeySelection.length > 0 ? apikeySelection[0] : "" apikeySelection.length > 0 ? apikeySelection[0] : ""
); );
const [refreshUrl, setRefreshUrl] = useState(""); const [refreshUrl, setRefreshUrl] = useState("");
const [oauth2Scopes, setOauth2Scopes] = useState([]);
const [projectCategories, setProjectCategories] = useState([]); const [projectCategories, setProjectCategories] = useState([]);
const [selectedCategory, setSelectedCategory] = useState(""); const [selectedCategory, setSelectedCategory] = useState("");
@@ -411,7 +410,12 @@ const AppCreator = (defaultprops) => {
const [appBuilding, setAppBuilding] = useState(false); const [appBuilding, setAppBuilding] = useState(false);
const [fileDownloadEnabled, setFileDownloadEnabled] = useState(false); const [fileDownloadEnabled, setFileDownloadEnabled] = useState(false);
const [actionAmount, setActionAmount] = useState(increaseAmount); 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 = { const defaultAuth = {
name: "", name: "",
type: "header", type: "header",
@@ -1748,17 +1752,25 @@ const AppCreator = (defaultprops) => {
//console.log("FLOW-1: ", value) //console.log("FLOW-1: ", value)
const flowkey = value.flow === undefined ? "flows" : "flow"; const flowkey = value.flow === undefined ? "flows" : "flow";
//console.log("FLOW: ", value[flowkey]) //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]) //console.log("FLOW2: ", value[flowkey][basekey])
if (value[flowkey] !== undefined && value[flowkey][basekey] !== undefined 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); setParameterName(value[flowkey][basekey].authorizationUrl);
// } else { } else {
// setOauth2Type("application") setOauth2Type("application")
} }
var tokenUrl = ""; var tokenUrl = "";
@@ -2499,10 +2511,16 @@ const AppCreator = (defaultprops) => {
scheme: "basic", scheme: "basic",
}; };
} else if (authenticationOption === "Oauth2") { } else if (authenticationOption === "Oauth2") {
console.log("oauth2: ", parameterName) console.log("oauth2: ", parameterName)
var newparamName = parameterName.replaceAll('"', ""); var newparamName = parameterName.replaceAll('"', "");
newparamName = newparamName.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 //parameterName, parameterValue, revocationUrl
data.components.securitySchemes["Oauth2"] = { data.components.securitySchemes["Oauth2"] = {
type: "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) { if (setExtraAuth.length > 0) {
@@ -2925,7 +2951,7 @@ const AppCreator = (defaultprops) => {
color="textSecondary" color="textSecondary"
style={{ marginTop: 10 }} style={{ marginTop: 10 }}
> >
Base Authorization URL for Oauth2 Authorization URL for Oauth2
</Typography> </Typography>
<TextField <TextField
required required
@@ -3082,7 +3108,7 @@ const AppCreator = (defaultprops) => {
}, },
}} }}
style={{ maxHeight: 80, overflowX: "hidden", overflowY: "auto" }} style={{ maxHeight: 80, overflowX: "hidden", overflowY: "auto" }}
placeholder="Available Oauth2 Scopes (enter to add)" placeholder="Available Oauth2 Scopes"
color="primary" color="primary"
fullWidth fullWidth
value={oauth2Scopes} value={oauth2Scopes}
@@ -5957,36 +5983,71 @@ const AppCreator = (defaultprops) => {
</Select> </Select>
</FormControl> </FormControl>
{authenticationOption === "Oauth2" ? {authenticationOption === "Oauth2" ?
<FormControl style={{ marginLeft: 350, maxWidth: 200, }} variant="outlined"> <FormControl style={{ marginLeft: 310, maxWidth: 240, }} variant="outlined">
{/* {/*
<Typography variant="body2"> <Typography variant="body2">
- Delegated: The user will get a popup for access their personal data. - 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. - Application: Permissions are set by the app creator in the 3rd party platform.
</Typography> </Typography>
*/} */}
<Select <div style={{display: "flex", flexDirection: "row", alignItems: "center", justifyContent: "space-between", }}>
fullWidth <Typography variant="body2" style={{ marginTop: 10, marginRight: 10, }} color="textSecondary">Oauth2 type</Typography>
label="Oauth2 type" <Select
onChange={(e) => { fullWidth
setOauth2Type(e.target.value); onChange={(e) => {
}} setOauth2Type(e.target.value);
value={oauth2Type}
style={{ if (e.target.value === "application" && oauth2GrantType === "") {
backgroundColor: inputColor, setOauth2GrantType("client_credentials")
color: "white", }
height: "50px", }}
}} value={oauth2Type}
> style={{
{["delegated", "application"].map((data, index) => ( backgroundColor: inputColor,
<MenuItem color: "white",
key={index} height: "50px",
style={{ backgroundColor: inputColor, color: "white" }} }}
value={data} >
{["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} {["client_credentials", "password"].map((data, index) => (
</MenuItem> <MenuItem
))} key={index}
</Select> style={{ backgroundColor: inputColor, color: "white" }}
value={data}
>
{data}
</MenuItem>
))}
</Select>
</div>
: null}
</FormControl> </FormControl>
: null} : null}
</span> </span>
+1 -1
View File
@@ -403,7 +403,7 @@ const Apps = (props) => {
return response.json(); return response.json();
}) })
.then((responseJson) => { .then((responseJson) => {
//console.log("Apps: ", responseJson) console.log("Apps: ", responseJson)
//responseJson = sortByKey(responseJson, "large_image") //responseJson = sortByKey(responseJson, "large_image")
//responseJson = sortByKey(responseJson, "is_valid") //responseJson = sortByKey(responseJson, "is_valid")
//setFilteredApps(responseJson.filter(app => !internalIds.includes(app.name) && !(!app.activated && app.generated))) //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 [expandedItem, setExpandedItem] = useState(-1);
const [inputUsecase, setInputUsecase] = useState({}); const [inputUsecase, setInputUsecase] = useState({});
const [prevSubcase, setPrevSubcase] = useState({})
const [editing, setEditing] = useState(false); const [editing, setEditing] = useState(false);
const [description, setDescription] = useState(""); const [description, setDescription] = useState("");
const [video, setVideo] = useState(""); const [video, setVideo] = useState("");
@@ -117,6 +119,42 @@ const UsecaseListComponent = (props) => {
const [mitreTags, setMitreTags] = useState([]); 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 = () => { const loadApps = () => {
fetch(`${globalUrl}/api/v1/apps`, { fetch(`${globalUrl}/api/v1/apps`, {
method: "GET", method: "GET",
@@ -163,35 +201,14 @@ const UsecaseListComponent = (props) => {
if (keys === undefined || keys === null || keys.length === 0) { if (keys === undefined || keys === null || keys.length === 0) {
return null 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) => { const getUsecase = (subcase, index, subindex) => {
subcase = parseUsecase(subcase) subcase = parseUsecase(subcase)
setPrevSubcase(subcase)
// Timeout 50ms to delay it slightly
//setTimeout(() => {
// setInputUsecase(subcase)
//}, 50)
fetch(`${globalUrl}/api/v1/workflows/usecases/${escape(subcase.name.replaceAll(" ", "_"))}`, { fetch(`${globalUrl}/api/v1/workflows/usecases/${escape(subcase.name.replaceAll(" ", "_"))}`, {
method: "GET", method: "GET",
@@ -214,8 +231,6 @@ const UsecaseListComponent = (props) => {
if (responseJson.success === false) { if (responseJson.success === false) {
parsedUsecase = subcase parsedUsecase = subcase
} else { } else {
console.log("FOUND: ", JSON.parse(JSON.stringify(responseJson)))
parsedUsecase = responseJson parsedUsecase = responseJson
parsedUsecase.srcimg = subcase.srcimg parsedUsecase.srcimg = subcase.srcimg
@@ -314,7 +329,7 @@ const UsecaseListComponent = (props) => {
}) })
.catch((error) => { .catch((error) => {
//toast(error.toString()); //toast(error.toString());
//setFrameworkLoaded(true) //setFrameworkLoaded(true)
}) })
} }
@@ -416,15 +431,16 @@ const UsecaseListComponent = (props) => {
return ( return (
<Grid id={fixedName} item xs={selectedItem ? 12 : 4} key={subindex} style={{minHeight: 110,}} onClick={() => { <Grid id={fixedName} item xs={selectedItem ? 12 : 4} key={subindex} style={{minHeight: 110,}} onClick={() => {
if (fixedName === "increase authentication") {
getUsecase(subcase, index, subindex)
return
}
//setSelectedWorkflows([]) //setSelectedWorkflows([])
if (selectedItem) { if (selectedItem) {
} else { } else {
getUsecase(subcase, index, subindex) getUsecase(subcase, index, subindex)
navigate(`/usecases?selected_object=${fixedName}`) 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={() => { <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 <IconButton
style={{}} style={{}}
index="close_selection"
onClick={(e) => { onClick={(e) => {
setExpandedItem(-1) setExpandedItem(-1)
setExpandedIndex(-1) setExpandedIndex(-1)
setEditing(false) setEditing(false)
setInputUsecase({}) setInputUsecase({})
}} }}
> >
<CloseIcon style={{ color: "white" }} /> <CloseIcon style={{ color: "white" }} />
@@ -1172,13 +1189,28 @@ const Dashboard = (props) => {
if (foundQuery !== null && foundQuery !== undefined) { if (foundQuery !== null && foundQuery !== undefined) {
setSelectedUsecaseCategory(foundQuery) setSelectedUsecaseCategory(foundQuery)
const newitem = removeParam("selected", cursearch); const newitem = removeParam("selected", cursearch);
navigate(curpath + newitem) 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"] const foundQuery2 = params["selected_object"]
if (foundQuery2 !== null && foundQuery2 !== undefined) { 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("_", " ") const queryName = foundQuery2.toLowerCase().replaceAll("_", " ")
// Waiting a bit for it to render // Waiting a bit for it to render
@@ -1198,7 +1230,7 @@ const Dashboard = (props) => {
} else { } else {
//console.log("Couldn't find item with name ", queryName) //console.log("Couldn't find item with name ", queryName)
} }
}, 100); }, 1000);
} }
} }
@@ -1251,6 +1283,7 @@ const Dashboard = (props) => {
}) })
} }
const getAvailableWorkflows = () => { const getAvailableWorkflows = () => {
fetch(globalUrl + "/api/v1/workflows", { fetch(globalUrl + "/api/v1/workflows", {
method: "GET", method: "GET",
@@ -1385,8 +1418,7 @@ const Dashboard = (props) => {
useEffect(() => { useEffect(() => {
getAvailableWorkflows() getAvailableWorkflows()
getFramework() getFramework()
//fetchUsecases()
}, []); }, []);
const fetchdata = (stats_id) => { const fetchdata = (stats_id) => {
+42 -102
View File
@@ -1,13 +1,12 @@
import React, { useEffect, useState } from "react"; 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 { BrowserView, MobileView } from "react-device-detect";
import { useParams, useNavigate, Link } from "react-router-dom"; import { useParams, useNavigate, Link } from "react-router-dom";
import { isMobile } from "react-device-detect"; import { isMobile } from "react-device-detect";
import theme from '../theme.jsx'; 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 { import {
Grid, Grid,
@@ -29,6 +28,8 @@ import {
import { import {
Link as LinkIcon, Link as LinkIcon,
Edit as EditIcon, Edit as EditIcon,
KeyboardArrowRight as KeyboardArrowRightIcon,
ExpandMore as ExpandMoreIcon,
} from "@mui/icons-material"; } from "@mui/icons-material";
const Body = { const Body = {
@@ -135,7 +136,7 @@ const Docs = (defaultprops) => {
}; };
const fetchDocList = () => { const fetchDocList = () => {
fetch(globalUrl + "/api/v1/docs", { fetch(`${globalUrl}/api/v1/docs`, {
method: "GET", method: "GET",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -147,9 +148,8 @@ const Docs = (defaultprops) => {
if (responseJson.success) { if (responseJson.success) {
setList(responseJson.list); setList(responseJson.list);
} else { } else {
setList([ setList(["# Error loading documentation. Please contact us if this persists.",]);
"# Error loading documentation. Please contact us if this persists.", toast("Failed loading documentation. Please reload the window")
]);
} }
setListLoaded(true); setListLoaded(true);
}) })
@@ -157,7 +157,7 @@ const Docs = (defaultprops) => {
}; };
const fetchDocs = (docId) => { const fetchDocs = (docId) => {
fetch(globalUrl + "/api/v1/docs/" + docId, { fetch(`${globalUrl}/api/v1/docs/${docId}`, {
method: "GET", method: "GET",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -166,8 +166,15 @@ const Docs = (defaultprops) => {
}) })
.then((response) => response.json()) .then((response) => response.json())
.then((responseJson) => { .then((responseJson) => {
if (responseJson.success) { if (responseJson.success === false) {
setData(responseJson.reason); //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) { if (docId === undefined) {
document.title = "Shuffle documentation introduction"; document.title = "Shuffle documentation introduction";
} else { } else {
@@ -365,18 +372,17 @@ const Docs = (defaultprops) => {
} }
const markdownStyle = { const markdownStyle = {
color: "rgba(255, 255, 255, 0.65)", color: "rgba(255, 255, 255, 0.90)",
overflow: "hidden", overflow: "hidden",
paddingBottom: 100, paddingBottom: 100,
margin: "auto", margin: "auto",
maxWidth: "100%", maxWidth: "100%",
minWidth: "100%", minWidth: "100%",
overflow: "hidden", overflow: "hidden",
fontSize: isMobile ? "1.3rem" : "1.0rem", fontSize: isMobile ? "1.3rem" : "1.1rem",
}; };
function OuterLink(props) { function OuterLink(props) {
console.log("Link: ", props.href)
if (props.href.includes("http") || props.href.includes("mailto")) { if (props.href.includes("http") || props.href.includes("mailto")) {
return ( return (
<a <a
@@ -402,7 +408,7 @@ const Docs = (defaultprops) => {
} }
function CodeHandler(props) { 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] : "" 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); const [hover, setHover] = useState(false);
console.log("Link: ", link)
if (link === undefined || link === null) { if (link === undefined || link === null) {
return 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! <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> </Typography>
</div> </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> </div>
const markdownComponents = {
img: Img,
code: CodeHandler,
h1: Heading,
h2: Heading,
h3: Heading,
h4: Heading,
h5: Heading,
h6: Heading,
a: OuterLink,
}
// PostDataBrowser Section // PostDataBrowser Section
const postDataBrowser = const postDataBrowser =
list === undefined || list === null ? null : ( list === undefined || list === null ? null : (
@@ -812,32 +777,22 @@ const Docs = (defaultprops) => {
mainpageInfo mainpageInfo
: :
<div id="markdown_wrapper_outer" style={markdownStyle}> <div id="markdown_wrapper_outer" style={markdownStyle}>
<ReactMarkdown <Markdown
components={{ components={markdownComponents}
img: Img,
code: CodeHandler,
h1: Heading,
h2: Heading,
h3: Heading,
h4: Heading,
h5: Heading,
h6: Heading,
a: OuterLink,
}}
id="markdown_wrapper" id="markdown_wrapper"
escapeHtml={false} escapeHtml={false}
skipHtml={false}
style={{ style={{
maxWidth: "100%", minWidth: "100%", maxWidth: "100%", minWidth: "100%",
}} }}
> >
{data} {data}
</ReactMarkdown> </Markdown>
</div> </div>
} }
</div> </div>
</div> </div>
); );
// remarkPlugins={[remarkGfm]}
const mobileStyle = { const mobileStyle = {
color: "white", color: "white",
@@ -849,6 +804,7 @@ const Docs = (defaultprops) => {
flexDirection: "column", flexDirection: "column",
}; };
const postDataMobile = const postDataMobile =
list === undefined || list === null ? null : ( list === undefined || list === null ? null : (
<div style={mobileStyle}> <div style={mobileStyle}>
@@ -899,18 +855,8 @@ const Docs = (defaultprops) => {
mainpageInfo mainpageInfo
: :
<div id="markdown_wrapper_outer" style={markdownStyle}> <div id="markdown_wrapper_outer" style={markdownStyle}>
<ReactMarkdown <Markdown
components={{ components={markdownComponents}
img: Img,
code: CodeHandler,
h1: Heading,
h2: Heading,
h3: Heading,
h4: Heading,
h5: Heading,
h6: Heading,
a: OuterLink,
}}
id="markdown_wrapper" id="markdown_wrapper"
escapeHtml={false} escapeHtml={false}
style={{ style={{
@@ -918,7 +864,7 @@ const Docs = (defaultprops) => {
}} }}
> >
{data} {data}
</ReactMarkdown> </Markdown>
</div> </div>
} }
<Divider <Divider
@@ -941,15 +887,9 @@ const Docs = (defaultprops) => {
</div> </div>
); );
//const imageModal =
// <Dialog modal
// open={imageModalOpen}
// </Dialog>
// {imageModal}
// Padding and zIndex etc set because of footer in cloud. // Padding and zIndex etc set because of footer in cloud.
const loadedCheck = ( 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> <BrowserView>{postDataBrowser}</BrowserView>
<MobileView>{postDataMobile}</MobileView> <MobileView>{postDataMobile}</MobileView>
</div> </div>
+11 -11
View File
@@ -1344,7 +1344,7 @@ const Workflows = (props) => {
}, i * 200); }, 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) => { const deduplicateIds = (data, skip_sanitize) => {
@@ -1982,16 +1982,16 @@ const Workflows = (props) => {
</div> </div>
</Tooltip> </Tooltip>
<Tooltip arrow title={ <Tooltip arrow title={
<span style={{}}> <div style={{width: "100%", minWidth: 250, maxWidth: 310, }}>
{data.image !== undefined && data.image !== null && data.image.length > 0 ? {data.image !== undefined && data.image !== null && data.image.length > 0 ?
<img src={data.image} alt={data.name} style={{backgroundColor: theme.palette.surfaceColor, maxHeight: 200, minHeigth: 200, borderRadius: theme.palette.borderRadius, }} /> <img src={data.image} alt={data.name} style={{backgroundColor: theme.palette.surfaceColor, maxWidth: 300, minWidth: 250, borderRadius: theme.palette.borderRadius, }} />
: null} : null}
<Typography> <Typography>
Edit {data.name} Edit {data.name}
</Typography> </Typography>
</span> </div>
} placement="bottom"> } placement="left">
<Typography <Typography
variant="body1" variant="body1"
style={{ style={{
marginBottom: 0, marginBottom: 0,
+1 -1
View File
@@ -9,7 +9,7 @@ require (
github.com/mackerelio/go-osstat v0.2.3 github.com/mackerelio/go-osstat v0.2.3
github.com/satori/go.uuid v1.2.0 github.com/satori/go.uuid v1.2.0
github.com/shirou/gopsutil v3.21.11+incompatible 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/api v0.28.1
k8s.io/apimachinery v0.28.1 k8s.io/apimachinery v0.28.1
k8s.io/client-go 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.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 h1:L76zWCD/7gIBuhr3feWZwzT4I8VCiLRd8ZAub/3EiO0=
github.com/shuffle/shuffle-shared v0.4.62/go.mod h1:X613gbo0dT3fnYvXDRwjQZyLC+T49T2nSQOrCV5QMlI= 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 h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= 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= 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 :) // Making it work on low-end machines even during busy times :)
// May cause some things to run slowly // May cause some things to run slowly
var maxConcurrency = 3 var maxConcurrency = 7
// Timeout if something rashes // Timeout if something rashes
var workerTimeoutEnv = os.Getenv("SHUFFLE_ORBORUS_EXECUTION_TIMEOUT") 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{} serviceOptions := types.ServiceCreateOptions{}
_, err = dockercli.ServiceCreate( _, err = dockercli.ServiceCreate(
ctx, 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) log.Printf("[ERROR] Failed to start worker container in environment %s: %s", environment, err)
return err return err
} else { } 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) //stats, err := cli.ContainerInspect(context.Background(), containerName)
@@ -813,7 +830,7 @@ func deployWorker(image string, identifier string, env []string, executionReques
// } // }
//} //}
} else { } 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 return nil
@@ -1298,7 +1315,6 @@ func main() {
ctx := context.Background() ctx := context.Background()
// Run by default from now // Run by default from now
//commenting for now as its stoppoing minikube //commenting for now as its stoppoing minikube
// zombiecheck(ctx, workerTimeout)
log.Printf("[INFO] Running towards %s (BASE_URL) with environment name %s", baseUrl, environment) log.Printf("[INFO] Running towards %s (BASE_URL) with environment name %s", baseUrl, environment)
@@ -1333,6 +1349,8 @@ func main() {
//deployServiceWorkers(workerImage) //deployServiceWorkers(workerImage)
} }
zombiecheck(ctx, workerTimeout)
client := shuffle.GetExternalClient(baseUrl) client := shuffle.GetExternalClient(baseUrl)
fullUrl := fmt.Sprintf("%s/api/v1/workflows/queue", baseUrl) fullUrl := fmt.Sprintf("%s/api/v1/workflows/queue", baseUrl)
log.Printf("[INFO] Finished configuring docker environment. Connecting to %s", fullUrl) log.Printf("[INFO] Finished configuring docker environment. Connecting to %s", fullUrl)
@@ -1389,7 +1407,7 @@ func main() {
// Should find data to send (memory etc.) // Should find data to send (memory etc.)
// Create timeout of max 4 seconds just in case // 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() defer cancel()
// Marshal and set body // Marshal and set body
@@ -1524,9 +1542,12 @@ func main() {
log.Printf("[INFO] Execution already handled (rerun of old executions?): %s", execution.ExecutionId) log.Printf("[INFO] Execution already handled (rerun of old executions?): %s", execution.ExecutionId)
toBeRemoved.Data = append(toBeRemoved.Data, execution) 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" { if swarmConfig != "run" && swarmConfig != "swarm" {
continue continue
} }
*/
} }
// Now, how do I execute this one? // 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"))) 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) err = deployWorker(workerImage, containerName, env, execution)
zombiecounter += 1 zombiecounter += 1
if err == nil { if err == nil {
@@ -1754,7 +1799,7 @@ func zombiecheck(ctx context.Context, workerTimeout int) error {
return nil 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{ containers, err := dockercli.ContainerList(ctx, types.ContainerListOptions{
All: true, All: true,
}) })
@@ -1771,10 +1816,11 @@ func zombiecheck(ctx context.Context, workerTimeout int) error {
stopContainers := []string{} stopContainers := []string{}
removeContainers := []string{} removeContainers := []string{}
log.Printf("[INFO] Baseimage: %s, Workertimeout: %d", baseimagename, int64(workerTimeout)) 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 { for _, container := range containers {
// Skip random containers. Only handle things related to Shuffle. // 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 shuffleFound := false
for _, item := range container.Labels { for _, item := range container.Labels {
if item == "shuffle" { if item == "shuffle" {
@@ -1785,7 +1831,7 @@ func zombiecheck(ctx context.Context, workerTimeout int) error {
// Check image name // Check image name
if !shuffleFound { 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 continue
} }
//} else { //} else {
@@ -1820,7 +1866,7 @@ func zombiecheck(ctx context.Context, workerTimeout int) error {
} }
// FIXME - add killing of apps with same execution ID too // 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 var options container.StopOptions
for _, containername := range stopContainers { for _, containername := range stopContainers {
log.Printf("[INFO] Stopping and removing container %s", containerNames[containername]) 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) 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) log.Printf("[INFO] Using default worker server url as previous is invalid: %s", streamUrl)
streamUrl = fmt.Sprintf("http://shuffle-workers:33333/api/v1/execute") 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/gorilla/mux v1.8.0
github.com/patrickmn/go-cache v2.1.0+incompatible github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/satori/go.uuid v1.2.0 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/api v0.28.3
k8s.io/apimachinery v0.28.3 k8s.io/apimachinery v0.28.3
k8s.io/client-go 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.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 h1:o+mMPRY4ourkE3R0qdi80jg6RlCtvAJ/VVrPk4y75Hk=
github.com/shuffle/shuffle-shared v0.4.57/go.mod h1:X613gbo0dT3fnYvXDRwjQZyLC+T49T2nSQOrCV5QMlI= 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 h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= 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= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
File diff suppressed because it is too large Load Diff