diff --git a/.env b/.env index 3bb7b21b..298128cc 100755 --- a/.env +++ b/.env @@ -49,27 +49,24 @@ DOCKER_API_VERSION=1.40 # Orborus/Proxy configurations HTTP_PROXY= HTTPS_PROXY= -# Decides if proxy configurations should be passed to workers or not SHUFFLE_PASS_WORKER_PROXY=TRUE -# Decides if proxy configurations should be passed to apps or not. Requires SHUFFLE_PASS_WORKER_PROXY=true SHUFFLE_PASS_APP_PROXY=TRUE -# Used to differentiate proxies for shuffle internal vs external traffic -SHUFFLE_INTERNAL_HTTP_PROXY= -SHUFFLE_INTERNAL_HTTPS_PROXY= +SHUFFLE_INTERNAL_HTTP_PROXY=NOPROXY +SHUFFLE_INTERNAL_HTTPS_PROXY=NOPROXY # Timezone-handler in Orborus, Worker and Apps TZ=Europe/Amsterdam # Used to FIND the containername. cgroup v2: issue 501 ORBORUS_CONTAINER_NAME= # Used for setting up a startup delay for Orborus -SHUFFLE_ORBORUS_STARTUP_DELAY= -SHUFFLE_SKIPSSL_VERIFY=true +SHUFFLE_ORBORUS_STARTUP_DELAY= +SHUFFLE_SKIPSSL_VERIFY=true # Used for controlling if the environment should run in kubernetes or not -IS_KUBERNETES=false +IS_KUBERNETES=false -SHUFFLE_BASE_IMAGE_NAME=shuffle -SHUFFLE_BASE_IMAGE_REGISTRY=ghcr.io +#SHUFFLE_BASE_IMAGE_NAME=shuffle +#SHUFFLE_BASE_IMAGE_REGISTRY=ghcr.io SHUFFLE_BASE_IMAGE_REPOSITORY=frikky -SHUFFLE_BASE_IMAGE_TAG_SUFFIX="-1.3.1" +#SHUFFLE_BASE_IMAGE_TAG_SUFFIX="-1.4.0" # The eth0 interface inside a container corresponds # to the virtual Ethernet interface that connects @@ -100,8 +97,8 @@ SHUFFLE_MAX_EXECUTION_DEPTH= DATASTORE_EMULATOR_HOST=shuffle-database:8000 #SHUFFLE_OPENSEARCH_URL=http://shuffle-opensearch:9200 SHUFFLE_OPENSEARCH_URL=https://shuffle-opensearch:9200 -SHUFFLE_OPENSEARCH_USERNAME=admin -SHUFFLE_OPENSEARCH_PASSWORD=admin +SHUFFLE_OPENSEARCH_USERNAME="admin" +SHUFFLE_OPENSEARCH_PASSWORD="StrongShufflePassword321!" SHUFFLE_OPENSEARCH_CERTIFICATE_FILE= SHUFFLE_OPENSEARCH_APIKEY= SHUFFLE_OPENSEARCH_CLOUDID= @@ -109,4 +106,8 @@ SHUFFLE_OPENSEARCH_PROXY= SHUFFLE_OPENSEARCH_INDEX_PREFIX= SHUFFLE_OPENSEARCH_SKIPSSL_VERIFY=true +#Tenzir related +SHUFFLE_TENZIR_URL= + + DEBUG_MODE=false diff --git a/.github/install-guide.md b/.github/install-guide.md index f033838a..aa763642 100755 --- a/.github/install-guide.md +++ b/.github/install-guide.md @@ -6,16 +6,19 @@ Shuffle Installation -Installation of Shuffle is currently available for docker and kubernetes. Looking for how to update Shuffle? Check the [updating guide](https://shuffler.io/docs/configuration#updating_shuffle) +Installation of Shuffle is currently available for [docker](https://shuffler.io/docs/configuration#production-readiness) and [kubernetes](https://shuffler.io/docs/configuration#Kubernetes). -This document outlines an introduction environment which is not scalable. [Read here](https://shuffler.io/docs/configuration#production_readiness) for information on production readiness. This also includes system requirements and configurations for Swarm or Kubernetes. +- Looking to run workflows onprem, but don't want to run the entire stack? Looks into [Environments & Orborus hosting Onprem](https://shuffler.io/docs/organizations#Environments) +- Looking for how to update Shuffle? Check the [updating guide](https://shuffler.io/docs/configuration#updating_shuffle) + +This document outlines an introduction environment which is **not** scalable. [Read here](https://shuffler.io/docs/configuration#production_readiness) for information on production readiness and scalability. This also includes system requirements and configurations for **Docker Swarm** or **Kubernetes**. # Docker - *nix -The Docker setup is done with docker-compose +The Docker setup is the default setup, and is ran with docker compose. This is [NOT a scalable build](https://shuffler.io/docs/configuration#production-readiness) without changes. **PS: if you're setting up Shuffle on Windows, go to the next step (Windows Docker setup)** -1. Make sure you have [Docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/install/) installed, and that you have a minimum of **2Gb of RAM** available. +1. Make sure you have [Docker](https://docs.docker.com/get-docker/) installed, and that you have a minimum of **2Gb of RAM** available. 2. Download Shuffle ```bash git clone https://github.com/Shuffle/Shuffle @@ -32,7 +35,7 @@ sudo swapoff -a # Disable swap 4. Run docker-compose. ```bash -docker-compose up -d +docker compose up -d ``` 5. Recommended for Opensearch to work well @@ -40,7 +43,7 @@ docker-compose up -d sudo sysctl -w vm.max_map_count=262144 # https://www.elastic.co/guide/en/elasticsearch/reference/current/vm-max-map-count.html ``` -When you're done, skip to the [After installation](#after-installation) step below. +When you're done, go to the [After installation](#after-installation) step below. ## Windows with WSL This step is for setting up with Docker on windows from scratch. @@ -57,9 +60,9 @@ This step is for setting up with Docker on windows from scratch. OUTER_HOSTNAME=YOUR.IP.HERE ``` -6. Run docker-compose +5. Run docker compose ```bash -docker-compose up -d +docker compose up -d ``` ### Configurations (high availability, scale, proxies, default users etc.) @@ -124,8 +127,8 @@ Large portions of the backend is written in another repository - [shuffle-shared ## Database - Opensearch Make sure this is running through the docker-compose, and that the backend points to it with SHUFFLE_OPENSEARCH_URL defined. -So essentially, what that means is: -1. Make sure you have docker-compose installed +What it means: +1. Make sure you have docker compose installed 2. Make sure you have the docker-compose.yml file from this repository 3. Run `docker-compose up opensearch -d` diff --git a/.github/workflows/dockerbuild.yaml b/.github/workflows/dockerbuild.yaml index 0411279e..a14dfaa9 100644 --- a/.github/workflows/dockerbuild.yaml +++ b/.github/workflows/dockerbuild.yaml @@ -3,7 +3,7 @@ name: dockerbuild on: push: branches: - - main + - 1.4.0 paths: - "**" - "!.github/**" @@ -19,23 +19,23 @@ jobs: include: - app: frontend path: frontend - version: 1.3.3 + version: 1.4.0 experimental: true - app: backend path: backend - version: 1.3.3 + version: 1.4.0 experimental: true - app: app_sdk path: backend/app_sdk - version: 1.3.3 + version: 1.4.0 experimental: true - app: orborus path: functions/onprem/orborus - version: 1.3.3 + version: 1.4.0 experimental: true - app: worker path: functions/onprem/worker - version: 1.3.3 + version: 1.4.0 experimental: true steps: - name: Checkout diff --git a/backend/Dockerfile b/backend/Dockerfile index cb287e8f..e34f9c11 100755 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.19.3-buster as builder +FROM golang:1.22 as builder # Add files RUN mkdir /app @@ -41,4 +41,4 @@ COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certifica WORKDIR /app EXPOSE 5001 -CMD ["./webapp"] \ No newline at end of file +CMD ["./webapp"] diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py index 3937b052..15338d89 100755 --- a/backend/app_sdk/app_base.py +++ b/backend/app_sdk/app_base.py @@ -25,6 +25,11 @@ import concurrent.futures from io import StringIO as StringBuffer, BytesIO from liquid import Liquid, defaults +from requests.packages.urllib3.exceptions import InsecureRequestWarning + +# Suppress the warning +requests.packages.urllib3.disable_warnings(InsecureRequestWarning) + runtime = os.getenv("SHUFFLE_SWARM_CONFIG", "") @@ -96,6 +101,7 @@ def md5_base64(a): @shuffle_filters.register def base64_encode(a): a = str(a) + try: return base64.b64encode(a.encode('utf-8')).decode() except: @@ -104,6 +110,17 @@ def base64_encode(a): @shuffle_filters.register def base64_decode(a): a = str(a) + + if "-" in a: + a = a.replace("-", "+", -1) + + if "_" in a: + a = a.replace("_", "/", -1) + + # Fix padding + if len(a) % 4 != 0: + a += "=" * (4 - len(a) % 4) + try: return base64.b64decode(a).decode("unicode_escape") except: @@ -121,7 +138,7 @@ def as_object(a): return json.loads(str(a)) @shuffle_filters.register -def ast(a): +def ast_eval(a): return ast.literal_eval(str(a)) @shuffle_filters.register @@ -231,7 +248,6 @@ def csv_parse(a): try: return json.dumps(allitems) except: - print("[ERROR] Failed dumping from JSON in csv parse") return allitems @shuffle_filters.register @@ -259,13 +275,6 @@ def split(base, sep): except: return base.split(sep) -#print(shuffle_filters.filters) -#print(Liquid("{{ '10' | plus: 1}}", filters=shuffle_filters.filters).render()) -#print(Liquid("{{ '10' | minus: 1}}", filters=shuffle_filters.filters).render()) -#print(Liquid("{{ asd | size }}", filters=shuffle_filters.filters).render()) -#print(Liquid("{{ 'asd' | md5 }}", filters=shuffle_filters.filters).render()) -#print(Liquid("{{ 'asd' | sha256 }}", filters=shuffle_filters.filters).render()) -#print(Liquid("{{ 'asd' | md5_base64 | base64_decode }}", filters=shuffle_filters.filters).render()) ### ### @@ -339,44 +348,39 @@ class AppBase: if self.proxy_config["https"].lower() == "noproxy": self.proxy_config["https"] = "" except Exception as e: - self.logger.info(f"[DEBUG] Failed setting proxy config: {e}. NOT important if running apps with webserver. This is NOT critical.") + self.logger.info(f"[WARNING] Failed setting proxy config: {e}. NOT important if running apps with webserver. This is NOT critical.") - self.logger.info(f"[DEBUG] Proxy config: {self.proxy_config}") if isinstance(self.action, str): try: self.action = json.loads(self.action) self.original_action = json.loads(self.action) except Exception as e: - self.logger.info(f"[DEBUG] Failed parsing action as JSON (init): {e}. NOT important if running apps with webserver. This is NOT critical.") - - #print(f"ACTION: {self.action}") + pass if len(self.base_url) == 0: self.base_url = self.url + + self.local_storage = [] + # Checks output for whether it should be automatically parsed or not def run_magic_parser(self, input_data): if not isinstance(input_data, str): - self.logger.info("[DEBUG] Not string. Returning from magic") return input_data # Don't touch existing JSON/lists if (input_data.startswith("[") and input_data.endswith("]")) or (input_data.startswith("{") and input_data.endswith("}")): - self.logger.info("[DEBUG] Already JSON-like. Returning from magic") return input_data if len(input_data) < 3: - self.logger.info("[DEBUG] Too short input data") return input_data # Don't touch large data. if len(input_data) > 100000: - self.logger.info("[DEBUG] Value too large. Returning from magic") return input_data if not "\n" in input_data and not "," in input_data: - self.logger.info("[DEBUG] No data to autoparse - requires newline or comma") return input_data new_input = input_data @@ -398,7 +402,6 @@ class AppBase: index += 1 continue - #print("FIX ITEM %s" % item) for subitem in item.split(splititem): new_return.insert(index, subitem) @@ -406,7 +409,7 @@ class AppBase: # Prevent large data or infinite loops if index > 10000: - self.logger.info(f"[DEBUG] Infinite loop. Returning default data.") + #self.logger.info(f"[DEBUG] Infinite loop. Returning default data.") return input_data fixed_return = [] @@ -466,14 +469,133 @@ class AppBase: "success": True, "status": request.status_code, "url": request.url, - "headers": parsedheaders, "body": jsondata, + "headers": parsedheaders, "cookies":cookies, }) except Exception as e: - print(f"[WARNING] Failed in request: {e}") return request.text + # Fixes pattern issues in json/liquid based on input and supplied patterns + def patternfix_string(self, liquiddata, patterns, regex_patterns, inputtype="liquid"): + if not inputtype or inputtype == "liquid": + if "{{" not in liquiddata or "}}" not in liquiddata: + return liquiddata + elif inputtype == "json": + liquiddata = liquiddata.strip() + + # Validating if it looks like json or not + if liquiddata[0] == "{" and liquiddata[len(liquiddata)-1] == "}": + pass + else: + if liquiddata[0] == "[" and liquiddata[len(liquiddata)-1] == "]": + pass + else: + return liquiddata + + # If it's already json, don't touch it + try: + json.loads(liquiddata) + return liquiddata + except Exception as e: + pass + else: + print("No replace handler for %s" % inputtype) + return liquiddata + + skipkeys = [" "] + newoutput = liquiddata[:] + for pattern in patterns: + keylocations = [] + parsedvalue = "" + record = False + index = -1 + for key in liquiddata: + + # Return instant if possible + if inputtype == "json": + try: + json.loads(newoutput) + return newoutput + except: + pass + + index += 1 + if not key: + if record: + keylocations.append(index) + parsedvalue += key + + continue + + if key in skipkeys: + if record: + keylocations.append(index) + parsedvalue += key + + continue + + if key == pattern[0] and not record: + record = True + + if key not in pattern: + keylocations = [] + parsedvalue = "" + record = False + + if record: + keylocations.append(index) + parsedvalue += key + + if len(parsedvalue) == 0: + continue + + evaluated_value = parsedvalue[:] + for skipkey in skipkeys: + evaluated_value = "".join(evaluated_value.split(skipkey)) + + if evaluated_value == pattern: + #print("Found matching: %s (%s)" % (parsedvalue, keylocations)) + #print("Should replace with: %s" % patterns[pattern]) + + newoutput = newoutput.replace(parsedvalue, patterns[pattern], -1) + + # Return instant if possible + if inputtype == "json": + try: + json.loads(newoutput) + return newoutput + except: + pass + + + for pattern in regex_patterns: + newlines = [] + for line in newoutput.split("\n"): + replaced_line = re.sub(pattern, regex_patterns[pattern], line) + newlines.append(replaced_line) + + newoutput = "\n".join(newlines) + + # Return instant if possible + if inputtype == "json": + try: + json.loads(newoutput) + return newoutput + except: + pass + + # Dont return json properly unless actually json + if inputtype == "json": + try: + json.loads(newoutput) + return newoutput + except: + # Returns original if json fixing didn't work + return liquiddata + + return newoutput + # FIXME: Add more info like logs in here. # Docker logs: https://forums.docker.com/t/docker-logs-inside-the-docker-container/68190/2 def send_result(self, action_result, headers, stream_path): @@ -481,40 +603,29 @@ class AppBase: action_result["status"] = "FAILURE" try: - #self.logger.info(f"[DEBUG] ACTION: {self.action}") if self.action["run_magic_output"] == True: - self.logger.warning(f"[INFO] Action result ran with Magic parser output.") action_result["result"] = self.run_magic_parser(action_result["result"]) - else: - self.logger.warning(f"[WARNING] Magic output not defined.") except KeyError as e: - #self.logger.warning(f"[DEBUG] Failed to run magic autoparser (send result) - keyerror: {e}") pass except Exception as e: - #self.logger.warning(f"[DEBUG] Failed to run magic autoparser (send result): {e}") pass # Try it with some magic action_result["completed_at"] = int(time.time_ns()) - self.logger.info(f"""[DEBUG] Inside Send result with status {action_result["status"]}""") #if isinstance(action_result, # FIXME: Add cleanup of parameters to not send to frontend here params = {} # I wonder if this actually works - self.logger.info(f"[DEBUG] Before last stream result") url = "%s%s" % (self.base_url, stream_path) - self.logger.info(f"[INFO] URL FOR RESULT (URL): {url}") try: log_contents = "disabled: add env SHUFFLE_LOGS_DISABLED=true to Orborus to re-enable logs for apps. Can not be enabled natively in Cloud except in Hybrid mode." if not os.getenv("SHUFFLE_LOGS_DISABLED") == "true": log_contents = self.log_capture_string.getvalue() - #print("RESULTS: %s" % log_contents) - self.logger.info(f"[WARNING] Got logs of length {len(log_contents)}") if len(action_result["action"]["parameters"]) == 0: action_result["action"]["parameters"] = [] @@ -531,7 +642,7 @@ class AppBase: }) except Exception as e: - print(f"[WARNING] Failed adding parameter for logs: {e}") + pass try: finished = False @@ -543,23 +654,22 @@ class AppBase: try: ret = requests.post(url, headers=headers, json=action_result, timeout=10, verify=False, proxies=self.proxy_config) - self.logger.info(f"""[DEBUG] Successful request result request: Status= {ret.status_code} (break on 200/201) & Response= {ret.text}. Action status: {action_result["status"]}""") + #self.logger.info(f"""[DEBUG] Successful result request: Status= {ret.status_code} (break on 200/201) & Action status: {action_result["status"]}. Response= {ret.text}""") if ret.status_code == 200 or ret.status_code == 201: finished = True break else: - self.logger.info(f"[ERROR] Bad resp {ret.status_code}: {ret.text}") + # FIXME: Add a checker for 403, and Proxy logs failing + self.logger.info(f"[ERROR] Bad resp ({ret.status_code}) in send_result for url '{url}'") time.sleep(sleeptime) # Proxyerrror except requests.exceptions.ProxyError as e: - self.logger.info(f"[ERROR] Proxy error for url {url}: {e}") self.proxy_config = {} continue except requests.exceptions.RequestException as e: - self.logger.info(f"[DEBUG] Request problem for url {url}: {e}") time.sleep(sleeptime) # Check if we have a read timeout. If we do, exit as we most likely sent the result without getting a good result @@ -576,25 +686,21 @@ class AppBase: #time.sleep(5) continue except TimeoutError as e: - self.logger.info(f"[DEBUG] Timeout or request: {e}") time.sleep(sleeptime) #time.sleep(5) continue except requests.exceptions.ConnectionError as e: - self.logger.info(f"[DEBUG] Connectionerror: {e}") time.sleep(sleeptime) #time.sleep(5) continue except http.client.RemoteDisconnected as e: - self.logger.info(f"[DEBUG] Remote: {e}") time.sleep(sleeptime) #time.sleep(5) continue except urllib3.exceptions.ProtocolError as e: - self.logger.info(f"[DEBUG] Protocol err: {e}") time.sleep(0.1) #time.sleep(5) @@ -606,24 +712,22 @@ class AppBase: # Not sure why this would work tho :) action_result["status"] = "FAILURE" 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.send_result(action_result, {"Content-Type": "application/json", "Authorization": "Bearer %s" % self.authorization}, "/api/v1/streams") return except requests.exceptions.ConnectionError as e: - self.logger.info(f"[DEBUG] Unexpected ConnectionError happened: {e}") + #self.logger.info(f"[DEBUG] Unexpected ConnectionError happened: {e}") + pass except TypeError as e: action_result["status"] = "FAILURE" action_result["result"] = json.dumps({"success": False, "reason": "Typeerror when sending to backend URL %s" % url}) - self.logger.info(f"[DEBUG] Before typeerror stream result: {e}") ret = requests.post("%s%s" % (self.base_url, stream_path), headers=headers, json=action_result, verify=False, proxies=self.proxy_config) #self.logger.info(f"[DEBUG] Result: {ret.status_code}") #if ret.status_code != 200: # pr - self.logger.info(f"[DEBUG] TypeError request: Status= {ret.status_code} & Response= {ret.text}") + #self.logger.info(f"[DEBUG] TypeError request: Status= {ret.status_code} & Response= {ret.text}") except http.client.RemoteDisconnected as e: self.logger.info(f"[DEBUG] Expected Remotedisconnect happened: {e}") except urllib3.exceptions.ProtocolError as e: @@ -638,7 +742,6 @@ class AppBase: #self.log_capture_string.close() #pass except Exception as e: - print(f"[WARNING] Failed to flush logs: {e}") pass #async def cartesian_product(self, L): @@ -658,13 +761,6 @@ class AppBase: #self.logger.info("ITS A LIST!") newlist = params - #self.full_execution = os.getenv("FULL_EXECUTION", "") - #self.logger.info(len(params)) - #self.logger.info(params.items()) - #self.logger.info(list(params.items())) - #self.logger.info(f"PARAM: {params}") - #self.logger.info(f"NEWLIST: {newlist}") - # FIXME: Also handle MULTI PARAM values = [] param_names = [] @@ -743,9 +839,6 @@ class AppBase: #self.logger.info(f"DATA: {data}") # 1594869a676630b397bc34f7dc0951a3 - #self.logger.info(f"VALUE URL: {url}") - #self.logger.info(f"RET: {ret.text}") - #self.logger.info(f"ID: {ret.status_code}") url = f"{self.url}/api/v1/orgs/{org_id}/validate_app_values" ret = requests.post(url, json=data, verify=False, proxies=self.proxy_config) if ret.status_code == 200: @@ -840,7 +933,6 @@ class AppBase: check_value = "" for param in self.original_action["parameters"]: if param["name"] == key: - #self.logger.info("PARAM: %s" % param) check_value = param["value"] # self.result_wrapper_count = 0 @@ -901,7 +993,6 @@ class AppBase: #self.logger.info(f"MERGE: {should_merge}") if isinstance(value, list): - self.logger.info(f"[DEBUG] Item {value} is a list.") if len(value) <= 1: if len(value) == 1: baseparams[key] = value[0] @@ -932,27 +1023,21 @@ class AppBase: self.logger.info("[DEBUG] NO multiplier. Running a single iteration.") paramlist.append(baseparams) elif len(listlengths) == 1: - self.logger.info("[DEBUG] NO MULTIPLIER NECESSARY. Length is %d" % len(listitems)) - for item in listitems: # This loops should always be length 1 for key, value in item.items(): if isinstance(value, int): - self.logger.info("\n[DEBUG] Should run key %s %d times from %s" % (key, value, baseparams[key])) if len(paramlist) == value: - self.logger.info("[DEBUG] List ALREADY exists - just changing values") for subloop in range(value): baseitem = copy.deepcopy(baseparams) paramlist[subloop][key] = baseparams[key][subloop] else: - self.logger.info("[DEBUG] List DOESNT exist - ADDING values") for subloop in range(value): baseitem = copy.deepcopy(baseparams) baseitem[key] = baseparams[key][subloop] paramlist.append(baseitem) else: - self.logger.info("[DEBUG] Multipliers to handle: %s" % listitems) newlength = 1 for item in listitems: for key, value in item.items(): @@ -1006,11 +1091,9 @@ class AppBase: if key != "body": value[0] = json.loads(value[0]) except json.decoder.JSONDecodeError as e: - self.logger.info("[WARNING] JSON casting error (1): %s" % e) + pass except TypeError as e: - self.logger.info("[WARNING] TypeError: %s" % e) - - self.logger.info("[DEBUG] POST initial list check") + pass try: if isinstance(value, list) and len(value) == 1 and isinstance(value[0], list): @@ -1020,7 +1103,6 @@ class AppBase: self.logger.info("[WARNING] Exception in loop wrapper: {e}") loop_wrapper[key] = 1 - self.logger.info(f"[DEBUG] Key {key} is a list: {value}") newparams[key] = value[0] has_loop = True else: @@ -1045,6 +1127,8 @@ class AppBase: #param_multiplier = await self.get_param_multipliers(newparams) param_multiplier = self.get_param_multipliers(newparams) + #self.logger.info("PARAM MULTIPLIER: %s" % param_multiplier) + # FIXME: This does a deduplication of the data new_params = self.validate_unique_fields(param_multiplier) #self.logger.info(f"NEW PARAMS: {new_params}") @@ -1070,7 +1154,6 @@ class AppBase: #self.logger.info(f"NEW PARAMS: {new_params}") param_multiplier = new_params - #self.logger.info("Returned with newparams of length %d", len(new_params)) #if isinstance(new_params, list) and len(new_params) == 1: # params = new_params[0] #else: @@ -1081,10 +1164,9 @@ class AppBase: # self.send_result(action_result, headers, stream_path) # return - self.logger.info("[INFO] Multiplier length: %d" % len(param_multiplier)) - #tmp = "" for subparams in param_multiplier: #self.logger.info(f"SUBPARAMS IN MULTI: {subparams}") + tmp = "" try: while True: @@ -1107,8 +1189,8 @@ class AppBase: else: raise Exception(json.dumps({ "success": False, - "reason": "You may be running an old version of this action. Please delete and remake the node.", "exception": f"TypeError: {e}", + "reason": "You may be running an old version of this action. Please delete and remake the node.", })) break @@ -1147,7 +1229,6 @@ class AppBase: except Exception as e: self.logger.warning("[ERROR] Failed to parse coroutine value for old app: {e}") - #self.logger.info("RET from execution: %s" % ret) new_value = tmp if tmp == None: new_value = "" @@ -1212,7 +1293,7 @@ class AppBase: return results # Downloads all files from a namespace - # Currently only working on local version of Shuffle + # Currently only working on local version of Shuffle (2023) def get_file_category_ids(self, category): org_id = self.full_execution["workflow"]["execution_org"]["id"] @@ -1283,8 +1364,6 @@ class AppBase: full_execution = self.full_execution org_id = full_execution["workflow"]["execution_org"]["id"] - self.logger.info("SHOULD GET FILES BASED ON ORG %s, workflow %s and value(s) %s" % (org_id, full_execution["workflow"]["id"], value)) - if isinstance(value, list): self.logger.info("IS LIST!") #if len(value) == 1: @@ -1294,9 +1373,16 @@ class AppBase: returns = [] for item in value: - self.logger.info("VALUE: %s" % item) + self.logger.info("FILE VALUE: %s" % item) + # Check if item is a dict, and if it is, check if it has the key "id" + if isinstance(item, dict): + if "file_id" in item: + item = item["file_id"] + elif "id" in item: + item = item["id"] + if len(item) != 36 and not item.startswith("file_"): - self.logger.info("Bad length for file value %s" % item) + self.logger.info("Bad length for file value: '%s'" % item) continue #return { # "filename": "", @@ -1312,7 +1398,6 @@ class AppBase: } ret1 = requests.get("%s%s" % (self.url, get_path), headers=headers, verify=False, proxies=self.proxy_config) - self.logger.info("RET1 (file get): %s" % ret1.text) if ret1.status_code != 200: returns.append({ "filename": "", @@ -1323,7 +1408,6 @@ class AppBase: 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, proxies=self.proxy_config) - self.logger.info("RET2 (file get) done") if ret2.status_code == 200: tmpdata = ret1.json() returndata = { @@ -1333,8 +1417,6 @@ class AppBase: } returns.append(returndata) - self.logger.info("RET3 (file get done)") - if len(returns) == 0: return { "success": False, @@ -1358,6 +1440,19 @@ class AppBase: "key": key, } + try: + newstorage = [] + for item in self.local_storage: + if item["execution_id"] == self.current_execution_id and item["key"] == key: + continue + + newstorage.append(item) + + self.local_storage = newstorage + + except Exception as e: + print("[ERROR] Failed DELETING current execution id local storage: %s" % e) + response = requests.post(url, json=data, verify=False, proxies=self.proxy_config) try: allvalues = response.json() @@ -1379,6 +1474,19 @@ class AppBase: "value": str(value), } + try: + newstorage = [] + for item in self.local_storage: + if item["execution_id"] == self.current_execution_id and item["key"] == key: + continue + + newstorage.append(item) + + self.local_storage = newstorage + + except Exception as e: + print("[ERROR] Failed SETTING current execution id local storage: %s" % e) + response = requests.post(url, json=data, verify=False, proxies=self.proxy_config) try: allvalues = response.json() @@ -1401,12 +1509,23 @@ class AppBase: "key": key, } + # Makes it so that loops for the same action doesn't re-ask the db unless necessary + try: + for item in self.local_storage: + if item["execution_id"] == self.current_execution_id and item["key"] == key: + # Max keeping the local cache properly for 5 seconds due to workflow continuations + elapsed_time = time.time() - item["time_set"] + if elapsed_time > 5: + break + + return item["data"] + except Exception as e: + print("[ERROR] Failed getting current execution id local storage: %s" % e) + value = requests.post(url, json=data, verify=False, proxies=self.proxy_config) try: allvalues = value.json() - self.logger.info("VAL1: ", allvalues) allvalues["key"] = key - self.logger.info("VAL2: ", allvalues) try: parsedvalue = json.loads(allvalues["value"]) @@ -1414,6 +1533,14 @@ class AppBase: except: self.logger.info("Parsing of value as JSON failed. Continue anyway!") + try: + newdata = json.loads(json.dumps(data)) + newdata["time_set"] = time.time() + newdata["data"] = allvalues + self.local_storage.append(newdata) + except Exception as e: + print("[ERROR] Failed in local storage append: %s" % e) + return allvalues except: self.logger.info("Value couldn't be parsed, or json dump of value failed") @@ -1459,7 +1586,6 @@ class AppBase: #self.logger.info(f"Ret CREATE: {ret.text}") cur_id = "" if ret.status_code == 200: - #self.logger.info("RET: %s" % ret.text) ret_json = ret.json() if not ret_json["success"]: self.logger.info("Not success in file upload creation.") @@ -1482,16 +1608,12 @@ class AppBase: } upload_path = "/api/v1/files/%s/upload?execution_id=%s" % (cur_id, full_execution["execution_id"]) - self.logger.info("Create path: %s" % create_path) files={"shuffle_file": (filename, curfile["data"])} #open(filename,'rb')} ret = requests.post("%s%s" % (self.url, upload_path), files=files, headers=new_headers, verify=False, proxies=self.proxy_config) - self.logger.info("Ret UPLOAD: %s" % ret.text) - self.logger.info("Ret2 UPLOAD: %d" % ret.status_code) - self.logger.info("IDS TO RETURN: %s" % file_ids) return file_ids #async def execute_action(self, action): @@ -1521,7 +1643,6 @@ class AppBase: pass self.action = copy.deepcopy(action) - self.logger.info(f"[DEBUG] Sending starting action result (EXECUTING). Param replace: {replace_params}") headers = { "Content-Type": "application/json", @@ -1550,7 +1671,6 @@ class AppBase: # Add async logger # self.console_logger.handlers[0].stream.set_execution_id() - #self.logger.info("Before initial stream result") # FIXME: Shouldn't skip this, but it's good for minimzing API calls #try: @@ -1580,7 +1700,6 @@ class AppBase: } resultsurl = "%s/api/v1/streams/results" % (self.base_url) - #self.logger.info("[DEBUG] Before FULLEXEC stream result url '%s'" % (resultsurl)) ret = requests.post( resultsurl, headers=headers, @@ -1655,12 +1774,28 @@ class AppBase: self.full_execution = fullexecution - #try: - # if "backend_url" in self.full_execution: - # self.url = self.full_execution["backend_url"] - # self.base_url = self.full_execution["backend_url"] - #except KeyError: - # pass + found_id = "" + try: + if "execution_id" in self.full_execution and len(self.full_execution["execution_id"]) > 0: + found_id = self.full_execution["execution_id"] + elif len(self.current_execution_id) > 0: + found_id = self.current_execution_id + except Exception as e: + print("[ERROR] Failed in get full exec") + + try: + contains_body = False + parameter_count = 0 + + if "parameters" in self.action: + parameter_count = len(self.action["parameters"]) + for param in self.action["parameters"]: + if param["name"] == "body": + contains_body = True + + print("[DEBUG][%s] Action name: %s, Params: %d, Has Body: %s" % (self.current_execution_id, self.action["name"], parameter_count, str(contains_body))) + except Exception as e: + print("[ERROR] Failed in init print handler: %s" % e) try: if replace_params == True: @@ -1668,7 +1803,8 @@ class AppBase: self.logger.info("[DEBUG] ID: %s vs %s" % (inner_action["id"], self.action["id"])) # In case of some kind of magic, we're just doing params - if inner_action["id"] == self.action["id"]: + if inner_action["id"] != self.action["id"]: + continue self.logger.info("FOUND!") if isinstance(self.action, str): @@ -1687,8 +1823,6 @@ class AppBase: except Exception as e: self.logger.info(f"[WARNING] Failed in replace params action parsing: {e}") - self.logger.info(f"[DEBUG] AFTER FULLEXEC stream result (init): {self.current_execution_id}") - # Gets the value at the parenthesis level you want def parse_nested_param(string, level): """ @@ -1807,9 +1941,9 @@ class AppBase: return f"join({data})" except (KeyError, IndexError) as e: - print(f"ERROR in join(): {e}") + pass except json.decoder.JSONDecodeError as e: - print(f"JSON ERROR in join(): {e}") + pass if "len" in thistype or "length" in thistype or "lenght" in thistype: #self.logger.info(f"Trying to length-parse: {data}") @@ -1914,7 +2048,6 @@ class AppBase: c_parentheses = parse_nested_param(parse_string, 0)[0] match_string = re.escape(c_parentheses) custom_casting = re.findall(fr"({wrapper_group})\({match_string}", parse_string) - print("[DEBUG] In ELSE: %s" % custom_casting) # check if a wrapper was found if len(custom_casting) != 0: inner_result = parse_type(c_parentheses, custom_casting[0]) @@ -1926,7 +2059,6 @@ class AppBase: else: parse_string = inner_result - #print("PARSE STRING: %s" % parse_string) return parse_string, True # Looks for parantheses to grab special cases within a string, e.g: @@ -2021,6 +2153,7 @@ class AppBase: return " ".join(newlist) # Parses JSON loops and such down to the item you're looking for + # Check recurse_test.py for examples and tests of this function # $nodename.#.id # $nodename.data.#min-max.info.id # $nodename.data.#1-max.info.id @@ -2048,7 +2181,7 @@ class AppBase: for innervalue in basejson: # 1. Check the next item (message) # 2. Call this function again - + try: ret, is_loop = recurse_json(innervalue, parsersplit[outercnt+1:]) except IndexError: @@ -2073,11 +2206,8 @@ class AppBase: if isinstance(seconditem, int): seconditem = str(seconditem) - #print("[DEBUG] ACTUAL PARSED: %s" % actualitem) - # Means it's a single item -> continue if seconditem == "": - print("[INFO] In first - handling %s. Len: %d" % (firstitem, len(basejson))) if str(firstitem).lower() == "max" or str(firstitem).lower() == "last" or str(firstitem).lower() == "end": firstitem = len(basejson)-1 elif str(firstitem).lower() == "min" or str(firstitem).lower() == "first": @@ -2085,14 +2215,12 @@ class AppBase: else: firstitem = int(firstitem) - print(f"[DEBUG] Post lower checks with item {firstitem}") tmpitem = basejson[int(firstitem)] try: newvalue, is_loop = recurse_json(tmpitem, parsersplit[outercnt+1:]) except IndexError: newvalue, is_loop = (tmpitem, parsersplit[outercnt+1:]) else: - print("[INFO] In ELSE - handling %s and %s" % (firstitem, seconditem)) if isinstance(firstitem, str): if firstitem.lower() == "max" or firstitem.lower() == "last" or firstitem.lower() == "end": firstitem = len(basejson)-1 @@ -2113,7 +2241,6 @@ class AppBase: else: seconditem = int(seconditem) - print(f"[DEBUG] Post lower checks 2: {firstitem} AND {seconditem}") newvalue = [] if int(seconditem) > len(basejson): seconditem = len(basejson) @@ -2121,12 +2248,10 @@ class AppBase: for i in range(int(firstitem), int(seconditem)+1): # 1. Check the next item (message) # 2. Call this function again - #self.logger.info("Base: %s" % basejson[i]) try: ret, tmp_loop = recurse_json(basejson[i], parsersplit[outercnt+1:]) except IndexError: - print("[DEBUG] INDEXERROR: ", parsersplit[outercnt]) #ret = innervalue ret, tmp_loop = recurse_json(basejson[i], parsersplit[outercnt:]) @@ -2137,16 +2262,13 @@ class AppBase: else: if len(value) == 0: return basejson, False - + try: if isinstance(basejson, list): - print("[WARNING] VALUE IN ISINSTANCE IS NOT TO BE USED (list): %s" % value) return basejson, False elif isinstance(basejson, bool): - print("[WARNING] VALUE IN ISINSTANCE IS NOT TO BE USED (bool): %s" % value) return basejson, False elif isinstance(basejson, int): - print("[WARNING] VALUE IN ISINSTANCE IS NOT TO BE USED (int): %s" % value) return basejson, False elif isinstance(basejson[value], str): try: @@ -2154,14 +2276,17 @@ class AppBase: basejson = json.loads(basejson[value]) else: # Should we sanitize here? - self.logger.info("[DEBUG] VALUE TO SANITIZE?: %s" % basejson[value]) - return str(basejson[value]), False + # Check if we are on the last item? + if outercnt == len(parsersplit)-1: + return str(basejson[value]), False + else: + pass + except json.decoder.JSONDecodeError as e: return str(basejson[value]), False else: basejson = basejson[value] except KeyError as e: - print("[WARNING] Running secondary value check with replacement of underscore in %s: %s" % (value, e)) if "_" in value: value = value.replace("_", " ", -1) elif " " in value: @@ -2169,54 +2294,67 @@ class AppBase: try: if isinstance(basejson, list): - print("[WARNING] VALUE IN ISINSTANCE IS NOT TO BE USED (list): %s" % value) return basejson, False elif isinstance(basejson, bool): - print("[WARNING] VALUE IN ISINSTANCE IS NOT TO BE USED (bool): %s" % value) return basejson, False elif isinstance(basejson, int): - print("[WARNING] VALUE IN ISINSTANCE IS NOT TO BE USED (int): %s" % value) return basejson, False elif isinstance(basejson[value], str): - print(f"[INFO] LOADING STRING '%s' AS JSON" % basejson[value]) try: - print("[DEBUG] BASEJSON: %s" % basejson) if (basejson[value].endswith("}") and basejson[value].endswith("}")) or (basejson[value].startswith("[") and basejson[value].endswith("]")): basejson = json.loads(basejson[value]) else: - return str(basejson[value]), False + + if outercnt == len(parsersplit)-1: + return str(basejson[value]), False + else: + pass + except json.decoder.JSONDecodeError as e: - print("[DEBUG] RETURNING BECAUSE '%s' IS A NORMAL STRING (1)" % basejson[value]) return str(basejson[value]), False else: basejson = basejson[value] - except KeyError as e: - print("\n\n[WARNING] Running third dot notation fix that always find the correct value %s: %s" % (value, e)) + # Check if previous key was handled or not + previouskey = parsersplit[outercnt-1] + + tmpval = previouskey + "." + value + if tmpval in basejson: + return basejson[tmpval], False try: currentsplitcnt = splitcnt + recursed_value = value handled = False + + #tmpbase = basejson + previouskey = value while True: newvalue = parsersplit[currentsplitcnt+1] if newvalue == "#" or newvalue == "": break recursed_value += "." + newvalue + found = False for key, value in basejson.items(): if recursed_value.lower() in key.lower(): found = True if found == False: - print("[INFO] DIDN'T FIND similar VALUE: ", recursed_value) - break + # Check if we are on the last key or not + return "", False if recursed_value in basejson: - print("[INFO] FOUND RECURSED VALUE: ", recursed_value) basejson = basejson[recursed_value] - handled = True + + # Whether to dig deeper or not + if isinstance(basejson, bool) or isinstance(basejson, int) or isinstance(basejson, str): + handled = False + else: + handled = True + break currentsplitcnt += 1 @@ -2226,21 +2364,14 @@ class AppBase: break except IndexError as e: - print("[DEBUG] INDEXERROR: ", parsersplit[outercnt]) - break - + return "", False outercnt += 1 - + except KeyError as e: - print("[INFO] Lower keyerror: %s" % e) return "", False except Exception as e: - print("[WARNING] Exception: %s" % e) - return basejson, False - - #return basejson - #return "KeyError: Couldn't find key: %s" % e + return "", False return basejson, False @@ -2256,7 +2387,6 @@ class AppBase: baseresult = "" appendresult = "" - #print("[INFO] Parsersplit length: %d" % len(parsersplit)) if (actionname_lower.startswith("exec ") or actionname_lower.startswith("webhook ") or actionname_lower.startswith("schedule ") or actionname_lower.startswith("userinput ") or actionname_lower.startswith("email_trigger ") or actionname_lower.startswith("trigger ")) and len(parsersplit) == 1: record = False for char in actionname_lower: @@ -2276,17 +2406,15 @@ class AppBase: if actionname_lower == "exec" or actionname_lower == "webhook" or actionname_lower == "schedule" or actionname_lower == "userinput" or actionname_lower == "email_trigger" or actionname_lower == "trigger": baseresult = execution_data["execution_argument"] elif actionname_lower == "shuffle_cache": - print("[DEBUG] SHOULD GET CACHE KEY: %s" % parsersplit) if len(parsersplit) > 1: actual_key = parsersplit[1] - print("[DEBUG] KEY: %s" % actual_key) cachedata = self.get_cache(actual_key) - print("CACHE: %s" % cachedata) parsersplit.pop(1) try: baseresult = json.dumps(cachedata) except json.decoder.JSONDecodeError as e: - print("[WARNING] Failed json dumping: %s" % e) + pass + else: if execution_data["results"] != None: @@ -2296,7 +2424,6 @@ class AppBase: baseresult = result["result"] break else: - print("[DEBUG] No results to get values from.") baseresult = "$" + parsersplit[0][1:] if len(baseresult) == 0: @@ -2309,10 +2436,8 @@ class AppBase: break except KeyError as e: - #print("[INFO] KeyError wf variables: %s" % e) pass except TypeError as e: - #print("[INFO] TypeError wf variables: %s" % e) pass if len(baseresult) == 0: @@ -2323,34 +2448,27 @@ class AppBase: baseresult = variable["value"] break except KeyError as e: - #print("[INFO] KeyError exec variables: %s" % e) pass except TypeError as e: - #print("[INFO] TypeError exec variables: %s" % e) pass except KeyError as error: - print(f"[DEBUG] KeyError in JSON: {error}") - - #print(f"[INFO] After first trycatch. Baseresult")#, baseresult) + pass # 2. Find the JSON data # Returns if there isn't any JSON in the base ($nodename) if len(baseresult) == 0: return ""+appendresult, False - #print("[INFO] After second return") # Returns if the result is JUST something like $nodename, not $nodename.value if len(parsersplit) == 1: returndata = str(baseresult)+str(appendresult) - print("[DEBUG] RETURNING!")#: %s" % returndata) return returndata, False baseresult = baseresult.replace(" True,", " true,") baseresult = baseresult.replace(" False", " false,") # Tries to actually read it as JSON with some stupid formatting - #print("[INFO] After third parser return - Formatted")#, baseresult) basejson = {} try: basejson = json.loads(baseresult) @@ -2359,10 +2477,8 @@ class AppBase: baseresult = baseresult.replace("\'", "\"") basejson = json.loads(baseresult) except json.decoder.JSONDecodeError as e: - print(f"[ERROR] Parser issue with JSON for {baseresult}: {e}") return str(baseresult)+str(appendresult), False - print("[INFO] After fourth parser return as JSON") # Finds the ACTUAL value which is in the $nodename.value.test - focusing on value.test data, is_loop = recurse_json(basejson, parsersplit[1:]) parseditem = data @@ -2371,24 +2487,19 @@ class AppBase: try: parseditem = json.dumps(parseditem) except json.decoder.JSONDecodeError as e: - print("[WARNING] Parseditem issue: %s" % e) pass if is_loop: - print("[DEBUG] DATA IS A LOOP - SHOULD WRAP") if parsersplit[-1] == "#": - print("[WARNING] SET DATA WRAPPER TO NORMAL!") parseditem = "${SHUFFLE_NO_SPLITTER%s}$" % json.dumps(data) else: # Return value: ${id[12345, 45678]}$ - print("[WARNING] SET DATA WRAPPER TO %s!" % parsersplit[-1]) parseditem = "${%s%s}$" % (parsersplit[-1], json.dumps(data)) returndata = str(parseditem)+str(appendresult) # New in 0.8.97: Don't return items without lists - #self.logger.info("RETURNDATA: %s" % returndata) #return returndata, is_loop # 0.9.70: @@ -2402,6 +2513,8 @@ class AppBase: except json.decoder.JSONDecodeError as e: return returndata, is_loop + + # Sending self as it's not a normal function def parse_liquid(template, self): @@ -2413,21 +2526,26 @@ class AppBase: return template if "${" in template and "}$" in template: - self.logger.info("[DEBUG] Shuffle loop shouldn't run in liquid. Data length: %d" % len(template)) + #self.logger.info("[DEBUG] Shuffle loop shouldn't run in liquid. Data length: %d" % len(template)) return template - #if not "{{" in template or not "}}" in template: - # if not "{%" in template or not "%}" in template: - # self.logger.info("Skipping liquid - missing {{ }} and {% %}") - # return template - #if not "{{" in template or not "}}" in template: - # return template + # New pattern fixer to help with bad liquid formats + try: + newoutput = self.patternfix_string(template, + { + "{{|": '{{ "" |', + }, + { + r'\{\{\s*\$[^|}]+\s*\|': '{{ "" |', + } + , + inputtype="liquid" + ) - #self.logger.info(globals()) - #if len(template) > 100: - # self.logger.info("[DEBUG] Running liquid with data of length %d" % len(template)) - #self.logger.info(f"[DEBUG] Data: {template}") + template = newoutput + except Exception as e: + print("[ERROR] Failed liquid parsing fix: %s" % e) all_globals = globals() all_globals["self"] = self @@ -2488,7 +2606,6 @@ class AppBase: newlines = [] thisline = [] for line in template.split("\n"): - #print("LINE: %s" % repr(line)) if "\"\"\"" in line or "\'\'\'" in line: if replace: skip_next = True @@ -2499,7 +2616,6 @@ class AppBase: thisline.append(line) if skip_next == True: if len(thisline) > 0: - #print(thisline) newlines.append(" ".join(thisline)) thisline = [] @@ -2524,7 +2640,6 @@ class AppBase: except TypeError as e: try: if "string as left operand" in f"{e}": - #print(f"HANDLE REPLACE: {template}") split_left = template.split("|") if len(split_left) < 2: return template @@ -2544,8 +2659,6 @@ class AppBase: return run.render(**globals()) except Exception as e: - print(f"SubError in Liquid: {e}") - self.action["parameters"].append({ "name": "liquid_general_error", "value": f"There was general error Liquid input (2). Details: {e}", @@ -2586,7 +2699,6 @@ class AppBase: self.action_result["result"] = json.dumps(data) except Exception as e: self.action_result["result"] = f"Failed to parse LiquidPy: {error_msg}" - print("[WARNING] Failed to set LiquidPy result") self.action_result["completed_at"] = int(time.time_ns()) self.send_result(self.action_result, headers, stream_path) @@ -2620,15 +2732,12 @@ class AppBase: try: value = json.dumps(value) except: - print("[WARNING] Json parsing issue in recursed value") pass if value == "${%s}" % key: - print("[WARNING] Deleting %s because key = value" % key) deletekeys.append(key) continue elif "${" in value and "}" in value: - print("[WARNING] Deleting %s because it contains ${ and }" % key) deletekeys.append(key) continue @@ -2638,9 +2747,9 @@ class AppBase: except json.decoder.JSONDecodeError as e: # Since here the data isn't at all JSON compatible..? # Seems to happen with newlines in variables being parsed in as strings? - print(f"[ERROR] Failed JSON replacement for OpenAPI keys (3) {e}. Value: {data}") + pass except Exception as e: - print(f"[ERROR] Failed as an exception (1): {e}") + pass try: for deletekey in deletekeys: @@ -2649,7 +2758,6 @@ class AppBase: except: pass except Exception as e: - print(f"[WARNING] Failed in deletekeys: {e}") return data try: @@ -2670,13 +2778,12 @@ class AppBase: try: data = json.dumps(newvalue) except json.decoder.JSONDecodeError as e: - print("[WARNING] JsonDecodeError: %s" % e) data = newvalue except json.decoder.JSONDecodeError as e: - print("[WARNING] Failed JSON replacement for OpenAPI keys (2) {e}") + pass except Exception as e: - print(f"[WARNING] Failed as an exception (2): {e}") + pass return data @@ -2695,7 +2802,7 @@ class AppBase: value = value.replace("\\\'", "\'") value = value.replace("\'", "\\\'") except Exception as e: - print(f"[WARNING] Failed to fix json string value: {e}") + pass return value @@ -2713,7 +2820,8 @@ class AppBase: #match = ".*?([$]{1}([a-zA-Z0-9 _-]+\.?){1}([a-zA-Z0-9#_-]+\.?){0,})" #match = ".*?([$]{1}([a-zA-Z0-9_-]+\.?){1}([a-zA-Z0-9#_-]+\.?){0,})" # Removed space - no longer ok. Force underscore. - match = "([$]{1}([a-zA-Z0-9_-]+\.?){1}([a-zA-Z0-9#_-]+\.?){0,})" # Removed .*? to make it work with large amounts of data + #match = "([$]{1}([a-zA-Z0-9_-]+\.?){1}([a-zA-Z0-9#_-]+\.?){0,})" # Removed .*? to make it work with large amounts of data + match = "([$]{1}([a-zA-Z0-9_@-]+\.?){1}([a-zA-Z0-9#_@-]+\.?){0,})" # Added @ to the regex # Extra replacements for certain scenarios escaped_dollar = "\\$" @@ -2735,7 +2843,6 @@ class AppBase: # Basic fix in case variant isn't set # Variant is ALWAYS STATIC_VALUE from mid 2021~ try: - #self.logger.info(f"[DEBUG] Parameter '{paramname}' of length {len(parameter['value'])}") parameter["variant"] = parameter["variant"] except: parameter["variant"] = "STATIC_VALUE" @@ -2760,8 +2867,9 @@ class AppBase: # Handles for loops etc. # FIXME: Should it dump to string here? Doesn't that defeat the purpose? # Trying without string dumping. - + #self.logger.info("TO BE REPLACED: %s" % to_be_replaced) value, is_loop = get_json_value(fullexecution, to_be_replaced) + #self.logger.info(f"\n\nType of value: {type(value)}") if isinstance(value, str): # Could we take it here? @@ -2772,32 +2880,30 @@ class AppBase: # 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, 1) elif isinstance(value, dict) or isinstance(value, list): # Changed from JSON dump to str() 28.05.2021 # This makes it so the parameters gets lists and dicts straight up - parameter["value"] = parameter["value"].replace(to_be_replaced, json.dumps(value)) + parameter["value"] = parameter["value"].replace(to_be_replaced, json.dumps(value), 1) #try: - # parameter["value"] = parameter["value"].replace(to_be_replaced, str(value)) - #except: # parameter["value"] = parameter["value"].replace(to_be_replaced, json.dumps(value)) + #except: + # parameter["value"] = parameter["value"].replace(to_be_replaced, str(value)) # self.logger.info("Failed parsing value as string?") else: self.logger.error("[ERROR] Unknown type %s" % type(value)) try: - parameter["value"] = parameter["value"].replace(to_be_replaced, json.dumps(value)) + parameter["value"] = parameter["value"].replace(to_be_replaced, json.dumps(value), 1) except json.decoder.JSONDecodeError as e: - parameter["value"] = parameter["value"].replace(to_be_replaced, value) + parameter["value"] = parameter["value"].replace(to_be_replaced, value, 1) - #self.logger.info("VALUE: %s" % parameter["value"]) else: - self.logger.info(f"[ERROR] Not running static variant regex parsing (slow) on value with length {len(parameter['value'])}. Max is 5Mb~.") + #self.logger.info(f"[ERROR] Not running static variant regex parsing (slow) on value with length {len(parameter['value'])}. Max is 5Mb~.") + pass if parameter["variant"] == "WORKFLOW_VARIABLE": self.logger.info("[DEBUG] Handling workflow variable") @@ -2896,7 +3002,7 @@ class AppBase: return "", parameter["value"], is_loop def run_validation(sourcevalue, check, destinationvalue): - self.logger.info("[DEBUG] Checking %s '%s' %s" % (sourcevalue, check, destinationvalue)) + #self.logger.info("[DEBUG] Checking %s '%s' %s" % (sourcevalue, check, destinationvalue)) if check == "=" or check.lower() == "equals": if str(sourcevalue).lower() == str(destinationvalue).lower(): @@ -2936,7 +3042,6 @@ class AppBase: continue if item.strip() in sourcevalue: - print("[INFO] Found %s in %s" % (item, sourcevalue)) return True elif check.lower() == "larger than" or check.lower() == "bigger than": @@ -2969,7 +3074,7 @@ class AppBase: return True except AttributeError as e: - print("[WARNING] Condition smaller than failed with values %s and %s: %s" % (sourcevalue, destinationvalue, e)) + pass try: destinationvalue = len(json.loads(destinationvalue)) @@ -2987,10 +3092,8 @@ class AppBase: try: found = re.search(str(destinationvalue), str(sourcevalue)) except re.error as e: - print("[WARNING] Regex error in condition (re.error): %s" % e) return False except Exception as e: - print("[WARNING] Regex error in condition (catchall): %s" % e) return False if found == None: @@ -3017,30 +3120,6 @@ class AppBase: if action["id"] == fullexecution["start"]: return True, "" - # Need to validate if the source is a trigger or not - # need to remove branches that are not from trigger to the startnode to make it all work - #if "workflow" in fullexecution["workflow"] and "triggers" in fullexecution["workflow"]: - # cnt = 0 - # found_branch_indexes = [] - # for branch in fullexecution["workflow"]["branches"]: - # if branch["destination_id"] != action["id"]: - # continue - - # # Check if the source is a trigger - # # if we can't find it as trigger, remove the branch - # print("Found relevant branch: %s" % branch) - # for action in fullexecution["workflow"]["actions"]: - # if action["id"] == branch["source_id"]: - # found_branch_indexes.append(branch["source_id"]) - # break - - # if len(found_branch_indexes) > 0: - # for i in sorted(found_branch_indexes, reverse=True): - # fullexecution["workflow"]["branches"].pop(i) - - # print("Removed %d branches" % len(found_branch_indexes)) - #else: - # print("[WARNING] No branches or triggers found in fullexecution for startnode") except Exception as error: self.logger.info(f"[WARNING] Failed checking startnode: {error}") #return True, "" @@ -3106,8 +3185,6 @@ class AppBase: successful_conditions = 0 total_conditions = len(branch["conditions"]) for condition in branch["conditions"]: - self.logger.info("[DEBUG] Getting condition value of %s" % condition) - # Parse all values first here sourcevalue = condition["source"]["value"] check, sourcevalue, is_loop = parse_params(action, fullexecution, condition["source"], self) @@ -3147,7 +3224,7 @@ class AppBase: if matching_branches > 0 and correct_branches > 0: return True, "" - self.logger.info("[DEBUG] Correct branches vs matching branches: %d vs %d" % (correct_branches, matching_branches)) + #self.logger.info("[DEBUG] Correct branches vs matching branches: %d vs %d" % (correct_branches, matching_branches)) return False, {"success": False, "reason": "Minimum of one branch's conditions must be correct to continue. Total: %d of %d" % (correct_branches, matching_branches)} @@ -3182,11 +3259,11 @@ class AppBase: #tmpresult = tmpresult.replace("'", "\"") tmpresult = json.dumps(tmpresult) except json.decoder.JSONDecodeError as e: - self.logger.info(f"[WARNING] Failed condition parsing {tmpresult} to string") + pass + # IF branches fail: Exit! if not branchcheck: - self.logger.info("Failed one or more branch conditions.") self.action_result["result"] = tmpresult self.action_result["status"] = "SKIPPED" self.action_result["completed_at"] = int(time.time_ns()) @@ -3200,8 +3277,6 @@ class AppBase: if " " in actionname: actionname.replace(" ", "_", -1) - #print("ACTION: ", action) - #print("exec: ", self.full_execution) #if action.generated: # actionname = actionname.lower() @@ -3229,14 +3304,6 @@ class AppBase: # What variables are necessary here tho hmm params = {} - # This replacement should happen in backend as part of params - # error log is useless - #try: - # for item in action["authentication"]: - # self.logger.info("AUTH PARAM: ", key, value) - # #params[item["key"]] = item["value"] - #except KeyError as e: - # self.logger.info(f"[WARNING] No authentication specified! Is this correct? err: {e}") # Fixes OpenAPI body parameters for later. newparams = [] @@ -3265,9 +3332,7 @@ class AppBase: # but this has changed to do lists within items and such if parameter["name"] == "body": bodyindex = counter - #self.logger.info("PARAM: %s" % parameter) - # FIXMe: This should also happen after liquid & param parsing.. try: values = parameter["value_replace"] if values != None: @@ -3307,7 +3372,6 @@ class AppBase: pass - #self.logger.info(f"""HANDLING BODY: {action["parameters"][counter]["value"]}""") action["parameters"][counter]["value"] = recurse_cleanup_script(action["parameters"][counter]["value"]) #self.logger.info(action["parameters"]) @@ -3322,7 +3386,7 @@ class AppBase: try: self.original_action = json.loads(json.dumps(action)) except Exception as e: - self.logger.info(f"[ERROR] Failed parsing action as JSON to original action. This COULD have bad effects on LOOPED executions: {e}") + pass # calltimes is used to handle forloops in the app itself. # 2 kinds of loop - one in gui with one app each, and one like this, @@ -3343,8 +3407,8 @@ class AppBase: if check: raise Exception(json.dumps({ "success": False, - "reason": "Parameter {parameter} has an issue", "exception": f"Value Error: {check}", + "reason": "Parameter {parameter} has an issue", })) #if parameter["name"] == "body": @@ -3364,227 +3428,111 @@ class AppBase: except KeyError: pass - #self.logger.info("Return value: %s" % value) actionname = action["name"] - #self.logger.info("Multicheck ", actualitem) - #self.logger.info("ITEM LENGTH: %d, Actual item: %s" % (len(actualitem), actualitem)) + + # Loops in general goes in here to be parsed out as one->multi if len(actualitem) > 0: multiexecution = True - # Loop WITHOUT JSON variables go here. - # Loop WITH variables go in else. - self.logger.info("Before first part in multiexec!") handled = False # Has a loop without a variable used inside - if len(actualitem[0]) > 2 and actualitem[0][1] == "SHUFFLE_NO_SPLITTER": - - self.logger.info("(1) Pre replacement: %s" % actualitem[0][2]) - tmpitem = value - - index = 0 - replacement = actualitem[index][2] - if replacement.endswith("}$"): - replacement = replacement[:-2] - - if replacement.startswith("\"") and replacement.endswith("\""): - replacement = replacement[1:len(replacement)-1] - - self.logger.info("POST replacement: %s" % replacement) - - #json_replacement = tmpitem.replace(actualitem[index][0], replacement, 1) - #self.logger.info("AFTER POST replacement: %s" % json_replacement) - json_replacement = replacement + + # This is here to handle for loops within variables.. kindof + # 1. Find the length of the longest array + # 2. Build an array with the base values based on parameter["value"] + # 3. Get the n'th value of the generated list from values + # 4. Execute all n answers + replacements = {} + curminlength = 0 + for replace in actualitem: try: - json_replacement = json.loads(replacement) + to_be_replaced = replace[0] + actualitem = replace[2] + if actualitem.endswith("}$"): + actualitem = actualitem[:-2] + + except IndexError: + self.logger.info("[WARNING] Indexerror") + continue + + try: + itemlist = json.loads(actualitem) + if len(itemlist) > minlength: + minlength = len(itemlist) + + if len(itemlist) > curminlength: + curminlength = len(itemlist) + except json.decoder.JSONDecodeError as e: + self.logger.info("JSON Error (replace): %s in %s" % (e, actualitem)) + + replacements[to_be_replaced] = actualitem + + + # Parses the data as string with length, split etc. before moving on. + #self.logger.info("In second part of else: %s" % (len(itemlist))) + # This is a result array for JUST this value.. + # What if there are more? + resultarray = [] + for i in range(0, curminlength): + tmpitem = json.loads(json.dumps(parameter["value"])) + for key, value in replacements.items(): + replacement = value try: - replacement = replacement.replace("\'", "\"", -1) - json_replacement = json.loads(replacement) - except: - self.logger.info("JSON error singular: %s" % e) + replacement = json.dumps(json.loads(value)[i]) + except IndexError as e: + self.logger.info(f"[ERROR] Failed handling value parsing with index: {e}") + pass - if len(json_replacement) > minlength: - minlength = len(json_replacement) + if replacement.startswith("\"") and replacement.endswith("\""): + replacement = replacement[1:len(replacement)-1] - self.logger.info("PRE new_replacement") - - new_replacement = [] - for i in range(len(json_replacement)): - if isinstance(json_replacement[i], dict) or isinstance(json_replacement[i], list): - tmp_replacer = json.dumps(json_replacement[i]) - newvalue = tmpitem.replace(str(actualitem[index][0]), str(tmp_replacer), 1) - else: - newvalue = tmpitem.replace(str(actualitem[index][0]), str(json_replacement[i]), 1) + #except json.decoder.JSONDecodeError as e: + #self.logger.info("REPLACING %s with %s" % (key, replacement)) + #replacement = parse_wrapper_start(replacement) + tmpitem = tmpitem.replace(key, replacement, -1) try: - newvalue = parse_liquid(newvalue, self) + tmpitem = parse_liquid(tmpitem, self) except Exception as e: self.logger.info(f"[WARNING] Failed liquid parsing in loop (2): {e}") - try: - newvalue = json.loads(newvalue) - except json.decoder.JSONDecodeError as e: - self.logger.info("DECODER ERROR: %s" % e) - pass - - new_replacement.append(newvalue) - - self.logger.info("New replacement: %s" % new_replacement) - - # FIXME: Should this use new_replacement? - tmpitem = tmpitem.replace(actualitem[index][0], replacement, 1) # This code handles files. - resultarray = [] isfile = False try: - self.logger.info("(1) ------------ PARAM: %s" % parameter["schema"]["type"]) if parameter["schema"]["type"] == "file" and len(value) > 0: - self.logger.info("(1) SHOULD HANDLE FILE IN MULTI. Get based on value %s" % tmpitem) - # This is silly :) - # Q: Is there something wrong with the download system? - # It seems to return "FILE CONTENT: %s" with the ID as %s - for tmp_file_split in json.loads(tmpitem): - self.logger.info("(1) PRE GET FILE %s" % tmp_file_split) + self.logger.info("(2) SHOULD HANDLE FILE IN MULTI. Get based on value %s" % parameter["value"]) + + for tmp_file_split in json.loads(parameter["value"]): file_value = self.get_file(tmp_file_split) - self.logger.info("(1) POST AWAIT %s" % file_value) resultarray.append(file_value) - self.logger.info("(1) FILE VALUE FOR VAL %s: %s" % (tmp_file_split, file_value)) + isfile = True - except NameError as e: - self.logger.info("(1) SCHEMA NAMEERROR IN FILE HANDLING: %s" % e) except KeyError as e: - self.logger.info("(1) SCHEMA KEYERROR IN FILE HANDLING: %s" % e) + self.logger.info("(2) SCHEMA ERROR IN FILE HANDLING: %s" % e) except json.decoder.JSONDecodeError as e: - self.logger.info("(1) JSON ERROR IN FILE HANDLING: %s" % e) + self.logger.info("(2) JSON ERROR IN FILE HANDLING: %s" % e) if not isfile: - self.logger.info("Resultarray (NOT FILE): %s" % resultarray) - params[parameter["name"]] = tmpitem - multi_parameters[parameter["name"]] = new_replacement - else: - self.logger.info("Resultarray (FILE): %s" % resultarray) - params[parameter["name"]] = resultarray - multi_parameters[parameter["name"]] = resultarray + #tmpitem = tmpitem.replace("\\\\", "\\", -1) + resultarray.append(tmpitem) - #if len(resultarray) == 0: - # self.logger.info("[WARNING] Returning empty array because the array length to be looped is 0 (1)") - # action_result["status"] = "SUCCESS" - # action_result["result"] = "[]" - # self.send_result(action_result, headers, stream_path) - # return + # With this parameter ready, add it to... a greater list of parameters. Rofl + if len(resultarray) == 0: + self.logger.info("[WARNING] Returning empty array because the array length to be looped is 0 (0)") + self.action_result["status"] = "SUCCESS" + self.action_result["result"] = "[]" + self.send_result(self.action_result, headers, stream_path) + return - multi_execution_lists.append(new_replacement) - #self.logger.info("MULTI finished: %s" % json_replacement) - else: - self.logger.info(f"(2) Pre replacement (loop with variables). Variables: {actualitem}") #% actualitem) - # This is here to handle for loops within variables.. kindof - # 1. Find the length of the longest array - # 2. Build an array with the base values based on parameter["value"] - # 3. Get the n'th value of the generated list from values - # 4. Execute all n answers - replacements = {} - curminlength = 0 - for replace in actualitem: - try: - to_be_replaced = replace[0] - actualitem = replace[2] - if actualitem.endswith("}$"): - actualitem = actualitem[:-2] + #self.logger.info("RESULTARRAY: %s" % resultarray) + if resultarray not in multi_execution_lists: + multi_execution_lists.append(resultarray) - except IndexError: - self.logger.info("[WARNING] Indexerror") - continue - - #self.logger.info(f"\n\nTMPITEM: {actualitem}\n\n") - #actualitem = parse_wrapper_start(actualitem) - #self.logger.info(f"\n\nTMPITEM2: {actualitem}\n\n") - - try: - itemlist = json.loads(actualitem) - if len(itemlist) > minlength: - minlength = len(itemlist) - - if len(itemlist) > curminlength: - curminlength = len(itemlist) - - except json.decoder.JSONDecodeError as e: - self.logger.info("JSON Error (replace): %s in %s" % (e, actualitem)) - - replacements[to_be_replaced] = actualitem - - - # Parses the data as string with length, split etc. before moving on. - - - #self.logger.info("In second part of else: %s" % (len(itemlist))) - # This is a result array for JUST this value.. - # What if there are more? - resultarray = [] - for i in range(0, curminlength): - tmpitem = json.loads(json.dumps(parameter["value"])) - for key, value in replacements.items(): - replacement = value - try: - replacement = json.dumps(json.loads(value)[i]) - except IndexError as e: - self.logger.info(f"[ERROR] Failed handling value parsing with index: {e}") - pass - - if replacement.startswith("\"") and replacement.endswith("\""): - replacement = replacement[1:len(replacement)-1] - #except json.decoder.JSONDecodeError as e: - - #self.logger.info("REPLACING %s with %s" % (key, replacement)) - #replacement = parse_wrapper_start(replacement) - tmpitem = tmpitem.replace(key, replacement, -1) - try: - tmpitem = parse_liquid(tmpitem, self) - except Exception as e: - self.logger.info(f"[WARNING] Failed liquid parsing in loop (2): {e}") - - - # This code handles files. - self.logger.info("(2) ------------ PARAM: %s" % parameter["schema"]["type"]) - isfile = False - try: - if parameter["schema"]["type"] == "file" and len(value) > 0: - self.logger.info("(2) SHOULD HANDLE FILE IN MULTI. Get based on value %s" % parameter["value"]) - - for tmp_file_split in json.loads(parameter["value"]): - self.logger.info("(2) PRE GET FILE %s" % tmp_file_split) - file_value = self.get_file(tmp_file_split) - self.logger.info("(2) POST AWAIT %s" % file_value) - resultarray.append(file_value) - self.logger.info("(2) FILE VALUE FOR VAL %s: %s" % (tmp_file_split, file_value)) - - - isfile = True - except KeyError as e: - self.logger.info("(2) SCHEMA ERROR IN FILE HANDLING: %s" % e) - except json.decoder.JSONDecodeError as e: - self.logger.info("(2) JSON ERROR IN FILE HANDLING: %s" % e) - - if not isfile: - tmpitem = tmpitem.replace("\\\\", "\\", -1) - resultarray.append(tmpitem) - - # With this parameter ready, add it to... a greater list of parameters. Rofl - self.logger.info("LENGTH OF ARR: %d" % len(resultarray)) - if len(resultarray) == 0: - self.logger.info("[WARNING] Returning empty array because the array length to be looped is 0 (0)") - self.action_result["status"] = "SUCCESS" - self.action_result["result"] = "[]" - self.send_result(self.action_result, headers, stream_path) - return - - #self.logger.info("RESULTARRAY: %s" % resultarray) - if resultarray not in multi_execution_lists: - multi_execution_lists.append(resultarray) - - multi_parameters[parameter["name"]] = resultarray + multi_parameters[parameter["name"]] = resultarray else: # Parses things like int(value) #self.logger.info("[DEBUG] Normal parsing (not looping)")#with data %s" % value) @@ -3596,7 +3544,7 @@ class AppBase: if str(value).startswith("b'") and str(value).endswith("'"): value = value[2:-1] except Exception as e: - print(f"Value rawbytes Exception: {e}") + pass params[parameter["name"]] = value multi_parameters[parameter["name"]] = value @@ -3613,13 +3561,11 @@ class AppBase: except KeyError as e: self.logger.info("SCHEMA ERROR IN FILE HANDLING: %s" % e) - - #remove_params.append(parameter["name"]) + # Fix lists here # FIXME: This doesn't really do anything anymore - self.logger.info("[DEBUG] CHECKING multi execution list: %d!" % len(multi_execution_lists)) + #self.logger.info("[DEBUG] CHECKING multi execution list: %d!" % len(multi_execution_lists)) if len(multi_execution_lists) > 0: - self.logger.info("\n [DEBUG] Multi execution list has more data: %d" % len(multi_execution_lists)) filteredlist = [] for listitem in multi_execution_lists: if listitem in filteredlist: @@ -3668,18 +3614,17 @@ class AppBase: if not multiexecution: # Runs a single iteration here new_params = self.validate_unique_fields(params) - self.logger.info(f"[DEBUG] Returned with newparams of length {len(new_params)}") if isinstance(new_params, list) and len(new_params) == 1: params = new_params[0] else: - 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["result"] = f"A non-unique value was found" self.action_result["completed_at"] = int(time.time_ns()) self.send_result(self.action_result, headers, stream_path) return - self.logger.info("[INFO] Running normal execution (not loop)\n\n") + #self.logger.info("[INFO] Running normal execution (not loop)\n\n") # Added literal evaluation of anything resembling a string # The goal is to parse objects that e.g. use single quotes and the like @@ -3742,6 +3687,7 @@ class AppBase: self.logger.info(f"[ERROR] Failed parsing timeout to int: {e}") #timeout = 30 + self.logger.info("[DEBUG][%s] Running function '%s' with timeout %d" % (self.current_execution_id, action["name"], timeout)) try: executor = concurrent.futures.ThreadPoolExecutor() @@ -3753,20 +3699,19 @@ class AppBase: future.cancel() newres = json.dumps({ "success": False, - "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), + "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, }) else: # The future is done, so we can just get the result from newres :) #newres = future.result() - #print("Future is done!") pass except concurrent.futures.TimeoutError as e: newres = json.dumps({ "success": False, - "reason": "Timeout error within %d seconds (2). This happens if we can't reach or use the API you're trying to use within the time limit. Configure SHUFFLE_APP_SDK_TIMEOUT=100 in Orborus to increase it to 100 seconds. Not changeable for cloud." % timeout, + "reason": "Timeout error (2) 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 @@ -3786,8 +3731,8 @@ class AppBase: newres = json.dumps({ "success": False, - "reason": "An exception occurred while running this function (1). See exception for more details and contact support if this persists (support@shuffler.io)", "exception": f"{type(e).__name__} - {e}", + "reason": "An exception occurred while running this function (1). See exception for more details and contact support if this persists (support@shuffler.io)", }) break elif "got an unexpected keyword argument" in errorstring: @@ -3803,8 +3748,8 @@ class AppBase: else: newres = json.dumps({ "success": False, - "reason": "You may be running an old version of this action. Try remaking the node, then contact us at support@shuffler.io if it doesn't work with all these details.", "exception": f"TypeError: {e}", + "reason": "You may be running an old version of this action. Try remaking the node, then contact us at support@shuffler.io if it doesn't work with all these details.", }) break except Exception as e: @@ -3817,8 +3762,8 @@ class AppBase: newres = json.dumps({ "success": False, - "reason": "An exception occurred while running this function (2). See exception for more details and contact support if this persists (support@shuffler.io)", "exception": f"{type(e).__name__} - {e}", + "reason": "An exception occurred while running this function (2). See exception for more details and contact support if this persists (support@shuffler.io)", }) break @@ -3841,14 +3786,13 @@ class AppBase: except Exception as e: self.logger.warning("[ERROR] Failed to parse coroutine value for old app: {e}") - self.logger.info("\n\n\n[INFO] Returned from execution with type(s) %s" % type(newres)) + #self.logger.info("\n\n\n[INFO] Returned from execution with type(s) %s" % type(newres)) #self.logger.info("\n[INFO] Returned from execution with %s of types %s" % (newres, type(newres)))#, newres) if isinstance(newres, tuple): - self.logger.info(f"[INFO] Handling return as tuple: {newres}") + #self.logger.info(f"[INFO] Handling return as tuple: {newres}") # Handles files. filedata = "" file_ids = [] - self.logger.info("TUPLE: %s" % newres[1]) if isinstance(newres[1], list): self.logger.info("[INFO] HANDLING LIST FROM RET") file_ids = self.set_files(newres[1]) @@ -3869,7 +3813,7 @@ class AppBase: result = json.dumps(tmp_result) elif isinstance(newres, str): - self.logger.info("[INFO] Handling return as string of length %d" % len(newres)) + #self.logger.info("[INFO] Handling return as string of length %d" % len(newres)) result += newres elif isinstance(newres, dict) or isinstance(newres, list): try: @@ -3880,7 +3824,7 @@ class AppBase: result += str(newres) except ValueError: result += "Failed autocasting. Can't handle %s type from function. Must be string" % type(newres) - self.logger.info("Can't handle type %s value from function" % (type(newres))) + self.logger.info("[ERROR] Can't handle type %s value from function" % (type(newres))) except Exception as e: self.logger.info("[ERROR] Failed to json dump. Returning as string.") result += str(newres) @@ -3891,13 +3835,13 @@ class AppBase: result += "Failed autocasting. Can't handle %s type from function. Must be string" % type(newres) self.logger.info("Can't handle type %s value from function" % (type(newres))) - #self.logger.info("[INFO] POST NEWRES RESULT!")#, result) else: #self.logger.info("[INFO] APP_SDK DONE: Starting MULTI execution (length: %d) with values %s" % (minlength, multi_parameters)) # 1. Use number of executions based on the arrays being similar # 2. Find the right value from the parsed multi_params - self.logger.info("[INFO] Running WITHOUT outer loop (looping)") + #self.logger.info("[INFO] Running WITH loop. MULTI: %s", multi_parameters) + self.logger.info("[INFO] Running WITH loop") json_object = False #results = await self.run_recursed_items(func, multi_parameters, {}) results = self.run_recursed_items(func, multi_parameters, {}) @@ -3907,7 +3851,6 @@ class AppBase: # Dump the result as a string of a list #self.logger.info("RESULTS: %s" % results) if isinstance(results, list) or isinstance(results, dict): - self.logger.info(f"JSON OBJECT? {json_object}") # This part is weird lol if json_object: @@ -3946,7 +3889,7 @@ class AppBase: if self.action_result["result"] == "": self.action_result["result"] = result - self.logger.debug(f"[DEBUG] Executed {action['label']}-{action['id']}")#with result: {result}") + #self.logger.debug(f"[DEBUG] Executed {action['label']}-{action['id']}")#with result: {result}") #self.logger.debug(f"Data: %s" % action_result) except TypeError as e: self.logger.info("[ERROR] TypeError issue: %s" % e) @@ -4025,16 +3968,16 @@ class AppBase: logger = logging.getLogger(f"{cls.__name__}") logger.setLevel(logging.DEBUG) - logger.info("[DEBUG] Normal execution.") + #logger.info("[DEBUG] Normal execution.") ############################################## exposed_port = os.getenv("SHUFFLE_APP_EXPOSED_PORT", "") - logger.info(f"[DEBUG] \"{runtime}\" - run indicates microservices. Port: \"{exposed_port}\"") + #logger.info(f"[DEBUG] \"{runtime}\" - run indicates microservices. Port: \"{exposed_port}\"") if runtime == "run" and exposed_port != "": # Base port is 33334. Exposed port may differ based on discovery from Worker port = int(exposed_port) - logger.info(f"[DEBUG] Starting webserver on port {port} (same as exposed port)") + #logger.info(f"[DEBUG] Starting webserver on port {port} (same as exposed port)") from flask import Flask, request from waitress import serve @@ -4049,7 +3992,6 @@ class AppBase: @flask_app.route("/api/v1/run", methods=["POST"]) def execute(): if request.method == "POST": - #print(request.get_json(force=True)) requestdata = {} try: requestdata = json.loads(request.data) @@ -4059,8 +4001,6 @@ class AppBase: "reason": f"Invalid Action data {e}", } - #logger.info(f"[DEBUG] Datatype: {type(requestdata)}: {requestdata}") - # Remaking class for each request app = cls(redis=None, logger=logger, console_logger=logger) @@ -4071,41 +4011,33 @@ class AppBase: try: app.full_execution = json.dumps(requestdata["workflow_execution"]) except Exception as e: - logger.info(f"[ERROR] Failed parsing full execution from workflow_execution: {e}") extra_info += f"\n{e}" try: app.action = requestdata["action"] except Exception as e: - logger.info(f"[ERROR] Failed parsing action: {e}") extra_info += f"\n{e}" try: app.authorization = requestdata["authorization"] app.current_execution_id = requestdata["execution_id"] except Exception as e: - logger.info(f"[ERROR] Failed parsing auth and exec id: {e}") extra_info += f"\n{e}" # BASE URL (backend) try: app.url = requestdata["url"] - logger.info(f"BACKEND URL (url): {app.url}") except Exception as e: - logger.info(f"[ERROR] Failed parsing url (backend): {e}") extra_info += f"\n{e}" # URL (worker) try: app.base_url = requestdata["base_url"] - logger.info(f"WORKER URL (base url): {app.base_url}") except Exception as e: - logger.info(f"[ERROR] Failed parsing base url (worker): {e}") extra_info += f"\n{e}" #await app.execute_action(app.action) - logger.info("[DEBUG] Done awaiting app action running") except Exception as e: return { "success": False, @@ -4148,12 +4080,12 @@ class AppBase: # Has to start like this due to imports in other apps # Move it outside everything? app = cls(redis=None, logger=logger, console_logger=logger) - #logger.info(f"[DEBUG] Action: {action}") if isinstance(action, str): - logger.info("[DEBUG] Normal execution (env var). Action is a string.") + #logger.info("[DEBUG] Normal execution (env var). Action is a string.") + pass elif isinstance(action, object): - logger.info("[DEBUG] OBJECT execution (cloud). Action is NOT a string.") + #logger.info("[DEBUG] OBJECT execution (cloud). Action is NOT a string.") app.action = action try: @@ -4174,7 +4106,8 @@ class AppBase: except: pass else: - self.logger.info("ACTION TYPE (unhandled): %s" % type(action)) + #self.logger.info("ACTION TYPE (unhandled): %s" % type(action)) + pass app.execute_action(app.action) diff --git a/backend/app_sdk/autocorrect_test.py b/backend/app_sdk/autocorrect_test.py new file mode 100644 index 00000000..a7624c76 --- /dev/null +++ b/backend/app_sdk/autocorrect_test.py @@ -0,0 +1,187 @@ +import re +import json + +input_data = """{ + "test4": $test, + "test5": , + "test6": "what" + } +""" + +input_data = """{ + "test0": {{ '' | default: [] }}, + "test": {{ | default: [] }}, + "test2": {{ $test.asd | default: [] }}, + "test3": {{ {"key": "val} | default: [] }}, + "test4": $test, + "test5": , + "test6": "what" + } +""" + + +liquiddata = "{{ $test.asd | some other stuff {{ $test.xyz | more stuff" +pattern = r'\{\{\s*\$[^|}]+\s*\|' + +replaced_data = re.sub(pattern, "{{ '' |", liquiddata) +print(replaced_data) + + +def patternfix_string(liquiddata, patterns, regex_patterns, inputtype="liquid"): + if not inputtype or inputtype == "liquid": + if "{{" not in liquiddata or "}}" not in liquiddata: + return liquiddata + elif inputtype == "json": + liquiddata = liquiddata.strip() + + # Validating if it looks like json or not + if liquiddata[0] == "{" and liquiddata[len(liquiddata)-1] == "}": + pass + else: + if liquiddata[0] == "[" and liquiddata[len(liquiddata)-1] == "]": + pass + else: + return liquiddata + + # If it's already json, don't touch it + try: + json.loads(liquiddata) + return liquiddata + except Exception as e: + pass + else: + print("No replace handler for %s" % inputtype) + return liquiddata + + skipkeys = [" "] + newoutput = liquiddata[:] + for pattern in patterns: + keylocations = [] + parsedvalue = "" + record = False + index = -1 + for key in liquiddata: + + # Return instant if possible + if inputtype == "json": + try: + json.loads(newoutput) + return newoutput + except: + pass + + index += 1 + if not key: + if record: + keylocations.append(index) + parsedvalue += key + + continue + + if key in skipkeys: + if record: + keylocations.append(index) + parsedvalue += key + + continue + + if key == pattern[0] and not record: + record = True + + if key not in pattern: + keylocations = [] + parsedvalue = "" + record = False + + if record: + keylocations.append(index) + parsedvalue += key + + if len(parsedvalue) == 0: + continue + + evaluated_value = parsedvalue[:] + for skipkey in skipkeys: + evaluated_value = "".join(evaluated_value.split(skipkey)) + + if evaluated_value == pattern: + #print("Found matching: %s (%s)" % (parsedvalue, keylocations)) + #print("Should replace with: %s" % patterns[pattern]) + + newoutput = newoutput.replace(parsedvalue, patterns[pattern], -1) + + # Return instant if possible + if inputtype == "json": + try: + json.loads(newoutput) + return newoutput + except: + pass + + + for pattern in regex_patterns: + newlines = [] + for line in newoutput.split("\n"): + replaced_line = re.sub(pattern, regex_patterns[pattern], line) + newlines.append(replaced_line) + + newoutput = "\n".join(newlines) + + # Return instant if possible + if inputtype == "json": + try: + json.loads(newoutput) + return newoutput + except: + pass + + # Dont return json properly unless actually json + if inputtype == "json": + try: + json.loads(newoutput) + return newoutput + except: + # Returns original if json fixing didn't work + return liquiddata + + return newoutput + +print("Start:\n%s" % input_data) + +try: + newinput = patternfix_string(input_data, + { + "{{|": '{{ "" |', + }, + { + #r'\{\{\s*|': "{{ '' |", + r'\{\{\s*\$[^|}]+\s*\|': '{{ "" |', + } + , + inputtype="liquid" + ) +except Exception as e: + print("[ERROR} Failed liquid parsing fix: %s" % e) + newinput = input_data + +try: + newinput = patternfix_string(newinput, + { + }, + { + r'\"\s*\:\s*,': '\": "",', + r'\"\s*\:\s*\$[^,]+\w*\,': '\": "",', + } + , + inputtype="json" + ) + + try: + json.loads(newinput) + print("It's json! Override.") + except Exception as e: + print("Bad json. DONT use the value at all: %s" % e) +except Exception as e: + print("[ERROR} Failed json parsing fix: %s" % e) + +print("\nEnd:\n%s" % newinput) diff --git a/backend/app_sdk/recurse_test.py b/backend/app_sdk/recurse_test.py index 9dfbd2c5..1cf2bfea 100644 --- a/backend/app_sdk/recurse_test.py +++ b/backend/app_sdk/recurse_test.py @@ -58,7 +58,7 @@ def recurse_json(basejson, parsersplit): # Means it's a single item -> continue if seconditem == "": - print("[INFO] In first - handling %s. Len: %d" % (firstitem, len(basejson))) + #print("[INFO] In first - handling %s. Len: %d" % (firstitem, len(basejson))) if str(firstitem).lower() == "max" or str(firstitem).lower() == "last" or str(firstitem).lower() == "end": firstitem = len(basejson)-1 elif str(firstitem).lower() == "min" or str(firstitem).lower() == "first": @@ -66,14 +66,14 @@ def recurse_json(basejson, parsersplit): else: firstitem = int(firstitem) - print(f"[DEBUG] Post lower checks with item {firstitem}") + #print(f"[DEBUG] Post lower checks with item {firstitem}") tmpitem = basejson[int(firstitem)] try: newvalue, is_loop = recurse_json(tmpitem, parsersplit[outercnt+1:]) except IndexError: newvalue, is_loop = (tmpitem, parsersplit[outercnt+1:]) else: - print("[INFO] In ELSE - handling %s and %s" % (firstitem, seconditem)) + #print("[INFO] In ELSE - handling %s and %s" % (firstitem, seconditem)) if isinstance(firstitem, str): if firstitem.lower() == "max" or firstitem.lower() == "last" or firstitem.lower() == "end": firstitem = len(basejson)-1 @@ -94,7 +94,7 @@ def recurse_json(basejson, parsersplit): else: seconditem = int(seconditem) - print(f"[DEBUG] Post lower checks 2: {firstitem} AND {seconditem}") + #print(f"[DEBUG] Post lower checks 2: {firstitem} AND {seconditem}") newvalue = [] if int(seconditem) > len(basejson): seconditem = len(basejson) @@ -106,7 +106,7 @@ def recurse_json(basejson, parsersplit): try: ret, tmp_loop = recurse_json(basejson[i], parsersplit[outercnt+1:]) except IndexError: - print("[DEBUG] INDEXERROR: ", parsersplit[outercnt]) + #print("[DEBUG] INDEXERROR (1): ", parsersplit[outercnt]) #ret = innervalue ret, tmp_loop = recurse_json(basejson[i], parsersplit[outercnt:]) @@ -115,20 +115,18 @@ def recurse_json(basejson, parsersplit): return newvalue, is_loop else: - print("IN ELSE WITH VALUE: %s" % value) if len(value) == 0: return basejson, False try: - print("PRINT:", basejson) if isinstance(basejson, list): - print("[WARNING] VALUE IN ISINSTANCE IS NOT TO BE USED (list): %s" % value) + #print("[WARNING] VALUE IN ISINSTANCE IS NOT TO BE USED (list): %s" % value) return basejson, False elif isinstance(basejson, bool): - print("[WARNING] VALUE IN ISINSTANCE IS NOT TO BE USED (bool): %s" % value) + #print("[WARNING] VALUE IN ISINSTANCE IS NOT TO BE USED (bool): %s" % value) return basejson, False elif isinstance(basejson, int): - print("[WARNING] VALUE IN ISINSTANCE IS NOT TO BE USED (int): %s" % value) + #print("[WARNING] VALUE IN ISINSTANCE IS NOT TO BE USED (int): %s" % value) return basejson, False elif isinstance(basejson[value], str): try: @@ -136,8 +134,16 @@ def recurse_json(basejson, parsersplit): basejson = json.loads(basejson[value]) else: # Should we sanitize here? - print("[DEBUG] VALUE TO SANITIZE?: %s" % basejson[value]) - return str(basejson[value]), False + #print("[DEBUG] VALUE TO SANITIZE FOR KEY '%s'?: %s" % (value, basejson[value])) + + # Check if we are on the last item? + if outercnt == len(parsersplit)-1: + #print("[DEBUG] LAST KEY") + return str(basejson[value]), False + else: + #print("[DEBUG] NOT LAST KEY") + pass + except json.decoder.JSONDecodeError as e: return str(basejson[value]), False else: @@ -151,42 +157,60 @@ def recurse_json(basejson, parsersplit): try: if isinstance(basejson, list): - print("[WARNING] VALUE IN ISINSTANCE IS NOT TO BE USED (list): %s" % value) + #print("[WARNING] VALUE IN ISINSTANCE IS NOT TO BE USED (list): %s" % value) return basejson, False elif isinstance(basejson, bool): - print("[WARNING] VALUE IN ISINSTANCE IS NOT TO BE USED (bool): %s" % value) + #print("[WARNING] VALUE IN ISINSTANCE IS NOT TO BE USED (bool): %s" % value) return basejson, False elif isinstance(basejson, int): - print("[WARNING] VALUE IN ISINSTANCE IS NOT TO BE USED (int): %s" % value) + #print("[WARNING] VALUE IN ISINSTANCE IS NOT TO BE USED (int): %s" % value) return basejson, False elif isinstance(basejson[value], str): - print(f"[INFO] LOADING STRING '%s' AS JSON" % basejson[value]) + #print(f"[INFO] LOADING STRING '%s' AS JSON" % basejson[value]) try: - print("[DEBUG] BASEJSON: %s" % basejson) + #print("[DEBUG] BASEJSON: %s" % basejson) if (basejson[value].endswith("}") and basejson[value].endswith("}")) or (basejson[value].startswith("[") and basejson[value].endswith("]")): basejson = json.loads(basejson[value]) else: - return str(basejson[value]), False + + if outercnt == len(parsersplit)-1: + #print("LAST KEY (2)") + return str(basejson[value]), False + else: + #print("NOT LAST KEY (2)") + pass + except json.decoder.JSONDecodeError as e: - print("[DEBUG] RETURNING BECAUSE '%s' IS A NORMAL STRING (1)" % basejson[value]) + #print("[DEBUG] RETURNING BECAUSE '%s' IS A NORMAL STRING (1)" % basejson[value]) return str(basejson[value]), False else: basejson = basejson[value] except KeyError as e: - print("\n\n[WARNING] Running third dot notation fix %s: %s" % (value, e)) + # Check if previous key was handled or not + previouskey = parsersplit[outercnt-1] + #print("[DEBUG] PREVIOUS KEY: ", previouskey) + + tmpval = previouskey + "." + value + #print("\n\n[WARNING] Running third dot notation fix '%s' on data %s: %s" % (value, basejson, e)) + if tmpval in basejson: + return basejson[tmpval], False try: - currentsplitcnt = splitcnt + recursed_value = value handled = False + + #tmpbase = basejson + previouskey = value while True: + #print("\n\n[DEBUG] CURRENTSPLITCNT: ", currentsplitcnt) newvalue = parsersplit[currentsplitcnt+1] if newvalue == "#" or newvalue == "": break recursed_value += "." + newvalue - print("\n\nRECURSED: ", recursed_value) + #print("\n\nRECURSED: ", recursed_value) found = False for key, value in basejson.items(): @@ -194,13 +218,27 @@ def recurse_json(basejson, parsersplit): found = True if found == False: - print("[INFO] DIDN'T FIND similar VALUE: ", recursed_value) - break + #print("[INFO] DIDN'T FIND similar VALUE: ", recursed_value) + + # Check if we are on the last key or not + return "", False + #if outercnt == len(parsersplit)-1: + # print("[DEBUG] LAST KEY (3)") + # break + #else: + # print("[DEBUG] NOT LAST KEY (3)") + # return "", False if recursed_value in basejson: - print("[INFO] FOUND RECURSED VALUE: ", recursed_value) + #print("[INFO] FOUND RECURSED VALUE: ", recursed_value) basejson = basejson[recursed_value] - handled = True + + # Whether to dig deeper or not + if isinstance(basejson, bool) or isinstance(basejson, int) or isinstance(basejson, str): + handled = False + else: + handled = True + break currentsplitcnt += 1 @@ -210,8 +248,8 @@ def recurse_json(basejson, parsersplit): break except IndexError as e: - print("[DEBUG] INDEXERROR: ", parsersplit[outercnt]) - break + print("[DEBUG] INDEXERROR (2):", parsersplit[outercnt]) + return "", False outercnt += 1 @@ -220,10 +258,7 @@ def recurse_json(basejson, parsersplit): return "", False except Exception as e: print("[WARNING] Exception: %s" % e) - return basejson, False - - #return basejson - #return "KeyError: Couldn't find key: %s" % e + return "", False return basejson, False @@ -231,21 +266,61 @@ print("[INFO] Starting") #input_data = "test" #input_data = "test2.data" -#input_data = "test2.test3.data" -input_data = "test2.test5.data.hello" -parsersplit = input_data.split(".") + + +# Matchwith basejson = { "test": "hello", "test2": { - "data": "hello2", + "test3": "hello2", "test3.data": "hello3", "test4.data.testing": { "value": "hello4" }, - "test5.data.hello": "wut" + "test5.data.hello": "wut", }, + "test3": ["hello", "hello2", "hello3"], + "test4": [{ + "id": "1", + }] } -ret, is_loop = recurse_json(basejson, parsersplit) -print("\n\nOUTPUT RET (%s): %s" % (input_data, ret)) +# Inputexamples (ALL should be True) +inputs = { + #"": "", + "badkey": "", + "test": "hello", + "test2.badkey": "", + "test2.test3": "hello2", + "test2.test3.data": "hello3", + "test2.test4.data.testing": "{'value': 'hello4'}", # FIXME: Doesn't work due to break vs return "", False in last exception + "test2.test4.data.testing.value": "hello4", # FIXME: Doesn't work due to break vs return "", False in last exception. Not fixed as we didn't find one of these yet. + "test2.test5.data.hello": "wut", + "test2.test5.data.badkey": "", + "test3.#1": "hello2", + "test4.#0.id": "1", + "test4.#1.id": "", +} + +outputs = [] +for key, value in inputs.items(): + parsersplit = key.split(".") + ret, is_loop = recurse_json(basejson, parsersplit) + print("\n\nOUTPUT RET (%s): %s" % (key, ret)) + + outputs.append("[%s]: %s = '%s' vs '%s'" % (str(ret) == str(value), key, ret, value)) + +print("\n\n%s" % "\n".join(outputs)) + +#input_data = "" +#input_data = "badkey" +#input_data = "test" +#input_data = "test2.data" +#input_data = "test2.test3.data" +#input_data = "test2.test4.data.testing.value.as" +#input_data = "test2.test5.data.hello" + + + + diff --git a/backend/go-app/docker.go b/backend/go-app/docker.go index a4565523..03312201 100755 --- a/backend/go-app/docker.go +++ b/backend/go-app/docker.go @@ -20,6 +20,7 @@ import ( //"github.com/docker/docker" "github.com/docker/docker/api/types" //"github.com/docker/docker/api/types/container" + "github.com/docker/docker/api/types/image" "github.com/docker/docker/client" newdockerclient "github.com/fsouza/go-dockerclient" "github.com/go-git/go-billy/v5" @@ -565,7 +566,7 @@ func imageCheckBuilder(images []string) error { return err } - filteredImages := []types.ImageSummary{} + filteredImages := []image.Summary{} for _, image := range allImages { found := false for _, repoTag := range image.RepoTags { @@ -636,10 +637,10 @@ func getDockerImage(resp http.ResponseWriter, request *http.Request) { All: true, }) - img := types.ImageSummary{} + img := image.Summary{} tagFound := "" - img2 := types.ImageSummary{} + img2 := image.Summary{} tagFound2 := "" alternativeNameSplit := strings.Split(version.Name, "/") @@ -798,7 +799,7 @@ func getDockerImage(resp http.ResponseWriter, request *http.Request) { // Downloads and activates an app from shuffler.io if possible func handleRemoteDownloadApp(resp http.ResponseWriter, ctx context.Context, user shuffle.User, appId string) { url := fmt.Sprintf("https://shuffler.io/api/v1/apps/%s/config", appId) - log.Printf("Downloading API from %s", url) + log.Printf("[DEBUG] Downloading API from URL %s", url) req, err := http.NewRequest( "GET", url, @@ -812,7 +813,7 @@ func handleRemoteDownloadApp(resp http.ResponseWriter, ctx context.Context, user return } - httpClient := &http.Client{} + httpClient := shuffle.GetExternalClient(url) newresp, err := httpClient.Do(req) if err != nil { log.Printf("[ERROR] Failed running auto-download request for %s: %s", appId, err) @@ -822,6 +823,7 @@ func handleRemoteDownloadApp(resp http.ResponseWriter, ctx context.Context, user } defer newresp.Body.Close() + respBody, err := ioutil.ReadAll(newresp.Body) if err != nil { log.Printf("[ERROR] Failed setting respbody for workflow download: %s", err) diff --git a/backend/go-app/go.mod b/backend/go-app/go.mod index fbb6f872..6e4b2ef2 100644 --- a/backend/go-app/go.mod +++ b/backend/go-app/go.mod @@ -1,122 +1,149 @@ -module shuffle-shared +module shuffle + +go 1.22.0 // replace github.com/shuffle/shuffle-shared => ../../../shuffle-shared -go 1.19 +toolchain go1.22.2 require ( - cloud.google.com/go/datastore v1.11.0 - cloud.google.com/go/storage v1.30.1 + cloud.google.com/go/datastore v1.15.0 + cloud.google.com/go/storage v1.40.0 github.com/basgys/goxml2json v1.1.0 github.com/carlescere/scheduler v0.0.0-20170109141437-ee74d2f83d82 - github.com/docker/docker v24.0.2+incompatible + github.com/docker/docker v26.1.0+incompatible github.com/frikky/kin-openapi v0.42.0 - github.com/fsouza/go-dockerclient v1.9.7 + github.com/fsouza/go-dockerclient v1.11.0 github.com/ghodss/yaml v1.0.0 github.com/go-git/go-billy/v5 v5.5.0 - github.com/go-git/go-git/v5 v5.11.0 - github.com/gorilla/mux v1.8.0 + github.com/go-git/go-git/v5 v5.12.0 + github.com/gorilla/mux v1.8.1 github.com/h2non/filetype v1.1.3 github.com/satori/go.uuid v1.2.0 - github.com/shuffle/shuffle-shared v0.5.72 - golang.org/x/crypto v0.16.0 - google.golang.org/api v0.125.0 - google.golang.org/grpc v1.55.0 + github.com/shuffle/shuffle-shared v0.6.27 + golang.org/x/crypto v0.22.0 + google.golang.org/api v0.176.1 + google.golang.org/grpc v1.63.2 + gopkg.in/src-d/go-git.v4 v4.13.1 gopkg.in/yaml.v3 v3.0.1 - k8s.io/api v0.22.5 - k8s.io/apimachinery v0.22.5 - k8s.io/client-go v0.22.5 + k8s.io/api v0.30.0 + k8s.io/apimachinery v0.30.0 + k8s.io/client-go v0.30.0 ) require ( - cloud.google.com/go v0.110.2 // indirect - cloud.google.com/go/compute v1.19.3 // indirect - cloud.google.com/go/compute/metadata v0.2.3 // indirect - cloud.google.com/go/iam v1.0.1 // indirect + cloud.google.com/go v0.112.1 // indirect + cloud.google.com/go/auth v0.3.0 // indirect + cloud.google.com/go/auth/oauth2adapt v0.2.2 // indirect + cloud.google.com/go/compute/metadata v0.3.0 // indirect + cloud.google.com/go/iam v1.1.7 // indirect dario.cat/mergo v1.0.0 // indirect github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/Masterminds/semver v1.5.0 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect - github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect + + github.com/ProtonMail/go-crypto v1.0.0 // indirect github.com/adrg/strutil v0.2.3 // indirect github.com/algolia/algoliasearch-client-go/v3 v3.18.1 // indirect - github.com/bitly/go-simplejson v0.5.0 // indirect + github.com/bitly/go-simplejson v0.5.1 // indirect + github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 // indirect github.com/bradfitz/slice v0.0.0-20180809154707-2b758aa73013 // indirect - github.com/cloudflare/circl v1.3.3 // indirect - github.com/containerd/containerd v1.6.18 // indirect + github.com/cloudflare/circl v1.3.7 // indirect + github.com/containerd/containerd v1.6.26 // indirect + github.com/containerd/log v0.1.0 // indirect github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/docker/distribution v2.8.2+incompatible // indirect + github.com/distribution/reference v0.6.0 // indirect github.com/docker/go-connections v0.4.0 // indirect github.com/docker/go-units v0.5.0 // indirect + github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect - github.com/frikky/schemaless v0.0.5 // indirect + + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/frikky/schemaless v0.0.11 // indirect + github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-logr/logr v1.2.4 // indirect - github.com/go-openapi/jsonpointer v0.19.5 // indirect - github.com/go-openapi/swag v0.19.5 // indirect + github.com/go-logr/logr v1.4.1 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.22.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.3 // indirect - github.com/google/go-cmp v0.6.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-github/v28 v28.1.1 // indirect github.com/google/go-querystring v1.0.0 // indirect github.com/google/gofuzz v1.2.0 // indirect - github.com/google/s2a-go v0.1.4 // indirect - github.com/google/uuid v1.3.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect - github.com/googleapis/gax-go/v2 v2.10.0 // indirect - github.com/googleapis/gnostic v0.5.5 // indirect + + github.com/google/s2a-go v0.1.7 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect + github.com/googleapis/gax-go/v2 v2.12.3 // indirect + github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect - github.com/klauspost/compress v1.11.13 // indirect - github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e // indirect - github.com/moby/patternmatcher v0.5.0 // indirect + github.com/klauspost/compress v1.15.9 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect github.com/moby/sys/sequential v0.5.0 // indirect + github.com/moby/sys/user v0.1.0 // indirect github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/morikuni/aec v1.0.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect - github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 // indirect - github.com/opencontainers/runc v1.1.5 // indirect + github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b // indirect github.com/opensearch-project/opensearch-go v1.1.0 // indirect github.com/opensearch-project/opensearch-go/v2 v2.3.0 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/sashabaranov/go-openai v1.19.2 // indirect - github.com/sergi/go-diff v1.1.0 // indirect - github.com/sirupsen/logrus v1.9.0 // indirect - github.com/skeema/knownhosts v1.2.1 // indirect + + github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/skeema/knownhosts v1.2.2 // indirect + github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect github.com/src-d/gcfg v1.4.0 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect + go.opentelemetry.io/otel v1.24.0 // indirect + go.opentelemetry.io/otel/metric v1.24.0 // indirect + go.opentelemetry.io/otel/trace v1.24.0 // indirect go4.org v0.0.0-20201209231011-d4a079459e60 // indirect - golang.org/x/mod v0.12.0 // indirect - golang.org/x/net v0.19.0 // indirect - golang.org/x/oauth2 v0.8.0 // indirect - golang.org/x/sys v0.15.0 // indirect - golang.org/x/term v0.15.0 // indirect + golang.org/x/mod v0.15.0 // indirect + golang.org/x/net v0.24.0 // indirect + golang.org/x/oauth2 v0.19.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.19.0 // indirect + golang.org/x/term v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect - golang.org/x/time v0.3.0 // indirect - golang.org/x/tools v0.13.0 // indirect - golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect + + golang.org/x/time v0.5.0 // indirect + golang.org/x/tools v0.18.0 // indirect google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect - google.golang.org/protobuf v1.30.0 // indirect + google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240314234333-6e1732d8331c // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be // indirect + google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - k8s.io/klog/v2 v2.30.0 // indirect - k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.1.2 // indirect - sigs.k8s.io/yaml v1.2.0 // indirect - + k8s.io/klog/v2 v2.120.1 // indirect + k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect + k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/yaml v1.3.0 // indirect ) diff --git a/backend/go-app/go.sum b/backend/go-app/go.sum index f119a704..358b0d50 100644 --- a/backend/go-app/go.sum +++ b/backend/go-app/go.sum @@ -16,25 +16,27 @@ cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHOb cloud.google.com/go v0.66.0/go.mod h1:dgqGAjKCDxyhGTtC9dAREQGUJpkceNm1yt590Qno0Ko= cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= -cloud.google.com/go v0.110.2 h1:sdFPBr6xG9/wkBbfhmUz/JmZC7X6LavQgcrVINrKiVA= -cloud.google.com/go v0.110.2/go.mod h1:k04UEeEtb6ZBRTv3dZz4CeJC3jKGxyhl0sAiVVquxiw= +cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM= +cloud.google.com/go v0.112.1/go.mod h1:+Vbu+Y1UU+I1rjmzeMOb/8RfkKJK2Gyxi1X6jJCZLo4= +cloud.google.com/go/auth v0.3.0 h1:PRyzEpGfx/Z9e8+lHsbkoUVXD0gnu4MNmm7Gp8TQNIs= +cloud.google.com/go/auth v0.3.0/go.mod h1:lBv6NKTWp8E3LPzmO1TbiiRKc4drLOfHsgmlH9ogv5w= +cloud.google.com/go/auth/oauth2adapt v0.2.2 h1:+TTV8aXpjeChS9M+aTtN/TjdQnzJvmzKFt//oWu7HX4= +cloud.google.com/go/auth/oauth2adapt v0.2.2/go.mod h1:wcYjgpZI9+Yu7LyYBg4pqSiaRkfEK3GQcpb7C/uyF1Q= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/compute v1.19.3 h1:DcTwsFgGev/wV5+q8o2fzgcHOaac+DKGC91ZlvpsQds= -cloud.google.com/go/compute v1.19.3/go.mod h1:qxvISKp/gYnXkSAD1ppcSOveRAmzxicEv/JlizULFrI= -cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= -cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= +cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/datastore v1.4.0/go.mod h1:d18825/a9bICdAIJy2EkHs9joU4RlIZ1t6l8WDdbdY0= -cloud.google.com/go/datastore v1.11.0 h1:iF6I/HaLs3Ado8uRKMvZRvF/ZLkWaWE9i8AiHzbC774= -cloud.google.com/go/datastore v1.11.0/go.mod h1:TvGxBIHCS50u8jzG+AW/ppf87v1of8nwzFNgEZU1D3c= -cloud.google.com/go/iam v1.0.1 h1:lyeCAU6jpnVNrE9zGQkTl3WgNgK/X+uWwaw0kynZJMU= -cloud.google.com/go/iam v1.0.1/go.mod h1:yR3tmSL8BcZB4bxByRv2jkSIahVmCtfKZwLYGBalRE8= +cloud.google.com/go/datastore v1.15.0 h1:0P9WcsQeTWjuD1H14JIY7XQscIPQ4Laje8ti96IC5vg= +cloud.google.com/go/datastore v1.15.0/go.mod h1:GAeStMBIt9bPS7jMJA85kgkpsMkvseWWXiaHya9Jes8= +cloud.google.com/go/iam v1.1.7 h1:z4VHOhwKLF/+UYXAJDFwGtNF0b6gjsW1Pk9Ml0U/IoM= +cloud.google.com/go/iam v1.1.7/go.mod h1:J4PMPg8TtyurAUvSmPj8FF3EDgY1SPRZxcUGrn7WXGA= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -45,40 +47,27 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.12.0/go.mod h1:fFLk2dp2oAhDz8QFKwqrjdJvxSp/W2g7nillojlL5Ho= -cloud.google.com/go/storage v1.30.1 h1:uOdMxAs8HExqBlnLtnQyP0YkvbiDpdGShGKtx6U/oNM= -cloud.google.com/go/storage v1.30.1/go.mod h1:NfxhC0UJE1aXSx7CIIbCf7y9HKT7BiccwkR7+P7gN8E= +cloud.google.com/go/storage v1.40.0 h1:VEpDQV5CJxFmJ6ueWNsKxcr1QAYOXEgxDa+sBbJahPw= +cloud.google.com/go/storage v1.40.0/go.mod h1:Rrj7/hKlG87BLqDJYtwR0fbPld8uJPbQ2ucUMY7Ir0g= dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/AdaLogics/go-fuzz-headers v0.0.0-20210715213245-6c3934b029d8 h1:V8krnnfGj4pV65YLUm3C0/8bl7V5Nry2Pwvy3ru/wLc= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20210715213245-6c3934b029d8/go.mod h1:CzsSbkDixRphAF5hS6wbMKq0eI6ccJRb7/A0M6JBnwg= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= -github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= -github.com/Azure/go-autorest/autorest v0.11.18/go.mod h1:dSiJPy22c3u0OtOKDNttNgqpNFY/GeWa7GH/Pz56QRA= -github.com/Azure/go-autorest/autorest/adal v0.9.13/go.mod h1:W/MM4U6nLxnIskrw4UwWzlHfGjwUS50aOsc/I3yuU8M= -github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= -github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= -github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= -github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= -github.com/Microsoft/go-winio v0.6.0 h1:slsWYD/zyx7lCXoZVlvQrj0hPTM1HI4+v1sIda2yDvg= -github.com/Microsoft/go-winio v0.6.0/go.mod h1:cTAf44im0RAYeL23bpB+fzCyDH2MJiz2BO69KH/soAE= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= -github.com/Microsoft/hcsshim v0.9.6 h1:VwnDOgLeoi2du6dAznfmspNqTiwczvjv4K7NxuY9jsY= -github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ= -github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903 h1:ZK3C5DtzV2nVAQTx5S5jQvMeDqWtD1By5mOoyY/xJek= -github.com/ProtonMail/go-crypto v0.0.0-20230518184743-7afd39499903/go.mod h1:8TI4H3IbrackdNgv+92dI+rhpCaLqM0IfpgCgenFvRE= -github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg= +github.com/Microsoft/hcsshim v0.9.10 h1:TxXGNmcbQxBKVWvjvTocNb6jrPyeHlk5EiDhhgHgggs= +github.com/Microsoft/hcsshim v0.9.10/go.mod h1:7pLA8lDk46WKDWlVsENo92gC0XFa8rbKfyFRBqxEbCc= github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= -github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= -github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= -github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ= -github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= +github.com/ProtonMail/go-crypto v1.0.0 h1:LRuvITjQWX+WIfr930YHG2HNfjR1uOfyf5vE0kC2U78= +github.com/ProtonMail/go-crypto v1.0.0/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/adrg/strutil v0.2.3 h1:WZVn3ItPBovFmP4wMHHVXUr8luRaHrbyIuLlHt32GZQ= github.com/adrg/strutil v0.2.3/go.mod h1:+SNxbiH6t+O+5SZqIj5n/9i5yUjR+S3XXVrjEcN2mxg= github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7 h1:uSoVVbwJiQipAclBbw+8quDsfcvFjOpI5iCf4p/cqCs= @@ -88,10 +77,8 @@ github.com/algolia/algoliasearch-client-go/v3 v3.18.1/go.mod h1:i7tLoP7TYDmHX3Q7 github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a/go.mod h1:lB+ZfQJz7igIIfQNfa7Ml4HSf2uFQQRzpGGRXenZAgY= github.com/aws/aws-sdk-go v1.42.27/go.mod h1:OGr6lGMAKGlG9CVrYnWYDKIyb829c6EVBRjxqjmPepc= github.com/aws/aws-sdk-go v1.44.263/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/aws/aws-sdk-go-v2 v1.18.0/go.mod h1:uzbQtefpm44goOPmdKyAlXSNcwlRgF3ePWVW6EtJvvw= @@ -108,64 +95,53 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.19.0/go.mod h1:BgQOMsg8av8jset59jely github.com/aws/smithy-go v1.13.5/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA= github.com/basgys/goxml2json v1.1.0 h1:4ln5i4rseYfXNd86lGEB+Vi652IsIXIvggKM/BhUKVw= github.com/basgys/goxml2json v1.1.0/go.mod h1:wH7a5Np/Q4QoECFIU8zTQlZwZkrilY0itPfecMw41Dw= -github.com/bitly/go-simplejson v0.5.0 h1:6IH+V8/tVMab511d5bn4M7EwGXZf9Hj6i2xSwkNEM+Y= -github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= -github.com/bradfitz/gomemcache v0.0.0-20221031212613-62deef7fc822 h1:hjXJeBcAMS1WGENGqDpzvmgS43oECTx8UXq31UBu0Jw= -github.com/bradfitz/gomemcache v0.0.0-20221031212613-62deef7fc822/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= +github.com/bitly/go-simplejson v0.5.1 h1:xgwPbetQScXt1gh9BmoJ6j9JMr3TElvuIyjR8pgdoow= +github.com/bitly/go-simplejson v0.5.1/go.mod h1:YOPVLzCfwK14b4Sff3oP1AmGhI9T9Vsg84etUnlyp+Q= +github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874 h1:N7oVaKyGp8bttX0bfZGmcGkjz7DLQXhAn3DNd3T0ous= +github.com/bradfitz/gomemcache v0.0.0-20230905024940-24af94b03874/go.mod h1:r5xuitiExdLAJ09PR7vBVENGvp4ZuTBeWTGtxuX3K+c= github.com/bradfitz/slice v0.0.0-20180809154707-2b758aa73013 h1:/P9/RL0xgWE+ehnCUUN5h3RpG3dmoMCOONO1CCvq23Y= github.com/bradfitz/slice v0.0.0-20180809154707-2b758aa73013/go.mod h1:pccXHIvs3TV/TUqSNyEvF99sxjX2r4FFRIyw6TZY9+w= -github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/carlescere/scheduler v0.0.0-20170109141437-ee74d2f83d82 h1:9bAydALqAjBfPHd/eAiJBHnMZUYov8m2PkXVr+YGQeI= github.com/carlescere/scheduler v0.0.0-20170109141437-ee74d2f83d82/go.mod h1:tyA14J0sA3Hph4dt+AfCjPrYR13+vVodshQSM7km9qw= +github.com/cenkalti/backoff/v4 v4.1.2 h1:6Yo7N8UP2K6LWZnW94DLVSSrbobcWdVzAYOisuDPIFo= +github.com/cenkalti/backoff/v4 v4.1.2/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= -github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= +github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= +github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= -github.com/containerd/containerd v1.6.18 h1:qZbsLvmyu+Vlty0/Ex5xc0z2YtKpIsb5n45mAMI+2Ns= -github.com/containerd/containerd v1.6.18/go.mod h1:1RdCUu95+gc2v9t3IL+zIlpClSmew7/0YS8O5eQZrOw= -github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/containerd/containerd v1.6.26 h1:VVfrE6ZpyisvB1fzoY8Vkiq4sy+i5oF4uk7zu03RaHs= +github.com/containerd/containerd v1.6.26/go.mod h1:I4TRdsdoo5MlKob5khDJS2EPT1l1oMNaE2MBm6FrwxM= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= -github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v24.0.2+incompatible h1:eATx+oLz9WdNVkQrr0qjQ8HvRJ4bOOxfzEo8R+dA3cg= -github.com/docker/docker v24.0.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= +github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= +github.com/docker/docker v26.1.0+incompatible h1:W1G9MPNbskA6VZWL7b3ZljTh0pXI68FpINx0GKaOdaM= +github.com/docker/docker v26.1.0+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= -github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= -github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= -github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 h1:RIB4cRk+lBqKK3Oy0r2gRX4ui7tuhiZq2SuTtTCi0/0= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= -github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs= +github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= +github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emirpasic/gods v1.12.0/go.mod h1:YfzfFFoVP/catgzJb4IKIqXjX78Ha8FMSDh3ymbK86o= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= @@ -173,59 +149,58 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/evanphx/json-patch v4.11.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= -github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= -github.com/form3tech-oss/jwt-go v3.2.3+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= -github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/frikky/kin-openapi v0.41.0/go.mod h1:ev9OZAw7Bv5p0w93j91++6a1ElPzGcCofst+kmrWsj4= github.com/frikky/kin-openapi v0.42.0 h1:d5Z6vnuQ6RnCCPIxZaDL+TH2ODLxT8abytOt+Zh+Kd0= github.com/frikky/kin-openapi v0.42.0/go.mod h1:ev9OZAw7Bv5p0w93j91++6a1ElPzGcCofst+kmrWsj4= +github.com/frikky/schemaless v0.0.9 h1:RzNLPkJq5c4nlm5iLiTndFcbeQxdMGJIj266wSGt2+8= +github.com/frikky/schemaless v0.0.9/go.mod h1:mooDxY+D6weHjhKvjy3+IE9S7P4g4cpNnidkdRv/cHQ= +github.com/frikky/schemaless v0.0.11 h1:c4r6CJX30XI+SoJdT9RlUd9qYSQlx6hvwGRtsypu+uM= +github.com/frikky/schemaless v0.0.11/go.mod h1:mooDxY+D6weHjhKvjy3+IE9S7P4g4cpNnidkdRv/cHQ= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/fsouza/go-dockerclient v1.9.7 h1:FlIrT71E62zwKgRvCvWGdxRD+a/pIy+miY/n3MXgfuw= -github.com/fsouza/go-dockerclient v1.9.7/go.mod h1:vx9C32kE2D15yDSOMCDaAEIARZpDQDFBHeqL3MgQy/U= +github.com/fsouza/go-dockerclient v1.11.0 h1:4ZAk6W7rPAtPXm7198EFqA5S68rwnNQORxlOA5OurCA= +github.com/fsouza/go-dockerclient v1.11.0/go.mod h1:0I3TQCRseuPTzqlY4Y3ajfsg2VAdMQoazrkxJTiJg8s= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= -github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4= +github.com/gliderlabs/ssh v0.3.7 h1:iV3Bqi942d9huXnzEF2Mt+CY9gLu8DNM4Obd+8bODRE= +github.com/gliderlabs/ssh v0.3.7/go.mod h1:zpHEXBstFnQYtGnB8k8kQLol82umzn/2/snG7alWVD8= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4= github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg= github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= -github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f h1:Pz0DHeFij3XFhoBRGUDPzSJ+w2UcK5/0JvF8DRI58r8= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII= -github.com/go-git/go-git/v5 v5.7.0 h1:t9AudWVLmqzlo+4bqdf7GY+46SUuRsx59SboFxkq2aE= -github.com/go-git/go-git/v5 v5.7.0/go.mod h1:coJHKEOk5kUClpsNlXrUvPrDxY3w3gjHvhcZd8Fodw8= -github.com/go-git/go-git/v5 v5.11.0 h1:XIZc1p+8YzypNr34itUfSvYJcv+eYdTnTvOZ2vD3cA4= github.com/go-git/go-git/v5 v5.11.0/go.mod h1:6GFcX2P3NM7FPBfpePbpLd21XxsgdAt+lKqXmCUiUCY= +github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= +github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= -github.com/go-logr/logr v0.4.0/go.mod h1:z6/tIYblkpsD+a4lm/fGIIU9mZ+XfAiaFtq7xTgseGU= -github.com/go-logr/logr v1.2.0/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.2 h1:ahHml/yUpnlb96Rp8HCvtYVPY8ZYpxq3g7UYchIYwbs= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= -github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= +github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= +github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -257,11 +232,13 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= +github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= +github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -274,7 +251,6 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -283,7 +259,6 @@ github.com/google/go-github/v28 v28.1.1/go.mod h1:bsqJWQX05omyWVmc00nEUql9mhQyv3 github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.1.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= @@ -291,6 +266,7 @@ github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXi github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= +github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= @@ -302,27 +278,23 @@ github.com/google/pprof v0.0.0-20200905233945-acf8798be1f7/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= +github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc= -github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= +github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k= -github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= +github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.10.0 h1:ebSgKfMxynOdxw8QQuFOKMgomqeLGPqNLQox2bo42zg= -github.com/googleapis/gax-go/v2 v2.10.0/go.mod h1:4UOEnMCrxsSqQ940WnTiD6qJ63le2ev3xfyagutxiPw= -github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= -github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw= -github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= -github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= +github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA= +github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= +github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg= github.com/h2non/filetype v1.1.3/go.mod h1:319b3zT68BvV+WRj7cwy856M2ehB3HqNOt6sy1HndBY= @@ -331,16 +303,13 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= -github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= -github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= @@ -350,54 +319,48 @@ github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4 github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.11.13 h1:eSvu8Tmq6j2psUJqJrLcWH6K3w5Dwc+qipbaA6eVEN4= -github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY= +github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/libgit2/git2go/v34 v34.0.0/go.mod h1:blVco2jDAw6YTXkErMMqzHLcAjKkwF0aWIRHBqiJkZ0= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e h1:hB2xlXdHp/pmPZq0y3QnmWAArdw9PqbmotexnWx/FU8= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= -github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mmcloughlin/avo v0.5.0/go.mod h1:ChHFdoV7ql95Wi7vuq2YT1bwCJqiWdZrQ1im3VujLYM= -github.com/moby/patternmatcher v0.5.0 h1:YCZgJOeULcxLw1Q+sVR636pmS7sPEn1Qo2iAN6M7DBo= -github.com/moby/patternmatcher v0.5.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= -github.com/moby/spdystream v0.2.0/go.mod h1:f7i0iNDQJ059oMTcWxx8MA/zKFIuD/lY+0GqbN2Wy8c= -github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/moby/sys/user v0.1.0 h1:WmZ93f5Ux6het5iituh9x2zAG7NFY9Aqi49jjE1PaQg= +github.com/moby/sys/user v0.1.0/go.mod h1:fKJhFOnsCN6xZ5gSfbM6zaHGgDJMrqt9/reuj4T7MmU= github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= -github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= -github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= -github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= -github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU= @@ -413,7 +376,8 @@ github.com/onsi/ginkgo/v2 v2.9.2/go.mod h1:WHcJJG2dIlcCqVfBAwUCrJxSPFb6v4azBwgxe github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k= github.com/onsi/ginkgo/v2 v2.9.7/go.mod h1:cxrmXWykAwTwhQsJOPfdIDiJ+l2RYq7U8hFU+M/1uw0= github.com/onsi/ginkgo/v2 v2.11.0/go.mod h1:ZhrRA5XmEE3x3rhlzamx/JJvujdZoJ2uvgI7kR0iZvM= -github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY= +github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= @@ -431,14 +395,12 @@ github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+q github.com/onsi/gomega v1.27.7/go.mod h1:1p8OOlwo2iUUDsHnOrjE5UKYJ+e3W8eQ3qSlRahPmr4= github.com/onsi/gomega v1.27.8/go.mod h1:2J8vzI/s+2shY9XHRApDkdgPo1TKT7P2u6fXeJKFnNQ= github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= +github.com/onsi/gomega v1.31.0 h1:54UJxxj6cPInHS3a35wm6BK/F9nHYueZ1NVujHDrnXE= +github.com/onsi/gomega v1.31.0/go.mod h1:DW9aCi7U6Yi40wNVAvT6kzFnEVEI5n3DloYBiKiT6zk= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= -github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799 h1:rc3tiVYb5z54aKaDfakKn0dDjIyPpTtszkjuMzyt7ec= -github.com/opencontainers/image-spec v1.0.3-0.20211202183452-c5a74bcca799/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= -github.com/opencontainers/runc v1.1.5 h1:L44KXEpKmfWDcS02aeGm8QNTFXTo2D+8MYGDIJ/GDEs= -github.com/opencontainers/runc v1.1.5/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg= -github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= -github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= +github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b h1:YWuSjZCQAPM8UUBLkYUk1e+rZcvWHJmFb6i6rM44Xs8= +github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ= github.com/opensearch-project/opensearch-go v1.1.0 h1:eG5sh3843bbU1itPRjA9QXbxcg8LaZ+DjEzQH9aLN3M= github.com/opensearch-project/opensearch-go v1.1.0/go.mod h1:+6/XHCuTH+fwsMJikZEWsucZ4eZMma3zNSeLrTtVGbo= github.com/opensearch-project/opensearch-go/v2 v2.3.0 h1:nQIEMr+A92CkhHrZgUhcfsrZjibvB3APXf2a1VwCmMQ= @@ -446,7 +408,6 @@ github.com/opensearch-project/opensearch-go/v2 v2.3.0/go.mod h1:8LDr9FCgUTVoT+5E github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= -github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= @@ -456,71 +417,44 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk= +github.com/sashabaranov/go-openai v1.19.2 h1:+dkuCADSnwXV02YVJkdphY8XD9AyHLUWwk6V7LB6EL8= +github.com/sashabaranov/go-openai v1.19.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= github.com/satori/go.uuid v1.2.0 h1:0uYX9dsZ2yD7q2RtLRtPSdGDWzjeM3TbMJP9utgA0ww= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= -github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/shuffle/shuffle-shared v0.4.66 h1:Aw4qOp0VsVJrRzW1sJhEy4OY4fRGlFErUD5+93RXL6g= -github.com/shuffle/shuffle-shared v0.4.66/go.mod h1:X613gbo0dT3fnYvXDRwjQZyLC+T49T2nSQOrCV5QMlI= -github.com/shuffle/shuffle-shared v0.4.80 h1:03OL+O8prwL9zq6Gnb9SRORPWi5+ThO0jPoxk+xctOo= -github.com/shuffle/shuffle-shared v0.4.80/go.mod h1:X613gbo0dT3fnYvXDRwjQZyLC+T49T2nSQOrCV5QMlI= -github.com/shuffle/shuffle-shared v0.4.95 h1:xr92/03/uQeJiDme9S8/vgF1KWyQgJ1KQXVE7nQMKis= -github.com/shuffle/shuffle-shared v0.4.95/go.mod h1:X613gbo0dT3fnYvXDRwjQZyLC+T49T2nSQOrCV5QMlI= -github.com/shuffle/shuffle-shared v0.4.96 h1:iaIB/HP9eKpw9DMMJZhSLDbKdHJt075kFYLHg9AaiiM= -github.com/shuffle/shuffle-shared v0.4.96/go.mod h1:X613gbo0dT3fnYvXDRwjQZyLC+T49T2nSQOrCV5QMlI= -github.com/shuffle/shuffle-shared v0.4.97 h1:1c8LdNteMykKNEV97vwP63oSP2tV/Uso3O4TC+oxdFQ= -github.com/shuffle/shuffle-shared v0.4.97/go.mod h1:X613gbo0dT3fnYvXDRwjQZyLC+T49T2nSQOrCV5QMlI= -github.com/shuffle/shuffle-shared v0.4.98 h1:pgsLdWUpxZ/q+eHpAjOCH9icOsmuO5u2olmirOldy5A= -github.com/shuffle/shuffle-shared v0.4.98/go.mod h1:X613gbo0dT3fnYvXDRwjQZyLC+T49T2nSQOrCV5QMlI= -github.com/shuffle/shuffle-shared v0.5.11 h1:Eqbs9o8E49QAL5/6aV6BfFtWSjLIvgET7AL3fa4OQTg= -github.com/shuffle/shuffle-shared v0.5.11/go.mod h1:X613gbo0dT3fnYvXDRwjQZyLC+T49T2nSQOrCV5QMlI= -github.com/shuffle/shuffle-shared v0.5.14 h1:d14u1e4k+qKgnf4Insq4x2S+0MMKlDqdyTTyVP3puRA= -github.com/shuffle/shuffle-shared v0.5.14/go.mod h1:X613gbo0dT3fnYvXDRwjQZyLC+T49T2nSQOrCV5QMlI= -github.com/shuffle/shuffle-shared v0.5.29 h1:n4vThl7v3mFVXbrIW71XREFdmZZo7mOBAWxnsdiNjDk= -github.com/shuffle/shuffle-shared v0.5.29/go.mod h1:X613gbo0dT3fnYvXDRwjQZyLC+T49T2nSQOrCV5QMlI= -github.com/shuffle/shuffle-shared v0.5.30 h1:ORWjQU3UJhdZY5mRsAoR2hvftNcJmiEekNKjbaMo7K8= -github.com/shuffle/shuffle-shared v0.5.30/go.mod h1:X613gbo0dT3fnYvXDRwjQZyLC+T49T2nSQOrCV5QMlI= -github.com/shuffle/shuffle-shared v0.5.31 h1:OV4IIfKWWFW66WjGvyXOmmsSz3p8pW9L1ge1mDo8ftM= -github.com/shuffle/shuffle-shared v0.5.31/go.mod h1:X613gbo0dT3fnYvXDRwjQZyLC+T49T2nSQOrCV5QMlI= -github.com/shuffle/shuffle-shared v0.5.44 h1:6WiFPIsij+IWvXY7vzVX7cUicb+PYOzhTbWF/gDmYeU= -github.com/shuffle/shuffle-shared v0.5.44/go.mod h1:X613gbo0dT3fnYvXDRwjQZyLC+T49T2nSQOrCV5QMlI= -github.com/shuffle/shuffle-shared v0.5.60 h1:R0BWYp/DlgyNPU0xVNSPhsWBnJ+LeVD3iLoDRZmtKMo= -github.com/shuffle/shuffle-shared v0.5.60/go.mod h1:oIZkx93Z7EvtiTXty7xO+ax63Fjz8MQvjXMxDN0Qws0= -github.com/shuffle/shuffle-shared v0.5.61 h1:Fkd7pvk8ypwaio1VvMOmJ6PQF9xCfNYx8RWjPnXjn5E= -github.com/shuffle/shuffle-shared v0.5.61/go.mod h1:oIZkx93Z7EvtiTXty7xO+ax63Fjz8MQvjXMxDN0Qws0= -github.com/shuffle/shuffle-shared v0.5.62 h1:L1la7++aqPLPsh1z3cuGIzH+NTuvBlmEU/T2mQvfEk8= -github.com/shuffle/shuffle-shared v0.5.62/go.mod h1:oIZkx93Z7EvtiTXty7xO+ax63Fjz8MQvjXMxDN0Qws0= -github.com/shuffle/shuffle-shared v0.5.65 h1:x4ZM+e0LK21rRtG0xbtnUb+6qU08+zNBQ6azUyhn8ck= -github.com/shuffle/shuffle-shared v0.5.65/go.mod h1:oIZkx93Z7EvtiTXty7xO+ax63Fjz8MQvjXMxDN0Qws0= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/shuffle/shuffle-shared v0.6.16 h1:dQBDRmb2Wgl3pEuewqjDvN6v6nUKr+1EvGSEja9zG6s= +github.com/shuffle/shuffle-shared v0.6.16/go.mod h1:HhQTn7xZZ69ZTc4EptO9OeNmgbKDyGlWAhFkUFUAHSA= +github.com/shuffle/shuffle-shared v0.6.18 h1:mKc3vGuCz9ubdqMwaLocSbEUZyso620CY73RBqcF6LI= +github.com/shuffle/shuffle-shared v0.6.18/go.mod h1:00QOcSPlUWMXzJj1D7pjcV9h6nVRWfSWqTM10+fhTd0= +github.com/shuffle/shuffle-shared v0.6.24 h1:gSUsI7o7DG0Z/AYtDQuSvhivWVV4MQPa5JU08YKAy2U= +github.com/shuffle/shuffle-shared v0.6.24/go.mod h1:rWkh1eWdIx7OqQzJ1+JzF3Hck1X/Ty1WkUtjLrp+CU4= +github.com/shuffle/shuffle-shared v0.6.26 h1:UZ4o3s+GPULLv/gy31SdhUw1ttvF+f1FfP18bxD7+Bw= +github.com/shuffle/shuffle-shared v0.6.26/go.mod h1:rWkh1eWdIx7OqQzJ1+JzF3Hck1X/Ty1WkUtjLrp+CU4= +github.com/shuffle/shuffle-shared v0.6.27 h1:q4qZD6bGZFIvZ5Y10unGr3N3rZ7OryWyvvaGgANZJZU= +github.com/shuffle/shuffle-shared v0.6.27/go.mod h1:rWkh1eWdIx7OqQzJ1+JzF3Hck1X/Ty1WkUtjLrp+CU4= 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/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/skeema/knownhosts v1.1.1 h1:MTk78x9FPgDFVFkDLTrsnnfCJl7g1C/nnKvePgrIngE= -github.com/skeema/knownhosts v1.1.1/go.mod h1:g4fPeYpque7P0xefxtGzV81ihjC8sX2IqpAoNkjxbMo= -github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= +github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= +github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= -github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= -github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/src-d/gcfg v1.4.0 h1:xXbNR5AlLSA315x2UO+fTSSAXCDf+Ar38/6oyGbDKQ4= github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= -github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -534,13 +468,10 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= -github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= -github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= -github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/xanzy/ssh-agent v0.2.1/go.mod h1:mLlQY/MoOhWBj+gOGMQkOeiEvkx+8pJSI+0Bx9h2kr4= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= @@ -558,7 +489,26 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= +go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= +go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= +go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= +go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.3.0 h1:R/OBkMoGgfy2fLhs2QhkCI1w4HLEQX92GCcJB6SSdNk= +go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.3.0/go.mod h1:VpP4/RMn8bv8gNo9uK7/IMY4mtWLELsS+JIP0inH0h4= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.3.0 h1:giGm8w67Ja7amYNfYMdme7xSp2pIxThWopw8+QP51Yk= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.3.0/go.mod h1:hO1KLR7jcKaDDKDkvI9dP/FIhpmna5lkqPUQdEjFAM8= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.3.0 h1:Ydage/P0fRrSPpZeCVxzjqGcI6iVmG2xb43+IR8cjqM= +go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.3.0/go.mod h1:QNX1aly8ehqqX1LEa6YniTU7VY9I6R3X/oPxhGdTceE= +go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= +go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= +go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw= +go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc= +go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= +go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= +go.opentelemetry.io/proto/otlp v0.11.0 h1:cLDgIBTf4lLOlztkhzAEdQsJ4Lj+i5Wc9k6Nn0K1VyU= +go.opentelemetry.io/proto/otlp v0.11.0/go.mod h1:QpEjXPrNQzrFDZgoTo49dgHR9RYRSrg3NAKnUGl9YpQ= go4.org v0.0.0-20201209231011-d4a079459e60 h1:iqAGo78tVOJXELHQFRjR6TMwItrvXH4hrGJ32I/NFF8= go4.org v0.0.0-20201209231011-d4a079459e60/go.mod h1:CIiUVy99QCPfoE13bO4EZaz5GZMZXMSBGhxRdsvzbkg= golang.org/x/arch v0.1.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= @@ -569,11 +519,7 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220826181053-bd7e27e6170d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.1.0/go.mod h1:RecgLatLF4+eUMCP1PoPZQb+cVrJcOPbHkTkbkB9sbw= @@ -581,10 +527,9 @@ golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2Uz golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -622,12 +567,12 @@ golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= +golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -641,7 +586,6 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -666,7 +610,6 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211209124913-491a49abca63/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= @@ -682,10 +625,9 @@ golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= -golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= -golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -695,8 +637,8 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210113160501-8b1d76fa0423/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8= -golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE= +golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg= +golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -709,9 +651,10 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -721,14 +664,12 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -743,7 +684,6 @@ golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200828194041-157a740278f4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -751,17 +691,12 @@ golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -780,13 +715,11 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -798,10 +731,9 @@ golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= -golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= -golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -818,16 +750,14 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -885,19 +815,19 @@ golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.2.0/go.mod h1:y4OqIKeOV/fWJetJ8bXPU1sEVniLMIyDAZWeHdV+NTA= golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= -golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= golang.org/x/tools v0.9.3/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= -golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= +golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= +golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= +golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -918,16 +848,17 @@ google.golang.org/api v0.31.0/go.mod h1:CL+9IBCa2WWU6gRuBWaKqGWLFFwbEUXkfeMkHLQW google.golang.org/api v0.32.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.125.0 h1:7xGvEY4fyWbhWMHf3R2/4w7L4fXyfpRGE9g6lp8+DCk= -google.golang.org/api v0.125.0/go.mod h1:mBwVAtz+87bEN6CbA1GtZPDOqY2R5ONPqJeIlvyo4Aw= +google.golang.org/api v0.176.1 h1:DJSXnV6An+NhJ1J+GWtoF2nHEuqB1VNoTfnIbjNvwD4= +google.golang.org/api v0.176.1/go.mod h1:j2MaSDYcvYV1lkZ1+SMW4IeF90SrEyFA+tluDYWRrFg= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -951,7 +882,6 @@ google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= @@ -962,17 +892,16 @@ google.golang.org/genproto v0.0.0-20200831141814-d751682dd103/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200914193844-75d14daec038/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200921151605-7abf4a1a14d5/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210113195801-ae06605f4595/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc h1:8DyZCyvI8mE1IdLy/60bS+52xfymkE72wv1asokgtao= -google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:xZnkP7mREFX5MORlOPEzLMr+90PPZQ2QWzrVTWfAq64= -google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc h1:kVKPf/IiYSBWEWtkIn6wZXwWGCnLKcC8oWfZvXjsGnM= -google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc h1:XSJ8Vk1SWuNr8S18z1NZSziL0CPIXLCCMDOEFtHBOFc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= +google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY= +google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo= +google.golang.org/genproto/googleapis/api v0.0.0-20240314234333-6e1732d8331c h1:kaI7oewGK5YnVwj+Y+EJBO/YN1ht8iTL9XkFHtVZLsc= +google.golang.org/genproto/googleapis/api v0.0.0-20240314234333-6e1732d8331c/go.mod h1:VQW3tUculP/D4B+xVCo+VgSq8As6wA9ZjHl//pmk+6s= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be h1:LG9vZxsWGOmUKieR8wPAUR3u3MpnYFQZROPIMaXh7/A= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240415180920-8c6c420018be/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -987,14 +916,11 @@ google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.34.1/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= -google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag= -google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8= +google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= +google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -1007,14 +933,12 @@ google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGj google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= @@ -1030,21 +954,18 @@ gopkg.in/src-d/go-git.v4 v4.13.1/go.mod h1:nx5NYcxdKxq5fpltdHnPa2Exj4Sx0EclMWZQb gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools/v3 v3.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= -gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= +gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= +gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -1052,27 +973,25 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/api v0.22.5 h1:xk7C+rMjF/EGELiD560jdmwzrB788mfcHiNbMQLIVI8= -k8s.io/api v0.22.5/go.mod h1:mEhXyLaSD1qTOf40rRiKXkc+2iCem09rWLlFwhCEiAs= -k8s.io/apimachinery v0.22.5 h1:cIPwldOYm1Slq9VLBRPtEYpyhjIm1C6aAMAoENuvN9s= -k8s.io/apimachinery v0.22.5/go.mod h1:xziclGKwuuJ2RM5/rSFQSYAj0zdbci3DH8kj+WvyN0U= -k8s.io/client-go v0.22.5 h1:I8Zn/UqIdi2r02aZmhaJ1hqMxcpfJ3t5VqvHtctHYFo= -k8s.io/client-go v0.22.5/go.mod h1:cs6yf/61q2T1SdQL5Rdcjg9J1ElXSwbjSrW2vFImM4Y= -k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= -k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= -k8s.io/klog/v2 v2.9.0/go.mod h1:hy9LJ/NvuK+iVyP4Ehqva4HxZG/oXyIS3n3Jmire4Ec= -k8s.io/klog/v2 v2.30.0 h1:bUO6drIvCIsvZ/XFgfxoGFQU/a4Qkh0iAlvUR7vlHJw= -k8s.io/klog/v2 v2.30.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= -k8s.io/kube-openapi v0.0.0-20211109043538-20434351676c/go.mod h1:vHXdDvt9+2spS2Rx9ql3I8tycm3H9FDfdUoIuKCefvw= -k8s.io/utils v0.0.0-20210819203725-bdf08cb9a70a/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b h1:wxEMGetGMur3J1xuGLQY7GEQYg9bZxKn3tKo5k/eYcs= -k8s.io/utils v0.0.0-20210930125809-cb0fa318a74b/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= +k8s.io/api v0.30.0 h1:siWhRq7cNjy2iHssOB9SCGNCl2spiF1dO3dABqZ8niA= +k8s.io/api v0.30.0/go.mod h1:OPlaYhoHs8EQ1ql0R/TsUgaRPhpKNxIMrKQfWUp8QSE= +k8s.io/apimachinery v0.30.0 h1:qxVPsyDM5XS96NIh9Oj6LavoVFYff/Pon9cZeDIkHHA= +k8s.io/apimachinery v0.30.0/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc= +k8s.io/client-go v0.30.0 h1:sB1AGGlhY/o7KCyCEQ0bPWzYDL0pwOZO4vAtTSh/gJQ= +k8s.io/client-go v0.30.0/go.mod h1:g7li5O5256qe6TYdAMyX/otJqMhIiGgTapdLchhmOaY= +k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= +k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag= +k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340/go.mod h1:yD4MZYeKMBwQKVht279WycxKyM84kkAx2DPrTXaeb98= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b h1:sgn3ZU783SCgtaSJjpcVVlRqd6GSnlTLKgpAAttJvpI= +k8s.io/utils v0.0.0-20230726121419-3b25d923346b/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/structured-merge-diff/v4 v4.1.2 h1:Hr/htKFmJEbtMgS/UD0N+gtgctAqz81t3nu+sPzynno= -sigs.k8s.io/structured-merge-diff/v4 v4.1.2/go.mod h1:j/nl6xW8vLS49O8YvXW1ocPhZawJtm+Yrr7PPRQ0Vg4= -sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= -sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= +sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= +sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8= diff --git a/backend/go-app/main.go b/backend/go-app/main.go index ddcb66df..62870a45 100755 --- a/backend/go-app/main.go +++ b/backend/go-app/main.go @@ -580,7 +580,7 @@ func handleRegister(resp http.ResponseWriter, request *http.Request) { Name: orgSetupName, Id: orgId, Org: orgSetupName, - Users: []shuffle.User{}, + Users: []shuffle.User{user}, Roles: []string{"admin", "user"}, CloudSync: false, } @@ -614,6 +614,8 @@ func handleRegister(resp http.ResponseWriter, request *http.Request) { Id: newOrg.Id, Name: newOrg.Name, } + + user.ActiveOrg = currentOrg } } } @@ -876,6 +878,11 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) { } org, err := shuffle.GetOrg(ctx, userInfo.ActiveOrg.Id) + if err != nil { + log.Printf("[DEBUG] Failed to get org during getinfo: %s", err) + } + + //if err == nil { if len(org.Id) > 0 { userInfo.ActiveOrg = shuffle.OrgMini{ @@ -964,6 +971,8 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) { tutorialsFinished = append(tutorialsFinished, tutorial) } + licensed := shuffle.IsLicensed(ctx, *org) + returnValue := shuffle.HandleInfo{ Success: true, Username: userInfo.Username, @@ -983,6 +992,7 @@ func handleInfo(resp http.ResponseWriter, request *http.Request) { Tutorials: tutorialsFinished, Priorities: orgPriorities, + Licensed: licensed, } returnData, err := json.Marshal(returnValue) @@ -1965,6 +1975,144 @@ func handleWebhookCallback(resp http.ResponseWriter, request *http.Request) { } +func handlePipelineCallback(resp http.ResponseWriter, request *http.Request) { + + if request.Method != "POST" { + request.Method = "POST" + } + + if request.Body == nil { + stringReader := strings.NewReader("") + request.Body = ioutil.NopCloser(stringReader) + } + + path := strings.Split(request.URL.String(), "/") + if len(path) < 4 { + resp.WriteHeader(403) + resp.Write([]byte(`{"success": false}`)) + return + } + + ctx := context.Background() + location := strings.Split(request.URL.String(), "/") + + var pipelineId string + + if location[1] == "api" { + if len(location) <= 4 { + log.Printf("[INFO] Couldn't handle location. Too short in pipeline: %d", len(location)) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + pipelineId = location[4] + } + + userAgent := request.Header.Get("User-Agent") + if strings.Contains(strings.ToLower(userAgent), "microsoftpreview") || strings.Contains(strings.ToLower(userAgent), "googlebot") { + log.Printf("[AUDIT] Blocking googlebot and microsoftbot for pielines. UA: '%s'", userAgent) + resp.WriteHeader(400) + resp.Write([]byte(`{"success": false, "reason": "Google/Microsoft preview bots not allowed. Please change the useragent."}`)) + return + } + + if len(pipelineId) != 45 { + log.Printf("[INFO] Couldn't handle pipeline. Too short in pipeline: %d", len(pipelineId)) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false, "reason": "pipeline ID not valid"}`)) + return + } + + pipelineId = pipelineId[9:] + + pipeline, err := shuffle.GetPipeline(ctx, pipelineId) + if err != nil { + log.Printf("[WARNING] Failed getting pipeline %s (callback): %s", pipelineId, err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + if pipeline.Status != "running" { + log.Printf("[WARNING] Not running %s because pipeline status is %s", pipeline.TriggerId, pipeline.Status) + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "The pipeline isn't running"}`))) + return + } + + if pipeline.WorkflowId == "" { + log.Printf("[DEBUG] Not running because pipeline isn't connected to any workflows") + resp.WriteHeader(401) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "No workflows are defined"}`))) + return + } + + body, err := ioutil.ReadAll(request.Body) + if err != nil { + log.Printf("[DEBUG] Body data error: %s", err) + resp.WriteHeader(401) + resp.Write([]byte(`{"success": false}`)) + return + } + + parsedBody := shuffle.GetExecutionbody(body) + newBody := shuffle.ExecutionStruct{ + Start: pipeline.StartNode, + ExecutionSource: "pipeline", + ExecutionArgument: parsedBody, + } + + workflow, err := shuffle.GetWorkflow(ctx, pipeline.WorkflowId) + if err == nil { + for _, branch := range workflow.Branches { + if branch.SourceID == pipeline.TriggerId { + log.Printf("[DEBUG] Found ID %s for pipeline", pipeline.TriggerId) + if branch.DestinationID != pipeline.StartNode { + newBody.Start = branch.DestinationID + break + } + } + } + } + + b, err := json.Marshal(newBody) + if err != nil { + log.Printf("[ERROR] Failed newBody marshaling for pipeline: %s", err) + resp.WriteHeader(500) + resp.Write([]byte(`{"success": false}`)) + return + } + + log.Printf("[INFO] Running pipeline for workflow %s with startnode %s", pipeline.WorkflowId, pipeline.StartNode) + + newWorkflow := shuffle.Workflow{ + ID: "", + } + + if len(pipeline.StartNode) == 0 { + log.Printf("[WARNING] No start node for pipeline %s - running with workflow default.", pipeline.TriggerId) + + } + + newRequest := &http.Request{ + URL: &url.URL{}, + Method: "POST", + Body: ioutil.NopCloser(bytes.NewReader(b)), + } + + workflowExecution, executionResp, err := handleExecution(pipeline.WorkflowId, newWorkflow, newRequest, pipeline.OrgId) + + if err == nil { + resp.WriteHeader(200) + resp.Write([]byte(fmt.Sprintf(`{"success": true, "execution_id": "%s"}`, workflowExecution.ExecutionId))) + return + } + + resp.WriteHeader(500) + resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, executionResp))) +} + func executeCloudAction(action shuffle.CloudSyncJob, apikey string) error { data, err := json.Marshal(action) if err != nil { @@ -1972,13 +2120,8 @@ func executeCloudAction(action shuffle.CloudSyncJob, apikey string) error { return err } - //transport := http.DefaultTransport.(*http.Transport).Clone() - //client := &http.Client{ - // Transport: transport, - //} - client := &http.Client{} - syncUrl := fmt.Sprintf("%s/api/v1/cloud/sync/handle_action", syncUrl) + client := shuffle.GetExternalClient(syncUrl) req, err := http.NewRequest( "POST", syncUrl, @@ -3044,6 +3187,26 @@ func buildSwaggerApp(resp http.ResponseWriter, body []byte, user shuffle.User, s return } + if len(user.ActiveOrg.Id) > 0 { + org, err := shuffle.GetOrg(ctx, user.ActiveOrg.Id) + if err != nil { + log.Printf("[ERROR] Failed getting org during image build (%s): %s", user.ActiveOrg.Id, err) + } else { + log.Printf("[INFO] Successfully uploaded app %s to org %s (2). Validating and distributing image to available environments in org.", api.ID, org.Id) + + imagenames := []string{ + fmt.Sprintf("%s_%s", api.Name, api.AppVersion), + fmt.Sprintf("%s_%s", api.Name, test.Id), + } + + err = shuffle.DistributeAppToEnvironments(ctx, *org, imagenames) + if err != nil { + log.Printf("[ERROR] Failed distributing app to environments: %s", err) + } + } + } + + log.Printf("[DEBUG] Successfully built app %s (%s)", api.Name, api.ID) if len(user.Id) > 0 { resp.WriteHeader(200) @@ -3483,8 +3646,8 @@ func remoteOrgJobHandler(org shuffle.Org, interval int) error { } - client := &http.Client{} syncUrl := fmt.Sprintf("%s/api/v1/cloud/sync", syncUrl) + client := shuffle.GetExternalClient(syncUrl) req, err := http.NewRequest( "POST", syncUrl, @@ -3560,8 +3723,8 @@ func runInitEs(ctx context.Context) { setUsers := false _ = setUsers if err != nil { - if fmt.Sprintf("%s", err) == "EOF" { - time.Sleep(7 * time.Second) + if fmt.Sprintf("%s", err) == "EOF" || strings.Contains(fmt.Sprintf("%s", err), "bad status") { + time.Sleep(10 * time.Second) runInitEs(ctx) return } @@ -3645,7 +3808,7 @@ func runInitEs(ctx context.Context) { if strings.Contains(os.Getenv("SHUFFLE_OPENSEARCH_URL"), "https") { log.Printf("[INFO] Waiting during init to make sure the opensearch instance is up and running with security features properly") - time.Sleep(15 * time.Second) + time.Sleep(30 * time.Second) } schedules, err := shuffle.GetAllSchedules(ctx, "ALL") @@ -3867,6 +4030,9 @@ func runInitEs(ctx context.Context) { log.Printf("[INFO] Running schedule for cleaning up or re-running unfinished workflows in %d environments.", len(environments)) for _, environment := range environments { + // Allowed without PROXY management as it's localhost + // client := shuffle.GetExternalClient(syncUrl) + httpClient := &http.Client{} url := fmt.Sprintf("http://localhost:5001/api/v1/environments/%s/stop", environment) req, err := http.NewRequest( @@ -4077,7 +4243,7 @@ func handleVerifyCloudsync(orgId string) (shuffle.SyncFeatures, error) { //r.HandleFunc("/api/v1/getorgs", handleGetOrgs).Methods("GET", "OPTIONS") syncURL := fmt.Sprintf("%s/api/v1/cloud/sync/get_access", syncUrl) - client := &http.Client{} + client := shuffle.GetExternalClient(syncURL) req, err := http.NewRequest( "GET", syncURL, @@ -4121,7 +4287,7 @@ func handleStopCloudSync(syncUrl string, org shuffle.Org) (*shuffle.Org, error) log.Printf("[INFO] Should run cloud sync disable for org %s with URL %s and sync key %s", org.Id, syncUrl, org.SyncConfig.Apikey) - client := &http.Client{} + client := shuffle.GetExternalClient(syncUrl) req, err := http.NewRequest( "DELETE", syncUrl, @@ -4292,7 +4458,6 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) { //log.Printf("Apidata: %s", tmpData.Apikey) // FIXME: Path - client := &http.Client{} apiPath := "/api/v1/cloud/sync/setup" if tmpData.Disable { if !org.CloudSync { @@ -4362,6 +4527,7 @@ func handleCloudSetup(resp http.ResponseWriter, request *http.Request) { bytes.NewBuffer(b), ) + client := shuffle.GetExternalClient(syncPath) newresp, err := client.Do(req) if err != nil { resp.WriteHeader(400) @@ -4754,6 +4920,7 @@ func initHandlers() { r.HandleFunc("/api/v1/users/checkusers", checkAdminLogin).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/users/getinfo", handleInfo).Methods("GET", "OPTIONS") + r.HandleFunc("/api/v1/users/apps", shuffle.HandleGetUserApps).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/users/generateapikey", shuffle.HandleApiGeneration).Methods("GET", "POST", "OPTIONS") r.HandleFunc("/api/v1/users/logout", shuffle.HandleLogout).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/users/getsettings", shuffle.HandleSettings).Methods("GET", "OPTIONS") @@ -4882,7 +5049,10 @@ func initHandlers() { r.HandleFunc("/api/v1/triggers/outlook/{key}", shuffle.HandleGetSpecificTrigger).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/triggers/gmail/register", shuffle.HandleNewGmailRegister).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/triggers/gmail/getFolders", shuffle.HandleGetGmailFolders).Methods("GET", "OPTIONS") - + r.HandleFunc("/api/v1/triggers/pipeline", shuffle.HandleNewPipelineRegister).Methods("POST", "OPTIONS") + //r.HandleFunc("/api/v1/triggers/pipeline/save", shuffle.HandleSavePipelineInfo).Methods("PUT", "OPTIONS") + r.HandleFunc("/api/v1/pipelines/{key}", handlePipelineCallback).Methods("POST", "GET", "PATCH", "PUT", "DELETE", "OPTIONS") + r.HandleFunc("/api/v1/triggers", shuffle.HandleGetTriggers).Methods("GET", "OPTIONS") //r.HandleFunc("/api/v1/triggers/gmail/routing", handleGmailRouting).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/triggers/gmail/{key}", shuffle.HandleGetSpecificTrigger).Methods("GET", "OPTIONS") @@ -4906,7 +5076,8 @@ func initHandlers() { r.HandleFunc("/api/v1/orgs/{orgId}/change", shuffle.HandleChangeUserOrg).Methods("POST", "OPTIONS") // Swaps to the org r.HandleFunc("/api/v1/orgs/{orgId}", shuffle.HandleDeleteOrg).Methods("DELETE", "OPTIONS") - + r.HandleFunc("/api/v1/orgs/{orgId}/suborgs", shuffle.HandleGetSubOrgs).Methods("GET", "OPTIONS") + // This is a new API that validates if a key has been seen before. // Not sure what the best course of action is for it. r.HandleFunc("/api/v1/environments/{key}/stop", shuffle.HandleStopExecutions).Methods("GET", "POST", "OPTIONS") @@ -4916,6 +5087,8 @@ func initHandlers() { r.HandleFunc("/api/v1/orgs/{orgId}/list_cache", shuffle.HandleListCacheKeys).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/orgs/{orgId}/get_cache", shuffle.HandleGetCacheKey).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/orgs/{orgId}/set_cache", shuffle.HandleSetCacheKey).Methods("POST", "OPTIONS") + r.HandleFunc("/api/v1/orgs/{orgId}/delete_cache", shuffle.HandleDeleteCacheKeyPost).Methods("POST", "OPTIONS") + r.HandleFunc("/api/v1/orgs/{orgId}/cache/{cache_key}", shuffle.HandleDeleteCacheKey).Methods("DELETE", "OPTIONS") r.HandleFunc("/api/v1/orgs/{orgId}/stats", shuffle.HandleGetStatistics).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/orgs/{orgId}/statistics", shuffle.HandleGetStatistics).Methods("GET", "OPTIONS") @@ -4936,6 +5109,7 @@ func initHandlers() { // PS: For cloud, this has to use cloud storage. // https://developer.box.com/reference/get-files-id-content/ // 1. Creating the "get file" option. Make it possible to run this in the frontend. + r.HandleFunc("/api/v1/files/download_remote", shuffle.HandleDownloadRemoteFiles).Methods("POST", "OPTIONS") r.HandleFunc("/api/v1/files/namespaces/{namespace}", shuffle.HandleGetFileNamespace).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/files/{fileId}/content", shuffle.HandleGetFileContent).Methods("GET", "OPTIONS") r.HandleFunc("/api/v1/files/create", shuffle.HandleCreateFile).Methods("POST", "OPTIONS") diff --git a/backend/go-app/walkoff.go b/backend/go-app/walkoff.go index 659c52b3..10df25b2 100755 --- a/backend/go-app/walkoff.go +++ b/backend/go-app/walkoff.go @@ -293,12 +293,35 @@ func handleGetWorkflowqueue(resp http.ResponseWriter, request *http.Request) { env, err := shuffle.GetEnvironment(ctx, orgId, "") timeNow := time.Now().Unix() if err == nil && len(env.Id) > 0 && len(env.Name) > 0 { + // Updates every 60 seconds~ if time.Now().Unix() > env.Edited+60 { - env.RunningIp = request.RemoteAddr + env.RunningIp = shuffle.GetRequestIp(request) + if len(orborusLabel) > 0 { + env.RunningIp = orborusLabel + } + + if request.Method == "POST" { + body, err := ioutil.ReadAll(request.Body) + if err == nil { + var envData shuffle.OrborusStats + err = json.Unmarshal(body, &envData) + if err == nil { + if envData.Swarm { + env.Licensed = true + env.RunType = "docker" + } + + if envData.Kubernetes { + env.RunType = "k8s" + } + } + } + } + env.Checkin = timeNow err = shuffle.SetEnvironment(ctx, env) if err != nil { - log.Printf("[WARNING] Failed updating environment: %s", err) + log.Printf("[ERROR] Failed updating environment: %s", err) } } } @@ -552,7 +575,9 @@ func handleGetStreamResults(resp http.ResponseWriter, request *http.Request) { ctx := context.Background() workflowExecution, err := shuffle.GetWorkflowExecution(ctx, actionResult.ExecutionId) if err != nil { - log.Printf("[WARNING][%s] Failed getting execution (streamresult): %s", actionResult.ExecutionId, err) + if len(actionResult.ExecutionId) > 0 { + log.Printf("[WARNING][%s] Failed getting execution (streamresult): %s", actionResult.ExecutionId, err) + } resp.WriteHeader(401) resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Bad authorization key or execution_id might not exist."}`))) return @@ -952,42 +977,7 @@ func deleteWorkflow(resp http.ResponseWriter, request *http.Request) { resp.Write([]byte(`{"success": true}`)) } -// Identifies what a category defined really is -func getWorkflowLocal(fileId string, request *http.Request) ([]byte, error) { - fullUrl := fmt.Sprintf("%s/api/v1/workflows/%s", localBase, fileId) - client := &http.Client{} - req, err := http.NewRequest( - "GET", - fullUrl, - nil, - ) - - if err != nil { - return []byte{}, err - } - - for key, value := range request.Header { - req.Header.Add(key, strings.Join(value, ";")) - } - - newresp, err := client.Do(req) - if err != nil { - return []byte{}, err - } - - body, err := ioutil.ReadAll(newresp.Body) - if err != nil { - return []byte{}, err - } - - // Temporary solution - if strings.Contains(string(body), "reason") && strings.Contains(string(body), "false") { - return []byte{}, errors.New(fmt.Sprintf("Failed getting workflow %s with message %s", fileId, string(body))) - } - - return body, nil -} func handleExecution(id string, workflow shuffle.Workflow, request *http.Request, orgId string) (shuffle.WorkflowExecution, string, error) { //go func() { @@ -1777,7 +1767,7 @@ func cloudExecuteAction(execution shuffle.WorkflowExecution) error { } syncURL := fmt.Sprintf("%s/api/v1/cloud/sync/execute_node", syncUrl) - client := &http.Client{} + client := shuffle.GetExternalClient(syncURL) req, err := http.NewRequest( "POST", syncURL, @@ -3216,6 +3206,12 @@ func setNewWorkflowApp(resp http.ResponseWriter, request *http.Request) { workflowapp.Generated = false workflowapp.Activated = true + if !shuffle.ArrayContains(workflowapp.Contributors, user.Id) { + workflowapp.Contributors = append(workflowapp.Contributors, user.Id) + } + + shuffle.SetAppRevision(ctx, workflowapp) + err = shuffle.SetWorkflowAppDatastore(ctx, workflowapp, workflowapp.ID) if err != nil { log.Printf("[WARNING] Failed setting workflowapp: %s", err) diff --git a/docker-compose.yml b/docker-compose.yml index 63124856..2c50d5a1 100755 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ version: '3' services: frontend: - image: ghcr.io/shuffle/shuffle-frontend:latest + image: ghcr.io/shuffle/shuffle-frontend:nightly container_name: shuffle-frontend hostname: shuffle-frontend ports: @@ -15,7 +15,7 @@ services: depends_on: - backend backend: - image: ghcr.io/shuffle/shuffle-backend:latest + image: ghcr.io/shuffle/shuffle-backend:nightly container_name: shuffle-backend hostname: ${BACKEND_HOSTNAME} # Here for debugging: @@ -34,7 +34,7 @@ services: - SHUFFLE_FILE_LOCATION=/shuffle-files restart: unless-stopped orborus: - image: ghcr.io/shuffle/shuffle-orborus:latest + image: ghcr.io/shuffle/shuffle-orborus:nightly container_name: shuffle-orborus hostname: shuffle-orborus networks: @@ -60,9 +60,10 @@ services: security_opt: - seccomp:unconfined opensearch: - image: opensearchproject/opensearch:2.11.0 + image: opensearchproject/opensearch:2.14.0 hostname: shuffle-opensearch container_name: shuffle-opensearch + env_file: .env environment: - "OPENSEARCH_JAVA_OPTS=-Xms2048m -Xmx2048m" # minimum and maximum Java heap size, recommend setting both to 50% of system RAM - bootstrap.memory_lock=true @@ -73,6 +74,7 @@ services: - node.name=shuffle-opensearch - node.store.allow_mmap=false - discovery.seed_hosts=shuffle-opensearch + - OPENSEARCH_INITIAL_ADMIN_PASSWORD=${SHUFFLE_OPENSEARCH_PASSWORD} ulimits: memlock: soft: -1 diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 4bf5bf0b..e9d6d683 100755 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,5 +1,5 @@ # Build environment -FROM node:18 as builder +FROM node:21 as builder RUN mkdir /usr/src/app WORKDIR /usr/src/app @@ -8,10 +8,10 @@ ENV PATH /usr/src/app/node_modules/.bin:$PATH COPY package.json /usr/src/app/package.json # Nocache yarn install -RUN yarn config set "strict-ssl" false -g -RUN yarn install --network-timeout 1000000 +#RUN yarn config set "strict-ssl" false -g +#RUN yarn install --network-timeout 1000000 -#RUN npm install +RUN npm install --legacy-peer-deps # copy only required files to not trigger rebuilding every time COPY ./certs /usr/src/app/certs/ @@ -21,7 +21,8 @@ COPY ./*.sh /usr/src/app/ COPY ./*.json /usr/src/app/ #RUN rm -rf /usr/src/app/node_modules/webpack -RUN yarn build +#RUN yarn build --verbose +RUN npm run build --loglevel verbose 2>&1 # Production environment FROM nginx:1.21.5 diff --git a/frontend/enable_subpathing.sh b/frontend/enable_subpathing.sh new file mode 100644 index 00000000..a6f76db4 --- /dev/null +++ b/frontend/enable_subpathing.sh @@ -0,0 +1,19 @@ +#!/bin/bash + +# Sekerovic Dragan, oneStep2 GmbH - 2023-09-08 - quick and dirty +# purpose of the script: make shuffle reverse proxyable/run shuffle on a subpath instead of root + +if $(ls frontend/src/App.jsx.orig.* 1>/dev/null 2>&1); then + echo "shuffle src already patched for subpathing!" + echo "Aborting ..." + exit 1 +fi + +[[ "$1" != "" ]] && SUBPATH=$1 || SUBPATH="/shuffle" + +cat $0.tpl | sed "s,SUBPATH,$SUBPATH," > $0.run +chmod 700 $0.run +./$0.run +rm ./$0.run + +exit 0 diff --git a/frontend/enable_subpathing.sh.tpl b/frontend/enable_subpathing.sh.tpl new file mode 100644 index 00000000..04680e38 --- /dev/null +++ b/frontend/enable_subpathing.sh.tpl @@ -0,0 +1,139 @@ +#!/bin/bash + +cd frontend + +cd src +cp App.jsx App.jsx.orig.`date -I` +sed 's,startsWith("/,startsWith("SUBPATH/,g' -i App.jsx +sed 's,path="/,path="SUBPATH/,g' -i App.jsx +sed 's,location = "/,location = "SUBPATH/,g' -i App.jsx +sed 's,window.location.origin;,"SUBPATH";,' -i App.jsx + +cd views +for d in $(ls *.jsx); do cp $d $d.orig.`date -I`; done +sed 's,pathname = "/,pathname = "SUBPATH/,g' -i *.jsx +sed 's,navigate("/,navigate("SUBPATH/,g' -i *.jsx +sed 's,navigate(`/,navigate(`SUBPATH/,g' -i *.jsx +sed 's,href="/,href="SUBPATH/,g' -i *.jsx +sed 's,path = "/,path = "SUBPATH/,g' -i *.jsx +sed 's,link={"/,link={"SUBPATH/,g' -i *.jsx +sed 's,to="/,to="SUBPATH/,g' -i *.jsx +sed 's,to={"/,to={"SUBPATH/,g' -i *.jsx +sed 's,${window.location.origin},SUBPATH,g' -i *.jsx +cd .. + +cd components +for d in $(ls *.jsx); do cp $d $d.orig.`date -I`; done +sed 's,to="/,to="SUBPATH/,g' -i *.jsx +sed 's,${window.location.origin},SUBPATH,g' -i *.jsx +cd ../.. + +cd confd/templates +cp nginx.conf nginx.conf.orig.`date -I` +sed 's,location / {,rewrite ^SUBPATH(/api/v1.*)$ $1 last;\n\n\t\tlocation / {,' -i nginx.conf +cd ../.. + +docker build -t ghcr.io/shuffle/shuffle-frontend:latest . + + +cat << EOF + +If you want to reverse proxy shuffle using subpath "SUBPATH" you need to configure +your web server - in this case nginx - to do the reverse proxying by at least adding + + location SUBPATH { + proxy_pass http://snooss-proxy:3421/; + rewrite SUBPATH/(.*)$ /$1 break; + + proxy_read_timeout 300s; + + # proxy header + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + # substitute html content response + proxy_set_header Accept-Encoding ""; # no compression allowed or sub_filter won't work + sub_filter 'href="/' 'href="SUBPATH/'; + sub_filter 'href:"/' 'href:"SUBPATH/'; + sub_filter '/static' 'SUBPATH/static'; + sub_filter '/images' 'SUBPATH/images'; + sub_filter_types *; + sub_filter_once off; + } + +to your nginx configuration. +EOF + + +exit 0#!/bin/bash + +cd frontend + +cd src +cp App.jsx App.jsx.orig.`date -I` +sed 's,startsWith("/,startsWith("SUBPATH/,g' -i App.jsx +sed 's,path="/,path="SUBPATH/,g' -i App.jsx +sed 's,location = "/,location = "SUBPATH/,g' -i App.jsx +sed 's,window.location.origin;,"SUBPATH";,' -i App.jsx + +cd views +for d in $(ls *.jsx); do cp $d $d.orig.`date -I`; done +sed 's,pathname = "/,pathname = "SUBPATH/,g' -i *.jsx +sed 's,navigate("/,navigate("SUBPATH/,g' -i *.jsx +sed 's,navigate(`/,navigate(`SUBPATH/,g' -i *.jsx +sed 's,href="/,href="SUBPATH/,g' -i *.jsx +sed 's,path = "/,path = "SUBPATH/,g' -i *.jsx +sed 's,link={"/,link={"SUBPATH/,g' -i *.jsx +sed 's,to="/,to="SUBPATH/,g' -i *.jsx +sed 's,to={"/,to={"SUBPATH/,g' -i *.jsx +sed 's,${window.location.origin},SUBPATH,g' -i *.jsx +cd .. + +cd components +for d in $(ls *.jsx); do cp $d $d.orig.`date -I`; done +sed 's,to="/,to="SUBPATH/,g' -i *.jsx +sed 's,${window.location.origin},SUBPATH,g' -i *.jsx +cd ../.. + +cd confd/templates +cp nginx.conf nginx.conf.orig.`date -I` +sed 's,location / {,rewrite ^SUBPATH(/api/v1.*)$ $1 last;\n\n\t\tlocation / {,' -i nginx.conf +cd ../.. + +docker build -t ghcr.io/shuffle/shuffle-frontend:latest . + + +cat << EOF + +If you want to reverse proxy shuffle using subpath "SUBPATH" you need to configure +your web server - in this case nginx - to do the reverse proxying by at least adding + + location SUBPATH { + proxy_pass http://snooss-proxy:3421/; + rewrite SUBPATH/(.*)$ /$1 break; + + proxy_read_timeout 300s; + + # proxy header + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + + # substitute html content response + proxy_set_header Accept-Encoding ""; # no compression allowed or sub_filter won't work + sub_filter 'href="/' 'href="SUBPATH/'; + sub_filter 'href:"/' 'href:"SUBPATH/'; + sub_filter '/static' 'SUBPATH/static'; + sub_filter '/images' 'SUBPATH/images'; + sub_filter_types *; + sub_filter_once off; + } + +to your nginx configuration. +EOF + + +exit 0 diff --git a/frontend/package.json b/frontend/package.json index c6421d1b..635c079f 100755 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,7 +1,7 @@ { "name": "shuffler", "homepage": "https://shuffler.io", - "version": "1.3.3", + "version": "1.4.0", "private": true, "dependencies": { "@babel/plugin-proposal-class-properties": "^7.18.6", @@ -59,6 +59,7 @@ "mui-chips-input": "^2.1.3", "mui-nested-menu": "^3.2.1", "react": "^18.2.0", + "react-ace": "^10.1.0", "react-alice-carousel": "^2.6.4", "react-avatar-editor": "^11.1.0", "react-beforeunload": "^2.2.1", @@ -131,6 +132,7 @@ "babel-plugin-transform-imports": "^2.0.0", "babel-preset-env": "^1.7.0", "babel-preset-es2015": "^6.24.1", + "postcss": "^8.4.38", "promise-window": "^1.2.1", "react-hot-loader": "^4.13.0", "webpack-cli": "^5.1.4" diff --git a/frontend/public/icons/docker.svg b/frontend/public/icons/docker.svg new file mode 100644 index 00000000..297bb83f --- /dev/null +++ b/frontend/public/icons/docker.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/frontend/public/icons/k8s.svg b/frontend/public/icons/k8s.svg new file mode 100644 index 00000000..bedd3b88 --- /dev/null +++ b/frontend/public/icons/k8s.svg @@ -0,0 +1,84 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/frontend/public/images/no_image.png b/frontend/public/images/no_image.png new file mode 100644 index 00000000..8c6937e2 Binary files /dev/null and b/frontend/public/images/no_image.png differ diff --git a/frontend/public/images/social/resized/discord.png b/frontend/public/images/social/resized/discord.png new file mode 100755 index 00000000..8873a34f Binary files /dev/null and b/frontend/public/images/social/resized/discord.png differ diff --git a/frontend/public/images/social/resized/github.png b/frontend/public/images/social/resized/github.png new file mode 100755 index 00000000..77416ea9 Binary files /dev/null and b/frontend/public/images/social/resized/github.png differ diff --git a/frontend/public/images/social/resized/twitter.png b/frontend/public/images/social/resized/twitter.png new file mode 100755 index 00000000..46c8b3d7 Binary files /dev/null and b/frontend/public/images/social/resized/twitter.png differ diff --git a/frontend/public/images/workflows/tenzir2.png b/frontend/public/images/workflows/tenzir2.png new file mode 100644 index 00000000..12c702bc Binary files /dev/null and b/frontend/public/images/workflows/tenzir2.png differ diff --git a/frontend/public/images/workflows/user_input.svg b/frontend/public/images/workflows/user_input.svg new file mode 100644 index 00000000..0ee42314 --- /dev/null +++ b/frontend/public/images/workflows/user_input.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index f2a79f65..dba2d5db 100755 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -56,10 +56,9 @@ if (window.location.port === "3000") { // Development on Github Codespaces if (globalUrl.includes("app.github.dev")) { - //globalUrl = globalUrl.replace("3000", "5001") - globalUrl = "https://frikky-shuffle-5gvr4xx62w64-5001.preview.app.github.dev" + globalUrl = globalUrl.replace("-3000.", "-5001.") + globalUrl = globalUrl.replace("-3001.", "-5001.") } -//console.log("global: ", globalUrl) const App = (message, props) => { @@ -177,34 +176,45 @@ const App = (message, props) => { curpath={curpath} setCurpath={setCurpath} /> - {!isLoaded ? null : - userdata.chat_disabled === true ? null : - - } -
+ {!isLoaded ? null : + userdata.chat_disabled === true ? null : + + } + +
+
+
+ {/*
*/} diff --git a/frontend/src/codeeditor-index.css b/frontend/src/codeeditor-index.css new file mode 100644 index 00000000..c87341f5 --- /dev/null +++ b/frontend/src/codeeditor-index.css @@ -0,0 +1,106 @@ +@import url('https://fonts.googleapis.com/css?family=Nunito+Sans'); + +body { + margin: 0; + padding: 0; + font-family: "Nunito Sans", sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +code { + font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", + monospace; +} + +.partner_container { + height: 200px; + position: relative; +} + +.imggrow { + padding: 60px auto 50px auto; + height: 100px; + width: auto !important; + margin-right: 1rem; + width: 100%; + transition: transform .2s; + border-radius: 10px; + float: center !important; + text-align: center !important; + justify-content: "center" !important; + background-color: #ffffff; + color: black; + position: relative; + overflow: hidden; + z-index: 1; +} + +.imggrow:hover{ + height: 230px; + transform: transform(1.5); + cursor: pointer; + border-color: #3585f9; + display: block; + box-shadow: 0 4px 8px #FC7305; +} + +.bad-marker { + position: absolute; + background-color: #7b322e; + color: white !important; + opacity: 1; +} + +.good-marker { + position: absolute; + background-color: #5ea36a; + color: white !important; + opacity: 0.6; +} + +/* Style for checkbox */ +.ais-RefinementList-checkbox { + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + width: 16px; + height: 16px; + border: 1px solid #595b5e; + border-radius: 4px; + background-color: #27292d; + cursor: pointer; + flex-shrink: 0; +} + +.ais-RefinementList-checkbox:checked { + background-color: rgb(248, 103, 67) !important; + border-color: rgb(248, 103, 67) !important; + width: 16px; + height: 16px; +} + +.ais-RefinementList-checkbox:checked::before { + content: "\2714"; + display: block; + width: 100%; + height: 100%; + text-align: center; + line-height: 18px; + color: #fff; + font-size: 14px; +} + +::-webkit-scrollbar { + width: 8px; +} + +::-webkit-scrollbar-thumb { + background-color: #494949; + border-radius: 5px; +} + +::-webkit-scrollbar-track { + background-color: rgb(26,26,26); +} + diff --git a/frontend/src/components/AppFramework.jsx b/frontend/src/components/AppFramework.jsx index c168d25d..00ef41c0 100644 --- a/frontend/src/components/AppFramework.jsx +++ b/frontend/src/components/AppFramework.jsx @@ -31,21 +31,23 @@ import { Delete as DeleteIcon, } from "@mui/icons-material"; -import * as edgehandles from "cytoscape-edgehandles"; + +import edgehandles from "cytoscape-edgehandles"; + import cytoscape from "cytoscape"; import { toast } from 'react-toastify'; -cytoscape.use(edgehandles); +cytoscape.use(edgehandles) export const findSpecificApp = (framework, inputcategory) => { // Get the frameworkinfo for the org and fill in if (framework === undefined || framework === null) { - console.log("findSpecificApp: framework is null") + //console.log("findSpecificApp: framework is null") return null } if (inputcategory === undefined || inputcategory === null) { - console.log("findSpecificApp: category is null") + //console.log("findSpecificApp: category is null") return null } @@ -59,7 +61,7 @@ export const findSpecificApp = (framework, inputcategory) => { return { name: "EDR :default", - large_image: parsedDatatypeImages["EDR & AV"], + large_image: parsedDatatypeImages()["EDR & AV"], count: 0, description: "", id: "", @@ -71,7 +73,7 @@ export const findSpecificApp = (framework, inputcategory) => { return { name: "COMMS :default", - large_image: parsedDatatypeImages["COMMS"], + large_image: parsedDatatypeImages()["COMMS"], count: 0, description: "", id: "", @@ -83,7 +85,7 @@ export const findSpecificApp = (framework, inputcategory) => { return { name: "COMMS :default", - large_image: parsedDatatypeImages["COMMS"], + large_image: parsedDatatypeImages()["COMMS"], count: 0, description: "", id: "", @@ -95,7 +97,7 @@ export const findSpecificApp = (framework, inputcategory) => { return { name: "ASSETS :default", - large_image: parsedDatatypeImages["ASSETS"], + large_image: parsedDatatypeImages()["ASSETS"], count: 0, description: "", id: "", @@ -107,7 +109,7 @@ export const findSpecificApp = (framework, inputcategory) => { return { name: "CASES :default", - large_image: parsedDatatypeImages["CASES"], + large_image: parsedDatatypeImages()["CASES"], count: 0, description: "", id: "", @@ -118,8 +120,8 @@ export const findSpecificApp = (framework, inputcategory) => { } return { - name: "EDR :default", - large_image: parsedDatatypeImages["EDR & AV"], + name: "IAM :default", + large_image: parsedDatatypeImages()["IAM"], count: 0, description: "", id: "", @@ -131,7 +133,7 @@ export const findSpecificApp = (framework, inputcategory) => { return { name: "Network :default", - large_image: parsedDatatypeImages["NETWORK"], + large_image: parsedDatatypeImages()["NETWORK"], count: 0, description: "", id: "", @@ -143,7 +145,7 @@ export const findSpecificApp = (framework, inputcategory) => { return { name: "INTEL :default", - large_image: parsedDatatypeImages["INTEL"], + large_image: parsedDatatypeImages()["INTEL"], count: 0, description: "", id: "", @@ -155,7 +157,7 @@ export const findSpecificApp = (framework, inputcategory) => { return { name: "SIEM :default", - large_image: parsedDatatypeImages["SIEM"], + large_image: parsedDatatypeImages()["SIEM"], count: 0, description: "", id: "", @@ -168,24 +170,30 @@ export const findSpecificApp = (framework, inputcategory) => { } const svgSize = "40px" -export const parsedDatatypeImages = { - "SIEM": encodeURI(`data:image/svg+xml;utf-8,`), +export const parsedDatatypeImages = () => { + const isWorkflow = window.location.pathname.includes("/workflows/") + const svgSize = isWorkflow ? "24px" : "40px" + const colorfill = isWorkflow ? "rgb(240,240,240)" : "rgb(248,90,62)" - "CASES": encodeURI(`data:image/svg+xml;utf-8,`), + return { + "SIEM": encodeURI(`data:image/svg+xml;utf-8,`), - "EDR & AV": encodeURI(`data:image/svg+xml;utf-8,`), + "CASES": encodeURI(`data:image/svg+xml;utf-8,`), - "INTEL": encodeURI(`data:image/svg+xml;utf-8,`), + "EDR & AV": encodeURI(`data:image/svg+xml;utf-8,`), - "COMMS": encodeURI(`data:image/svg+xml;utf-8,`), + "INTEL": encodeURI(`data:image/svg+xml;utf-8,`), - "NETWORK": encodeURI(`data:image/svg+xml;utf-8,`), + "COMMS": encodeURI(`data:image/svg+xml;utf-8,`), + + "NETWORK": encodeURI(`data:image/svg+xml;utf-8,`), - "INTEL": encodeURI(`data:image/svg+xml;utf-8,`), + "INTEL": encodeURI(`data:image/svg+xml;utf-8,`), - "ASSETS": encodeURI(`data:image/svg+xml;utf-8,`), + "ASSETS": encodeURI(`data:image/svg+xml;utf-8,`), - "IAM": encodeURI(`data:image/svg+xml;utf-8,`), + "IAM": encodeURI(`data:image/svg+xml;utf-8,`), + } } export const usecases = { @@ -654,7 +662,7 @@ const AppFramework = (props) => { const handleLoadNextSuggestion = (frameworkData) => { - console.log("Should check for next apps to load from App suggestion model") + //console.log("Should check for next apps to load from App suggestion model") //fetch(globalUrl + "/api/v1/workflows/usecases", { //credentials: "include", //cors: "no-cors", @@ -720,7 +728,7 @@ const AppFramework = (props) => { } const showRecommendations = (changed, frameworkData) => { - console.log("Inside recommendation loader") + //console.log("Inside recommendation loader") setChangedApp(changed) handleLoadNextSuggestion(frameworkData) @@ -761,7 +769,7 @@ const AppFramework = (props) => { //console.log("APptype, changed, framework: ", apptype.toLowerCase(), alternativeChanged.toLowerCase(), changed.toLowerCase(), frameworkData) if (changed.toLowerCase() === apptype.toLowerCase() || changed.toLowerCase().includes(apptype.toLowerCase()) || alternativeChanged.toLowerCase() === apptype.toLowerCase() || alternativeChanged.toLowerCase().includes(apptype.toLowerCase())) { potential = true - console.log("Potential: !", apptype) + //console.log("Potential: !", apptype) if (frameworkData[apptype] !== undefined && frameworkData[apptype].name !== undefined && frameworkData[apptype].name !== null && frameworkData[apptype].name.length > 0) { usecase.items[itemtype].app = frameworkData[apptype] @@ -819,10 +827,10 @@ const AppFramework = (props) => { continue } } else { - console.log("No usecase to try to match it to (usecase.usecase_references in UsecaseSearch)") + //console.log("No usecase to try to match it to (usecase.usecase_references in UsecaseSearch)") } - console.log("Usecase: ", usecase) + //console.log("Usecase: ", usecase) if (matches.length === usecase.items.length) { usecase.color = "#c51152" usecase.type = usecaseTypes[key].name @@ -833,8 +841,8 @@ const AppFramework = (props) => { } // FIXME: Check if a usecase has already been handled - console.log("") - console.log("GOT USECASES: ", showusecases) + //console.log("") + //console.log("GOT USECASES: ", showusecases) // FIXME: Just showing one usecase at a time for now if (showusecases.length > 0) { @@ -894,38 +902,37 @@ const AppFramework = (props) => { parsedUsecase.process.push(edge.data) } - fetch(globalUrl + "/api/v1/workflows/usecases", { - method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - body: JSON.stringify(parsedUsecase), - credentials: "include", - }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 for framework!"); - } + fetch(globalUrl + "/api/v1/workflows/usecases", { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(parsedUsecase), + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for framework!"); + } - return response.json(); - }) - .then((responseJson) => { - if (responseJson.success === false) { - if (responseJson.reason !== undefined) { - toast("Failed updating: " + responseJson.reason) - } else { - toast("Failed to update framework for your org.") - } + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === false) { + if (responseJson.reason !== undefined) { + toast("Failed updating: " + responseJson.reason) } else { - toast("Updated usecase.") + toast("Failed to update framework for your org.") } - }) - .catch((error) => { - toast(error.toString()); - //setFrameworkLoaded(true) - }) - } + } else { + toast("Updated usecase.") + } + }) + .catch((error) => { + toast(error.toString()); + }) + } const activateApp = (appid) => { fetch(globalUrl+"/api/v1/apps/"+appid+"/activate", { @@ -937,7 +944,7 @@ const AppFramework = (props) => { credentials: "include", }) .then((response) => { - if (response.status !== 200) { + if (response.status !== 200 || response.status !== 202) { console.log("Failed to activate") } @@ -945,7 +952,11 @@ const AppFramework = (props) => { }) .then((responseJson) => { if (responseJson.success === false) { - toast("Failed to activate the app") + var msgString = "Failed to activate the app" + if (responseJson.reason !== undefined) { + msgString += ": " + responseJson.reason + } + toast(msgString) } else { //toast("App activated for your organization! Refresh the page to use the app.") } @@ -957,45 +968,54 @@ const AppFramework = (props) => { } const setFrameworkItem = (data) => { - console.log("Setting framework item: ", data, isCloud) if (!isCloud) { activateApp(data.id) } - fetch(globalUrl + "/api/v1/apps/frameworkConfiguration", { - method: "POST", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - body: JSON.stringify(data), - credentials: "include", - }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 for framework!"); - } + fetch(globalUrl + "/api/v1/apps/frameworkConfiguration", { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(data), + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for framework!"); + } - return response.json(); - }) - .then((responseJson) => { - if (responseJson.success === false) { - if (responseJson.reason !== undefined) { - toast("Failed updating: " + responseJson.reason) - } else { - toast("Failed to update framework for your org.") + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === false) { + if (responseJson.reason !== undefined) { + toast("Failed getting appframework: " + responseJson.reason) + } else { + toast("Failed to update framework for your org.") - } - } + } + } else { + if (data.id === "remove") { + frameworkData[data.type] = {} + frameworkData[data.type.toLowerCase()] = {} + } else { + frameworkData[data.type] = data + frameworkData[data.type.toLowerCase()] = data + } - //setFrameworkLoaded(true) - //setFrameworkData(responseJson) - }) - .catch((error) => { - toast(error.toString()); - //setFrameworkLoaded(true) - }) - } + setDiscoveryData({}) + if (setFrameworkData !== undefined) { + setFrameworkData(frameworkData) + } + } + }) + .catch((error) => { + toast(error.toString()); + //setFrameworkLoaded(true) + }) + } useEffect(() => { if (!window.location.pathname.includes("usecases")) { @@ -1058,7 +1078,6 @@ const AppFramework = (props) => { frameworkData[keys[key]] = submitValue } - console.log("Frameworkdata: ", frameworkData) setFrameworkData(frameworkData) if (discoveryData.large_image !== undefined && discoveryData.large_image !== null && discoveryData.large_image.includes("storage.googleapis.com")) { @@ -1076,8 +1095,6 @@ const AppFramework = (props) => { const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; - - const imgSize = 50; var parsedFrameworkData = frameworkData === undefined ? {} : frameworkData @@ -1263,7 +1280,7 @@ const AppFramework = (props) => { var parsedStyle2 = { "ghost": "yes", - "border-width": "7px", + "border-width": "7px", } /* @@ -1399,7 +1416,6 @@ const AppFramework = (props) => { const onNodeSelect = (event) => { var data = event.target.data(); - console.log("Node: ", data) if (data.id === "SHUFFLE") { event.target.unselect() return @@ -1465,6 +1481,15 @@ const AppFramework = (props) => { const foundMiddleImage = userdata !== undefined && userdata !== null && userdata.active_org !== undefined && userdata.active_org.image !== undefined && userdata.active_org.image !== null && userdata.active_org.image !== "" ? userdata.active_org.image : '/images/Shuffle_logo.png' + const siemcheck = parsedFrameworkData.SIEM.large_image === undefined || (parsedFrameworkData.SIEM.name !== undefined && parsedFrameworkData.SIEM.name !== null && parsedFrameworkData.SIEM.name.includes(":default")) + const iamcheck = parsedFrameworkData.IAM.large_image === undefined || (parsedFrameworkData.IAM.name !== undefined && parsedFrameworkData.IAM.name !== null && parsedFrameworkData.IAM.name.includes(":default")) + const casescheck = parsedFrameworkData.Cases.large_image === undefined || (parsedFrameworkData.Cases.name !== undefined && parsedFrameworkData.Cases.name !== null && parsedFrameworkData.Cases.name.includes(":default")) + const assetscheck = parsedFrameworkData.Assets.large_image === undefined || (parsedFrameworkData.Assets.name !== undefined && parsedFrameworkData.Assets.name !== null && parsedFrameworkData.Assets.name.includes(":default")) + const intelcheck = parsedFrameworkData.Intel.large_image === undefined || (parsedFrameworkData.Intel.name !== undefined && parsedFrameworkData.Intel.name !== null && parsedFrameworkData.Intel.name.includes(":default")) + const commscheck = parsedFrameworkData.Comms.large_image === undefined || (parsedFrameworkData.Comms.name !== undefined && parsedFrameworkData.Comms.name !== null && parsedFrameworkData.Comms.name.includes(":default")) + const edrcheck = parsedFrameworkData["EDR & AV"].large_image === undefined || (parsedFrameworkData["EDR & AV"].name !== undefined && parsedFrameworkData["EDR & AV"].name !== null && parsedFrameworkData["EDR & AV"].name.includes(":default")) + const networkcheck = parsedFrameworkData.Network.large_image === undefined || (parsedFrameworkData.Network.name !== undefined && parsedFrameworkData.Network.name !== null && parsedFrameworkData.Network.name.includes(":default")) + const fontSize = `${12*scale}px` const defaultSize = `${85*scale}px` @@ -1484,12 +1509,12 @@ const AppFramework = (props) => { name: parsedFrameworkData.Cases.name === undefined ? "" : parsedFrameworkData.Cases.name, description: parsedFrameworkData.Cases.description === undefined ? "" : parsedFrameworkData.Cases.description, app_id: parsedFrameworkData.Cases.id === undefined ? "" : parsedFrameworkData.Cases.id, - text_margin_y: parsedFrameworkData.Cases.large_image === undefined ? textMarginDefault : textMarginImage, - margin_x: parsedFrameworkData.Cases.large_image === undefined ? `${32*scale}px` : "0px", - margin_y: parsedFrameworkData.Cases.large_image === undefined ? `${19*scale}px` : `0px`, - width: parsedFrameworkData.Cases.large_image === undefined ? iconSize : defaultSize, - height: parsedFrameworkData.Cases.large_image === undefined ? iconSize : defaultSize, - large_image: parsedFrameworkData.Cases.large_image === undefined ? parsedDatatypeImages["CASES"] : parsedFrameworkData.Cases.large_image, + text_margin_y: casescheck ? textMarginDefault : textMarginImage, + margin_x: casescheck ? `${32*scale}px` : "0px", + margin_y: casescheck ? `${19*scale}px` : `0px`, + width: casescheck ? iconSize : defaultSize, + height: casescheck ? iconSize : defaultSize, + large_image: parsedFrameworkData.Cases.large_image === undefined ? parsedDatatypeImages()["CASES"] : parsedFrameworkData.Cases.large_image, label: securityFramework[0].text.toUpperCase(), id: securityFramework[0].text.toUpperCase(), @@ -1501,6 +1526,7 @@ const AppFramework = (props) => { } }, { + group: "nodes", data: { font_size: fontSize, @@ -1512,12 +1538,12 @@ const AppFramework = (props) => { name: parsedFrameworkData.IAM.name === undefined ? "" : parsedFrameworkData.IAM.name, description: parsedFrameworkData.IAM.description === undefined ? "" : parsedFrameworkData.IAM.description, app_id: parsedFrameworkData.IAM.id === undefined ? "" : parsedFrameworkData.IAM.id, - text_margin_y: parsedFrameworkData.IAM.large_image === undefined ? textMarginDefault : textMarginImage, - margin_x: parsedFrameworkData.IAM.large_image === undefined ? `${32*scale}px` : "0px", - margin_y: parsedFrameworkData.IAM.large_image === undefined ? `${19*scale}px` : `0px`, - width: parsedFrameworkData.IAM.large_image === undefined ? iconSize : defaultSize, - height: parsedFrameworkData.IAM.large_image === undefined ? iconSize : defaultSize, - large_image: parsedFrameworkData.IAM.large_image === undefined ? parsedDatatypeImages["IAM"] : parsedFrameworkData.IAM.large_image, + text_margin_y: iamcheck ? textMarginDefault : textMarginImage, + margin_x: iamcheck ? `${32*scale}px` : "0px", + margin_y: iamcheck ? `${19*scale}px` : `0px`, + width: iamcheck ? iconSize : defaultSize, + height: iamcheck ? iconSize : defaultSize, + large_image: parsedFrameworkData.IAM.large_image === undefined ? parsedDatatypeImages()["IAM"] : parsedFrameworkData.IAM.large_image, label: securityFramework[3].text.toUpperCase(), id: securityFramework[3].text.toUpperCase(), @@ -1540,12 +1566,12 @@ const AppFramework = (props) => { name: parsedFrameworkData.Assets.name === undefined ? "" : parsedFrameworkData.Assets.name, description: parsedFrameworkData.Assets.description === undefined ? "" : parsedFrameworkData.Assets.description, app_id: parsedFrameworkData.Assets.id === undefined ? "" : parsedFrameworkData.Assets.id, - text_margin_y: parsedFrameworkData.Assets.large_image === undefined ? textMarginDefault : textMarginImage, - margin_x: parsedFrameworkData.Assets.large_image === undefined ? `${32*scale}px` : "0px", - margin_y: parsedFrameworkData.Assets.large_image === undefined ? `${19*scale}px` : `0px`, - width: parsedFrameworkData.Assets.large_image === undefined ? iconSize : defaultSize, - height: parsedFrameworkData.Assets.large_image === undefined ? iconSize : defaultSize, - large_image: parsedFrameworkData.Assets.large_image === undefined ? parsedDatatypeImages["ASSETS"] : parsedFrameworkData.Assets.large_image, + text_margin_y: assetscheck ? textMarginDefault : textMarginImage, + margin_x: assetscheck ? `${32*scale}px` : "0px", + margin_y: assetscheck ? `${19*scale}px` : `0px`, + width: assetscheck ? iconSize : defaultSize, + height: assetscheck ? iconSize : defaultSize, + large_image: parsedFrameworkData.Assets.large_image === undefined ? parsedDatatypeImages()["ASSETS"] : parsedFrameworkData.Assets.large_image, label: securityFramework[2].text.toUpperCase(), id: securityFramework[2].text.toUpperCase(), @@ -1568,12 +1594,12 @@ const AppFramework = (props) => { name: parsedFrameworkData.Intel.name === undefined ? "" : parsedFrameworkData.Intel.name, description: parsedFrameworkData.Intel.description === undefined ? "" : parsedFrameworkData.Intel.description, app_id: parsedFrameworkData.Intel.id === undefined ? "" : parsedFrameworkData.Intel.id, - text_margin_y: parsedFrameworkData.Intel.large_image === undefined ? textMarginDefault : textMarginImage, - margin_x: parsedFrameworkData.Intel.large_image === undefined ? `${32*scale}px` : "0px", - margin_y: parsedFrameworkData.Intel.large_image === undefined ? `${19*scale}px` : `0px`, - width: parsedFrameworkData.Intel.large_image === undefined ? iconSize : defaultSize, - height: parsedFrameworkData.Intel.large_image === undefined ? iconSize : defaultSize, - large_image: parsedFrameworkData.Intel.large_image === undefined ? parsedDatatypeImages["INTEL"] : parsedFrameworkData.Intel.large_image, + text_margin_y: intelcheck ? textMarginDefault : textMarginImage, + margin_x: intelcheck ? `${32*scale}px` : "0px", + margin_y: intelcheck ? `${19*scale}px` : `0px`, + width: intelcheck ? iconSize : defaultSize, + height: intelcheck ? iconSize : defaultSize, + large_image: parsedFrameworkData.Intel.large_image === undefined ? parsedDatatypeImages()["INTEL"] : parsedFrameworkData.Intel.large_image, label: securityFramework[4].text.toUpperCase(), id: securityFramework[4].text.toUpperCase(), @@ -1596,12 +1622,12 @@ const AppFramework = (props) => { name: parsedFrameworkData.Comms.name === undefined ? "" : parsedFrameworkData.Comms.name, description: parsedFrameworkData.Comms.description === undefined ? "" : parsedFrameworkData.Comms.description, app_id: parsedFrameworkData.Comms.id === undefined ? "" : parsedFrameworkData.Comms.id, - text_margin_y: parsedFrameworkData.Comms.large_image === undefined ? textMarginDefault : textMarginImage, - margin_x: parsedFrameworkData.Comms.large_image === undefined ? `${32*scale}px` : "0px", - margin_y: parsedFrameworkData.Comms.large_image === undefined ? `${19*scale}px` : `0px`, - width: parsedFrameworkData.Comms.large_image === undefined ? iconSize : defaultSize, - height: parsedFrameworkData.Comms.large_image === undefined ? iconSize : defaultSize, - large_image: parsedFrameworkData.Comms.large_image === undefined ? parsedDatatypeImages["COMMS"] : parsedFrameworkData.Comms.large_image, + text_margin_y: commscheck ? textMarginDefault : textMarginImage, + margin_x: commscheck ? `${32*scale}px` : "0px", + margin_y: commscheck ? `${19*scale}px` : `0px`, + width: commscheck ? iconSize : defaultSize, + height: commscheck ? iconSize : defaultSize, + large_image: parsedFrameworkData.Comms.large_image === undefined ? parsedDatatypeImages()["COMMS"] : parsedFrameworkData.Comms.large_image, label: securityFramework[5].text.toUpperCase(), id: securityFramework[5].text.toUpperCase(), @@ -1624,12 +1650,12 @@ const AppFramework = (props) => { name: parsedFrameworkData["EDR & AV"].name === undefined ? "" : parsedFrameworkData["EDR & AV"].name, description: parsedFrameworkData["EDR & AV"].description === undefined ? "" : parsedFrameworkData["EDR & AV"].description, app_id: parsedFrameworkData["EDR & AV"].id === undefined ? "" : parsedFrameworkData["EDR & AV"].id, - text_margin_y: parsedFrameworkData["EDR & AV"].large_image === undefined ? textMarginDefault : textMarginImage, - margin_x: parsedFrameworkData["EDR & AV"].large_image === undefined ? `${32*scale}px` : "0px", - margin_y: parsedFrameworkData["EDR & AV"].large_image === undefined ? `${19*scale}px` : `0px`, - width: parsedFrameworkData["EDR & AV"].large_image === undefined ? iconSize : defaultSize, - height: parsedFrameworkData["EDR & AV"].large_image === undefined ? iconSize : defaultSize, - large_image: parsedFrameworkData["EDR & AV"].large_image === undefined ? parsedDatatypeImages["EDR & AV"] : parsedFrameworkData["EDR & AV"].large_image, + text_margin_y: edrcheck ? textMarginDefault : textMarginImage, + margin_x: edrcheck ? `${32*scale}px` : "0px", + margin_y: edrcheck ? `${19*scale}px` : `0px`, + width: edrcheck ? iconSize : defaultSize, + height: edrcheck ? iconSize : defaultSize, + large_image: parsedFrameworkData["EDR & AV"].large_image === undefined ? parsedDatatypeImages()["EDR & AV"] : parsedFrameworkData["EDR & AV"].large_image, label: securityFramework[7].text.toUpperCase(), id: securityFramework[7].text.toUpperCase(), @@ -1652,12 +1678,12 @@ const AppFramework = (props) => { name: parsedFrameworkData.Network.name === undefined ? "" : parsedFrameworkData.Network.name, description: parsedFrameworkData.Network.description === undefined ? "" : parsedFrameworkData.Network.description, app_id: parsedFrameworkData.Network.id === undefined ? "" : parsedFrameworkData.Network.id, - text_margin_y: parsedFrameworkData.Network.large_image === undefined ? textMarginDefault : textMarginImage, - margin_x: parsedFrameworkData.Network.large_image === undefined ? `${32*scale}px` : "0px", - margin_y: parsedFrameworkData.Network.large_image === undefined ? `${19*scale}px` : `0px`, - width: parsedFrameworkData.Network.large_image === undefined ? iconSize : defaultSize, - height: parsedFrameworkData.Network.large_image === undefined ? iconSize : defaultSize, - large_image: parsedFrameworkData.Network.large_image === undefined ? parsedDatatypeImages["NETWORK"] : parsedFrameworkData.Network.large_image, + text_margin_y: networkcheck ? textMarginDefault : textMarginImage, + margin_x: networkcheck ? `${32*scale}px` : "0px", + margin_y: networkcheck ? `${19*scale}px` : `0px`, + width: networkcheck ? iconSize : defaultSize, + height: networkcheck ? iconSize : defaultSize, + large_image: parsedFrameworkData.Network.large_image === undefined ? parsedDatatypeImages()["NETWORK"] : parsedFrameworkData.Network.large_image, label: securityFramework[6].text.toUpperCase(), id: securityFramework[6].text.toUpperCase(), animate: false, @@ -1679,12 +1705,12 @@ const AppFramework = (props) => { name: parsedFrameworkData.SIEM.name === undefined ? "" : parsedFrameworkData.SIEM.name, description: parsedFrameworkData.SIEM.description === undefined ? "" : parsedFrameworkData.SIEM.description, app_id: parsedFrameworkData.SIEM.id === undefined ? "" : parsedFrameworkData.SIEM.id, - text_margin_y: parsedFrameworkData.SIEM.large_image === undefined ? textMarginDefault : textMarginImage, - margin_x: parsedFrameworkData.SIEM.large_image === undefined ? `${32*scale}px` : "0px", - margin_y: parsedFrameworkData.SIEM.large_image === undefined ? `${19*scale}px` : `0px`, - width: parsedFrameworkData.SIEM.large_image === undefined ? iconSize : defaultSize, - height: parsedFrameworkData.SIEM.large_image === undefined ? iconSize : defaultSize, - large_image: parsedFrameworkData.SIEM.large_image === undefined ? parsedDatatypeImages["SIEM"] : parsedFrameworkData.SIEM.large_image, + text_margin_y: siemcheck ? textMarginDefault : textMarginImage, + margin_x: siemcheck ? `${32*scale}px` : "0px", + margin_y: siemcheck ? `${19*scale}px` : `0px`, + width: siemcheck ? iconSize : defaultSize, + height: siemcheck ? iconSize : defaultSize, + large_image: parsedFrameworkData.SIEM.large_image === undefined ? parsedDatatypeImages()["SIEM"] : parsedFrameworkData.SIEM.large_image, label: securityFramework[1].text.toUpperCase(), id: securityFramework[1].text.toUpperCase(), animate: false, @@ -1820,8 +1846,8 @@ const AppFramework = (props) => { const UsecaseHandler = (props) => { const { data, index, diff } = props - const leftImage = data.left_image !== undefined ? parsedDatatypeImages[data.left_image.toUpperCase()] : undefined - const rightImage = data.right_image !== undefined ? parsedDatatypeImages[data.right_image.toUpperCase()] : undefined + const leftImage = data.left_image !== undefined ? parsedDatatypeImages()[data.left_image.toUpperCase()] : undefined + const rightImage = data.right_image !== undefined ? parsedDatatypeImages()[data.right_image.toUpperCase()] : undefined if (leftImage === undefined) { console.log("LEFT MISSING: ", leftImage) @@ -1988,6 +2014,7 @@ const AppFramework = (props) => { const bgColor = color === undefined || color === null || color.length === 0 ? theme.palette.surfaceColor : color return (
+ {/*
{ inputSearch={changedApp} apps={apps} /> -
- {injectedApps.map((apps, appindex) => { +
+ */} + + {/*injectedApps.map((apps, appindex) => { var categoryTop = 100 var categoryLeft = 100 @@ -2028,8 +2057,11 @@ const AppFramework = (props) => { return null } + const scale = 0.9 + const offsetTop = -70 + const offsetLeft = 0 return ( -
+
{apps.map((app, appIndex) => { return ( { } if (setFrameworkData !== undefined) { - console.log("Setting frameworkdata") // Find discoveryData.id var keys = [] for (const [key, value] of Object.entries(frameworkData)) { @@ -2088,7 +2119,7 @@ const AppFramework = (props) => { })}
) - })} + })*/} {showOptions === false ? null :
@@ -2197,7 +2228,7 @@ const AppFramework = (props) => { if (foundelement !== undefined && foundelement !== null) { //console.log("element: ", foundelement) //console.log("DISC: ", discoveryData) - foundelement.data("large_image", parsedDatatypeImages[discoveryData.id.toUpperCase()]) + foundelement.data("large_image", parsedDatatypeImages()[discoveryData.id.toUpperCase()]) foundelement.data("text_margin_y", "14px") foundelement.data("margin_x", "32px") foundelement.data("margin_y", "19x") diff --git a/frontend/src/components/AppGrid.jsx b/frontend/src/components/AppGrid.jsx index 5bb08119..0f3fedef 100644 --- a/frontend/src/components/AppGrid.jsx +++ b/frontend/src/components/AppGrid.jsx @@ -1,385 +1,2051 @@ -import React, {useEffect, useState} from 'react'; +import React, { useEffect, useState, useRef } from "react"; -import theme from '../theme.jsx'; -import ReactGA from 'react-ga4'; -import {Link} from 'react-router-dom'; -import { removeQuery } from '../components/ScrollToTop.jsx'; +import theme from "../theme.jsx"; +import ReactGA from "react-ga4"; +import { Link } from "react-router-dom"; +import { removeQuery } from "../components/ScrollToTop.jsx"; +import { useMemo } from "react"; -import { - Search as SearchIcon, - CloudQueue as CloudQueueIcon, - Code as CodeIcon -} from '@mui/icons-material'; +import { Tabs, Tab, Collapse } from "@mui/material"; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import ExpandLessIcon from "@mui/icons-material/ExpandLess"; +import { + Search as SearchIcon, + CloudQueue as CloudQueueIcon, + Code as CodeIcon, + CollectionsOutlined, + CookieSharp, +} from "@mui/icons-material"; +import { toast } from "react-toastify" +import ClearIcon from '@mui/icons-material/Clear'; +import Box from '@mui/material/Box'; -import algoliasearch from 'algoliasearch/lite'; -import { InstantSearch, Configure, connectSearchBox, connectHits, connectHitInsights } from 'react-instantsearch-dom'; +import CircularProgress from '@mui/material/CircularProgress'; -import aa from 'search-insights' +import algoliasearch from "algoliasearch/lite"; +import { + InstantSearch, + Configure, + connectSearchBox, + connectHits, + connectHitInsights, + RefinementList, + ClearRefinements, + connectStateResults +} from "react-instantsearch-dom"; -import { - Zoom, - Grid, - Paper, - TextField, - ButtonBase, - InputAdornment, - Typography, - Button, - Tooltip -} from '@mui/material'; +import aa from "search-insights"; +import { useLocation } from 'react-router-dom'; -const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240") +import "./FilterCSS.css"; + +import { + Zoom, + Grid, + Paper, + TextField, + ButtonBase, + InputAdornment, + Typography, + Button, + Tooltip, +} from "@mui/material"; + +const searchClient = algoliasearch( + "JNSS5CFDZZ", + "db08e40265e2941b9a7d8f644b6e5240" +); //const searchClient = algoliasearch("L55H18ZINA", "a19be455e7e75ee8f20a93d26b9fc6d6") -const AppGrid = props => { - const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, userdata, isHeader } = props + +const AppGrid = (props) => { + const { + maxRows, + showName, + showSuggestion, + isMobile, + globalUrl, + parsedXs, + isHeader, + } = props; const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; - const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows - const xs = parsedXs === undefined || parsedXs === null ? isMobile ? 6 : 2 : parsedXs - //const [apps, setApps] = React.useState([]); - //const [filteredApps, setFilteredApps] = React.useState([]); - const [formMail, setFormMail] = React.useState(""); - const [message, setMessage] = React.useState(""); - const [formMessage, setFormMessage] = React.useState(""); + const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows; + const xs = + parsedXs === undefined || parsedXs === null ? (isMobile ? 6 : 3) : parsedXs; - const buttonStyle = {borderRadius: 30, height: 50, width: 220, margin: isMobile ? "15px auto 15px auto" : 20, fontSize: 18,} + const [formMail, setFormMail] = React.useState(""); + const [message, setMessage] = React.useState(""); + const [formMessage, setFormMessage] = React.useState(""); - const innerColor = "rgba(255,255,255,0.65)" - const borderRadius = 3 - window.title = "Shuffle | Apps | Find and integrate any app" + const buttonStyle = { + borderRadius: 30, + height: 50, + width: 220, + margin: isMobile ? "15px auto 15px auto" : 20, + fontSize: 18, + }; + const innerColor = "rgba(255,255,255,0.65)"; + const borderRadius = 3; + window.title = "Shuffle | Apps | Find and integrate any app"; + const noImage = "/public/no_image.png"; - const submitContact = (email, message) => { - const data = { - "firstname": "", - "lastname": "", - "title": "", - "companyname": "", - "email": email, - "phone": "", - "message": message, - } - - const errorMessage = "Something went wrong. Please contact frikky@shuffler.io directly." + const submitContact = (email, message) => { + const data = { + firstname: "", + lastname: "", + title: "", + companyname: "", + email: email, + phone: "", + message: message, + }; - fetch(globalUrl+"/api/v1/contact", { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(data), - }) - .then(response => response.json()) - .then(response => { - if (response.success === true) { - setFormMessage(response.reason) - //toast("Thanks for submitting!") - } else { - setFormMessage(errorMessage) - } + const errorMessage = + "Something went wrong. Please contact frikky@shuffler.io directly."; - setFormMail("") - setMessage("") + fetch(globalUrl + "/api/v1/contact", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), }) - .catch(error => { - setFormMessage(errorMessage) - console.log(error) - }); - } + .then((response) => response.json()) + .then((response) => { + if (response.success === true) { + setFormMessage(response.reason); + //toast("Thanks for submitting!") + } else { + setFormMessage(errorMessage); + } - const SearchBox = ({currentRefinement, refine, isSearchStalled} ) => { - var defaultSearch = "" - //useEffect(() => { - if (window !== undefined && window.location !== undefined && window.location.search !== undefined && window.location.search !== null) { - const urlSearchParams = new URLSearchParams(window.location.search) - const params = Object.fromEntries(urlSearchParams.entries()) - const foundQuery = params["q"] - if (foundQuery !== null && foundQuery !== undefined) { - console.log("Got query: ", foundQuery) - refine(foundQuery) - defaultSearch = foundQuery - } - } - //}, []) + setFormMail(""); + setMessage(""); + }) + .catch((error) => { + setFormMessage(errorMessage); + console.log(error); + }); + }; - return ( -
- - - - ), - }} - autoComplete='off' - type="search" - color="primary" - placeholder="Find Apps..." - id="shuffle_search_field" - onChange={(event) => { - // Remove "q" from URL - removeQuery("q") + const SearchBox = ({ currentRefinement, refine, isSearchStalled, searchQuery, setSearchQuery }) => { + var defaultSearch = ""; - refine(event.currentTarget.value) - }} - limit={5} - /> - {/*isSearchStalled ? 'My search is stalled' : ''*/} - - ) - } - var workflowDelay = -50 - const Hits = ({ hits, insights }) => { - const [mouseHoverIndex, setMouseHoverIndex] = useState(-1) - var counted = 0 + //useEffect(() => { + if ( + window !== undefined && + window.location !== undefined && + window.location.search !== undefined && + window.location.search !== null + ) { + const urlSearchParams = new URLSearchParams(window.location.search); + const params = Object.fromEntries(urlSearchParams.entries()); + const foundQuery = params["q"]; + if (foundQuery !== null && foundQuery !== undefined) { + console.log("Got query: ", foundQuery); + refine(foundQuery); + defaultSearch = foundQuery; + searchQuery = foundQuery + } + } + //}, []) - //console.log(hits) - //var curhits = hits - //if (hits.length > 0 && defaultApps.length === 0) { - // setDefaultApps(hits) - //} - //const [defaultApps, setDefaultApps] = React.useState([]) - //console.log(hits) - //if (hits.length > 0 && hits.length !== innerHits.length) { - // setInnerHits(hits) - //} + const handleSearch = () => { + refine(searchQuery.trim()); + }; - return ( - - {hits.map((data, index) => { + return ( +
+ + + + ), + endAdornment: ( + + {searchQuery.length > 0 && ( + { + setSearchQuery('') + removeQuery("q"); + refine('') + }} + /> + )} + + + ), - workflowDelay += 50 + }} + autoComplete="off" + color="primary" + placeholder="Search more than 2500 Apps" + id="shuffle_search_field" + onChange={(event) => { + setSearchQuery(event.currentTarget.value); + removeQuery("q"); + refine(event.currentTarget.value); + }} + limit={5} + /> + {/*isSearchStalled ? 'My search is stalled' : ''*/} + + ); + }; - const paperStyle = { - backgroundColor: index === mouseHoverIndex ? "rgba(255,255,255,0.8)" : theme.palette.inputColor, - color: index === mouseHoverIndex ? theme.palette.inputColor : "rgba(255,255,255,0.8)", - border: `1px solid ${innerColor}`, - padding: isHeader ? null : 15, - cursor: "pointer", - position: "relative", - minHeight: 116, - } - - if (counted === 12/xs*rowHandler) { - return null - } + const [currTab, setCurrTab] = useState(0); + const location = useLocation(); - counted += 1 - var parsedname = "" - for (var key = 0; key < data.name.length; key++) { - var character = data.name.charAt(key) - if (character === character.toUpperCase()) { - //console.log(data.name[key], data.name[key+1]) - if (data.name.charAt(key+1) !== undefined && data.name.charAt(key+1) === data.name.charAt(key+1).toUpperCase()) { - } else { - parsedname += " " - } - } + useEffect(() => { + const queryParams = new URLSearchParams(location.search); + const tabParam = queryParams.get('tab'); + if (tabParam === 'org_apps') { + setCurrTab(1); + } else if (tabParam === 'my_apps') { + setCurrTab(2); + } else { + setCurrTab(0); + } + }, [location.search]); - parsedname += character - } - - parsedname = (parsedname.charAt(0).toUpperCase()+parsedname.substring(1)).replaceAll("_", " ") - const appUrl = isCloud ? `/apps/${data.objectID}?queryID=${data.__queryID}` : `https://shuffler.io/apps/${data.objectID}?queryID=${data.__queryID}` - return ( - - - - { - setMouseHoverIndex(index) - /* - ReactGA.event({ - category: "app_grid_view", - action: `search_bar_click`, - label: "", - }) - */ - }} onMouseOut={() => { - setMouseHoverIndex(-1) - }} onClick={() => { - if (isCloud) { - ReactGA.event({ - category: "app_grid_view", - action: `app_${parsedname}_${data.id}_click`, - label: "", - }) - } + const handleTabChange = (event, newTab) => { + setCurrTab(newTab); + const newQueryParam = newTab === 0 ? 'all_apps' : newTab === 1 ? 'org_apps' : 'my_apps'; + const queryParams = new URLSearchParams(location.search); + queryParams.set('tab', newQueryParam); + queryParams.delete('q'); + window.history.replaceState({}, '', `${location.pathname}?${queryParams.toString()}`); + }; - //const searchClient = algoliasearch("L55H18ZINA", "a19be455e7e75ee8f20a93d26b9fc6d6") - console.log(searchClient) - aa('init', { - appId: searchClient.appId, - apiKey: searchClient.transporter.queryParameters["x-algolia-api-key"] - }) - const timestamp = new Date().getTime() - aa('sendEvents', [ - { - eventType: 'click', - eventName: 'Product Clicked', - index: 'appsearch', - objectIDs: [data.objectID], - timestamp: timestamp, - queryID: data.__queryID, - positions: [data.__position], - userToken: userdata === undefined || userdata === null || userdata.id === undefined ? "unauthenticated" : userdata.id, - } - ]) - }}> - - {data.name} - -
- {index === mouseHoverIndex || showName === true ? - parsedname - : - null - } - {data.generated ? - - {data.invalid ? - - : - - } - - : - - - - } - - - - - ) - })} - - ) - } + const [isLoggedIn, setIsLoggedIn] = useState(false); + const [isLoading, setIsLoading] = useState(true) - const CustomSearchBox = connectSearchBox(SearchBox) - const CustomHits = connectHits(Hits) - //const CustomHits = connectHitInsights(aa)(Hits) - const selectButtonStyle = { - minWidth: 150, - maxWidth: 150, - minHeight: 50, - } + // Component to fetch all public app from the algolia. + const Hits = ({ + hits, + insights, + setIsAnyAppActivated, + searchQuery + }) => { + const [mouseHoverIndex, setMouseHoverIndex] = useState(-1); + var counted = 0; + const [hoverEffect, setHoverEffect] = useState(-1); - return ( -
- {/* -
- -
- */} -
- -
- -
- - -
- {showSuggestion === true ? -
- - Can't find what you're looking for? - -
- setFormMail(e.target.value)} - /> - setMessage(e.target.value)} - /> -
- - {formMessage} -
- : null - } + const normalizedString = (name) => { + if (typeof name === 'string') { + return name.replace(/_/g, ' '); + } else { + return name; + } + }; - - - Search by - - - Algolia logo - - -
-
- ) -} + //check user login and get user info. + const [allActivatedAppIds, setAllActivatedAppIds] = useState(null); + const [userdata, setUserdata] = useState([]); + + useEffect(() => { + var baseurl = globalUrl; + fetch(baseurl + "/api/v1/getinfo", { + credentials: "include", + headers: { + 'Content-Type': 'application/json', + }, + }) + .then(response => response.json()) + .then(responseJson => { + if (responseJson.success) { + setUserdata(responseJson); + setAllActivatedAppIds(responseJson.active_apps) + setIsLoggedIn(true); + } else { + setIsLoggedIn(false); + } + setIsLoading(false) + }) + .catch(error => { + console.log("Failed login check: ", error); + }); + }, [currTab]); + + //Function for activation and deactivation of app + const handleActivateButton = (event, data, type) => { + + //use prevent default so it will stop redirection to the app page + event.preventDefault(); + if (!isLoggedIn) { + toast.error("Please log in to your account to activate the app.") + return; + } + if (type === "activate") { + toast.success(`The ${normalizedString(data.name)} app is activating. Please wait...`); + } + if (type === "deactivate") { + toast.success(`The ${normalizedString(data.name)} app is deactivating. Please wait...`); + } + + const baseURL = globalUrl; + const url = `${baseURL}/api/v1/apps/${data.objectID}/${type}`; + + fetch(url, { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + }, + credentials: "include", + }) + .then((response) => response.json()) + .then((responseJson) => { + if (responseJson.success === false) { + toast.error(responseJson.reason); + } else { + toast.success(`App ${type}d Successfully!`); + if (type === 'activate') { + setAllActivatedAppIds(prev => [...prev, data.objectID]); + setIsAnyAppActivated(true); + } + if (type === 'deactivate') { + const updatedIds = allActivatedAppIds.filter(id => id !== data.objectID); + setAllActivatedAppIds(updatedIds); + } + } + }) + .catch(error => { + console.log("app error: ", error.toString()); + }); + } + + let workflowDelay = 0; + const isHeader = true; + const paperStyle = { + color: "rgba(241, 241, 241, 1)", + padding: isHeader ? null : 15, + cursor: "pointer", + maxWidth: 339, + maxHeight: 96, + borderRadius: 8, + transition: 'background-color 0.3s ease', + }; + + const [showNoAppFound, setShowNoAppFound] = useState(false); + + //show some delay to show the "App Not Found." so it doesn't not show while changing tab. + useEffect(() => { + const timer = setTimeout(() => { + setShowNoAppFound(true); + }, 1000); + return () => clearTimeout(timer); + }, []); + + return ( +
+ {!isLoading ? ( +
+ {hits.length === 0 && searchQuery.length >= 0 && showNoAppFound ? ( + No App Found + ) : ( + +
+ {hits.map((data, index) => { + const appUrl = + isCloud + ? `/apps/${data.objectID}?queryID=${data.__queryID}` + : `https://shuffler.io/apps/${data.objectID}?queryID=${data.__queryID}`; + + return ( + + + + { + setMouseHoverIndex(index); + }} + onMouseLeave={() => { + setMouseHoverIndex(-1); + }} + > + + ) : ( + + )} +
+ )} +
+
+
+ + + +
+
+ ); + }) + } +
+ + )} +
+ ) : ( +
+ )} +
+ ); + }; + + var workflowDelay = -50; + + const CustomClearRefinements = connectStateResults(({ searchResults, ...rest }) => { + const hasFilters = searchResults && searchResults.nbHits !== searchResults.nbSortedHits; + return Clear All }} {...rest} disabled={!hasFilters} />; + }); + + //Component to Filter all apps base on category + const FilterAllAppsByCategory = () => { + const [isCategoreListExpanded, setIsCategoreListExpanded] = useState(true); + + const toggleRefinementList = () => { + setIsCategoreListExpanded((prevState) => !prevState); + }; + + const categoryButtonStyling = { + cursor: "pointer", + color: "white", + border: "none", + backgroundColor: "transparent", + fontSize: 16, + display: "flex", + width: "100%", + height: 30, + flexDirection: "row", + textTransform: 'none', + fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)" + } + + return ( +
+ + +
+ + +
+
+
+ ); + }; + + //Component to filter all apps base on Action label + const FilterByActionLabel = () => { + + const [isActionLabelExpanded, setIsActionLabelExpanded] = useState(false); + + const toogleActionLabel = () => { + setIsActionLabelExpanded((prevState) => !prevState); + }; + + const actionLabelButtonStyling = { + cursor: "pointer", + color: "white", + border: "none", + backgroundColor: "transparent", + fontSize: 16, + display: "flex", + flexDirection: "row", + width: "100%", + height: 30, + textTransform: 'none', + fontWeight: 400 + } + + return ( +
+ + + +
+ + +
+
+
+ ); + }; + + //component to filter all apps base on the created with like 'App Editor' or 'Python' + const FilterByCreatedWith = () => { + const [isCreatedWithExpanded, setIsCreatedWithExpanded] = useState(false); + + const toogleCreatedWith = () => { + setIsCreatedWithExpanded((prevState) => !prevState); + }; + + const transformRefinementListItems = items => + items.map(item => ({ + ...item, + label: item.label === 'true' ? 'App Editor' : 'Python', + })); + + const createdWithButtonStyling = { + cursor: "pointer", + color: "white", + border: "none", + backgroundColor: "transparent", + fontSize: 16, + display: "flex", + flexDirection: "row", + alignItems: "center", + width: "100%", + height: 30, + textTransform: 'none', + fontWeight: 400 + } + + return ( +
+ + + +
+ + +
+
+
+ ); + }; + + const FilterCreatedBy = () => { + const [isCreatedByExpanded, setIscreatedByExpanded] = useState(false); + useState(false); + + const toogleCreatedBy = () => { + setIscreatedByExpanded((prevState) => !prevState); + }; + + const createdByButtonStyling = { + cursor: "pointer", + color: "white", + border: "none", + backgroundColor: "transparent", + fontSize: 16, + display: "flex", + flexDirection: "row", + alignItems: "center", + whiteSpace: "nowrap", + width: "100%", + height: 30, + textTransform: 'none', + opacity: '0.5' + } + + return ( +
+ + + {isCreatedByExpanded && ( + <> + {/* */} + + {/* */} + + + )} +
+ ); + }; + + const FilterForAllApps = () => { + return ( +
+ + Filter By + + + + + +
+ ); + }; + + + + const boxStyle = { + color: "white", + flex: "1", + marginLeft: isHeader ? null : 10, + marginRight: isHeader ? null : 10, + paddingLeft: isHeader ? null : 30, + paddingRight: isHeader ? null : 30, + paddingBottom: isHeader ? null : 30, + display: "flex", + flexDirection: "column", + overflowX: "visible", + backgroundColor: "rgba(33, 33, 33, 1)", + borderRadius: 16, + marginTop: 24, + width: 741, + height: 741, + }; + + + //Component to display all apps. + const AllApps = ({ setIsAnyAppActivated }) => { + var [searchQuery, setSearchQuery] = useState(""); + + return ( +
+ + +
+ ); + }; + + //Search box for the orgs and users apps + const SearchBoxForOrgAndUserApp = ({ searchQuery, setSearchQuery }) => { + + return ( +
+ + + + ), + endAdornment: ( + + {searchQuery.length > 0 && ( + { + setSearchQuery(''); + }} + /> + )} + + + ), + }} + autoComplete="off" + color="primary" + placeholder="Search your Activated or Self-built apps" + id="shuffle_search_field" + onChange={(event) => { + setSearchQuery(event.currentTarget.value); + }} + limit={5} + /> + {/*isSearchStalled ? 'My search is stalled' : ''*/} + + ) + } + + + const [userApps, setUserApps] = useState([]); + const [orgApps, setOrgApps] = useState([]); + + useEffect(() => { + if (currTab === 2) { + const baseUrl = globalUrl; + const userAppsUrl = `${baseUrl}/api/v1/users/me/apps`; + fetch(userAppsUrl, { + method: "GET", + credentials: "include", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => response.json()) + .then((data) => { + setUserApps(data); + setIsLoading(false); + }) + .catch((err) => { + console.error("Error fetching user apps:", err); + }); + } else if (currTab === 1) { + const baseUrl = globalUrl; + const appsUrl = `${baseUrl}/api/v1/apps`; + fetch(appsUrl, { + method: "GET", + credentials: "include", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => response.json()) + .then((data) => { + setOrgApps(data); + setIsLoading(false) + }) + .catch((err) => { + console.error("Error fetching apps:", err); + }); + } + }, [currTab]); + //Component to display category List for User and Orgs app + const FilterUsersAndOrgsAppByCategory = ({ selectedCategoryForUsersAndOgsApps, setselectedCategoryForUsersAndOgsApps }) => { + + const [isCategoreListExpanded, setIsCategoryListExpanded] = useState(true); + + const toogleCategoryList = () => { + setIsCategoryListExpanded((prevState) => !prevState); + }; + + const [appsToFilter, setAppsToFilter] = useState([]); + useEffect(() => { + if (currTab === 1) { + setAppsToFilter(orgApps) + } + if (currTab === 2) { + setAppsToFilter(userApps) + } + }) + + //Display top 9 category from the database + const findTopCategories = () => { + const categoryCountMap = {}; + + // Check if userAndOrgsApp is an array before iterating over it and Find top 10 Category from the apps + if (Array.isArray(appsToFilter)) { + appsToFilter.forEach((app) => { + const categories = app.categories; + + if (categories && categories.length > 0) { + categories.forEach((category) => { + categoryCountMap[category] = (categoryCountMap[category] || 0) + 1; + }); + } + }); + + const categoryArray = Object.keys(categoryCountMap).map((category) => ({ + category, + count: categoryCountMap[category], + })); + + categoryArray.sort((a, b) => b.count - a.count); + + const topCategories = categoryArray.slice(0, 7); + + return topCategories; + } + }; + + const topCategories = findTopCategories(); + + const handleCheckboxChange = (category) => { + if (selectedCategoryForUsersAndOgsApps.includes(category)) { + setselectedCategoryForUsersAndOgsApps(selectedCategoryForUsersAndOgsApps.filter((item) => item !== category)); + } else { + setselectedCategoryForUsersAndOgsApps([...selectedCategoryForUsersAndOgsApps, category]); + } + }; + + const handleClearFilter = () => { + setselectedCategoryForUsersAndOgsApps([]); + }; + + const categorysButtonStyling = { + cursor: "pointer", + color: "white", + border: "none", + backgroundColor: "transparent", + fontSize: 16, + display: "flex", + width: "100%", + height: 30, + flexDirection: "row", + textTransform: 'none', + fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)", + marginBottom: isCategoreListExpanded && 10 + } + return ( +
+ +
+ {!isLoading && ( + +
+ {topCategories.map((data, index) => ( + + ))} + + +
+
+ )} +
+
+ ); + }; + + const FilterUsersAndOrgsAppByActionLabel = ({ selectedTagsForUserAndOrgApps, setSelectedTagsForUserAndOrgApps }) => { + + const [isActionLabelExpanded, setIsActionLabelExpanded] = useState(false); + const toggleActionLabel = () => { + setIsActionLabelExpanded((prevState) => !prevState); + }; + const [appsToFilter, setAppsToFilter] = useState([]); + useEffect(() => { + if (currTab === 1) { + setAppsToFilter(orgApps) + } + if (currTab === 2) { + setAppsToFilter(userApps) + } + }) + + //Find top 9 tags from the database + const findTopTags = () => { + const tagCountMap = {}; + + if (Array.isArray(appsToFilter)) { + appsToFilter.forEach((app) => { + const tags = app.tags; + if (tags && tags.length > 0) { + tags.forEach((tag) => { + tagCountMap[tag] = (tagCountMap[tag] || 0) + 1; + }); + } + }); + } + + const tagArray = Object.keys(tagCountMap).map((tag) => ({ + tag, + count: tagCountMap[tag], + })); + + tagArray.sort((a, b) => b.count - a.count); + + const topTags = tagArray.slice(0, 8); + + return topTags; + }; + + + const topTags = findTopTags(); + + const handleCheckboxChange = (index) => { + const category = topTags[index].tag; + const updatedCheckboxStates = [...selectedTagsForUserAndOrgApps]; + + if (updatedCheckboxStates.includes(category)) { + setSelectedTagsForUserAndOrgApps(updatedCheckboxStates.filter((item) => item !== category)); + } else { + setSelectedTagsForUserAndOrgApps([...updatedCheckboxStates, category]); + } + }; + const handleClearFilter = () => { + setSelectedTagsForUserAndOrgApps([]); + }; + + const actionLabelButtonStyling = { + cursor: 'pointer', + color: 'white', + border: 'none', + backgroundColor: 'transparent', + fontSize: 16, + display: 'flex', + width: '100%', + height: 30, + flexDirection: 'row', + textTransform: 'none', + fontFamily: 'var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)', + marginBottom: isActionLabelExpanded && 15 + }; + + return ( +
+ + +
+ {topTags && topTags.length > 0 && topTags.map((data, index) => ( + + ))} + +
+
+
+ ); + }; + + + + + const FilterUsersAndOrgsAppByCreatedWith = ({ selectedOptionOfCreatedWith, setSelectedOptionOfCreatedWith }) => { + + const [isCreatedWithExpanded, setIsCreatedWithExpanded] = useState(false); + + const toogleCreatedWith = () => { + setIsCreatedWithExpanded((prevState) => !prevState); + }; + + const AppCreatedWithOptions = ['App Editor', 'Python'] + + const handleCheckboxChange = (index) => { + const category = AppCreatedWithOptions[index]; + const updatedCheckboxStates = [...selectedOptionOfCreatedWith]; + if (updatedCheckboxStates.includes(category)) { + setSelectedOptionOfCreatedWith(updatedCheckboxStates.filter((item) => item !== category)); + } else { + setSelectedOptionOfCreatedWith([...updatedCheckboxStates, category]); + } + }; + + + const handleClearFilter = () => { + setSelectedOptionOfCreatedWith([]); + }; + + const createdWithButtonStyling = { + cursor: "pointer", + color: "white", + border: "none", + backgroundColor: "transparent", + fontSize: 16, + display: "flex", + flexDirection: "row", + alignItems: "center", + width: "100%", + height: 30, + textTransform: 'none', + marginBottom: isCreatedWithExpanded && 16, + fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)" + } + + return ( +
+ + + +
+ {AppCreatedWithOptions.map((data, index) => ( + + + ))} + + +
+
+
+ ); + }; + + const FilterUsersAndOrgsAppCreatedBy = () => { + + const [isCreatedByExpanded, setIscreatedByExpanded] = useState(false); + useState(false); + + const toogleCreatedBy = () => { + setIscreatedByExpanded((prevState) => !prevState); + }; + + const [isButtonDisable, setIsButtonDisable] = useState(true) + + const createdByButtonStyling = { + cursor: "pointer", + color: "white", + border: "none", + backgroundColor: isButtonDisable ? '#3c3c3c. ' : "transparent", + fontSize: 16, + display: "flex", + flexDirection: "row", + alignItems: "center", + whiteSpace: "nowrap", + width: "100%", + height: 30, + textTransform: 'none', + opacity: '0.5' + } + + return ( +
+ + + {isCreatedByExpanded && ( + <> + + + + )} +
+ ); + }; + + const FilterForUserAndOrgApps = ({ setselectedCategoryForUsersAndOgsApps, setSelectedTagsForUserAndOrgApps, setSelectedOptionOfCreatedWith, selectedCategoryForUsersAndOgsApps, selectedTagsForUserAndOrgApps, selectedOptionOfCreatedWith }) => { + + return ( +
+ {isLoggedIn === true && ( +
+ + Filter By + + + + + +
+ )} +
+ ) + } + + useEffect(() => { + if (currTab === 1 || currTab === 2) { + setIsLoading(true); + } + }, [currTab]) + + //Component to fetch all apps created by user and Org + const UserAndOrgApps = ({ selectedCategoryForUsersAndOgsApps, selectedTagsForUserAndOrgApps, selectedOptionOfCreatedWith, setselectedCategoryForUsersAndOgsApps, setSelectedTagsForUserAndOrgApps, setSelectedOptionOfCreatedWith }) => { + + const [searchQuery, setSearchQuery] = useState(""); + const [appsToShow, setAppsToShow] = useState([]); + useEffect(() => { + if (currTab === 1) { + setAppsToShow(orgApps) + } + if (currTab === 2) { + setAppsToShow(userApps) + } + }, [currTab]) + + useEffect(() => { + setselectedCategoryForUsersAndOgsApps([]); + setSelectedTagsForUserAndOrgApps([]); + setSelectedOptionOfCreatedWith([]); + }, [currTab]) + + //Search app base on app name, category and tag + const filteredUserAppdata = Array.isArray(appsToShow) ? appsToShow.filter((app) => { + const matchesSearchQuery = ( + searchQuery === "" || + app.name.toLowerCase().includes(searchQuery.toLowerCase()) || + (app.tags && app.tags.some(tag => + tag.toLowerCase().includes(searchQuery.toLowerCase()) + )) || + (app.categories && app.categories.some((category) => + category.toLowerCase().includes(searchQuery.toLowerCase()) + )) + ); + + const matchesSelectedCategories = ( + selectedCategoryForUsersAndOgsApps.length === 0 || + (app.categories && app.categories.some(category => + selectedCategoryForUsersAndOgsApps.includes(category) + )) + ); + const matchesSelectedTags = ( + selectedTagsForUserAndOrgApps.length === 0 || + (app.tags && selectedTagsForUserAndOrgApps.some(tag => + app.tags.includes(tag) + )) + ); + + const matchesSelectedOption = ( + selectedOptionOfCreatedWith.length === 0 || + selectedOptionOfCreatedWith.includes('App Editor') && app.generated === true || + selectedOptionOfCreatedWith.includes('Python') && app.generated === false + ); + + return matchesSearchQuery && matchesSelectedCategories && matchesSelectedTags && matchesSelectedOption; + }) : []; + + + const [mouseHoverIndex, setMouseHoverIndex] = useState(-1); + var counted = 0; + const [showNoAppFound, setShowNoAppFound] = useState(false); + + //show some delay to show the "App Not Found." so it doesn't not show while changing tab. + useEffect(() => { + const timer = setTimeout(() => { + setShowNoAppFound(true); + }, 1000); + return () => clearTimeout(timer); + }, []); + + return ( +
+ {isLoading ? ( + + ) : ( +
+ {isLoggedIn ? ( +
+ + {((filteredUserAppdata.length === 0 && showNoAppFound) && isLoggedIn && !isLoading) ? ( + No App Found + ) : ( +
+
+ +
+ {filteredUserAppdata.map((data, index) => { + const isMouseOverOnCloudIcon = false; + const xs = 12; + const rowHandler = 12; + const searchClient = {}; + const userdata = {}; + + const paperStyle = { + backgroundColor: mouseHoverIndex === index ? "rgba(26, 26, 26, 1)" : "#1A1A1A", + color: "rgba(241, 241, 241, 1)", + padding: isHeader ? null : 15, + cursor: "pointer", + position: "relative", + width: 339, + height: 96, + borderRadius: 8, + }; + + var parsedname = ""; + for (var key = 0; key < data.name.length; key++) { + var character = data.name.charAt(key); + if (character === character.toUpperCase()) { + if ( + data.name.charAt(key + 1) !== undefined && + data.name.charAt(key + 1) === + data.name.charAt(key + 1).toUpperCase() + ) { + } else { + parsedname += " "; + } + } + parsedname += character; + } + + parsedname = ( + parsedname.charAt(0).toUpperCase() + parsedname.substring(1) + ).replaceAll("_", " "); + + const normalizedString = (name) => { + if (typeof name === 'string') { + return name.replace(/_/g, ' '); + } else { + return name; + } + }; + + const appUrl = + isCloud === true + ? `/apps/${data.id}` + : `https://shuffler.io/apps/${data.id}`; + + return ( + + + + { + setMouseHoverIndex(index); + }} + onMouseOut={() => { + setMouseHoverIndex(-1); + }} + > + + {data.name} + +
+
+ {normalizedString(data.name)} +
+
+ {data.categories !== null + ? normalizedString(data.categories).join(", ") + : "NA"} +
+
+ {data.tags && + data.tags.map((tag, tagIndex) => ( + + {normalizedString(tag)} + {tagIndex < data.tags.length - 1 ? ", " : ""} + + ))} +
+ {/* )} */} +
+
+
+
+
+
+ ); + }) + } +
+
+
+
+ )} +
+ ) : ( + Please login to your account first to view {`${currTab === 1 ? "Organization" : "My"}`} Apps.
+ Or signup to create a new account.
+ )} +
+ )} +
+ ); + }; + + + const AppTab = ({ selectedCategoryForUsersAndOgsApps, selectedTagsForUserAndOrgApps, selectedOptionOfCreatedWith, setselectedCategoryForUsersAndOgsApps, setSelectedTagsForUserAndOrgApps, setSelectedOptionOfCreatedWith }) => { + const [isAnyAppActivated, setIsAnyAppActivated] = useState(false); + + return ( +
+
+ + + {isAnyAppActivated && }Organization Apps
+ sx={{ + color: currTab === 1 ? "#F86743" : "inherit", + border: 'none', + height: 44, + fontSize: 16, + flex: 1, + textTransform: 'none', + fontWeight: 400, + paddingBottom: 3, + fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)", + }} + > + + + + {currTab === 0 ? ( + + ) : currTab === 1 || currTab === 2 ? ( + + ) : null} +
+ + ); + }; + + const CustomSearchBox = connectSearchBox(SearchBox); + const CustomHits = connectHits(Hits); + + const DisplayAllAppsTab = () => { + const [selectedCategoryForUsersAndOgsApps, setselectedCategoryForUsersAndOgsApps] = useState([]); + const [selectedTagsForUserAndOrgApps, setSelectedTagsForUserAndOrgApps] = useState([]); + const [selectedOptionOfCreatedWith, setSelectedOptionOfCreatedWith] = useState([]); + + return ( +
+ +
+ {currTab === 0 ? ( + + ) : ( + + )} + +
+ +
+
+ ); + }; + + return ( +
+
+ + {showSuggestion === true ? ( +
+ + Can't find what you're looking for? + +
+ setFormMail(e.target.value)} + /> + setMessage(e.target.value)} + /> +
+ + + {formMessage} + +
+ ) : null} +
+
+ ); +}; export default AppGrid; diff --git a/frontend/src/components/AppSearchButtons.jsx b/frontend/src/components/AppSearchButtons.jsx index 553b7e41..262843ad 100644 --- a/frontend/src/components/AppSearchButtons.jsx +++ b/frontend/src/components/AppSearchButtons.jsx @@ -37,7 +37,7 @@ import { } from '@mui/material'; const AppSearchButtons = (props) => { - const { userdata, globalUrl, appFramework, moreButton, finishedApps, appType, totalApps, index, onNodeSelect, setDiscoveryData, appName, AppImage, setDefaultSearch, discoveryData, checkLogin, setMissing, } = props + const { userdata, globalUrl, appFramework, moreButton, finishedApps, appType, totalApps, index, onNodeSelect, setDiscoveryData, appName, AppImage, setDefaultSearch, discoveryData, checkLogin, setMissing, getAppFramework, } = props const ref = useRef() let navigate = useNavigate(); @@ -48,8 +48,6 @@ const AppSearchButtons = (props) => { useEffect(() => { if (newSelectedApp !== undefined && setMissing != undefined) { - console.log("AppSearchButtons: setMissing is defined!") - const submitAppFramework = { "description": newSelectedApp.description, "id": newSelectedApp.objectID, @@ -60,11 +58,17 @@ const AppSearchButtons = (props) => { setFrameworkItem(submitAppFramework) setMissing(newSelectedApp) + + if (getAppFramework !== undefined) { + setTimeout(() => { + getAppFramework() + }, 1000) + } } }, [newSelectedApp]) if (appType === undefined) { - console.log("Apptype is required in AppSearchButtons") + //console.log("Apptype is required in AppSearchButtons") return null; } diff --git a/frontend/src/components/Billing.jsx b/frontend/src/components/Billing.jsx index 4152ce2a..e52bdce7 100644 --- a/frontend/src/components/Billing.jsx +++ b/frontend/src/components/Billing.jsx @@ -21,6 +21,7 @@ import { TextField, InputAdornment, IconButton, + Chip, Checkbox, Tooltip, } from "@mui/material"; @@ -39,9 +40,10 @@ import { //import { useAlert import { typecost, typecost_single, } from "../views/HandlePaymentNew.jsx"; import BillingStats from "../components/BillingStats.jsx"; +import { handlePayasyougo } from "../views/HandlePaymentNew.jsx" const Billing = (props) => { - const { globalUrl, userdata, serverside, billingInfo, stripeKey, selectedOrganization, handleGetOrg, } = props; + const { globalUrl, userdata, serverside, billingInfo, stripeKey, selectedOrganization, handleGetOrg, clickedFromOrgTab } = props; //const alert = useAlert(); let navigate = useNavigate(); @@ -118,10 +120,11 @@ const Billing = (props) => { minHeight: 280, maxWidth: 400, width: "100%", - backgroundColor: theme.palette.surfaceColor, - borderRadius: theme.palette.borderRadius, + backgroundColor: theme.palette.platformColor, + borderRadius: theme.palette.borderRadius*2, border: "1px solid rgba(255,255,255,0.3)", marginRight: 10, + marginTop: 15, } const isCloud = @@ -271,6 +274,7 @@ const Billing = (props) => { const [signatureOpen, setSignatureOpen] = React.useState(false); const [tosChecked, setTosChecked] = React.useState(subscription.eula_signed) + const [hovered, setHovered] = React.useState(false) var top_text = "Base Cloud Access" if (subscription.limit === undefined && subscription.level === undefined || subscription.level === null || subscription.level === 0) { @@ -319,8 +323,16 @@ const Billing = (props) => { newPaperstyle.border = "1px solid #f85a3e" } + if (hovered) { + newPaperstyle.backgroundColor = theme.palette.surfaceColor + } + return ( - + setHovered(true)} + onMouseLeave={() => setHovered(false)} + > {
+ {top_text === "Base Cloud Access" && userdata.has_card_available === true ? + { + console.log("Clicked chip") + }} + variant="outlined" + color="primary" + /> + : null} {top_text} - {isCloud && highlight === true ? + + {top_text === "Base Cloud Access" && userdata.has_card_available === false ? + + : null} + {isCloud && highlight === true && top_text !== "Base Cloud Access" ? { : null}
- {(highlight === true && (subscription.name === "Pay as you go" && subscription.limit <= 10000) || subscription.name === "Open Source") ? + {isCloud && (highlight === true && (subscription.name === "Pay as you go" && subscription.limit <= 10000) || subscription.name === "Open Source") ? {subscription.name.includes("Scale") ? "" : - "You are not subscribed to any plan and are using the free plan with max 10,000 app runs per month. Upgrade to deactivate this limit." + + userdata.has_card_available === true ? + "While you have a card attached to your account, Shuffle will no longer prevent workflows from running. Billing will occur at the start of each month." + : + `You are not subscribed to any plan and are using the free plan with max 10,000 app runs per month. Upgrade to deactivate this limit.` } + Billing email: {selectedOrganization.org} {/*isCloud ? : null*/} + + {userdata.has_card_available === true ? + + : null} + : null} {showSupport ? @@ -874,22 +968,31 @@ const Billing = (props) => { }); }; + const isChildOrg = userdata.active_org.creator_org !== "" && userdata.active_org.creator_org !== undefined && userdata.active_org.creator_org !== null return ( -
+
{addDealModal} - - Billing - + {clickedFromOrgTab? +

Billing & Licensing

: + + Billing & Licensing + } + {clickedFromOrgTab? + {isCloud ? + "Get more out of Shuffle by adding your credit card, such as no App Run limitations, and priority support from our team. We use Stripe to manage subscriptions and do not store any of your billing information. You can manage your subscription and billing information below." + : + "Shuffle is an Open Source automation platform, and no license is required. We do however offer a Scale license with HA guarantees, along with support hours. By buying a license on https://shuffler.io, you can get access to the license immediately, and if Cloud Syncronisation is enabled, the UI in your local instance will also update." + }: {isCloud ? - "We use Stripe to manage subscriptions and do not store any of your billing information. You can manage your subscription and billing information below." + "Get more out of Shuffle by adding your credit card, such as no App Run limitations, and priority support from our team. We use Stripe to manage subscriptions and do not store any of your billing information. You can manage your subscription and billing information below." : - "Shuffle is an Open Source automation platform, and no license is required to use it. You may however activate Cloud Sync, get our Scale license, get help with Kubernetes, or talk to Shuffle's Support team to get automation help." + "Shuffle is an Open Source automation platform, and no license is required. We do however offer a Scale license with HA guarantees, along with support hours. By buying a license on https://shuffler.io, you can get access to the license immediately, and if Cloud Syncronisation is enabled, the UI in your local instance will also update." } - + } {userdata.support === true ? -
+
For sales: Create  New Cloud Contract @@ -912,8 +1015,16 @@ const Billing = (props) => { : null } + + + {isChildOrg ? + + Billing is handled by your parent organisation. Reach out to support@shuffler.io if you have questions about this. + + : null} +
- {isCloud && billingInfo.subscription !== undefined && billingInfo.subscription !== null ? + {isCloud && billingInfo.subscription !== undefined && billingInfo.subscription !== null ? isChildOrg ? null : { /> : null} + {isCloud && selectedOrganization.subscriptions !== undefined && selectedOrganization.subscriptions !== null && - selectedOrganization.subscriptions.length > 0 ? - + selectedOrganization.subscriptions.length > 0 && + !isChildOrg ? selectedOrganization.subscriptions .reverse() .map((sub, index) => { @@ -1036,7 +1148,7 @@ const Billing = (props) => {
- {isCloud && + {/*isCloud && selectedOrganization.partner_info !== undefined && selectedOrganization.partner_info.reseller === true ? (
@@ -1220,17 +1332,18 @@ const Billing = (props) => { />
- ) : null} -
+ ) : null*/} +
- Shuffle Utilization + Utilization & Stats
{ const AppStats = (defaultprops) => { - const { globalUrl, selectedOrganization, userdata, isCloud, inputWorkflows, } = defaultprops; + const { globalUrl, selectedOrganization, userdata, isCloud, inputWorkflows,clickedFromOrgTab } = defaultprops; const [keys, setKeys] = useState([]) const [searches, setSearches] = useState([]); @@ -113,8 +113,6 @@ const AppStats = (defaultprops) => { return } - console.log("START TIME", starttime, endtime) - var url = `${globalUrl}/api/v1/workflows/${workflow.id}/executions/count` if (starttime !== "") { @@ -178,8 +176,6 @@ const AppStats = (defaultprops) => { const allData = Promise.all(promises); allData.then((data) => { - console.log("IN ALL DATA") - var total = 0 for (var i = 0; i < data.length; i++) { if (data[i].runcount !== undefined) { @@ -640,13 +636,13 @@ const AppStats = (defaultprops) => { const data = (
- + All shown statistics are gathered from Your Organization Statistics - This is a feature to help give you more insight into Shuffle, and to understand your utilization of the Shuffle platform. The billing tracker is in Beta, and is always calculated manually before being invoiced. + >Your Organisation Statistics. + It exists to give you more insight into your workflows, and to understand your utilization of the Shuffle platform. The billing tracker is in Beta, and is always calculated manually before being invoiced.
@@ -707,7 +703,7 @@ const AppStats = (defaultprops) => { textAlign: "center", padding: 40, margin: 5, - marginLeft: 90, + marginLeft: clickedFromOrgTab? null:90, backgroundColor: theme.palette.platformColor, border: "1px solid rgba(255,255,255,0.3)", maxWidth: 300, diff --git a/frontend/src/components/Branding.jsx b/frontend/src/components/Branding.jsx index 9dd40791..48466476 100644 --- a/frontend/src/components/Branding.jsx +++ b/frontend/src/components/Branding.jsx @@ -15,7 +15,7 @@ import { //import { useAlert const Branding = (props) => { - const { globalUrl, userdata, serverside, billingInfo, stripeKey, selectedOrganization, handleGetOrg, } = props; + const { globalUrl, userdata, serverside, billingInfo,clickedFromOrgTab, stripeKey, selectedOrganization, handleGetOrg, } = props; //const alert = useAlert(); const [publishingInfo, setPublishingInfo] = useState(""); const [publishRequirements, setPublishRequirements] = useState([]) @@ -103,8 +103,8 @@ const Branding = (props) => { } return ( -
-

+
+

Branding

diff --git a/frontend/src/components/CacheView.jsx b/frontend/src/components/CacheView.jsx index 89cde359..434b604a 100644 --- a/frontend/src/components/CacheView.jsx +++ b/frontend/src/components/CacheView.jsx @@ -1,9 +1,10 @@ import React, { useState, useEffect } from "react"; import theme from "../theme.jsx"; -import { toast } from 'react-toastify'; -import ReactJson from "react-json-view"; - +import { toast } from 'react-toastify'; +import ReactJson from "react-json-view"; + import { + Typography, Tooltip, Divider, TextField, @@ -21,6 +22,8 @@ import { } from "@mui/material"; import { + Link as LinkIcon, + AutoFixHigh as AutoFixHighIcon, Edit as EditIcon, FileCopy as FileCopyIcon, SelectAll as SelectAllIcon, @@ -43,7 +46,7 @@ import { Visibility as VisibilityIcon, VisibilityOff as VisibilityOffIcon, } from "@mui/icons-material"; -import { validateJson, } from "../views/Workflows.jsx"; +import { validateJson, } from "../views/Workflows.jsx"; const scrollStyle1 = { height: 100, @@ -61,9 +64,9 @@ const scrollStyle2 = { overflow: "scroll", } - + const CacheView = (props) => { - const { globalUrl, userdata, serverside, orgId } = props; + const { globalUrl, userdata, serverside, orgId, isSelectedDataStore } = props; const [orgCache, setOrgCache] = React.useState(""); const [listCache, setListCache] = React.useState([]); const [addCache, setAddCache] = React.useState(""); @@ -76,10 +79,9 @@ const CacheView = (props) => { const [dataValue, setDataValue] = React.useState({}); const [editCache, setEditCache] = React.useState(false); const [show, setShow] = useState({}); - + useEffect(() => { listOrgCache(orgId); - console.log("orgid", orgId); }, []); const listOrgCache = (orgId) => { @@ -91,84 +93,47 @@ const CacheView = (props) => { }, credentials: "include", }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 for apps :O!"); - return; - } + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for list cache :O!"); + return; + } - return response.json(); - }) - .then((responseJson) => { - if (responseJson.success === true) { - setListCache(responseJson.keys); - } + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === true) { + setListCache(responseJson.keys); + } - if (responseJson.cursor !== undefined && responseJson.cursor !== null && responseJson.cursor !== "") { - setCacheCursor(responseJson.cursor); - } - }) - .catch((error) => { - toast(error.toString()); - }); + if (responseJson.cursor !== undefined && responseJson.cursor !== null && responseJson.cursor !== "") { + setCacheCursor(responseJson.cursor); + } + }) + .catch((error) => { + toast(error.toString()); + }); }; - // const getCacheList = (orgId) => { - // fetch(`${globalUrl}/api/v1/orgs/${orgId}/get_cache`, { - // method: "GET", - // headers: { - // "Content-Type": "application/json", - // Accept: "application/json", - // }, - // credentials: "include", - // }) - // .then((response) => { - // if (response.status !== 200) { - // console.log("Status not 200 for WORKFLOW EXECUTION :O!"); - // } - - - // return response.json(); - // }) - // .then((responseJson) => { - // if (responseJson.success !== false) { - // console.log("Found cache: ", responseJson) - // setListCache(responseJson) - // } else { - // console.log("Couldn't find the creator profile (rerun?): ", responseJson) - // // If the current user is any of the Shuffle Creators - // // AND the workflow doesn't have an owner: allow editing. - // // else: Allow suggestions? - // //console.log("User: ", userdata) - // //if (rerun !== true) { - // // getUserProfile(userdata.id, true) - // //} - // } - // }) - // .catch((error) => { - // console.log("Get userprofile error: ", error); - // }) - // } - const deleteCache = (orgId, key) => { toast("Attempting to delete Cache"); - + // method: "DELETE", - const method = "POST" - //const url = `${globalUrl}/api/v1/orgs/${orgId}/cache/${key}` - const url = `${globalUrl}/api/v1/orgs/${orgId}/delete_cache` - const parsed = { - "org_id": orgId, - "key": key, - } - + const method = "POST" + //const url = `${globalUrl}/api/v1/orgs/${orgId}/cache/${key}` + const url = `${globalUrl}/api/v1/orgs/${orgId}/delete_cache` + const parsed = { + "org_id": orgId, + "key": key, + } + fetch(url, { - method: method, + method: method, headers: { Accept: "application/json", }, - body: JSON.stringify(parsed), + body: JSON.stringify(parsed), credentials: "include", }) .then((response) => { @@ -189,8 +154,6 @@ const CacheView = (props) => { const editOrgCache = (orgId) => { const cache = { key: dataValue.key , value: value }; setCacheInput([cache]); - console.log("cache:", cache) - console.log("cache input: ", cacheInput) fetch(globalUrl + `/api/v1/orgs/${orgId}/set_cache`, { @@ -255,6 +218,20 @@ const CacheView = (props) => { }); }; + const isValidJson = validateJson(value) + const autoFixJson = (inputvalue) => { + console.log("inputvalue: ", inputvalue) + try { + var parsedjson = JSON.parse(inputvalue) + + // setValue() with the parsed json as string + setValue(JSON.stringify(parsedjson, null, 2)) + } catch (e) { + console.log("Error parsing JSON: ", e) + //return JSON.stringify(inputvalue); + } + } + const modalView = ( // console.log("key:", dataValue.key), //console.log("value:",dataValue.value), @@ -301,33 +278,54 @@ const CacheView = (props) => { onChange={(e) => setKey(e.target.value)} />
-
- Value +
+
+ + Value - ({isValidJson.valid === true ? "Valid" : "Invalid"} JSON) + + + { + autoFixJson(value) + }} + > + + + +
setValue(e.target.value)} />
- - - + />} + + {listCache === undefined || listCache === null ? null : listCache.map((data, index) => { - var bgColor = "#27292d"; + var bgColor = isSelectedDataStore? "#212121":"#27292d"; if (index % 2 === 0) { - bgColor = "#1f2023"; + bgColor = isSelectedDataStore? "#1A1A1A":"#1f2023"; } - - const validate = validateJson(data.value); - console.log("Past validate: ", validate); + const validate = validateJson(data.value); return ( - + { - //handleReactJsonClipboard(copy); - }} - displayDataTypes={false} - onSelect={(select) => { - //HandleJsonCopy(showResult, select, data.action.label); - //console.log("SELECTED!: ", select); - }} - name={"value"} - /> - : - data.value + primary={validate.valid ? + { + //handleReactJsonClipboard(copy); + }} + displayDataTypes={false} + onSelect={(select) => { + //HandleJsonCopy(showResult, select, data.action.label); + //console.log("SELECTED!: ", select); + }} + name={"value"} + /> + : + data.value } /> { style={{ padding: "6px" }} onClick={() => { setEditCache(true) - setDataValue({"key":data.key,"value":data.value}) + setDataValue({ + "key": data.key, + "value":data.value + }) + setValue(data.value) setModalOpen(true) }} > @@ -483,9 +488,27 @@ const CacheView = (props) => { + + + { + window.open(`${globalUrl}/api/v1/orgs/${orgId}/cache/${data.key}?type=text&authorization=${data.public_authorization}`, "_blank"); + }} + > + + + + diff --git a/frontend/src/components/ConfigureWorkflow.jsx b/frontend/src/components/ConfigureWorkflow.jsx index 81e9fd3d..5678f61e 100755 --- a/frontend/src/components/ConfigureWorkflow.jsx +++ b/frontend/src/components/ConfigureWorkflow.jsx @@ -77,10 +77,7 @@ const ConfigureWorkflow = (props) => { const [checkStarted, setCheckStarted] = React.useState(false); useEffect(() => { - console.log("Required actions: ", requiredActions) if (requiredActions.length === 0) { - console.log("No more actions? Set parent to done?") - if (setConfigurationFinished !== undefined) { setConfigurationFinished(true) } @@ -190,7 +187,7 @@ const ConfigureWorkflow = (props) => { console.log("No apps loaded: ", apps); if (setConfigureWorkflowModalOpen !== undefined) { - setConfigureWorkflowModalOpen(false); + setConfigureWorkflowModalOpen(false) } return null; @@ -201,7 +198,7 @@ const ConfigureWorkflow = (props) => { const newactions = []; for (let [key, keyval] in Object.entries(workflow.actions)) { - const action = workflow.actions[key]; + var action = JSON.parse(JSON.stringify(workflow.actions[key])) var newaction = { large_image: action.large_image, app_name: action.app_name, @@ -222,15 +219,68 @@ const ConfigureWorkflow = (props) => { action.app_name = action.app_name.slice(0, -4) } + if (action.app_name === "Integration Framework") { + var selected_app = "" + for (var paramkey in action.parameters) { + const param = action.parameters[paramkey] + + if (param.name === "app_name") { + selected_app = param.value + break + } + } + + for (var appauth in appAuthentication) { + if (appAuthentication[appauth].app.name.toLowerCase() === selected_app.toLowerCase()) { + newaction.auth_done = true + break + } + } + + if (newaction.auth_done) { + continue + } + + for (var appkey in apps) { + if (apps[appkey].name.toLowerCase() === selected_app.toLowerCase()) { + newaction.app_name = apps[appkey].name + newaction.app_version = apps[appkey].app_version + newaction.app = apps[appkey] + newaction.app_id = apps[appkey].id + + newaction.must_activate = false + newaction.must_authenticate = true + + newaction.steps.push({ + "title": "Authenticate app", + "type": "authenticate", + "required": true, + }) + + action.app_id = apps[appkey].id + action.authentication_id = "" + action.authentication = { + "required": true, + } + + break + } + } + + if (newaction.app.id === undefined) { + console.log("Failed to find app: ", selected_app) + continue + } + + newaction.update_version = "1.1.0" + } + // ID match OR name match + version match //const app = apps.find((app) => app.id === action.app_id || (app.name === action.app_name && (app.app_version === action.app_version || (app.loop_versions !== null && app.loop_versions.includes(action.app_version))))) - // // without version match const newappname = action.app_name.toLowerCase().replaceAll(" ", "_") - const app = apps.find((app) => app.id === action.app_id || app.name.toLowerCase().replaceAll(" ", "_") === newappname) - - + var app = apps.find((app) => app.id === action.app_id || app.name.toLowerCase().replaceAll(" ", "_") === newappname) if (app === undefined || app === null) { @@ -246,6 +296,19 @@ const ConfigureWorkflow = (props) => { "required": true, }) } else { + if (action.authentication_id !== "" && app.authentication.required === true) { + var authFound = false + for (var authkey in appAuthentication) { + if (appAuthentication[authkey].id === action.authentication_id) { + authFound = true + break + } + } + + if (!authFound) { + action.authentication_id = "" + } + } if (action.authentication_id === "" && app.authentication.required === true && action.parameters !== undefined && action.parameters !== null) { // Check if configuration is filled or not @@ -292,9 +355,11 @@ const ConfigureWorkflow = (props) => { //console.log(newaction.app_name,"AUTH: ", newaction.must_authenticate, " ACTIVATE: ", newaction.must_activate) if (newaction.must_authenticate) { - var authenticationOptions = []; + + var authenticationOptions = [] for (let [key,keyval] in Object.entries(appAuthentication)) { - const auth = appAuthentication[key]; + const auth = appAuthentication[key] + if (auth.app.name === app.name && auth.active) { //console.log("Found auth: ", auth) authenticationOptions.push(auth); @@ -403,17 +468,18 @@ const ConfigureWorkflow = (props) => { continue; } - requiredTriggers.push(trigger); + requiredTriggers.push(trigger) } } - if (requiredTriggers.length === 0 && requiredVariables.length === 0 && newactions.length === 0 && setConfigureWorkflowModalOpen !== undefined) { - setConfigureWorkflowModalOpen(false); + if (setConfigureWorkflowModalOpen !== undefined && requiredTriggers.length === 0 && requiredVariables.length === 0 && newactions.length === 0 ) { + console.log("No required triggers, variables or actions. Closing modal.") + setConfigureWorkflowModalOpen(false) } - setRequiredTriggers(requiredTriggers); - setRequiredVariables(requiredVariables); - setRequiredActions(newactions); + setRequiredTriggers(requiredTriggers) + setRequiredVariables(requiredVariables) + setRequiredActions(newactions) } if (appAuthentication !== undefined && previousAuth !== undefined && appAuthentication.length !== previousAuth.length) { @@ -535,7 +601,7 @@ const ConfigureWorkflow = (props) => { , @@ -606,10 +672,10 @@ const ConfigureWorkflow = (props) => { .then((responseJson) => { if (responseJson.success === false) { if (responseJson.reason !== undefined) { - toast("Failed to activate the app: "+responseJson.reason); - } else { - toast("Failed to activate the app"); - } + toast("Failed to activate the app: "+responseJson.reason); + } else { + toast("Failed to activate the app"); + } } else { toast("App activated for your organization!"); } @@ -705,9 +771,9 @@ const ConfigureWorkflow = (props) => { }) .then((responseJson) => { if (!responseJson.success) { - toast("Failed to set app auth: " + responseJson.reason); + toast("Failed to set app authentication: " + responseJson.reason); } else { - toast("App auth set for app " + app.name.replace("_", " ")); + toast("App authentication set for app " + app.name.replace("_", " ")); setFinalized(true) setOpened(false) } @@ -731,6 +797,7 @@ const ConfigureWorkflow = (props) => { parsedName = (parsedName.charAt(0).toUpperCase() + parsedName.slice(1)).replaceAll("_", " "); + console.log("AUTH Action: ", action) return ( {
{ setOpened(!opened); + + // Scroll to it + const element = document.getElementById("app-config"); + if (element) { + // Scroll down 100px + setTimeout(() => { + element.scrollIntoView({ + behavior: "smooth", + top: 100, + }) + + //element.scrollIntoView({ + // behavior: "smooth", + // block: "center", + // inline: "center" + //}); + }, 250) + } + }} >
@@ -900,6 +986,7 @@ const ConfigureWorkflow = (props) => { parsedName = parsedName.substring(0, parsedName.length - 4); } + return ( {action.must_authenticate ? @@ -1044,7 +1131,7 @@ const ConfigureWorkflow = (props) => { src={action.large_image} /> - Activate + Activate {action.app_name.replaceAll("_", " ")} : @@ -1318,7 +1405,7 @@ const ConfigureWorkflow = (props) => { : null} - + {requiredActions.map((data, index) => { // AppWrapper = Default in a workflow, only shows with steps diff --git a/frontend/src/components/DiscordChat.jsx b/frontend/src/components/DiscordChat.jsx new file mode 100644 index 00000000..e1d226b8 --- /dev/null +++ b/frontend/src/components/DiscordChat.jsx @@ -0,0 +1,222 @@ +import React, { useState, useEffect } from 'react'; +import algoliasearch from 'algoliasearch'; +import theme from '../theme.jsx'; +import { InstantSearch, connectSearchBox, connectHits } from 'react-instantsearch-dom'; +import { + Grid, + Paper, + TextField, + Typography, + Button, + InputAdornment, + Avatar, + List, + ListItem, + ListItemAvatar, + ListItemText, +} from '@mui/material'; +import { Search as SearchIcon } from '@mui/icons-material'; + + +const searchClient = algoliasearch("JNSS5CFDZZ", "1e5f29b1550939855de5915eac3bf5f7"); + +const DiscordChat = props => { + const { isMobile, globalUrl } = props + const [value, setValue] = useState(""); + const [formMail, setFormMail] = React.useState(""); + const [message, setMessage] = React.useState(""); + const [formMessage, setFormMessage] = React.useState(""); + const buttonStyle = {borderRadius: 30, height: 50, width: 220, margin: isMobile ? "15px auto 15px auto" : 20, fontSize: 18,} + + const borderRadius = 3 + + const submitContact = (email, message) => { + const data = { + "firstname": "", + "lastname": "", + "title": "", + "companyname": "", + "email": email, + "phone": "", + "message": message, + } + + const errorMessage = "Something went wrong. Please contact frikky@shuffler.io directly." + + fetch(globalUrl+"/api/v1/contact", { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }) + .then(response => response.json()) + .then(response => { + if (response.success === true) { + setFormMessage(response.reason) + //toast("Thanks for submitting!") + } else { + setFormMessage(errorMessage) + } + + setFormMail("") + setMessage("") + }) + .catch(error => { + setFormMessage(errorMessage) + console.log(error) + }); + } + + const SearchBox = ({ currentRefinement, refine }) => { + return ( +
+ refine(event.currentTarget.value)} + placeholder="Search Discord Chats" + style={{ backgroundColor: theme.palette.inputColor, borderRadius: borderRadius, margin: 10, width: "100%", }} + InputProps={{ + style: { + color: "white", + fontSize: "1em", + height: 50, + }, + startAdornment: ( + + + + ), + }} + /> + + ); + }; + + const highlightText = (text) => { + if (!Array.isArray(text.matchedWords) || text.matchedWords.length === 0) { + return text.value; + } + + let highlightedText = ''; + let currentIndex = 0; + + text.matchedWords.forEach((word, index) => { + const startIndex = text.value.toLowerCase().indexOf(word.toLowerCase(), currentIndex); + const endIndex = startIndex + word.length; + highlightedText += text.value.substring(currentIndex, startIndex); + highlightedText += `${text.value.substring(startIndex, endIndex)}`; + currentIndex = endIndex; + }); + + highlightedText += text.value.substring(currentIndex); + return ; + }; + + const Hits = ({ hits }) => { + const handleHitClick = (url) => { + const modifiedUrl = url.replace('https://ptb.discord.com/', 'https://discord.com/'); + window.open(modifiedUrl, '_blank'); + }; + if (hits.length === 0) { + return No results found. Try refining your search.; + } + return ( + + {hits.map((chat, index) => ( + handleHitClick(chat.url)} style={{ cursor: "pointer", borderBottom: "1px solid rgba(255,255,255,0.4)" }}> + + + + + + ))} + + ); + }; + + const CustomSearchBox = connectSearchBox(SearchBox); + const CustomHits = connectHits(Hits); + + return ( +
+ +
+ +
+
+ +
+
+
+ + Can't find what you're looking for? + +
+ setFormMail(e.target.value)} + /> + setMessage(e.target.value)} + /> +
+ + {formMessage} +
+ + + Search by + + + Algolia logo + + +
+ ); +}; + +export default DiscordChat; diff --git a/frontend/src/components/EditWorkflow.jsx b/frontend/src/components/EditWorkflow.jsx index 05e57184..9ac7fabe 100644 --- a/frontend/src/components/EditWorkflow.jsx +++ b/frontend/src/components/EditWorkflow.jsx @@ -11,9 +11,9 @@ import { Badge, Avatar, Grid, - InputLabel, - Select, - ListSubheader, + InputLabel, + Select, + ListSubheader, Paper, Tooltip, Divider, @@ -22,6 +22,7 @@ import { IconButton, Menu, MenuItem, + Link, FormControlLabel, Chip, Switch, @@ -49,12 +50,15 @@ import { } from '@mui/x-date-pickers' import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs' +import { useStyles } from '../views/AppCreator.jsx' import { ExpandLess as ExpandLessIcon, ExpandMore as ExpandMoreIcon, Publish as PublishIcon, OpenInNew as OpenInNewIcon, + Add as AddIcon, + Remove as RemoveIcon, } from "@mui/icons-material"; const EditWorkflow = (props) => { @@ -74,6 +78,11 @@ const EditWorkflow = (props) => { const [name, setName] = React.useState(workflow.name !== undefined ? workflow.name : "") const [dueDate, setDueDate] = React.useState(workflow.due_date !== undefined && workflow.due_date !== null && workflow.due_date !== 0 ? dayjs(workflow.due_date*1000) : dayjs().subtract(1, 'day')) + console.log("WORKFLOW: ", workflow) + const [inputQuestions, setInputQuestions] = React.useState(workflow.input_questions !== undefined && workflow.input_questions !== null ? JSON.parse(JSON.stringify(workflow.input_questions)) : []) + + const classes = useStyles(); + // Gets the generated workflow const getGeneratedWorkflow = (workflow_id) => { fetch(globalUrl + "/api/v1/workflows/" + workflow_id, { @@ -146,8 +155,6 @@ const EditWorkflow = (props) => { const newWorkflow = isEditing === true ? false : true const priority = userdata === undefined || userdata === null ? null : userdata.priorities.find(prio => prio.type === "usecase" && prio.active === true) - console.log("PRIO: ", priority) - var upload = ""; var total_count = 0 @@ -228,7 +235,91 @@ const EditWorkflow = (props) => {
- +
+ {/* + + */} + +
+ +
{ @@ -354,6 +445,9 @@ const EditWorkflow = (props) => { {showMoreClicked === true ? + + +
Status @@ -450,6 +544,7 @@ const EditWorkflow = (props) => { margin="dense" fullWidth /> + { innerWorkflow.default_return_value = event.target.value @@ -469,11 +564,223 @@ const EditWorkflow = (props) => { margin="dense" fullWidth /> + + + MSSP Suborg Distribution (beta - contact support@shuffler.io) + + {userdata !== undefined && userdata !== null && userdata.orgs !== undefined && userdata.orgs !== null && userdata.orgs.length > 0 ? + userdata.orgs.filter(org => org.creator_org === userdata.active_org.id).length === 0 ? + + You can only distribute to suborgs from a parent org. + + : + + : + + + Create a sub-org to distribute workflows to suborgs. + + + } + + + + Input fields + + + Input fields are fields that will be used during the startup of the workflow. These will be formatted in JSON and is most commonly used from the workflow run page. + + + + {inputQuestions.map((data, index) => { + console.log("Inputfield: ", data) + + return ( +
+ { + inputQuestions[index].name = e.target.value + setInputQuestions(inputQuestions) + setUpdate(Math.random()); + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style: { + color: "white", + minHeight: 50, + }, + }} + /> + { + inputQuestions[index].value = e.target.value + setInputQuestions(inputQuestions) + setUpdate(Math.random()); + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style: { + color: "white", + minHeight: 50, + }, + }} + /> + +
+ ) + })} + + : null} - + + { setShowMoreClicked(!showMoreClicked); }} @@ -485,71 +792,7 @@ const EditWorkflow = (props) => { - - - - + {newWorkflow === true ? diff --git a/frontend/src/components/Files.jsx b/frontend/src/components/Files.jsx index 670cba97..692d2e62 100644 --- a/frontend/src/components/Files.jsx +++ b/frontend/src/components/Files.jsx @@ -16,27 +16,32 @@ import { Divider, Select, MenuItem, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + Typography, } from "@mui/material"; import { + Link as LinkIcon, OpenInNew as OpenInNewIcon, Edit as EditIcon, CloudDownload as CloudDownloadIcon, Delete as DeleteIcon, FileCopy as FileCopyIcon, - Cached as CachedIcon, + Cached as CachedIcon, Publish as PublishIcon, - Clear as ClearIcon, - Add as AddIcon, + Clear as ClearIcon, + Add as AddIcon, } from "@mui/icons-material"; -//import { useAlert import Dropzone from "../components/Dropzone.jsx"; -import CodeEditor from "../components/ShuffleCodeEditor.jsx"; +import ShuffleCodeEditor from "../components/ShuffleCodeEditor1.jsx"; import theme from "../theme.jsx"; const Files = (props) => { - const { globalUrl, userdata, serverside, selectedOrganization, isCloud, } = props; + const { globalUrl, userdata, serverside, selectedOrganization, isCloud,isSelectedFiles } = props; const [files, setFiles] = React.useState([]); const [selectedNamespace, setSelectedNamespace] = React.useState("default"); @@ -45,9 +50,16 @@ const Files = (props) => { const [fileContent, setFileContent] = React.useState(""); const [openEditor, setOpenEditor] = React.useState(false); const [renderTextBox, setRenderTextBox] = React.useState(false); + const [loadFileModalOpen, setLoadFileModalOpen] = React.useState(false); + + const [field1, setField1] = React.useState(""); + const [field2, setField2] = React.useState(""); + const [downloadUrl, setDownloadUrl] = React.useState("https://github.com/shuffle/standards") + const [downloadBranch, setDownloadBranch] = React.useState("main"); + const [downloadFolder, setDownloadFolder] = React.useState("translation_standards"); //const alert = useAlert(); - const allowedFileTypes = ["txt", "py", "yaml", "yml","json", "html", "js", "csv", "log"] + const allowedFileTypes = ["txt", "py", "yaml", "yml","json", "html", "js", "csv", "log", "eml", "msg", "md", "xml", "sh", "bat", "ps1", "psm1", "psd1", "ps1xml", "pssc", "psc1", "response"] var upload = ""; const handleKeyDown = (event) => { @@ -67,7 +79,7 @@ const Files = (props) => { } - const runUpdateText = (text) =>{ + const runUpdateText = (text) =>{ fetch(`${globalUrl}/api/v1/files/${openFileId}/edit`, { method: "PUT", headers: { @@ -76,17 +88,34 @@ const Files = (props) => { }, body:text, credentials: "include", - }).then((response) => { + }) + .then((response) => { if (response.status !== 200) { console.log("Can't update file"); } return response.json(); - }) - //console.log(text); + }) + .then((responseJson) => { + if (responseJson.success === true) { + toast("Successfully updated file"); + } + }) + .catch((error) => { + toast("Error updating file: " + error.toString()); + }) } - const getFiles = () => { - fetch(globalUrl + "/api/v1/files", { + const getFiles = (namespace) => { + var parsedurl = `${globalUrl}/api/v1/files` + if (namespace === undefined || namespace === "default") { + + } else if (namespace !== undefined && namespace !== null && namespace !== "") { + parsedurl = `${globalUrl}/api/v1/files/namespaces/${namespace}?ids=true` + } else if (selectedNamespace !== undefined && selectedNamespace !== null && selectedNamespace !== "default" && selectedNamespace !== "") { + parsedurl = `${globalUrl}/api/v1/files/namespaces/${selectedNamespace}?ids=true` + } + + fetch(parsedurl, { method: "GET", headers: { "Content-Type": "application/json", @@ -104,12 +133,23 @@ const Files = (props) => { }) .then((responseJson) => { if (responseJson.files !== undefined && responseJson.files !== null) { - setFiles(responseJson.files); - } else { + setFiles(responseJson.files); + } else if (responseJson.list !== undefined && responseJson.list !== null) { + // Set the "namespace" field in all items + if (namespace !== undefined && namespace !== null) { + responseJson.list.forEach((item) => { + item.namespace = namespace + item.filename = item.name + item.workflow_id = "global" + }) + } + + setFiles(responseJson.list); + } else { setFiles([]); } - if (responseJson.namespaces !== undefined && responseJson.namespaces !== null) { + if (responseJson.namespaces !== undefined && responseJson.namespaces !== null && (fileNamespaces.length === 0 || responseJson.namespaces.length > fileNamespaces.length)) { setFileNamespaces(responseJson.namespaces); } }) @@ -119,9 +159,214 @@ const Files = (props) => { }; useEffect(() => { - getFiles(); + getFiles(selectedNamespace) }, []); + const importStandardsFromUrl = (url, folder) => { + if (url === undefined || url === null || url.length < 5) { + toast("Please enter a valid URL"); + return; + } + + if (folder === undefined || folder === null || folder.length < 2) { + toast("Please enter a valid folder name") + return + } + + const parsedData = { + url: url, + path: folder, + field_3: downloadBranch || "master", + }; + + if (field1.length > 0) { + parsedData["field_1"] = field1; + } + + if (field2.length > 0) { + parsedData["field_2"] = field2; + } + + toast(`Getting files from url ${url}. This may take a while if the repository is large. Please wait...`); + fetch(globalUrl + "/api/v1/files/download_remote", { + method: "POST", + mode: "cors", + headers: { + Accept: "application/json", + }, + body: JSON.stringify(parsedData), + credentials: "include", + }) + .then((response) => { + if (response.status === 200) { + toast("Successfully loaded files from " + downloadUrl); + setLoadFileModalOpen(false); + } + + return response.json(); + }) + .then((responseJson) => { + if (!responseJson.success) { + if (responseJson.reason !== undefined) { + toast("Failed loading: " + responseJson.reason); + } else { + toast("Failed loading"); + } + } + }) + .catch((error) => { + toast(error.toString()); + }); + } + + const handleGithubValidation = () => { + importStandardsFromUrl(downloadUrl, downloadFolder); + } + + const fileDownloadModal = loadFileModalOpen ? + {}} + PaperProps={{ + style: { + backgroundColor: theme.palette.surfaceColor, + color: "white", + minWidth: "800px", + minHeight: "320px", + }, + }} + > + +
+ Load Files from Github +
+ + Files will be loaded from the repository and branch you specify, with the focus on files in one folder at a time. This is NOT recursive. + +
+ + Repository URL (supported: github, gitlab, bitbucket) + setDownloadUrl(e.target.value)} + placeholder="https://github.com/shuffle/standards" + fullWidth + /> +
+ + + Branch (default value is "main"): + + setDownloadBranch(e.target.value)} + placeholder="master" + fullWidth + /> + + + + Folder (can use / for subfolders): + + setDownloadFolder(e.target.value)} + placeholder="translation_standards" + fullWidth + /> + +
+ + Authentication (optional - private repos etc): + +
+ setField1(e.target.value)} + type="username" + placeholder="Username / APIkey (optional)" + fullWidth + /> + setField2(e.target.value)} + type="password" + placeholder="Password (optional)" + fullWidth + /> +
+
+ + + + +
+ : null + const deleteFile = (file) => { fetch(globalUrl + "/api/v1/files/" + file.id, { method: "DELETE", @@ -140,7 +385,7 @@ const Files = (props) => { }) .then((responseJson) => { if (responseJson.success) { - toast("Successfully deleted file " + file.name); + toast("Successfully deleted file") } else if ( responseJson.reason !== undefined && responseJson.reason !== null @@ -150,8 +395,6 @@ const Files = (props) => { setTimeout(() => { getFiles(); }, 1500); - - console.log(responseJson); }) .catch((error) => { toast(error.toString()); @@ -195,10 +438,10 @@ const Files = (props) => { }; const downloadFile = (file) => { - fetch(globalUrl + "/api/v1/files/" + file.id + "/content", { - method: "GET", - credentials: "include", - }) + fetch(globalUrl + "/api/v1/files/" + file.id + "/content", { + method: "GET", + credentials: "include", + }) .then((response) => { if (response.status !== 200) { console.log("Status not 200 for apps :O!"); @@ -254,7 +497,7 @@ const Files = (props) => { }); }; - const handleCreateFile = (filename, file) => { + const handleCreateFile = (filename, file) => { var data = { filename: filename, org_id: selectedOrganization.id, @@ -355,7 +598,7 @@ const Files = (props) => { } setTimeout(() => { - getFiles(); + getFiles() }, 2500); }; @@ -374,31 +617,49 @@ const Files = (props) => { style={{ maxWidth: window.innerWidth > 1366 ? 1366 : 1200, margin: "auto", - padding: 20, + padding: isSelectedFiles ? null : 20, }} onDrop={uploadFile} > -
-
-

Files

- +
+ + + setLoadFileModalOpen(true)} + > + + + + + {fileDownloadModal} + +
+

Files

+ Files from Workflows are a way to store as well as edit files.{" "} Learn more
+ + + @@ -418,7 +679,7 @@ const Files = (props) => { }} /> + + {fileNamespaces !== undefined && fileNamespaces !== null && fileNamespaces.length > 1 ? ( @@ -442,8 +705,13 @@ const Files = (props) => { }} value={selectedNamespace} onChange={(event) => { - console.log("CHANGE NAMESPACE: ", event.target); setSelectedNamespace(event.target.value); + + if (event.target.value === "all" || event.target.value === "default") { + getFiles() + } else { + getFiles(event.target.value) + } }} > {fileNamespaces.map((data, index) => { @@ -453,7 +721,7 @@ const Files = (props) => { value={data} style={{ color: "white" }} > - {data} + {data.replaceAll("_", " ")} ); })} @@ -487,24 +755,26 @@ const Files = (props) => { } - {renderTextBox && { - handleKeyDown(event); - }} - InputProps={{ - style: { - color: "white", - }, - }} - color="primary" - placeholder="File category name" - required - margin="dense" - defaultValue={""} - autoFocus - />}
- { + handleKeyDown(event); + }} + InputProps={{ + style: { + color: "white", + }, + }} + color="primary" + placeholder="File category name" + required + margin="dense" + defaultValue={""} + autoFocus + />}
+ + { key = {fileContent} //https://reactjs.org/docs/reconciliation.html#recursing-on-children runUpdateText = {runUpdateText} /> - + {isSelectedFiles?null: + />} - - + + { /> { return null; } - var bgColor = "#27292d"; + var bgColor = isSelectedFiles ? "#212121":"#27292d"; if (index % 2 === 0) { - bgColor = "#1f2023"; + bgColor = isSelectedFiles ? "#1A1A1A":"#1f2023"; } const filenamesplit = file.filename.split(".") @@ -585,8 +855,8 @@ const Files = (props) => { > { /> @@ -615,7 +885,7 @@ const Files = (props) => { }} /> - ) : ( + : ( { minWidth: 100, maxWidth: 100, overflow: "hidden", + textAlign: isSelectedFiles?"center":null }} /> { minWidth: 75, maxWidth: 75, overflow: "hidden", + textAlign:isSelectedFiles?"center":null, marginLeft: 10, }} /> @@ -702,6 +976,32 @@ const Files = (props) => { + {/* + + + { + // Open the file, without downloading it + window.open(`${globalUrl}/api/v1/files/${file.id}/content?type=text&authorization=${file.public_authorization}`, "_blank noreferrer noopener") + }} + > + + + + + */} { diff --git a/frontend/src/components/FilterCSS.css b/frontend/src/components/FilterCSS.css new file mode 100644 index 00000000..348fcbb1 --- /dev/null +++ b/frontend/src/components/FilterCSS.css @@ -0,0 +1,85 @@ +/* Style for the outer container */ +.ais-RefinementList { + justify-content: center; + } + + /* Style for the list */ + .ais-RefinementList-list { + list-style-type: none; + padding: 0; + } + + /* Style for each list item */ + .ais-RefinementList-item { + margin-bottom: 5px; + } + + /* Style for label */ + .ais-RefinementList-label { + cursor: pointer; + display: flex; + gap: 5px; + } + + /* Style for checkbox */ + .ais-RefinementList-checkbox { + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; + width: 16px; + height: 16px; + border: 1px solid #595b5e; + border-radius: 4px; + background-color: #27292d; + cursor: pointer; + flex-shrink: 0; + margin-top: "10px"; + } + + .ais-RefinementList-checkbox:checked { + background-color: rgb(248, 103, 67); + border-color: rgb(248, 103, 67); + width: 16px; + height: 16px; + } + + /* Style for checked state - inner dot */ + .ais-RefinementList-checkbox:checked::before { + content: "\2714"; + display: block; + width: 100%; + height: 100%; + text-align: center; + line-height: 18px; + color: #fff; + font-size: 14px; + } + + /* Style for hover state */ + /* .ais-RefinementList-checkbox:hover { + border-color: rgb(248, 103, 67); + } */ + + /* Style for label text */ + .ais-RefinementList-labelText { + /* margin-left: 5px; */ + font-size: 16px; + display: inline-flex; + flex-direction: row; + cursor: pointer; + } + + /* Style for count */ + .ais-RefinementList-count { + /* margin-left: 5px; + font-weight: bold; + color: gray; */ + display: none; + } + .ais-ClearRefinements-button { + cursor: pointer; + color: rgb(248, 103, 67); + border: none; + font-size: 16px; + background-color: transparent; + } \ No newline at end of file diff --git a/frontend/src/components/Header.jsx b/frontend/src/components/Header.jsx index dbf68fe2..77faee2f 100644 --- a/frontend/src/components/Header.jsx +++ b/frontend/src/components/Header.jsx @@ -155,9 +155,21 @@ const { globalUrl, setNotifications, notifications, isLoggedIn, removeCookie, ho .then(() => { // Log out anyway removeCookie("session_token", {path: "/"}) + removeCookie("__session", {path: "/"}) + removeCookie("_session", {path: "/"}) + removeCookie("session_token", {path: "/"}) + removeCookie("__session", {path: "/"}) + removeCookie("_session", {path: "/"}) + removeCookie("session_token", {path: "/"}) + removeCookie("__session", {path: "/"}) + removeCookie("_session", {path: "/"}) + removeCookie("session_token", {path: "/"}) + removeCookie("__session", {path: "/"}) + removeCookie("_session", {path: "/"}) + window.location.pathname = "/" }) .catch(error => { @@ -615,7 +627,7 @@ const { globalUrl, setNotifications, notifications, isLoggedIn, removeCookie, ho
- +
@@ -879,6 +891,11 @@ const { globalUrl, setNotifications, notifications, isLoggedIn, removeCookie, ho const namesplit = regionsplit[0].split("/") regiontag = namesplit[namesplit.length - 1] + if (regiontag === "california") { + regiontag = "us" + } else if (regiontag === "frankfurt") { + regiontag = "fr" + } } } diff --git a/frontend/src/components/LicencePopup.jsx b/frontend/src/components/LicencePopup.jsx new file mode 100644 index 00000000..1c12d7fd --- /dev/null +++ b/frontend/src/components/LicencePopup.jsx @@ -0,0 +1,957 @@ +import React, { useState, useEffect } from "react"; +import ReactGA from 'react-ga4'; + +import theme from "../theme.jsx"; +import { useTheme } from "@mui/styles"; +import countries from "../components/Countries.jsx"; +import { + Box, + Paper, + Typography, + Divider, + Button, + Grid, + Card, + Dialog, + DialogTitle, + DialogContent, + TextField, + InputAdornment, + IconButton, + Chip, + Checkbox, + Tooltip, + Slider, + DialogActions, + CardContent, + ButtonGroup, +} from "@mui/material"; + +import { useNavigate, Link } from "react-router-dom"; +import { Autocomplete } from "@mui/material"; +import { toast } from "react-toastify" + +import { + Cached as CachedIcon, + ContentCopy as ContentCopyIcon, + Draw as DrawIcon, + Close as CloseIcon, + Done as DoneIcon, + Clear as ClearIcon, + AddTask as AddTaskIcon, +} from "@mui/icons-material"; + +import { typecost, typecost_single, } from "../views/HandlePaymentNew.jsx"; +import { handlePayasyougo } from "../views/HandlePaymentNew.jsx" + +const LicencePopup = (props) => { + const { globalUrl, userdata, serverside, billingInfo, stripeKey, setModalOpen, isLoggedIn, isMobile } = props; + //const alert = useAlert(); + let navigate = useNavigate(); + const isCloud = typeof window === 'undefined' || window === undefined || window.location === undefined ? true : window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; + + const [selectedDealModalOpen, setSelectedDealModalOpen] = React.useState(false); + const [dealList, setDealList] = React.useState([]); + const [dealName, setDealName] = React.useState(""); + const [dealAddress, setDealAddress] = React.useState(""); + const [dealType, setDealType] = React.useState("MSSP"); + const [selectedOrganization, setSelectedOrganization] = React.useState({}); + const [dealCountry, setDealCountry] = React.useState("United States"); + const [dealCurrency, setDealCurrency] = React.useState("USD"); + const [dealStatus, setDealStatus] = React.useState("initiated"); + const [dealValue, setDealValue] = React.useState(""); + const [dealDiscount, setDealDiscount] = React.useState(""); + const [dealerror, setDealerror] = React.useState(""); + const [variant, setVariant] = useState(0) + const [shuffleVariant, setShuffleVariant] = useState(isCloud ? 0 : 1) + + + // const parsedFields = maxFields === undefined ? 300 : maxFields + const initialShuffleVariant = isCloud ? 0 : 1; + const [paymentType, setPaymentType] = useState(0) + const [currentPrice, setCurrentPrice] = useState(129) + const [isLoaded, setIsLoaded] = useState(false) + const [errorMessage, setErrorMessage] = useState("") + const [highlight, setHighlight] = useState(false) + + // Cloud + const [calculatedApps, setCalculatedApps] = useState(600) + const [calculatedCost, setCalculatedCost] = useState("$600") + const [selectedValue, setSelectedValue] = useState(100) + + // Onprem + const [calculatedCores, setCalculatedCores] = useState('600') + const [onpremSelectedValue, setOnpremSelectedValue] = useState(8) + + const payasyougo = "Pay as you go" + const stripe = typeof window === 'undefined' || window.location === undefined ? "" : props.stripeKey === undefined ? "" : window.Stripe ? window.Stripe(props.stripeKey) : "" + + const paperStyle = { + padding: 20, + borderRadius: theme.palette.borderRadius, + height: "100%", + } + + billingInfo.subscription = { + "active": true, + "name": "Pay as you go", + "price": typecost_single, + "currency": "USD", + "currency_text": "$", + "interval": "app run / month", + "description": "Pay as you go", + "features": [ + "Basic Support", + "Limited App Runs (10.000)", + ], + "limit": 10000, + } + + const sendSignatureRequest = (subscription) => { + const url = `${globalUrl}/api/v1/orgs/${selectedOrganization.id}`; + + fetch(url, { + body: JSON.stringify({ + org_id: selectedOrganization.id, + subscription: subscription, + }), + mode: "cors", + method: "POST", + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then((response) => { + if (response.status !== 200) { + console.log("Error in response"); + } + return response.json(); + }) + .then((responseJson) => { + console.log("Response from signature request: ", responseJson); + }) + .catch((error) => { + console.log("Error: ", error); + }) + } + + const SubscriptionObject = (props) => { + const { globalUrl, index, userdata, serverside, billingInfo, stripeKey, selectedOrganization, handleGetOrg, subscription, highlight, } = props; + + const [signatureOpen, setSignatureOpen] = React.useState(false); + const [tosChecked, setTosChecked] = React.useState(subscription.eula_signed) + const [hovered, setHovered] = React.useState(false) + + var top_text = "Base Cloud Access" + if (subscription.limit === undefined && subscription.level === undefined || subscription.level === null || subscription.level === 0) { + subscription.name = "Enterprise" + subscription.currency_text = "$" + subscription.price = subscription.level * 180 + subscription.limit = subscription.level * 100000 + subscription.interval = subscription.recurrence + subscription.features = [ + "Includes " + subscription.limit + " app runs/month. ", + "Multi-Tenancy and Region-Selection", + "And all other features from /pricing", + ] + } + + var newPaperstyle = JSON.parse(JSON.stringify(paperStyle)) + if (subscription.name === "Enterprise" && subscription.active === true) { + top_text = "Current Plan" + + newPaperstyle.border = "1px solid #f85a3e" + } + + var showSupport = false + if (subscription.name.includes("default")) { + top_text = "Custom Contract" + newPaperstyle.border = "1px solid #f85a3e" + showSupport = true + } + + if (subscription.name.includes("App Run Units")) { + top_text = "Cloud Access" + showSupport = true + } + + if (subscription.name.includes("Open Source")) { + top_text = "Open Source" + showSupport = true + } + + if (subscription.name.includes("Scale")) { + top_text = "Scale access" + } + + if (highlight === true) { + // Add an "Upgrade now" button + // newPaperstyle.border = "1px solid #f85a3e" + } + + return ( + +
+ setHovered(true)} + // onMouseLeave={() => setHovered(false)} + > + + + { + e.preventDefault(); + setSignatureOpen(false); + setTosChecked(false) + }} + > + + + + Read and Accept the EULA + + + { + setTosChecked(e.target.checked) + }} + inputProps={{ 'aria-label': 'primary checkbox' }} + /> + { + setTosChecked(!tosChecked) + }}> + Accept + + + By clicking the “accept” button, you are signing the document, electronically agreeing that it has the same legal validity and effects as a handwritten signature, and that you have the competent authority to represent and sign on behalf an entity. Need support or have questions? Contact us at support@shuffler.io. + + +
+ +
+
+
+ +
+ {top_text === "Base Cloud Access" && userdata.has_card_available === true ? + { + console.log("Clicked chip") + }} + variant="outlined" + color="primary" + /> + : null} + + {top_text} + + + {top_text === "Base Cloud Access" && userdata.has_card_available === false ? + + : null} + {isCloud && highlight === true && top_text !== "Base Cloud Access" ? + + { + setSignatureOpen(true) + }} + > + + + + : null} +
+ +
+ + {subscription.name} + + + {subscription.currency_text !== undefined ? +
+ + {subscription.currency_text}{subscription.price} + + + / {subscription.interval} + +
+ : null} + + + Features + +
    + {subscription.features !== undefined && subscription.features !== null ? + subscription.features.map((feature, index) => { + var parsedFeature = feature + if (feature.includes("Documentation: ")) { + parsedFeature = + + Documentation to get started + + } + + if (feature.includes("Worker License: ")) { + const fieldId = "webhook_uri_field_" + index + parsedFeature = + + + Use the {feature.split("Worker License: ")[0]} Worker + + { }} + InputProps={{ + endAdornment: + + { + var copyText = document.getElementById(fieldId); + if (copyText !== undefined && copyText !== null) { + console.log("NAVIGATOR: ", navigator); + const clipboard = navigator.clipboard; + if (clipboard === undefined) { + toast("Can only copy over HTTPS (port 3443)"); + return; + } + + navigator.clipboard.writeText(copyText.value); + copyText.select(); + copyText.setSelectionRange( + 0, + 99999 + ); /* For mobile devices */ + + /* Copy the text inside the text field */ + document.execCommand("copy"); + toast("Copied Webhook URL"); + } else { + console.log("Couldn't find webhook URI field: ", copyText); + } + }} + edge="end" + > + + + + }} + fullWidth + /> + + } + + return ( +
  • + + {parsedFeature} + +
  • + ) + }) + : null} +
+ {isCloud ? + Billing email: {selectedOrganization.org} + : null} +
+ + + + +
+ {/* +
+ + Schedule Call Now +
+ */} + +
+
+ ) + } + + useEffect(() => { + console.log("New variant: ", shuffleVariant) + + if (shuffleVariant === 1) { + setCalculatedCost("$600") + setSelectedValue(8) + } else { + setCalculatedCost("$540") + setSelectedValue(300) + } + }, [shuffleVariant]) + + if (typeof window === 'undefined' || window.location === undefined) { + return null + } + + const setMonthlyCost = (variant, paymentType) => { + setErrorMessage("") + if (variant === 0 && paymentType === 0) { + setCurrentPrice(129) + } else if (variant === 0 && paymentType === 1) { + setCurrentPrice(155) + } else if (variant === 1 && paymentType === 0) { + setCurrentPrice(999) + } else if (variant === 1 && paymentType === 1) { + setCurrentPrice(1199) + } else if (variant === 2 && paymentType === 0) { + setCurrentPrice(15) + } else if (variant === 2 && paymentType === 1) { + setCurrentPrice(18) + } + } + + const handleChange = (event, newValue) => { + console.log("Event, value: ", event.target, newValue) + + if (shuffleVariant === 1) { + setSelectedValue(newValue) + if (newValue === 32) { + setCalculatedCost(`Get A Quote`) + } else { + setCalculatedCost(`$${newValue * 75}`) + } + } else { + setSelectedValue(newValue) + if (newValue < 300) { + setCalculatedCost(`Pay as you go`) + } else if (newValue === 1000) { + setCalculatedCost(`Get A Quote`) + } else { + setCalculatedCost(`$${newValue * 1000 * typecost}`) + } + } + } + + if (!isLoaded) { + setIsLoaded(true) + + const tmpsearch = typeof window === 'undefined' || window.location === undefined ? "" : window.location.search + const tmpVar = new URLSearchParams(tmpsearch).get("variant") + if (tmpVar !== undefined && tmpVar !== null && tmpVar < 3) { + setVariant(parseInt(tmpVar)) + } + + const tmpType = new URLSearchParams(tmpsearch).get("payment_type") + if (tmpType !== undefined && tmpType !== null && tmpType < 2) { + setPaymentType(parseInt(tmpType)) + } + + const modal = new URLSearchParams(tmpsearch).get("payment_modal") + if (modal !== undefined && modal !== null && modal === "open") { + setModalOpen(true) + } + + const tmpView = new URLSearchParams(tmpsearch).get("view") + if (tmpView !== undefined && tmpView !== null && tmpView === "failure") { + setErrorMessage("Something went wrong with your payment. Please try again.") + } + + const urlSearchParams = new URLSearchParams(window.location.search); + const params = Object.fromEntries(urlSearchParams.entries()); + const foundTab = params["tab"]; + if (foundTab !== null && foundTab !== undefined) { + if (foundTab === "onprem") { + setShuffleVariant(1); + } else if (foundTab === "cloud") { + setShuffleVariant(0); + } + } + + const foundHighlight = params["highlight"]; + if (foundHighlight !== null && foundHighlight !== undefined) { + setHighlight(true) + } + } + + //const skipFreemode = window.location.pathname.startsWith("/admin") + const skipFreemode = false + const maxwidth = isMobile ? "91%" : skipFreemode ? 1100 : 1200 + const activeIcon = + const inActiveIcon = + const defaultTaskIcon = + + const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)" + const level1Button = + + + const level2Button = + + + const level3Button = skipFreemode ? null : + + + + const cardStyle = { + // height: "100%", + // width: "100%", + // textAlign: "center", + color: "white", + } + + + const isLoggedInHandler = () => { + if (calculatedCost === payasyougo) { + handlePayasyougo(props.userdata) + return + } + + const priceItem = window.location.origin === "https://shuffler.io" ? + shuffleVariant === 0 ? "app_executions" : "cores" + : + shuffleVariant === 0 ? "price_1MROFrDzMUgUjxHShcSxgHO1" : "price_1NXjQqDzMUgUjxHSg690R4FP" + + const successUrl = `${window.location.origin}/admin?admin_tab=billing&payment=success` + const failUrl = `${window.location.origin}/pricing?admin_tab=billing&payment=failure` + + console.log("Priceitem: ", priceItem, shuffleVariant) + var checkoutObject = { + lineItems: [ + { + price: priceItem, + quantity: shuffleVariant === 0 ? selectedValue / 100 : selectedValue, + }, + ], + mode: "subscription", + billingAddressCollection: "auto", + successUrl: successUrl, + cancelUrl: failUrl, + clientReferenceId: props.userdata.active_org.id, + } + + if (stripe === undefined || stripe === null || stripe.redirectToCheckout === undefined) { + window.open("https://shuffler.io/admin?admin_tab=billing&payment=stripe_error", "_self") + } + + stripe.redirectToCheckout(checkoutObject) + .then(function (result) { + console.log("SUCCESS STRIPE?: ", result) + + ReactGA.event({ + category: "pricing", + action: "add_card_success", + label: "", + }) + }) + .catch(function (error) { + console.error("STRIPE ERROR: ", error) + + ReactGA.event({ + category: "pricing", + action: "add_card_error", + label: "", + }) + }) + } + + return ( +
+ + + {isCloud && billingInfo.subscription !== undefined && billingInfo.subscription !== null ? + + : !isCloud ? + + + {/* + + */} + + : null} + + {isCloud && + selectedOrganization.subscriptions !== undefined && + selectedOrganization.subscriptions !== null && + selectedOrganization.subscriptions.length > 0 ? + selectedOrganization.subscriptions + .reverse() + .map((sub, index) => { + return ( + + ) + }) + : null} + + + + {errorMessage.length > 0 ? Error: {errorMessage} : null} + +
+ + +
+ {shuffleVariant === 1 ? "Scale" : "Enterprise"} + + + {shuffleVariant === 0 ? + "SaaS / Cloud - Per Month" + : + "Open Source + Scale License" + } + + + { + + if (calculatedCores === "Get A Quote") { + console.log("Clicked on get a quote") + if (window.drift !== undefined) { + window.drift.api.startInteraction({ interactionId: 340785 }) + } + } + }}>{calculatedCost} + For {shuffleVariant === 1 ? `${selectedValue} CPU cores` : `${selectedValue}k App Runs`}: +
+ + { + handleChange(event, newValue) + }} + marks + value={selectedValue} + step={shuffleVariant === 0 ? 100 : 4} + min={shuffleVariant === 0 ? 100 : 8} + max={shuffleVariant === 0 ? 1000 : 32} + valueLabelDisplay="auto" + /> +
+ +
+
+ {defaultTaskIcon} + Priority Support +
+ +
+ {defaultTaskIcon} + + {shuffleVariant === 0 ? "Multi-Tenant" : "Lightning-Fast Workflows"} + +
+ +
+ {defaultTaskIcon} + + {shuffleVariant === 0 ? "Multi-Region Tenants" : "High Availability"} + +
+ +
+ {defaultTaskIcon} + Help with Workflow and App development +
+
+
+ + + + + + + + +
+ ) +} + +export default LicencePopup; diff --git a/frontend/src/components/NewHeader.jsx b/frontend/src/components/NewHeader.jsx index 558d0c72..0757f6fc 100644 --- a/frontend/src/components/NewHeader.jsx +++ b/frontend/src/components/NewHeader.jsx @@ -5,6 +5,7 @@ import { BrowserView, MobileView } from "react-device-detect"; import { useNavigate, Link } from "react-router-dom"; import ReactGA from "react-ga4"; +import LicencePopup from "../components/LicencePopup.jsx"; import SearchField from "../components/Searchfield.jsx"; import { Paper, @@ -18,15 +19,18 @@ import { MenuItem, Select, Button, + ButtonGroup, Grid, IconButton, Divider, LinearProgress, - AppBar, + Dialog, + DialogTitle, } from "@mui/material"; import { + Close as CloseIcon, MeetingRoom as MeetingRoomIcon, HelpOutline as HelpOutlineIcon, Settings as SettingsIcon, @@ -37,11 +41,10 @@ import { EmojiObjects as EmojiObjectsIcon, Business as BusinessIcon, Polyline as PolylineIcon, -} from "@mui/icons-material"; - -import { + Add as AddIcon, Analytics as AnalyticsIcon, Lightbulb as LightbulbIcon, + ExpandMore as ExpandMoreIcon, } from "@mui/icons-material"; const hoverColor = "#f85a3e"; @@ -52,13 +55,15 @@ const Header = (props) => { globalUrl, setNotifications, notifications, - isLoaded, + isLoaded, isLoggedIn, removeCookie, homePage, userdata, - isMobile, + isMobile, serverside, + curpath, + billingInfo } = props; const [HomeHoverColor, setHomeHoverColor] = useState(hoverOutColor); @@ -67,9 +72,13 @@ const Header = (props) => { const [DocsHoverColor, setDocsHoverColor] = useState(hoverOutColor); const [HelpHoverColor, setHelpHoverColor] = useState(hoverOutColor); const [isHeader, setIsHeader] = React.useState(false); + const [modalOpen, setModalOpen] = useState(false); const [anchorEl, setAnchorEl] = React.useState(null); const [anchorElAvatar, setAnchorElAvatar] = React.useState(null); const [subAnchorEl, setSubAnchorEl] = React.useState(null); + const [upgradeHovered, setUpgradeHovered] = React.useState(false); + const [showTopbar, setShowTopbar] = useState(false) + const stripeKey = typeof window === 'undefined' || window.location === undefined ? "" : window.location.origin === "https://shuffler.io" ? "pk_live_XAxwE2Fp9DEbEcNYw4UKmyby00vIlIPPRp" : "pk_test_EdxgKfqmQGXY5JLjdBqtuhCw00BHbiKJDB" let navigate = useNavigate(); const handleClick = (event) => { @@ -81,7 +90,7 @@ const Header = (props) => { setAnchorElAvatar(null); }; // Should be based on some path - const logoCheck = !homePage ? null : null + const logoCheck = !homePage ? null : null const hrefStyle = { color: hoverOutColor, @@ -89,24 +98,25 @@ const Header = (props) => { }; const menuText = { - textTransform: "none", - color: "#FFF", - textAlign: "center", - fontSize: 16, - fontStyle: "normal", - fontWeight: 400, - lineHeight: "normal", + textTransform: "none", + color: "#FFF", + textAlign: "center", + fontSize: 16, + fontStyle: "normal", + fontWeight: 400, + lineHeight: "normal", } const isCloud = serverside === true || typeof window === "undefined" ? true : window.location.host === "localhost:3002" || - window.location.host === "shuffler.io"; + window.location.host === "shuffler.io" || + window.location.host === "localhost:5002"; const clearNotifications = () => { // Don't really care about the logout - + toast("Clearing notifications") fetch(`${globalUrl}/api/v1/notifications/clear`, { credentials: "include", @@ -187,7 +197,20 @@ const Header = (props) => { removeCookie("session_token", { path: "/" }); removeCookie("session_token", { path: "/" }); removeCookie("session_token", { path: "/" }); + + removeCookie("__session", { path: "/" }); + removeCookie("__session", { path: "/" }); + removeCookie("__session", { path: "/" }); + + removeCookie("__session", { path: "/" }); window.location.pathname = "/"; + + localStorage.setItem("globalUrl", "") + + // Delete userinfo from localstorage + localStorage.removeItem("apps") + localStorage.removeItem("workflows") + localStorage.removeItem("userinfo") }) .catch((error) => { console.log(error); @@ -235,16 +258,17 @@ const Header = (props) => { setLoginHoverColor(hoverOutColor); }; - const notificationWidth = 335 - const imagesize = 22; - const boxColor = "#86c142"; + const notificationWidth = 335 + const imagesize = 22; + const boxColor = "#86c142"; - const NotificationItem = (props) => { - const {data} = props + 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) { @@ -264,7 +288,7 @@ const Header = (props) => { : null, cursor: "pointer", marginRight: 10, - }; + } image = foundOrg.image === "" ? ( @@ -278,7 +302,7 @@ const Header = (props) => { alt={foundOrg.name} src={foundOrg.image} style={imageStyle} - onClick={() => {}} + onClick={() => { }} /> ); @@ -296,31 +320,31 @@ const Header = (props) => { borderBottom: "1px solid rgba(255,255,255,0.4)", }} > - {data.reference_url !== undefined && data.reference_url !== null && data.reference_url.length > 0 ? - - - {data.title} ({data.amount}) - - - : - - {data.title} - - } + {data.reference_url !== undefined && data.reference_url !== null && data.reference_url.length > 0 ? + + + {data.title} ({data.amount}) + + + : + + {data.title} + + } - {data.image !== undefined && data.image !== null && data.image.length > 0 ? - {data.title} - : - null - } - - {data.description} - + {data.image !== undefined && data.image !== null && data.image.length > 0 ? + {data.title} + : + null + } + + {data.description} +
{data.read === false ? ( + ) : null} - ) : null} +
- - Notifications generated made by Shuffle to help you discover issues or - improvements. - Learn more + + Notifications generated made by Shuffle to help you discover issues or + improvements. + Learn more {notifications.map((data, index) => { - if (data.read) { - return null - } + if (data.read) { + return null + } - return ; + return ; })} @@ -450,18 +483,26 @@ const Header = (props) => { .then(function (response) { if (response.status !== 200) { console.log("Error in response"); + } else { + localStorage.removeItem("apps") + localStorage.removeItem("workflows") + localStorage.removeItem("userinfo") } return response.json(); }) .then(function (responseJson) { - console.log("In here?") + console.log("In here?") if (responseJson.success === true) { if (responseJson.region_url !== undefined && responseJson.region_url !== null && responseJson.region_url.length > 0) { console.log("Region Change: ", responseJson.region_url); localStorage.setItem("globalUrl", responseJson.region_url); //globalUrl = responseJson.region_url } + if (responseJson["reason"] === "SSO_REDIRECT") { + window.location.href = responseJson["url"] + return + } setTimeout(() => { window.location.reload(); @@ -469,11 +510,11 @@ const Header = (props) => { toast("Successfully changed active organization - refreshing!"); } else { - if (responseJson.reason !== undefined && responseJson.reason !== null && responseJson.reason.length > 0) { - toast(responseJson.reason); - } else { - toast("Failed changing org. Try again or contact support@shuffler.io if this persists."); - } + if (responseJson.reason !== undefined && responseJson.reason !== null && responseJson.reason.length > 0) { + toast(responseJson.reason); + } else { + toast("Failed changing org. Try again or contact support@shuffler.io if this persists."); + } } }) .catch((error) => { @@ -490,13 +531,13 @@ const Header = (props) => { rel="noopener noreferrer" target="_blank" > - + {}} + onClick={(event) => { }} > Discord Community Join { // Should be based on some path const parsedAvatar = userdata.avatar !== undefined && - userdata.avatar !== null && - userdata.avatar.length > 0 + userdata.avatar !== null && + userdata.avatar.length > 0 ? userdata.avatar - : ""; + : "" const avatarMenu = ( @@ -533,6 +574,7 @@ const Header = (props) => { alt="Your username here" src={parsedAvatar} /> + { handleClose(); }} > - Admin + Organization - + + { + handleClose(); + }} + > + Account + + + + + { + handleClose(); + }} + > + Notifications + + + + {/*notificationMenu*/} + { @@ -583,7 +646,6 @@ const Header = (props) => { Use Cases - { @@ -593,16 +655,7 @@ const Header = (props) => { Creator page - - { - handleClose(); - }} - > - Settings - - - + { @@ -613,11 +666,11 @@ const Header = (props) => { >  Logout - + - - Version: 1.3.3 - + + Version: 1.4.0 + ); @@ -628,7 +681,66 @@ const Header = (props) => { marginBottom: "auto", marginRight: 10, }; - + + const modalView = + { + setModalOpen(false); + }} + PaperProps={{ + style: { + color: "white", + minWidth: 850, + minHeight: 370, + padding: 20, + backgroundColor: "rgba(0, 0, 0, 1)", + borderRadius: theme.palette.borderRadius, + }, + }} + > + + + Upgrade your plan + + { + if (isCloud) { + ReactGA.event({ + category: "header", + action: "close_Upgread_popup", + label: "", + })}; + setModalOpen(false); + }} + style={{ + marginLeft: "auto", + position: "absolute", + top: 20, + right: 20, + }} + > + + + +
+ +
+
+ // Handle top bar or something const defaultTop = -2 const loginTextBrowser = !isLoggedIn ? ( @@ -641,163 +753,163 @@ const Header = (props) => { textAlign: "center", }} > - + + + + { + if (isCloud) { + ReactGA.event({ + category: "header", + action: "home_click", + label: "", + }); + } + }} + > + shuffle logo + + + + + + + + + + + {isCloud ? ( - - { - if (isCloud) { - ReactGA.event({ - category: "header", - action: "home_click", - label: "", - }); - } - }} - > - shuffle logo - - - - - - - + - {isCloud ? ( - - - - - - ) : null} - - - - - - - -
- - - - - - - - - + + + + + + +
+ + + + + + + + + - +
) : (
@@ -830,91 +942,91 @@ const Header = (props) => { style={{ color: HomeHoverColor, cursor: "pointer" }} > - + logo
- -
- {/* + +
+ {/* */} - - Workflows - -
- - - - -
- {/* + + Workflows + +
+ +
+ + +
+ {/* */} - - Apps - -
- -
- {/* + + Apps + +
+ +
+ {/*
Dashboard
*/} - - -
- {/* + + +
+ {/* */} - - Docs - -
- -
+ + Docs + +
+ +
- +
-
+
{ }} > {avatarMenu} - {notificationMenu} - {/*supportMenu*/} + {/*notificationMenu*/} + {supportMenu} {logoCheck} {userdata === undefined || - userdata.orgs === undefined || - userdata.orgs === null || - userdata.orgs.length <= 1 ? null : ( + userdata.orgs === undefined || + userdata.orgs === null || + userdata.orgs.length <= 0 ? null : ( )} {/* Show on cloud, if not suborg and if not customer/pov/internal */} - {isCloud && - (userdata.org_status === undefined || - userdata.org_status === null || - userdata.org_status.length === 0) ? ( - - - - - - ) : null} + { + userdata.licensed !== undefined && + userdata.licensed !== null && + userdata.licensed === false ? + + + + + + : null} {userdata === undefined || - userdata.app_execution_limit === undefined || - userdata.app_execution_usage === undefined || - userdata.app_execution_usage < 1000 ? null : ( + userdata.app_execution_limit === undefined || + userdata.app_execution_usage === undefined || + userdata.app_execution_usage < 1000 ? null : (
{ textAlign: "center", cursor: "pointer", borderRadius: theme.palette.borderRadius, - marginRight: 10, marginTop: 5, backgroundColor: theme.palette.surfaceColor, minWidth: 60, @@ -1153,14 +1308,14 @@ const Header = (props) => { border: userdata.app_execution_usage / userdata.app_execution_limit >= - 0.9 - ? "#f86a3e" + 0.9 + ? "2px solid #f86a3e" : null, }} onClick={() => { console.log( userdata.appe_execution_usage / - userdata.app_execution_limit + userdata.app_execution_limit ); if (window.drift !== undefined) { window.drift.api.startInteraction({ @@ -1344,29 +1499,114 @@ const Header = (props) => { ); // - // - /* - !isLoggedIn ? -
- - {loginTextBrowser} - -
- : - */ - return !isMobile ? - - - {loginTextBrowser} - - - : - {loginTextMobile} + // + /* + !isLoggedIn ? +
+ + {loginTextBrowser} + +
+ : + */ + + const topbarHeight = showTopbar ? 40 : 0 + const topbar = !showTopbar ? null : + curpath === "/" || curpath.includes("/docs/") || curpath === "/pricing" || curpath === "/contact" || curpath === "/search" ? + +
+ + Shuffle 1.4 is out! Read more about  + + { + ReactGA.event({ + category: "landingpage", + action: "click_header_features", + label: "", + }) + + //if (window.drift !== undefined) { + // window.drift.api.startInteraction({ interactionId: 341911 }) + //} else { + // console.log("Couldn't find drift in window.drift and not .drift-open-chat with querySelector: ", window.drift) + //} + }} style={{ cursor: "pointer", textDecoration: "none", color: "rgba(255,255,255,0.8)" }}> + Features + + + ,  + + { + ReactGA.event({ + category: "landingpage", + action: "click_header_pricing", + label: "", + }) + + navigate("/pricing") + + //if (window.drift !== undefined) { + // window.drift.api.startInteraction({ interactionId: 341911 }) + //} else { + // console.log("Couldn't find drift in window.drift and not .drift-open-chat with querySelector: ", window.drift) + //} + }} style={{ cursor: "pointer", textDecoration: "none", color: "rgba(255,255,255,0.8)" }}> + Pricing + + +  and  + + { + ReactGA.event({ + category: "landingpage", + action: "click_header_creators", + label: "", + }) + + navigate("/creators") + + //if (window.drift !== undefined) { + // window.drift.api.startInteraction({ interactionId: 341911 }) + //} else { + // console.log("Couldn't find drift in window.drift and not .drift-open-chat with querySelector: ", window.drift) + //} + }} style={{ cursor: "pointer", textDecoration: "none", color: "rgba(255,255,255,0.8)" }}> + Earning as a Creator + + + + { setShowTopbar(false) }}> + + +
+
+ : + null + + return !isMobile ? +
+ + + {topbar} + +
+ {loginTextBrowser} +
+ {modalView} +
+
+ : + {loginTextMobile} }; export default Header; diff --git a/frontend/src/components/Oauth2Auth.jsx b/frontend/src/components/Oauth2Auth.jsx index f175d683..40c043a4 100755 --- a/frontend/src/components/Oauth2Auth.jsx +++ b/frontend/src/components/Oauth2Auth.jsx @@ -70,8 +70,6 @@ const registeredApps = [ "microsoft_teams", "microsoft_teams_user_access", "todoist", - "microsoft_sentinel", - "microsoft_365_defender", "google_chat", "google_sheets", "google_drive", @@ -379,7 +377,7 @@ const AuthenticationOauth2 = (props) => { }, "fields": parsedFields, "type": "oauth2-app", - "reference_workflow": workflowIdNew, + //"reference_workflow": workflowId, } if (setNewAppAuth !== undefined) { @@ -412,7 +410,7 @@ const AuthenticationOauth2 = (props) => { if (offlineAccess === true && !scopes.includes("offline_access")) { console.log("IN scope 2") - if (!authenticationType.redirect_uri.includes("google")) { + if (!authenticationType.redirect_uri.includes("google") && !authenticationType.redirect_uri.includes("slack")) { console.log("Appending offline access") scopes.push("offline_access") } @@ -476,8 +474,6 @@ const AuthenticationOauth2 = (props) => { url = `${authenticationType.redirect_uri}?client_id=${client_id}&redirect_uri=${redirectUri}&response_type=code&prompt=admin_consent&scope=${resources}&state=${state}&access_type=offline`; } - console.log("URL: ", url) - // Force new consent //const url = `${authenticationType.redirect_uri}?client_id=${client_id}&redirect_uri=${redirectUri}&response_type=code&scope=${resources}&prompt=consent&state=${state}&access_type=offline`; @@ -530,9 +526,8 @@ const AuthenticationOauth2 = (props) => { //} //while(open === true) } catch (e) { - toast( - "Failed authentication - probably bad credentials. Try again" - ); + toast("Failed authentication - probably bad credentials. Try again") + setButtonClicked(false); } @@ -719,7 +714,7 @@ const AuthenticationOauth2 = (props) => { - Oauth2 requires a client ID and secret to authenticate, defined in the remote system. Your redirect URL is {window.location.origin}/set_authentication -  + Oauth2 requires a client ID and secret to authenticate, defined in the remote system. {authenticationType.type === "oauth2-app" ? null : Your redirect URL is {window.location.origin}/set_authentication - } { } - + OR @@ -810,18 +805,20 @@ const AuthenticationOauth2 = (props) => { setOauthUrl(data.value); } - const defaultValue = data.name === "url" && authenticationType.token_uri !== undefined && authenticationType.token_uri !== null && authenticationType.token_uri.length > 0 && (authenticationType.authorizationUrl === undefined || authenticationType.authorizationUrl === null || authenticationType.authorizationUrl.length === 0) ? authenticationType.token_uri : data.value === undefined || data.value === null ? "" : data.value - const fieldname = data.name === "url" && authenticationType.grant_type !== undefined && authenticationType.grant_type !== null && authenticationType.grant_type.length > 0 ? "Token URL" : data.name + 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.type === "oauth2-app" ? 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 && authenticationType.type === "oauth2-app" ? "Token URL" : data.name return ( -
+
{fieldname} {data.schema !== undefined && - data.schema !== null && - data.schema.type === "bool" ? ( + data.schema !== null && + data.schema.type === "bool" ? - ) : ( + : { //const [oauthUrl, setOauthUrl] = React.useState("") }} /> - )} + }
- ); + ) })} { variant="contained" fullWidth onClick={() => { + toast.info("Starting authentication process", { + "autoClose": 1500, + }) + handleOauth2Request(clientId, clientSecret, oauthUrl, selectedScopes); }} color="primary" diff --git a/frontend/src/components/OrgHeader.jsx b/frontend/src/components/OrgHeader.jsx index 95020bb1..5f7fbbbd 100644 --- a/frontend/src/components/OrgHeader.jsx +++ b/frontend/src/components/OrgHeader.jsx @@ -24,9 +24,7 @@ const useStyles = makeStyles({ }, }); -const defaultImage = - "data:image/png;base64,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"; - +const defaultImage = theme.palette.defaultImage const OrgHeader = (props) => { const { userdata, @@ -124,10 +122,9 @@ const OrgHeader = (props) => { ); var imageData = file.length > 0 ? file : fileBase64; - imageData = - imageData === undefined || imageData.length === 0 - ? defaultImage - : imageData; + imageData = imageData === undefined || imageData.length === 0 ? defaultImage : imageData + + const imageInfo = ( { minWidth: 174, minHeight: 174, objectFit: "contain", + borderRadius: theme.shape.borderRadius, }} /> ); @@ -166,12 +164,9 @@ const OrgHeader = (props) => { ? null : "1px solid #f85a3e", cursor: "pointer", - backgroundColor: - imageData !== undefined && imageData.length > 0 - ? null - : theme.palette.inputColor, maxWidth: 174, maxHeight: 174, + borderRadius: theme.shape.borderRadius, }} onClick={() => { upload.click(); diff --git a/frontend/src/components/OrgHeaderexpanded.jsx b/frontend/src/components/OrgHeaderexpanded.jsx index 268350a2..2efae52a 100644 --- a/frontend/src/components/OrgHeaderexpanded.jsx +++ b/frontend/src/components/OrgHeaderexpanded.jsx @@ -2,243 +2,257 @@ import React, { useEffect } from "react"; import { makeStyles } from "@mui/styles"; import theme from '../theme.jsx'; -import { toast } from "react-toastify" +import { toast } from "react-toastify" +import Chip from '@mui/material/Chip'; +import Stack from '@mui/material/Stack'; +import SubflowSuggestions from "../components/SubflowSuggestions.jsx"; import { - FormControl, - InputLabel, - Paper, - OutlinedInput, - Checkbox, - Card, - Tooltip, - FormControlLabel, - Typography, - Switch, - Select, - MenuItem, - Divider, - TextField, - Button, - Tabs, - Tab, - Grid, - IconButton, - Autocomplete, + FormControl, + InputLabel, + Paper, + OutlinedInput, + Checkbox, + Card, + Tooltip, + FormControlLabel, + Typography, + Switch, + Select, + MenuItem, + Divider, + TextField, + Button, + Tabs, + Tab, + Grid, + IconButton, + Autocomplete, + Dialog, + DialogTitle, + DialogActions, + DialogContent, + Box } from "@mui/material"; import { - ExpandLess as ExpandLessIcon, - ExpandMore as ExpandMoreIcon, + ExpandLess as ExpandLessIcon, + ExpandMore as ExpandMoreIcon, Save as SaveIcon, } from "@mui/icons-material"; const useStyles = makeStyles({ - notchedOutline: { - borderColor: "#f85a3e !important", - }, -}); + notchedOutline: { + borderColor: "#f85a3e !important", + }, +}) const OrgHeaderexpanded = (props) => { - const { - userdata, - selectedOrganization, - setSelectedOrganization, - globalUrl, - isCloud, + const { + userdata, + selectedOrganization, + setSelectedOrganization, + globalUrl, + isCloud, adminTab, - } = props; + } = props; - const classes = useStyles(); - const defaultBranch = "master"; + const classes = useStyles(); + const defaultBranch = "master"; - const [orgName, setOrgName] = React.useState(selectedOrganization.name); - const [orgDescription, setOrgDescription] = React.useState( - selectedOrganization.description - ); + const [orgName, setOrgName] = React.useState(selectedOrganization.name); + const [orgDescription, setOrgDescription] = React.useState( + selectedOrganization.description + ); - const [appDownloadUrl, setAppDownloadUrl] = React.useState( - selectedOrganization.defaults === undefined - ? "https://github.com/frikky/shuffle-apps" - : selectedOrganization.defaults.app_download_repo === undefined || - selectedOrganization.defaults.app_download_repo.length === 0 - ? "https://github.com/frikky/shuffle-apps" - : selectedOrganization.defaults.app_download_repo - ); - const [appDownloadBranch, setAppDownloadBranch] = React.useState( - selectedOrganization.defaults === undefined - ? defaultBranch - : selectedOrganization.defaults.app_download_branch === undefined || - selectedOrganization.defaults.app_download_branch.length === 0 - ? defaultBranch - : selectedOrganization.defaults.app_download_branch - ); - const [workflowDownloadUrl, setWorkflowDownloadUrl] = React.useState( - selectedOrganization.defaults === undefined - ? "https://github.com/frikky/shuffle-apps" - : selectedOrganization.defaults.workflow_download_repo === undefined || - selectedOrganization.defaults.workflow_download_repo.length === 0 - ? "https://github.com/frikky/shuffle-workflows" - : selectedOrganization.defaults.workflow_download_repo - ); - const [workflowDownloadBranch, setWorkflowDownloadBranch] = React.useState( - selectedOrganization.defaults === undefined - ? defaultBranch - : selectedOrganization.defaults.workflow_download_branch === undefined || - selectedOrganization.defaults.workflow_download_branch.length === 0 - ? defaultBranch - : selectedOrganization.defaults.workflow_download_branch - ); - const [ssoEntrypoint, setSsoEntrypoint] = React.useState( - selectedOrganization.sso_config === undefined - ? "" - : selectedOrganization.sso_config.sso_entrypoint === undefined || - selectedOrganization.sso_config.sso_entrypoint.length === 0 - ? "" - : selectedOrganization.sso_config.sso_entrypoint - ); - const [ssoCertificate, setSsoCertificate] = React.useState( - selectedOrganization.sso_config === undefined - ? "" - : selectedOrganization.sso_config.sso_certificate === undefined || - selectedOrganization.sso_config.sso_certificate.length === 0 - ? "" - : selectedOrganization.sso_config.sso_certificate - ); - const [notificationWorkflow, setNotificationWorkflow] = React.useState( - selectedOrganization.defaults === undefined - ? "" - : selectedOrganization.defaults.notification_workflow === undefined || - selectedOrganization.defaults.notification_workflow.length === 0 - ? "" - : selectedOrganization.defaults.notification_workflow - ); + const [appDownloadUrl, setAppDownloadUrl] = React.useState( + selectedOrganization.defaults === undefined + ? "https://github.com/frikky/shuffle-apps" + : selectedOrganization.defaults.app_download_repo === undefined || + selectedOrganization.defaults.app_download_repo.length === 0 + ? "https://github.com/frikky/shuffle-apps" + : selectedOrganization.defaults.app_download_repo + ); + const [appDownloadBranch, setAppDownloadBranch] = React.useState( + selectedOrganization.defaults === undefined + ? defaultBranch + : selectedOrganization.defaults.app_download_branch === undefined || + selectedOrganization.defaults.app_download_branch.length === 0 + ? defaultBranch + : selectedOrganization.defaults.app_download_branch + ); + const [workflowDownloadUrl, setWorkflowDownloadUrl] = React.useState( + selectedOrganization.defaults === undefined + ? "https://github.com/frikky/shuffle-apps" + : selectedOrganization.defaults.workflow_download_repo === undefined || + selectedOrganization.defaults.workflow_download_repo.length === 0 + ? "https://github.com/frikky/shuffle-workflows" + : selectedOrganization.defaults.workflow_download_repo + ); + const [workflowDownloadBranch, setWorkflowDownloadBranch] = React.useState( + selectedOrganization.defaults === undefined + ? defaultBranch + : selectedOrganization.defaults.workflow_download_branch === undefined || + selectedOrganization.defaults.workflow_download_branch.length === 0 + ? defaultBranch + : selectedOrganization.defaults.workflow_download_branch + ); + const [ssoEntrypoint, setSsoEntrypoint] = React.useState( + selectedOrganization.sso_config === undefined + ? "" + : selectedOrganization.sso_config.sso_entrypoint === undefined || + selectedOrganization.sso_config.sso_entrypoint.length === 0 + ? "" + : selectedOrganization.sso_config.sso_entrypoint + ); + const [ssoCertificate, setSsoCertificate] = React.useState( + selectedOrganization.sso_config === undefined + ? "" + : selectedOrganization.sso_config.sso_certificate === undefined || + selectedOrganization.sso_config.sso_certificate.length === 0 + ? "" + : selectedOrganization.sso_config.sso_certificate + ); + const [SSORequired, setSSORequired] = React.useState(selectedOrganization.sso_config === undefined + ? false + : selectedOrganization.sso_config.SSORequired === undefined + ? false + : selectedOrganization.sso_config.SSORequired); - const [documentationReference, setDocumentationReference] = React.useState( - selectedOrganization.defaults === undefined - ? "" - : selectedOrganization.defaults.documentation_reference === undefined || - selectedOrganization.defaults.documentation_reference.length === 0 - ? "" - : selectedOrganization.defaults.documentation_reference - ); - const [openidClientId, setOpenidClientId] = React.useState( - selectedOrganization.sso_config === undefined - ? "" - : selectedOrganization.sso_config.client_id === undefined || - selectedOrganization.sso_config.client_id.length === 0 - ? "" - : selectedOrganization.sso_config.client_id - ); - const [openidClientSecret, setOpenidClientSecret] = React.useState( - selectedOrganization.sso_config === undefined - ? "" - : selectedOrganization.sso_config.client_secret === undefined || - selectedOrganization.sso_config.client_secret.length === 0 - ? "" - : selectedOrganization.sso_config.client_secret - ); - const [openidAuthorization, setOpenidAuthorization] = React.useState( - selectedOrganization.sso_config === undefined - ? "" - : selectedOrganization.sso_config.openid_authorization === undefined || - selectedOrganization.sso_config.openid_authorization.length === 0 - ? "" - : selectedOrganization.sso_config.openid_authorization - ); - const [openidToken, setOpenidToken] = React.useState( - selectedOrganization.sso_config === undefined - ? "" - : selectedOrganization.sso_config.openid_token === undefined || - selectedOrganization.sso_config.openid_token.length === 0 - ? "" - : selectedOrganization.sso_config.openid_token + const [notificationWorkflow, setNotificationWorkflow] = React.useState( + selectedOrganization.defaults === undefined + ? "" + : selectedOrganization.defaults.notification_workflow === undefined || + selectedOrganization.defaults.notification_workflow.length === 0 + ? "" + : selectedOrganization.defaults.notification_workflow + ); + + const [documentationReference, setDocumentationReference] = React.useState( + selectedOrganization.defaults === undefined + ? "" + : selectedOrganization.defaults.documentation_reference === undefined || + selectedOrganization.defaults.documentation_reference.length === 0 + ? "" + : selectedOrganization.defaults.documentation_reference + ); + const [openidClientId, setOpenidClientId] = React.useState( + selectedOrganization.sso_config === undefined + ? "" + : selectedOrganization.sso_config.client_id === undefined || + selectedOrganization.sso_config.client_id.length === 0 + ? "" + : selectedOrganization.sso_config.client_id + ); + const [openidClientSecret, setOpenidClientSecret] = React.useState( + selectedOrganization.sso_config === undefined + ? "" + : selectedOrganization.sso_config.client_secret === undefined || + selectedOrganization.sso_config.client_secret.length === 0 + ? "" + : selectedOrganization.sso_config.client_secret + ); + const [openidAuthorization, setOpenidAuthorization] = React.useState( + selectedOrganization.sso_config === undefined + ? "" + : selectedOrganization.sso_config.openid_authorization === undefined || + selectedOrganization.sso_config.openid_authorization.length === 0 + ? "" + : selectedOrganization.sso_config.openid_authorization + ); + const [openidToken, setOpenidToken] = React.useState( + selectedOrganization.sso_config === undefined + ? "" + : selectedOrganization.sso_config.openid_token === undefined || + selectedOrganization.sso_config.openid_token.length === 0 + ? "" + : selectedOrganization.sso_config.openid_token ) - const [workflows, setWorkflows] = React.useState([]) - const [workflow, setWorkflow] = React.useState({}) + const [workflows, setWorkflows] = React.useState([]) + const [workflow, setWorkflow] = React.useState({}) - const getAvailableWorkflows = (trigger_index) => { - fetch(globalUrl + "/api/v1/workflows", { - method: "GET", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - credentials: "include", - }) - .then((response) => { - if (response.status !== 200) { - console.log("Status not 200 for workflows :O!"); - return; - } - return response.json(); - }) - .then((responseJson) => { - if (responseJson !== undefined) { - setWorkflows(responseJson) + const getAvailableWorkflows = (trigger_index) => { + fetch(globalUrl + "/api/v1/workflows", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for workflows :O!"); + return; + } + return response.json(); + }) + .then((responseJson) => { + if (responseJson !== undefined) { + setWorkflows(responseJson) - if (selectedOrganization.defaults !== undefined && selectedOrganization.defaults.notification_workflow !== undefined) { + if (selectedOrganization.defaults !== undefined && selectedOrganization.defaults.notification_workflow !== undefined) { - const workflow = responseJson.find((workflow) => workflow.id === selectedOrganization.defaults.notification_workflow) - if (workflow !== undefined && workflow !== null) { - setWorkflow(workflow) - } - } - } - }) - .catch((error) => { - console.log("Error getting workflows: " + error); - }) - } + const workflow = responseJson.find((workflow) => workflow.id === selectedOrganization.defaults.notification_workflow) + if (workflow !== undefined && workflow !== null) { + setWorkflow(workflow) + } + } + } + }) + .catch((error) => { + console.log("Error getting workflows: " + error); + }) + } - useEffect(() => { - getAvailableWorkflows() - }, []) + useEffect(() => { + getAvailableWorkflows() + }, []) - const handleEditOrg = ( - name, - description, - orgId, - image, - defaults, - sso_config - ) => { + const handleEditOrg = ( + name, + description, + orgId, + image, + defaults, + sso_config + ) => { - const data = { - name: name, - description: description, - org_id: orgId, - image: image, - defaults: defaults, - sso_config: sso_config, - }; + const data = { + name: name, + description: description, + org_id: orgId, + image: image, + defaults: defaults, + sso_config: sso_config, + }; - const url = globalUrl + `/api/v1/orgs/${selectedOrganization.id}`; - fetch(url, { - mode: "cors", - method: "POST", - body: JSON.stringify(data), - credentials: "include", - crossDomain: true, - withCredentials: true, - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }) - .then((response) => - response.json().then((responseJson) => { - if (responseJson["success"] === false) { - toast("Failed updating org: ", responseJson.reason); - } else { - toast("Successfully edited org!"); - } - }) - ) - .catch((error) => { - toast("Err: " + error.toString()); - }); - }; + const url = globalUrl + `/api/v1/orgs/${selectedOrganization.id}`; + fetch(url, { + mode: "cors", + method: "POST", + body: JSON.stringify(data), + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then((response) => + response.json().then((responseJson) => { + if (responseJson["success"] === false) { + toast("Failed updating org: ", responseJson.reason); + } else { + toast("Successfully edited org!"); + } + }) + ) + .catch((error) => { + toast("Err: " + error.toString()); + }); + }; const handleWorkflowSelectionUpdate = (e, isUserinput) => { @@ -249,51 +263,55 @@ const OrgHeaderexpanded = (props) => { setWorkflow(e.target.value) setNotificationWorkflow(e.target.value.id) - toast("Updated notification workflow. Don't forget to save!") + toast("Updated notification workflow. Don't forget to save!") } - const orgSaveButton = ( - -
- -
-
- ); + const orgSaveButton = ( + +
+ +
+
+ ); + + const toggleBetweenRequiredOrOptional = (event) => { + setSSORequired(event.target.checked); + }; return (
@@ -301,150 +319,157 @@ const OrgHeaderexpanded = (props) => { Notification Workflow + {/* - - Add a Workflow that receives notifications from Shuffle when an error occurs in one of your workflows - + */} -
+ + +
{workflows !== undefined && workflows !== null && workflows.length > 0 ? { - if ( - option === undefined || - option === null || - option.name === undefined || - option.name === null - ) { - return "No Workflow Selected"; - } - - const newname = ( - option.name.charAt(0).toUpperCase() + option.name.substring(1) - ).replaceAll("_", " "); - return newname; - }} - options={workflows} - fullWidth - style={{ - backgroundColor: theme.palette.inputColor, - height: 50, - borderRadius: theme.palette.borderRadius, - }} - onChange={(event, newValue) => { - console.log("Found value: ", newValue) - - var parsedinput = { target: { value: newValue } } - - // For variables - if (typeof newValue === 'string' && newValue.startsWith("$")) { - parsedinput = { - target: { - value: { - "name": newValue, - "id": newValue, - "actions": [], - "triggers": [], - } - } + id="notification_workflow_search" + autoHighlight + freeSolo + //autoSelect + value={workflow} + classes={{ inputRoot: classes.inputRoot }} + ListboxProps={{ + style: { + backgroundColor: theme.palette.inputColor, + color: "white", + }, + }} + getOptionLabel={(option) => { + if ( + option === undefined || + option === null || + option.name === undefined || + option.name === null + ) { + return "No Workflow Selected"; } - } - handleWorkflowSelectionUpdate(parsedinput) - }} - renderOption={(props, data, state) => { - if (data.id === workflow.id) { - data = workflow; - } + const newname = ( + option.name.charAt(0).toUpperCase() + option.name.substring(1) + ).replaceAll("_", " "); + return newname; + }} + options={workflows} + fullWidth + style={{ + backgroundColor: theme.palette.inputColor, + height: 50, + borderRadius: theme.palette.borderRadius, + }} + onChange={(event, newValue) => { + console.log("Found value: ", newValue) - return ( - - {data.image !== undefined && data.image !== null && data.image.length > 0 ? - {data.name} - : null} - - Choose {data.name} - - - } placement="bottom"> - { - var parsedinput = { target: { value: data } } - handleWorkflowSelectionUpdate(parsedinput) - }} - > - {data.name} - - - ) - }} - renderInput={(params) => { - return ( - - ); - }} - /> - : - { - setNotificationWorkflow(e.target.value); - }} - InputProps={{ - classes: { - notchedOutline: classes.notchedOutline, - }, - style: { - color: "white", - }, - }} - /> - } -
- {orgSaveButton} -
+ var parsedinput = { target: { value: newValue } } + + // For variables + if (typeof newValue === 'string' && newValue.startsWith("$")) { + parsedinput = { + target: { + value: { + "name": newValue, + "id": newValue, + "actions": [], + "triggers": [], + } + } + } + } + + handleWorkflowSelectionUpdate(parsedinput) + }} + renderOption={(props, data, state) => { + if (data.id === workflow.id) { + data = workflow; + } + + return ( + + {data.image !== undefined && data.image !== null && data.image.length > 0 ? + {data.name} + : null} + + Choose {data.name} + + + } placement="bottom"> + { + var parsedinput = { target: { value: data } } + handleWorkflowSelectionUpdate(parsedinput) + }} + > + {data.name} + + + ) + }} + renderInput={(params) => { + return ( + + ); + }} + /> + : + { + setNotificationWorkflow(e.target.value); + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style: { + color: "white", + }, + }} + /> + } +
+ {orgSaveButton} +
- + Org Documentation reference @@ -477,10 +502,25 @@ const OrgHeaderexpanded = (props) => { /> - {isCloud ? null : - - OpenID connect - + {/* {isCloud ? null : */} +
+
+ + {SSORequired ? 'Required' : 'Optional'} +
+ Make SAML SSO or OpenID Authentication Required or Optional for Your Organization. +
+
+
+ + OpenID connect + Client ID @@ -560,7 +600,7 @@ const OrgHeaderexpanded = (props) => { - + Authorization URL @@ -631,11 +671,11 @@ const OrgHeaderexpanded = (props) => { - } + {/* } */} {/*isCloud ? null : */} - - SAML SSO (v1.1) - + + SAML SSO (v1.1) + SSO Entrypoint (IdP) @@ -710,11 +750,11 @@ const OrgHeaderexpanded = (props) => { - {isCloud ? - + {isCloud ? + IdP URL for Shuffle: https://shuffler.io/api/v1/login_sso - : null} + : null} {isCloud ? null : ( @@ -870,4 +910,4 @@ const OrgHeaderexpanded = (props) => { ) } -export default OrgHeaderexpanded; +export default OrgHeaderexpanded; diff --git a/frontend/src/components/ParsedAction.jsx b/frontend/src/components/ParsedAction.jsx index 68366fe4..48f99556 100755 --- a/frontend/src/components/ParsedAction.jsx +++ b/frontend/src/components/ParsedAction.jsx @@ -8,6 +8,7 @@ import { validateJson, GetIconInfo } from "../views/Workflows.jsx"; import { GetParsedPaths } from "../views/Apps.jsx"; import { sortByKey } from "../views/AngularWorkflow.jsx"; import { NestedMenuItem } from "mui-nested-menu"; +import { parsedDatatypeImages } from "../components/AppFramework.jsx"; //import { useAlert import { @@ -86,14 +87,9 @@ import { AutoFixHigh as AutoFixHighIcon, Circle as CircleIcon, SquareFoot as SquareFootIcon, + Storage as StorageIcon, } from '@mui/icons-material'; - -//import CodeMirror from "@uiw/react-codemirror"; -//import "codemirror/keymap/sublime"; -//import "codemirror/theme/gruvbox-dark.css"; -//import ShuffleCodeEditor from "../components/ShuffleCodeEditor.jsx"; - const useStyles = makeStyles({ notchedOutline: { borderColor: "#f85a3e !important", @@ -123,7 +119,7 @@ const openApiFieldDesc = "Generated by OpenAPI body example"; const ParsedAction = (props) => { const { workflow, - files, + files, setWorkflow, setAction, setSelectedAction, @@ -168,7 +164,10 @@ const ParsedAction = (props) => { expansionModalOpen, setExpansionModalOpen, + + listCache, + apps, setEditorData, setcodedata, setAiQueryModalOpen, @@ -183,6 +182,8 @@ const ParsedAction = (props) => { const [autoCompleting, setAutocompleting] = React.useState(false); + const isIntegration = selectedAction.app_id === "integration" + useEffect(() => { if (setLastSaved !== undefined) { setLastSaved(false) @@ -433,20 +434,59 @@ const ParsedAction = (props) => { }) } + /* actionlist.push({ type: "Shuffle DB", name: "Shuffle DB", value: "$shuffle_cache", highlight: "shuffle_cache", autocomplete: "shuffle_cache", - example: "", + example: { + "what": "", + "unique gmail ids new": "", + }, }) + */ - if ( - workflow.workflow_variables !== null && - workflow.workflow_variables !== undefined && - workflow.workflow_variables.length > 0 - ) { + var cachekey = { + type: "Shuffle DB", + name: "Shuffle DB", + value: "$shuffle_cache", + highlight: "shuffle_cache", + autocomplete: "shuffle_cache", + example: "", + } + + if (listCache !== undefined && listCache !== null && listCache.keys !== undefined && listCache.keys !== null && listCache.keys.length > 0) { + cachekey.example = {} + + for (var i in listCache.keys) { + const item = listCache.keys[i] + if (item.key === undefined || item.key === null || item.key.length === 0) { + continue + } + + var itemvalue = item.value === undefined || item.value === null ? "" : item.value + try{ + if (itemvalue.length > 10000) { + itemvalue = "" + } + + } catch (e) { + itemvalue = "" + } + + var itemkey = item.key.split(" ").join("_") + cachekey.example[itemkey] = { + "value": itemvalue, + } + } + } else { + } + + actionlist.push(cachekey) + + if (workflow.workflow_variables !== null && workflow.workflow_variables !== undefined && workflow.workflow_variables.length > 0) { for (let [key,keyval] in Object.entries(workflow.workflow_variables)) { const item = workflow.workflow_variables[key]; actionlist.push({ @@ -460,96 +500,119 @@ const ParsedAction = (props) => { } } - // FIXME: Add values from previous executions if they exist - if ( - workflow.execution_variables !== null && - workflow.execution_variables !== undefined && - workflow.execution_variables.length > 0 - ) { + if (workflow.execution_variables !== null && workflow.execution_variables !== undefined && workflow.execution_variables.length > 0) { for (let [key,keyval] in Object.entries(workflow.execution_variables)) { - const item = workflow.execution_variables[key]; + const item = workflow.execution_variables[key] + + var exampleoutput = "" + for (let execkey in workflowExecutions) { + const exec = workflowExecutions[execkey] + if (exec["execution_variables"] === undefined || exec["execution_variables"] === null) { + continue + } + + const foundExec = exec.execution_variables.find((exvar) => exvar.name === item.name) + if (!foundExec) { + continue + } + + if (foundExec.value !== undefined && foundExec.value !== null && foundExec.value.length > 0) { + exampleoutput = foundExec.value + break + } + } + actionlist.push({ type: "execution_variable", name: item.name, value: item.value, id: item.id, autocomplete: `${item.name.split(" ").join("_")}`, - example: "", + example: exampleoutput, }); } } // Loops parent nodes' old results to fix autocomplete - if (getParents !== undefined) { - var parents = getParents(selectedAction); + if (getParents !== undefined) { + var parents = getParents(selectedAction) if (parents.length > 1) { - for (let [key,keyval] in Object.entries(parents)) { - const item = parents[key]; - if (item.label === "Execution Argument") { - continue; + var labels = [] + //for (let [parentkey, parentkeyval] in Object.entries(parents)) { + for (let parentkey in parents) { + const parentNode = parents[parentkey] + if (parentNode.label === "Execution Argument") { + continue } - var exampledata = item.example === undefined || item.example === null ? "" : item.example; + //if (labels.includes(item.label)) { + // continue + //} + + labels.push(parentNode.label) + + var exampledata = parentNode.example === undefined || parentNode.example === null ? "" : parentNode.example // Find previous execution and their variables //exampledata === "" && if (workflowExecutions.length > 0) { // Look for the ID const found = false; - for (let [key,keyval] in Object.entries(workflowExecutions)) { - if ( - workflowExecutions[key].results === undefined || - workflowExecutions[key].results === null - ) { + for (let wfkey in workflowExecutions) { + if (workflowExecutions[wfkey].results === undefined || workflowExecutions[wfkey].results === null) { + continue; } - var foundResult = workflowExecutions[key].results.find( - (result) => result.action.id === item.id - ); + var foundResult = workflowExecutions[wfkey].results.find((result) => result.action.id === parentNode.id) + if (foundResult === undefined || foundResult === null) { - continue; + continue } - if (foundResult.result !== undefined && foundResult.result !== null) { - foundResult = foundResult.result - } + if (foundResult.result !== undefined && foundResult.result !== null) { + foundResult = foundResult.result + } - const valid = validateJson(foundResult) - if (valid.valid) { - if (valid.result.success === false) { - //console.log("Skipping success false autocomplete") - } else { - exampledata = valid.result; - break; - } + const valid = validateJson(foundResult) + if (valid.valid) { + if (valid.result.success === false) { + //console.log("Skipping success false autocomplete") + } else { + + // FIXME: Have a merge system to allow to use kind of any key from that node in the last 10-20 execs + //if (exampledata.length > 0) { + // exampledata = valid.result + //} else { + // exampledata = valid.result + //} + + exampledata = valid.result + break + } } else { - exampledata = foundResult; - } + exampledata = foundResult + } } } // 1. Take - const itemlabelComplete = - item.label === null || item.label === undefined - ? "" - : item.label.split(" ").join("_"); + const itemlabelComplete = parentNode.label === null || parentNode.label === undefined ? "" : parentNode.label.split(" ").join("_"); const actionvalue = { type: "action", - id: item.id, - name: item.label, + id: parentNode.id, + name: parentNode.label, autocomplete: itemlabelComplete, example: exampledata, - }; + } - actionlist.push(actionvalue); + actionlist.push(actionvalue) } } - //console.log("ACTIONLIST: ", actionlist) setActionlist(actionlist); - } + } } }); @@ -678,7 +741,6 @@ const ParsedAction = (props) => { selectedAction.parameters[count]["value_replace"] = paramcheck["value_replace"]; } - console.log("RESULT: ", selectedAction); setSelectedAction(selectedAction); //setUpdate(Math.random()) return; @@ -1040,66 +1102,219 @@ const ParsedAction = (props) => { }; - const returnHelperText = (name, value) => { - if (name === undefined || value === undefined) { - return "" - } - - var helperText = "" - //console.log("DATA: ", name, value) - if (name.includes("url")) { - if (value.includes("localhost") || value.includes("127.0.0.1")) { - helperText = "Can't use localhost. Please change to an external IP or hostname." - } - } - - return helperText + const returnHelperText = (name, value) => { + if (name === undefined || value === undefined || name === null || value === null || name === "" || value === "") { + //console.log("RETURNING EMPTY") + return "" } - //console.log("AUTH: ", authenticationType) - if (authenticationType !== undefined && authenticationType !== null && authenticationType.type === "oauth2") { - /* - return ( - - You must authenticate before using oauth2 apps. - - ) - */ + var helperText = "" + if (name.includes("url")) { + if (value.includes("localhost") || value.includes("127.0.0.1")) { + helperText = "Can't use localhost. Please change to your external IP." + } } - //console.log("APP: ", selectedApp) + // Helpertext for openapi fields + //if (helperText === "" && name === "body" && selectedApp.generated && selectedApp.activated) { + // helperText = + //} + + return helperText + } + // FIXME: Issue #40 - selectedActionParameters not reset - if ( - Object.getOwnPropertyNames(selectedAction).length > 0 && - selectedActionParameters.length > 0 - ) { + if (Object.getOwnPropertyNames(selectedAction).length > 0 && selectedActionParameters.length > 0) { + + var wrapperapp = { + "id": "", + "name": "noapp", + "large_image": "", + } + + var actionname = selectedAction.name.toLowerCase() + if (actionname === "email" || actionname === "communication") { + actionname = "comms" + } + + if (isIntegration) { + + // Check if actionname uppercase is in the parsedDatatypeImages() dictionary + if (parsedDatatypeImages()[actionname.toUpperCase()] !== undefined) { + var newimage = parsedDatatypeImages()[actionname.toUpperCase()] + //newimage = + wrapperapp.large_image = newimage + } else { + console.log("Couldn't find actionname: ", actionname) + } + } + var authWritten = false; + var noAppSelected = false + const paramIndex = selectedAction.parameters.findIndex((param) => param.name === "app_name") + if (paramIndex === -1 || selectedAction.parameters[paramIndex].value === "" || selectedAction.parameters[paramIndex].value === "noapp") { + // Check the actual value and if it's the same + noAppSelected = true + } return (
- 0 ? +
+
{ + + selectedAction.example = "noapp" + selectedAction.large_image = newimage + if (cy !== undefined && cy !== null) { + const foundnode = cy.getElementById(selectedAction.id) + if (foundnode !== undefined && foundnode !== null) { + foundnode.data("large_image", newimage) + } + } + + const iconInfo = GetIconInfo(selectedAction) + if (iconInfo !== undefined && iconInfo !== null) { + selectedAction.fillGradient = iconInfo.fillGradient + + selectedAction.iconBackground = iconInfo.iconBackgroundColor + selectedAction.fillstyle = "linear-gradient" + } + + if (paramIndex === -1) { + console.log("Couldn't find app_name parameter") + selectedAction.parameters.push({ + name: "app_name", + value: wrapperapp.name, + autocompleted: false, + }) + } else { + selectedAction.parameters[paramIndex].value = wrapperapp.name + } + + setSelectedAction(selectedAction) + setUpdate(Math.random()) + + }}> + +
+ +
+
+
+ {apps.map((app, appIndex) => { + if (app.categories === undefined || app.categories === null || app.categories.length === 0) { + return null + } + + var found = false + + + for (var key in app.categories) { + if (app.categories[key].toLowerCase() !== actionname) { + continue + } + + found = true + break + } + + if (!found) { + return null + } + + var isAppSelected = false + const paramIndex = selectedAction.parameters.findIndex((param) => param.name === "app_name") + if (paramIndex > -1) { + // Check the actual value and if it's the same + if (selectedAction.parameters[paramIndex].value === app.name) { + isAppSelected = true + } + } + + return ( +
{ + selectedAction.example = "" + selectedAction.large_image = app.large_image + if (cy !== undefined && cy !== null) { + const foundnode = cy.getElementById(selectedAction.id) + if (foundnode !== undefined && foundnode !== null) { + foundnode.data("large_image", app.large_image) + } + } + + if (paramIndex === -1) { + console.log("Couldn't find app_name parameter") + selectedAction.parameters.push({ + name: "app_name", + value: app.name, + autocompleted: false, + }) + } else { + selectedAction.parameters[paramIndex].value = app.name + } + + setSelectedAction(selectedAction) + setUpdate(Math.random()) + + }}> + + + +
+ ) + })} +
+ : null + : + + - - {selectedAction.template === true && selectedAction.matching_actions !== undefined && selectedAction.matching_actions !== null && selectedAction.matching_actions.length > 0 ? -
- - Select an app you want to use - + Parameters + + + } + {selectedAction.template === true && selectedAction.matching_actions !== undefined && selectedAction.matching_actions !== null && selectedAction.matching_actions.length > 0 ? +
+ + Select an app you want to use + {
) : null} + {selectedActionParameters.map((data, count) => { if (data.variant === "") { data.variant = "STATIC_VALUE"; } + if (isIntegration && data.name === "app_name") { + return null + } + // selectedAction.selectedAuthentication = e.target.value // selectedAction.authentication_id = e.target.value.id if ( @@ -1265,20 +1485,20 @@ const ParsedAction = (props) => { ); } - // Added autofill to make this ALOT simpler - if (isCloud && (selectedAction.app_name === "Shuffle Tools" || selectedAction.app_name === "email") && (selectedAction.name === "send_email_shuffle" || selectedAction.name === "send_sms_shuffle") && data.name === "apikey") { - if (selectedActionParameters[count].length === 0) { - selectedAction.parameters[count].value = "TMP: Will be replaced during execution if cloud" - setSelectedAction(selectedAction) - } + // Added autofill to make this ALOT simpler + if (isCloud && (selectedAction.app_name === "Shuffle Tools" || selectedAction.app_name === "email") && (selectedAction.name === "send_email_shuffle" || selectedAction.name === "send_sms_shuffle") && data.name === "apikey") { + if (selectedActionParameters[count].length === 0) { + selectedAction.parameters[count].value = "TMP: Will be replaced during execution if cloud" + setSelectedAction(selectedAction) + } - return null - } + return null + } var staticcolor = "inherit"; var actioncolor = "inherit"; var varcolor = "inherit"; - var multiline; + var multiline if ( data.multiline !== undefined && data.multiline !== null && @@ -1287,6 +1507,11 @@ const ParsedAction = (props) => { multiline = true; } + // make data.value from array to comma separated string if it is an array + if (data.value !== undefined && data.value !== null && Array.isArray(data.value)) { + data.value = data.value.join(",") + } + if ( data.value !== undefined && data.value !== null && @@ -1297,17 +1522,13 @@ const ParsedAction = (props) => { } var placeholder = "Value"; - if ( - data.example !== undefined && - data.example !== null && - data.example.length > 0 - ) { + if (data.example !== undefined && data.example !== null && data.example.length > 0) { + placeholder = data.example; if (data.name === "url" && data.value !== undefined && data.value !== null && data.value.length === 0) { data.value = data.example; } - // In case of data.example if (data.value === undefined || data.value === null) { data.value = "" @@ -1337,6 +1558,17 @@ const ParsedAction = (props) => { */ } + if (selectedAction.name === "custom_action" && data.name === "body") { + for (var key in selectedActionParameters) { + const param = selectedActionParameters[key] + if (param.name === "method") { + if (param.value === "GET") { + return null + } + } + } + } + if (data.name.startsWith("${") && data.name.endsWith("}")) { const paramcheck = selectedAction.parameters.find((param) => param.name === "body"); @@ -1358,6 +1590,13 @@ const ParsedAction = (props) => { } } + + var showCacheConfig = false + if (data.name === "key" && selectedAction.name.includes("cache") && selectedAction.app_name === "Shuffle Tools") { + // Show a key popout button + showCacheConfig = true + } + var disabled = false; var rows = "3"; var openApiHelperText = "This is an OpenAPI specific field"; @@ -1608,10 +1847,14 @@ const ParsedAction = (props) => { setExpansionModalOpen(true) //setcodedata(data.value) + var parsedvalue = data.value + if (parsedvalue === undefined || parsedvalue === null) { + parsedvalue = "" + } setEditorData({ "name": data.name, - "value": data.value, + "value": parsedvalue, "field_number": count, "actionlist": actionlist, "field_id": clickedFieldId, @@ -1642,15 +1885,12 @@ const ParsedAction = (props) => { ), }} multiline={data.name.startsWith("${") && data.name.endsWith("}") ? true : multiline} - helperText={returnHelperText(data.name, data.value)} onClick={() => { - console.log("Clicked field: ", clickedFieldId, data.name) /* setExpansionModalOpen(false); */ if (setScrollConfig !== undefined && scrollConfig !== null && scrollConfig !== undefined && scrollConfig.selected !== clickedFieldId) { - console.log("IN SCROLL CONFIG!") scrollConfig.selected = clickedFieldId setScrollConfig(scrollConfig) @@ -1682,18 +1922,7 @@ const ParsedAction = (props) => { //changeActionParameterCodemirror(event, count, data) changeActionParameter(event, count, data); }} - helperText={baseHelperText.length > 0 ? baseHelperText : - selectedApp.generated && - selectedApp.activated && - data.name === "body" ? ( - - {openApiHelperText} - - ) : data.name.startsWith("${") && data.name.endsWith("}") ? - null : null - } + helperText={returnHelperText(data.name, data.value)} onBlur={(event) => { baseHelperText = calculateHelpertext(event.target.value) if (setLastSaved !== undefined) { @@ -2000,6 +2229,13 @@ const ParsedAction = (props) => { changeActionParameter(e, count, data); } + var multi = false + if (selectedActionParameters[count].multiselect !== undefined && selectedActionParameters[count].multiselect !== null && selectedActionParameters[count].multiselect === true) { + multi = true + + selectedActionParameters[count].value = selectedActionParameters[count].value.split(",") + } + datafield = ( { - console.log("VAL: ", event.target.value) - console.log("App: ", selectedApp) const newversion = selectedApp.versions.find( (tmpApp) => tmpApp.version == event.target.value - ); + ) - console.log("NEWVERSION: ", newversion); if (newversion !== undefined && newversion !== null) { - getApp(newversion.id, true); + getApp(newversion.id, true) } + + // Change in all actions in the workflow at the same time and add a toast.success() about it + for (var actionkey in workflow.actions) { + const action = workflow.actions[actionkey] + if (action.app_name === selectedAction.app_name) { + workflow.actions[actionkey].app_version = event.target.value + } + } + + toast.success("Changed version of all nodes to "+event.target.value) }} style={{ marginTop: 10, @@ -3007,15 +3314,6 @@ const ParsedAction = (props) => { ) : null}
-
Name @@ -3228,7 +3526,6 @@ const ParsedAction = (props) => { } } - console.log("DID NAME REPLACE ACTUALLY WORK? - may be missing it in certain triggers"); setWorkflow(workflow); setUpdate(Math.random()); baselabel = name @@ -3265,10 +3562,10 @@ const ParsedAction = (props) => { )} {selectedApp.name !== undefined && - selectedAction.authentication !== null && - selectedAction.authentication !== undefined && - selectedAction.authentication.length === 0 && - requiresAuthentication ? ( + selectedAction.authentication !== null && + selectedAction.authentication !== undefined && + selectedAction.authentication.length === 0 && + requiresAuthentication ? (
{ fullWidth variant="contained" onClick={() => { - console.log(authenticationType); //if (authenticationType.type === "oauth2" && authenticationType.redirect_uri !== undefined && authenticationType.redirect_uri !== null) { // return null //} @@ -3368,6 +3664,8 @@ const ParsedAction = (props) => { style={{ backgroundColor: theme.palette.inputColor, color: "white", + maxWidth: 500, + overflowX: "auto", }} value={data} > @@ -3421,13 +3719,13 @@ const ParsedAction = (props) => {
) : null} - {showEnvironment !== undefined && showEnvironment && environments.length > 1 ? ( + {showEnvironment !== undefined && showEnvironment && environments.length > 1 && !isIntegration ? (
Environment {setSelectedAuth(event.target.value) + console.log("event.target.value: ",event.target.value) + console.log("Selected auth: ", selectedAuth) + }} + label="Available authentications" + required={true} + > + {(selectedAppDetails.auth_config && selectedAppDetails.auth_config.required == false || (selectedAppDetails.authentication && selectedAppDetails.authentication.required == false)) ? "No authentication required" : selectedAppDetails.authentication_data.map((option) => ( + +
+ + {option.label} +
+
))} + + } + + + + Provide additional required details: + + { + setTextFieldOneValue(e.target.value) + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style: { + color: "white", + }, + }} /> + { + setTextFieldValue(e.target.value) + }} + InputProps={{ + classes: { + notchedOutline: classes.notchedOutline, + }, + style: { + color: "white", + }, + }} /> + + : + <> + 0) ? false : true} + // // setAuthenticationModalOpen={false} + selectedApp={{...selectedAppDetails,authentication: selectedAppDetails.auth_config}} + // getAppAuthentication={selectedAppDetails.name} + /> + + } + + + + + + + +) : null + + return ( +
+ {modalView} + + {/*{testWorkflowModal} */} +
+ {renderChips(notificationAppDetails.length > 0 ? notificationAppDetails : emailData)} +
+
+ ) +} + +export default SubflowSuggestions; diff --git a/frontend/src/components/UsecaseSearch.jsx b/frontend/src/components/UsecaseSearch.jsx index bc7a6aad..474fcce8 100644 --- a/frontend/src/components/UsecaseSearch.jsx +++ b/frontend/src/components/UsecaseSearch.jsx @@ -1261,15 +1261,15 @@ const UsecaseSearch = (props) => { credentials: "include", }) .then((response) => { - if (response.status !== 200) { - console.log("Failed to activate") - } - return response.json() }) .then((responseJson) => { if (responseJson.success === false) { - toast("Failed to activate the app") + var msgString = "Failed to activate the app" + if (responseJson.reason !== undefined) { + msgString += ": " + responseJson.reason + } + toast(msgString) } else { //toast("App activated for your organization! Refresh the page to use the app.") } diff --git a/frontend/src/components/WorkflowTemplatePopup.jsx b/frontend/src/components/WorkflowTemplatePopup.jsx index 6d3413dc..13a6735f 100644 --- a/frontend/src/components/WorkflowTemplatePopup.jsx +++ b/frontend/src/components/WorkflowTemplatePopup.jsx @@ -5,6 +5,7 @@ import theme from '../theme.jsx'; import { useNavigate, Link, useParams } from "react-router-dom"; import AppSearchButtons from "../components/AppSearchButtons.jsx"; import { isMobile } from "react-device-detect"; +import RenderCytoscape from "../components/RenderCytoscape.jsx"; import { Button, Typography, @@ -29,7 +30,7 @@ import WorkflowTemplatePopup2 from "./WorkflowTemplatePopup.jsx"; import ConfigureWorkflow from "../components/ConfigureWorkflow.jsx"; const WorkflowTemplatePopup = (props) => { - const { userdata, appFramework, globalUrl, img1, srcapp, img2, dstapp, title, description, visualOnly, apps, isLoggedIn, isHomePage } = props; + const { userdata, appFramework, globalUrl, img1, srcapp, img2, dstapp, title, description, visualOnly, apps, isLoggedIn, isHomePage, getAppFramework, } = props; const [isActive, setIsActive] = useState(false); const [isHovered, setIsHovered] = useState(false); @@ -39,17 +40,51 @@ const WorkflowTemplatePopup = (props) => { const [workflow, setWorkflow] = useState({}); const [showLoginButton, setShowLoginButton] = useState(false); const [appAuthentication, setAppAuthentication] = React.useState(undefined); - const [missingSource, setMissingSource] = React.useState(undefined) const [missingDestination, setMissingDestination] = React.useState(undefined); + const [configurationFinished, setConfigurationFinished] = React.useState(false) + const [appSetupDone, setAppSetupDone] = React.useState(false) - const [configurationFinished, setConfigurationFinished] = React.useState(false); + const [requestSent, setRequestSent] = React.useState(false) + + const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; + let navigate = useNavigate(); + useEffect(() => { + if (modalOpen !== true) { + return + } + + if (!srcapp.includes(":default") && !dstapp.includes(":default")) { + if (appSetupDone === false && setAppSetupDone !== undefined) { + setAppSetupDone(true) + } + + getGeneratedWorkflow() + } + + if (missingSource !== undefined && missingDestination !== undefined) { + if (appSetupDone === false && setAppSetupDone !== undefined) { + setAppSetupDone(true) + } + } + + if (getAppFramework !== undefined) { + setTimeout(() => { + getAppFramework() + }, 500) + } + }, [modalOpen, missingSource, missingDestination]) useEffect(() => { - }, [missingSource, missingDestination]) + //console.log("IN USEEFFECT FOR CONFIG: ", configurationFinished) + if (configurationFinished === true && workflow.id !== undefined && workflow.id !== null && workflow.id !== "") { + toast.success("Generation Successful. Redirecting to the workflow..") + setTimeout(() => { + navigate("/workflows/" + workflow.id) + }, 2000) + } + }, [configurationFinished, workflow]) - const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; - let navigate = useNavigate(); const imagestyleWrapper = { height: 40, @@ -272,18 +307,21 @@ const WorkflowTemplatePopup = (props) => { // middle:[] // name: "Email analysis" // source:{app_id: "accdaaf2eeba6a6ed43b2efc0112032d", app_name + if (requestSent === true) { + return + } if (srcapp.includes(":default") || dstapp.includes(":default")) { toast("You need to select both a source and destination app before generating this workflow.") - if (srcapp.includes(":default")) { + if (srcapp !== undefined && srcapp !== null && srcapp.includes(":default")) { setMissingSource({ "type": srcapp.split(":")[0], }) } - if (dstapp.includes(":default")) { + if (dstapp !== undefined && dstapp !== null && dstapp.includes(":default")) { setMissingDestination({ "type": dstapp.split(":")[0], }) @@ -309,9 +347,8 @@ const WorkflowTemplatePopup = (props) => { }, } + setRequestSent(true) const url = isCloud ? `${globalUrl}/api/v1/workflows/merge` : `https://shuffler.io/api/v1/workflows/merge` - //const url = `https://shuffler.io/api/v1/workflows/merge` - fetch(url, { method: "POST", headers: { @@ -324,6 +361,7 @@ const WorkflowTemplatePopup = (props) => { .then((response) => { if (response.status !== 200) { //console.log("Status not 200 for framework!"); + setRequestSent(false) } setWorkflowLoading(false) @@ -341,6 +379,7 @@ const WorkflowTemplatePopup = (props) => { if (responseJson.success === false) { //console.log("Error in workflow template: ", responseJson.error); + setRequestSent(false) const defaultMessage = "Failed to generate workflow the workflow - the Shuffle team has been notified. Contact support@shuffler.io for further assistance." if (responseJson.reason !== undefined && responseJson.reason !== null && responseJson.reason !== "") { @@ -366,10 +405,21 @@ const WorkflowTemplatePopup = (props) => { }) .catch((error) => { console.log("err in framework: ", error.toString()); + setRequestSent(false) setWorkflowLoading(false) }) } + if (modalOpen === true && !srcapp.includes(":default") && !dstapp.includes(":default")) { + if (appSetupDone === false && setAppSetupDone !== undefined) { + setAppSetupDone(true) + } + + if (workflow.id === undefined && workflowLoading === false && errorMessage === "") { + getGeneratedWorkflow() + } + } + const isFinished = () => { // Look for configuration fields being done in the current modal // 1. Start by finding the modal @@ -388,6 +438,13 @@ const WorkflowTemplatePopup = (props) => { } const ModalView = () => { + if (modalOpen === false) { + return null + } + + const divHeight = 500 + const divWidth = 500 + return ( {
Generating the Workflow... - +
:
@@ -475,9 +532,9 @@ const WorkflowTemplatePopup = (props) => {
} - {(missingSource !== undefined || missingDestination !== undefined) ? + {(appSetupDone === false && missingSource !== undefined || missingDestination !== undefined) ? - {"Find relevevant Apps for this Usecase"} + {"Find relevant Apps for this Usecase"} : null} @@ -491,6 +548,8 @@ const WorkflowTemplatePopup = (props) => { AppImage={missingSource.image} setMissing={setMissingSource} + + getAppFramework={getAppFramework} />
: null} @@ -505,6 +564,8 @@ const WorkflowTemplatePopup = (props) => { AppImage={missingDestination.image} setMissing={setMissingDestination} + + getAppFramework={getAppFramework} />
: null} @@ -521,28 +582,22 @@ const WorkflowTemplatePopup = (props) => { setConfigurationFinished={setConfigurationFinished} /> - {errorMessage === "" && configurationFinished === true && workflow.id !== undefined && workflowLoading === false ? - - - {/* */} - - Workflow generated! + {/*errorMessage === "" && configurationFinished === true && workflow.id !== undefined && workflowLoading === false ? + + { + // Open in new tab + window.open("/workflows/" + workflow.id, "_blank") + }} + > + + Workflow Successfully Generated! - : null} - - {/*errorMessage === "" ? - : null*/} + ) @@ -648,6 +703,7 @@ const WorkflowTemplatePopup = (props) => { : ""}
+
diff --git a/frontend/src/defaultCytoscapeStyle.jsx b/frontend/src/defaultCytoscapeStyle.jsx index 7e7132fd..d36c8e8d 100644 --- a/frontend/src/defaultCytoscapeStyle.jsx +++ b/frontend/src/defaultCytoscapeStyle.jsx @@ -2,7 +2,16 @@ const data = [ { selector: "node", css: { - label: "data(label)", + label: function(element) { + var elementname = element.data("label") + if (elementname === null || elementname === undefined) { + return "" + } + + elementname = elementname.replaceAll("_", " ", -1) + elementname = elementname.charAt(0).toUpperCase() + elementname.slice(1) + return elementname + }, "text-valign": "center", "font-family": "Segoe UI, Tahoma, Geneva, Verdana, sans-serif, sans-serif", "font-weight": "lighter", @@ -34,6 +43,20 @@ const data = [ "z-index": 5001, }, }, + { + selector: `node[buttonType="ACTIONSUGGESTION"]`, + css: { + label: "data(label)", + shape: "roundrectangle", + "height": "18px", + "width": "145px", + "background-color": "#212121", + "border-color": "#81c784", + "z-index": 10000, + "border-radius": "10px", + "text-margin-x": "0px", + }, + }, { selector: `node[type="ACTION"]`, css: { @@ -65,6 +88,36 @@ const data = [ "text-wrap": "wrap", }, }, + { + selector: `node[app_name="Integration Framework"]`, + css: { + width: "60px", + height: "60px", + "z-index": 5000, + + //'border-width': 3, + //'border-color': 'transparent', + //'border-style': 'solid', + //'border-gradient': 'linear-gradient(to right, #FF0000, #FF7F00, #FFFF00, #00FF00, #0000FF, #4B0082, #8A2BE2)' + }, + }, + { + selector: `node[example="noapp"]`, + css: { + // Make background image padding on the left side 20px + "background-width": "75%", + "background-height": "75%", + "background-position-x": "17px", + "background-position-y": "17px", + + "background-color": "data(iconBackground)", + "background-fill": "data(fillstyle)", + "background-gradient-direction": "to-bottom-right", + "background-gradient-stop-colors": "data(fillGradient)", + // Change transparency of background + "background-opacity": "0.3", + }, + }, { selector: `node[app_name="Shuffle Tools"]`, css: { @@ -120,7 +173,7 @@ const data = [ { selector: `node[type="TRIGGER"]`, css: { - shape: "octagon", + shape: "round-octagon", "border-radius": "5px", "border-color": "orange", "background-color": "#213243", diff --git a/frontend/src/index.css b/frontend/src/index.css index 4188b885..9b1fc96c 100755 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -18,6 +18,11 @@ code { margin-right: -13px; } +.toc:hover { + background-color: gray; + color: white; +} + /* .cm-string{ z-index: -1; } @@ -30,4 +35,17 @@ code { .CodeMirror-selectedtext{ background-color: rgba(28, 47, 69, 0.6) !important; color: rgb(255, 255, 255) !important; -} \ No newline at end of file +} + +::-webkit-scrollbar { + width: 8px; +} + +::-webkit-scrollbar-thumb { + background-color: #494949; + border-radius: 5px; +} + +::-webkit-scrollbar-track { + background-color: rgb(26,26,26); +} diff --git a/frontend/src/theme.jsx b/frontend/src/theme.jsx index 37b32b4d..6e66e28c 100644 --- a/frontend/src/theme.jsx +++ b/frontend/src/theme.jsx @@ -24,14 +24,24 @@ const theme = createTheme(adaptV4Theme({ platformColor: "#1c1c1d", backgroundColor: "#1a1a1a", green: "#5cc879", - borderRadius: 5, + borderRadius: 10, defaultBorder: "1px solid rgba(255,255,255,0.3)", - jsonTheme: "brewer", + + //jsonTheme: "brewer", + //jsonTheme: "chalk", + //jsonTheme: "monokai", + //jsonTheme: "google", + //jsonTheme: "tomorrow", + jsonIconStyle: "round", + jsonTheme: "summerfruit", + jsonCollapseStringsAfterLength: 100, + reactJsonStyle: { + padding: 5, + width: "98%", borderRadius: 5, border: "1px solid rgba(255,255,255,0.7)", - padding: 5, - width: "100%", + overflowX: "auto", }, textFieldStyle: { backgroundColor: "#383B40", @@ -52,11 +62,10 @@ const theme = createTheme(adaptV4Theme({ boxShadow: 1, fontSize: 11, }, - defaultImage: - "data:image/png;base64,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", + defaultImage: "/images/no_image.png", }, typography: { - fontFamily: `"Roboto", "Helvetica", "Arial", sans-serif`, + fontFamily: `"Roboto", "Helvetica", "Arial", "inter", sans-serif`, useNextVariants: true, h1: { fontSize: 40, diff --git a/frontend/src/views/Admin.jsx b/frontend/src/views/Admin.jsx index 1bacd611..aab163de 100755 --- a/frontend/src/views/Admin.jsx +++ b/frontend/src/views/Admin.jsx @@ -5,8 +5,6 @@ import { makeStyles } from "@mui/styles"; import { useNavigate, Link } from "react-router-dom"; import countries from "../components/Countries.jsx"; -import CodeEditor from "../components/ShuffleCodeEditor.jsx"; -import getLocalCodeData from "../components/ShuffleCodeEditor.jsx"; import CacheView from "../components/CacheView.jsx"; import { @@ -45,7 +43,7 @@ import { CircularProgress, Box, InputAdornment, - Autocomplete + Autocomplete, } from "@mui/material"; import { @@ -72,13 +70,15 @@ import { Business as BusinessIcon, Visibility as VisibilityIcon, VisibilityOff as VisibilityOffIcon, - + Cancel as CancelIcon, + Dns as DnsIcon, + Help as HelpIcon, Flag as FlagIcon, FmdGood as FmdGoodIcon, } from "@mui/icons-material"; //import { useAlert -import { ToastContainer, toast } from "react-toastify" +import { ToastContainer, toast } from "react-toastify"; import Dropzone from "../components/Dropzone.jsx"; import HandlePaymentNew from "../views/HandlePaymentNew.jsx"; import OrgHeader from "../components/OrgHeader.jsx"; @@ -107,39 +107,50 @@ const MenuProps = { getContentAnchorEl: () => null, }; - const FileCategoryInput = (props) => { const isSet = props.isSet; - console.log("inside filecategoryinput"); - console.log("isset value" , isSet); - if (isSet){ + console.log("inside filecategoryinput"); + console.log("isset value", isSet); + if (isSet) { return ( - - )} + + ); } - +}; const Admin = (props) => { - const { globalUrl, userdata, serverside, checkLogin, notifications, setNotifications, } = props; + const { + globalUrl, + userdata, + serverside, + checkLogin, + notifications, + setNotifications, + } = props; var to_be_copied = ""; const classes = useStyles(); let navigate = useNavigate(); + const [logsViewModal, setLogsViewModal] = React.useState(false); + const [userLogViewing, setUserLogViewing] = React.useState({}); + const [ipSelected, setIpSelected] = React.useState(""); + const [logsLoading, setLogsLoading] = React.useState(true); + const [logs, setLogs] = React.useState([]); const [firstRequest, setFirstRequest] = React.useState(true); const [orgRequest, setOrgRequest] = React.useState(true); const [modalUser, setModalUser] = React.useState({}); @@ -151,12 +162,19 @@ const Admin = (props) => { const [loading, setLoading] = React.useState(false); const [selectedOrganization, setSelectedOrganization] = React.useState({}); - + //console.log("Selected: ", selectedOrganization) + const [appAuthenticationGroupModalOpen , setAppAuthenticationGroupModalOpen] = React.useState(false); + const [appsForAppAuthGroup, setAppsForAppAuthGroup] = React.useState([]); + const [appAuthenticationGroupName, setAppAuthenticationGroupName] = React.useState(""); + const [appAuthenticationGroupDescription, setAppAuthenticationGroupDescription] = React.useState(""); + const [appAuthenticationGroups, setAppAuthenticationGroups] = React.useState([]); const [organizationFeatures, setOrganizationFeatures] = React.useState({}); const [loginInfo, setLoginInfo] = React.useState(""); const [curTab, setCurTab] = React.useState(0); const [users, setUsers] = React.useState([]); + const [subOrgs, setSubOrgs] = useState([]); + const [parentOrg, setParentOrg] = React.useState(null); const [organizations, setOrganizations] = React.useState([]); const [orgSyncResponse, setOrgSyncResponse] = React.useState(""); const [userSettings, setUserSettings] = React.useState({}); @@ -168,9 +186,13 @@ const Admin = (props) => { const [selectedUser, setSelectedUser] = React.useState({}); const [newUsername, setNewUsername] = React.useState(""); const [newPassword, setNewPassword] = React.useState(""); - const [selectedUserModalOpen, setSelectedUserModalOpen] = React.useState(false); - const [selectedAuthentication, setSelectedAuthentication] = React.useState({}); - const [selectedAuthenticationModalOpen, setSelectedAuthenticationModalOpen] = React.useState(false); + const [selectedUserModalOpen, setSelectedUserModalOpen] = + React.useState(false); + const [selectedAuthentication, setSelectedAuthentication] = React.useState( + {}, + ); + const [selectedAuthenticationModalOpen, setSelectedAuthenticationModalOpen] = + React.useState(false); const [authenticationFields, setAuthenticationFields] = React.useState([]); const [showArchived, setShowArchived] = React.useState(false); const [isDropzone, setIsDropzone] = React.useState(false); @@ -184,13 +206,48 @@ const Admin = (props) => { const [showApiKey, setShowApiKey] = useState(false); const [billingInfo, setBillingInfo] = React.useState({}); const [selectedStatus, setSelectedStatus] = React.useState([]); - + const [webHooks, setWebHooks] = React.useState([]); + const [allSchedules, setAllSchedules] = React.useState([]); + const [pipelines, setPipelines] = React.useState([]); const [, forceUpdate] = React.useState(); + const [showDeleteAccountTextbox, setShowDeleteAccountTextbox] = + React.useState(false); + const [deleteAccountText, setDeleteAccountText] = React.useState(""); + const [regionChangeModalOpen, setRegionChangeModalOpen] = + React.useState(false); + useEffect(() => { - getUsers() + getUsers(); + + setTimeout(() => { + const urlSearchParams = new URLSearchParams(window.location.search); + const params = Object.fromEntries(urlSearchParams.entries()); + const foundTab = params["admin_tab"] + if (foundTab !== null && foundTab !== undefined) { + if (adminTab === 3) { + window.scroll({ + top: 450, + left: 0, + behavior: "smooth", + }); + } + } + }, 1500); }, []); + useEffect(() => { + const urlSearchParams = new URLSearchParams(window.location.search); + const params = Object.fromEntries(urlSearchParams.entries()); + const foundTab = params["admin_tab"] + if (foundTab !== null && foundTab !== undefined) { + window.scroll({ + top: 450, + left: 0, + behavior: "smooth", + }) + } + }, [adminTab]); useEffect(() => { if (isDropzone) { @@ -199,7 +256,117 @@ const Admin = (props) => { } }, [isDropzone]); - const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; + useEffect(() => { + if ( + userdata.orgs !== undefined && + userdata.orgs !== null && + userdata.orgs.length > 0 + ) { + handleGetSubOrgs(userdata.active_org.id); + } else console.log("error in user data"); + }, [userdata]); + + useEffect(() => { + handleGetAllTriggers() + }, []); + + const isCloud = + window.location.host === "localhost:3002" || + window.location.host === "shuffler.io"; + + const setSelectedRegion = (region) => { + // send a POST request to /api/v1/orgs/{org_id}/region with the region as the body + var data = { + dst_region: region, + }; + + toast("Changing region to " + region + "...This may take a few minutes."); + + fetch(`${globalUrl}/api/v1/orgs/${selectedOrganization.id}/change/region`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + body: JSON.stringify(data), + timeOut: 1000, + }).then((response) => { + if (response.status !== 200) { + toast("Failed to change region!"); + } else { + toast("Region changed successfully! Reloading in 5 seconds.."); + // Reload the page in 2 seconds + setTimeout(() => { + window.location.reload(); + }, 5000); + } + + return response.json(); + }); + }; + + const RegionChangeModal = () => { + // Show from options: "us-west2", "europe-west2", "europe-west3", "northamerica-northeast1" + var regions = [ + "us-west2", + "europe-west2", + "europe-west3", + "northamerica-northeast1", + ]; + return ( + setRegionChangeModalOpen(false)} + aria-labelledby="alert-dialog-title" + aria-describedby="alert-dialog-description" + > + Change region + + + Region + + + + + + + + ); + }; const get2faCode = (userId) => { fetch(`${globalUrl}/api/v1/users/${userId}/get2fa`, { @@ -264,6 +431,40 @@ const Admin = (props) => { }); }; + const createAppAuthenticationGroup = (name, description, appAuthIds) => { + let app_auths = appAuthIds.map((appAuthId) => { + return { id: appAuthId }; + }) + + fetch(globalUrl + "/api/v1/apps/authentication/group", { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + body: JSON.stringify({ + label: name, + description: description, + app_auths: app_auths + }), + }) + .then((response) => { + if (response.status !== 200) { + throw new Error("Failed to create app authentication group"); + } + + return response.json(); + }) + .then((responseJson) => { + // getAppAuthenticationGroups(); + toast("App authentication group created"); + }) + .catch((error) => { + toast(error.toString()); + }); + }; + const categories = [ { name: "Ticketing", @@ -291,190 +492,249 @@ const Admin = (props) => { ] */ - //const alert = useAlert(); - const handleStatusChange = (event) => { - const { value } = event.target; - setSelectedStatus(value); + //const alert = useAlert(); + const handleStatusChange = (event) => { + const { value } = event.target; + setSelectedStatus(value); - - handleEditOrg( - "", - "", - selectedOrganization.id, - "", - {}, - {}, - value.length === 0 ? ["none"] : value, - ) - } + handleEditOrg( + "", + "", + selectedOrganization.id, + "", + {}, + {}, + value.length === 0 ? ["none"] : value, + ); + }; - // Basically just a simple way to get a generated email - // This also may help understand how to communicate with users - // both inside and outside Shuffle - // This could also be generated on the backend - const mailsendingButton = (org) => { - if (org === undefined || org === null) { - return "" - } + // Basically just a simple way to get a generated email + // This also may help understand how to communicate with users + // both inside and outside Shuffle + // This could also be generated on the backend + const mailsendingButton = (org) => { + if (org === undefined || org === null) { + return ""; + } - if (users.length === 0) { - return "" - } + if (users.length === 0) { + return ""; + } - // 1 mail based on users that have only apps - // Another based on those doing workflows - // Another based on those trying usecases(?) or templates - // - // Start based on edr, siem & ticketing - // Talk about enrichment? - // Check suggested usecases - // Check suggested workflows - var your_apps = "- Connecting " + // 1 mail based on users that have only apps + // Another based on those doing workflows + // Another based on those trying usecases(?) or templates + // + // Start based on edr, siem & ticketing + // Talk about enrichment? + // Check suggested usecases + // Check suggested workflows + var your_apps = "- Connecting "; - var subject_add = 0 - var subject = "POC to automate " + var subject_add = 0; + var subject = "POC to automate "; - if (org.security_framework !== undefined && org.security_framework !== null) { - if (org.security_framework.cases.name !== undefined && org.security_framework.cases.name !== null && org.security_framework.cases.name !== "") { - your_apps += org.security_framework.cases.name.replace("_", " ", -1).replace(" API", "", -1) + ", " + if ( + org.security_framework !== undefined && + org.security_framework !== null + ) { + if ( + org.security_framework.cases.name !== undefined && + org.security_framework.cases.name !== null && + org.security_framework.cases.name !== "" + ) { + your_apps += + org.security_framework.cases.name + .replace("_", " ", -1) + .replace(" API", "", -1) + ", "; - if (subject_add < 2) { - if (subject_add === 1) { - subject += " and " - } + if (subject_add < 2) { + if (subject_add === 1) { + subject += " and "; + } - subject_add += 1 - subject += org.security_framework.cases.name.replace("_", " ", -1).replace(" API", "", -1) - } - } + subject_add += 1; + subject += org.security_framework.cases.name + .replace("_", " ", -1) + .replace(" API", "", -1); + } + } - if (org.security_framework.siem.name !== undefined && org.security_framework.siem.name !== null && org.security_framework.siem.name !== "") { - your_apps += org.security_framework.siem.name.replace("_", " ", -1).replace(" API", "", -1) + ", " - if (subject_add < 2) { - if (subject_add === 1) { - subject += " and " - } + if ( + org.security_framework.siem.name !== undefined && + org.security_framework.siem.name !== null && + org.security_framework.siem.name !== "" + ) { + your_apps += + org.security_framework.siem.name + .replace("_", " ", -1) + .replace(" API", "", -1) + ", "; + if (subject_add < 2) { + if (subject_add === 1) { + subject += " and "; + } - subject_add += 1 - subject += org.security_framework.siem.name.replace("_", " ", -1).replace(" API", "", -1) - } - } + subject_add += 1; + subject += org.security_framework.siem.name + .replace("_", " ", -1) + .replace(" API", "", -1); + } + } - if (org.security_framework.communication.name !== undefined && org.security_framework.communication.name !== null && org.security_framework.communication.name !== "") { - your_apps += org.security_framework.communication.name.replace("_", " ", -1).replace(" API", "", -1) + ", " + if ( + org.security_framework.communication.name !== undefined && + org.security_framework.communication.name !== null && + org.security_framework.communication.name !== "" + ) { + your_apps += + org.security_framework.communication.name + .replace("_", " ", -1) + .replace(" API", "", -1) + ", "; - if (subject_add < 2) { - if (subject_add === 1) { - subject += " and " - } + if (subject_add < 2) { + if (subject_add === 1) { + subject += " and "; + } - subject_add += 1 - subject += org.security_framework.communication.name.replace("_", " ", -1).replace(" API", "", -1) - } - } + subject_add += 1; + subject += org.security_framework.communication.name + .replace("_", " ", -1) + .replace(" API", "", -1); + } + } - if (org.security_framework.edr.name !== undefined && org.security_framework.edr.name !== null && org.security_framework.edr.name !== "") { - your_apps += org.security_framework.edr.name.replace("_", " ", -1).replace(" API", "", -1) + ", " + if ( + org.security_framework.edr.name !== undefined && + org.security_framework.edr.name !== null && + org.security_framework.edr.name !== "" + ) { + your_apps += + org.security_framework.edr.name + .replace("_", " ", -1) + .replace(" API", "", -1) + ", "; - if (subject_add < 2) { - if (subject_add === 1) { - subject += " and " - } + if (subject_add < 2) { + if (subject_add === 1) { + subject += " and "; + } - subject_add += 1 - subject += org.security_framework.edr.name.replace("_", " ", -1).replace(" API", "", -1) - } - } + subject_add += 1; + subject += org.security_framework.edr.name + .replace("_", " ", -1) + .replace(" API", "", -1); + } + } - if (org.security_framework.intel.name !== undefined && org.security_framework.intel.name !== null && org.security_framework.intel.name !== "") { - your_apps += org.security_framework.intel.name.replace("_", " ", -1).replace(" API", "", -1) + ", " + if ( + org.security_framework.intel.name !== undefined && + org.security_framework.intel.name !== null && + org.security_framework.intel.name !== "" + ) { + your_apps += + org.security_framework.intel.name + .replace("_", " ", -1) + .replace(" API", "", -1) + ", "; - if (subject_add < 2) { - if (subject_add === 1) { - subject += " and " - } + if (subject_add < 2) { + if (subject_add === 1) { + subject += " and "; + } - subject_add += 1 - subject += org.security_framework.intel.name.replace("_", " ", -1).replace(" API", "", -1) - } - } + subject_add += 1; + subject += org.security_framework.intel.name + .replace("_", " ", -1) + .replace(" API", "", -1); + } + } + // Remove comma + //subject += "?" + your_apps = your_apps.substring(0, your_apps.length - 2); + } - // Remove comma - //subject += "?" - your_apps = your_apps.substring(0, your_apps.length - 2) - } + // Add usecases they may not have tried (from recommendations): org.priorities where item type is usecase + var usecases = "- Building usecases like "; + const active_usecase = org.priorities.filter( + (item) => item.type === "usecase" && item.active === true, + ); + if (active_usecase.length > 0) { + for (var i = 0; i < active_usecase.length; i++) { + if (active_usecase[i].name.includes("Suggested Usecase: ")) { + usecases += + active_usecase[i].name.replace("Suggested Usecase: ", "", -1) + + ", "; + } else { + usecases += active_usecase[i].name + ", "; + } + } + usecases = usecases.substring(0, usecases.length - 2); + } - // Add usecases they may not have tried (from recommendations): org.priorities where item type is usecase - var usecases = "- Building usecases like " - const active_usecase = org.priorities.filter((item) => item.type === "usecase" && item.active === true) - if (active_usecase.length > 0) { - for (var i = 0; i < active_usecase.length; i++) { - if (active_usecase[i].name.includes("Suggested Usecase: ")) { - usecases += active_usecase[i].name.replace("Suggested Usecase: ", "", -1) + ", " - } else { - usecases += active_usecase[i].name + ", " - } - } + if (your_apps.length <= 15) { + your_apps = ""; + } - usecases = usecases.substring(0, usecases.length - 2) - } + if (usecases.length <= 30) { + usecases = ""; + } - if (your_apps.length <= 15) { - your_apps = "" - } + var workflow_amount = "a few"; + var admins = ""; - if (usecases.length <= 30) { - usecases = "" - } + // Loop users + var lastLogin = 0; + for (var i = 0; i < users.length; i++) { + if (users[i].username.includes("shuffler")) { + continue; + } - var workflow_amount = "a few" - var admins = "" + if (users[i].role === "admin") { + admins += users[i].username + ","; + } - // Loop users - var lastLogin = 0 - for (var i = 0; i < users.length; i++) { - if (users[i].username.includes("shuffler")) { - continue - } + const data = users[i]; + for (var i = 0; i < data.login_info.length; i++) { + if (data.login_info[i].timestamp > lastLogin) { + lastLogin = data.login_info[i].timestamp; + } + } + } - if (users[i].role === "admin") { - admins += users[i].username + "," - } + // Remove last comma + admins = admins.substring(0, admins.length - 1); - const data = users[i] - for (var i = 0; i < data.login_info.length; i++) { - if (data.login_info[i].timestamp > lastLogin) { - lastLogin = data.login_info[i].timestamp - } - } - } + if (your_apps.length > 5) { + your_apps += "%0D%0A"; + } + if (usecases.length > 5) { + usecases += "%0D%0A"; + } - // Remove last comma - admins = admins.substring(0, admins.length - 1) + // Get drift username from userdata.username before @ in email + const username = userdata.username.substring( + 0, + userdata.username.indexOf("@"), + ); - if (your_apps.length > 5) { - your_apps += "%0D%0A" - } + // Check if timestamp is more than 2 weeks ago and add "a while back" to the message + const timeComparison = 1209600; + const extra_timestamp_text = + lastLogin === 0 + ? 0 + : Date.now() / 1000 - lastLogin > timeComparison + ? " a while back" + : ""; + console.log("LAST LOGIN: " + lastLogin, extra_timestamp_text); - if (usecases.length > 5) { - usecases += "%0D%0A" - } + // Check if cloud sync is active, and if so, add a message about it + const cloudSyncInfo = + selectedOrganization.cloud_sync === true + ? "- Scale your onprem installation" + : ""; - // Get drift username from userdata.username before @ in email - const username = userdata.username.substring(0, userdata.username.indexOf("@")) - - // Check if timestamp is more than 2 weeks ago and add "a while back" to the message - const timeComparison = 1209600 - const extra_timestamp_text = lastLogin === 0 ? 0 : (Date.now()/1000 - lastLogin) > timeComparison ? " a while back" : "" - console.log("LAST LOGIN: " + lastLogin, extra_timestamp_text) - - // Check if cloud sync is active, and if so, add a message about it - const cloudSyncInfo = selectedOrganization.cloud_sync === true ? "- Scale your onprem installation" : "" - - var body = `Hey,%0D%0A%0D%0AI noticed you tried to use Shuffle${extra_timestamp_text}, and thought you may be interested in a POC. It looks like you have ${workflow_amount} workflows made, but it still doesn't look like you are getting what you wanted out of Shuffle. If you're interested, I'd love to set up a quick call to see if we can help you get more out of Shuffle. %0D%0A%0D%0A + var body = `Hey,%0D%0A%0D%0AI noticed you tried to use Shuffle${extra_timestamp_text}, and thought you may be interested in a POC. It looks like you have ${workflow_amount} workflows made, but it still doesn't look like you are getting what you wanted out of Shuffle. If you're interested, I'd love to set up a quick call to see if we can help you get more out of Shuffle. %0D%0A%0D%0A Some of the things we can help with:%0D%0A ${your_apps} @@ -483,18 +743,17 @@ ${usecases} - Multi-Tenancy and creating special usecases%0D%0A ${cloudSyncInfo}%0D%0A -If you're interested, please let me know a time that works for you, or set up a call here: https://drift.me/${username}` - - return `mailto:${admins}?bcc=frikky@shuffler.io,binu@shuffler.io&subject=${subject}&body=${body}` - } +If you're interested, please let me know a time that works for you, or set up a call here: https://drift.me/${username}`; + 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") - } + //changeDistributed(data, !isDistributed) + console.log("Should change distribution to be shared among suborgs"); + + editAuthenticationConfig(data.id, "suborg_distribute"); + }; const deleteAuthentication = (data) => { toast("Deleting auth " + data.label); @@ -520,13 +779,39 @@ If you're interested, please let me know a time that works for you, or set up a }, 1000); //toast("Successfully deleted authentication!") } - }) + }), ) .catch((error) => { console.log("Error in userdata: ", error); }); }; + const handleGetAllTriggers = () => { + fetch(globalUrl + "/api/v1/triggers", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for getting all triggers"); + } + + return response.json(); + }) + .then((responseJson) => { + setWebHooks(responseJson.webhooks || []); // Handling the case where the result is null or undefined + setAllSchedules(responseJson.schedules || []); + // setPipelines(responseJson.pipelines || []); + }) + .catch((error) => { + // toast(error.toString()); + }); + }; + const deleteSchedule = (data) => { // FIXME - add some check here ROFL console.log("INPUT: ", data); @@ -552,24 +837,212 @@ If you're interested, please let me know a time that works for you, or set up a if (responseJson["success"] === false) { toast("Failed stopping schedule"); } else { - setTimeout(() => { - getSchedules(); - }, 1500); - //toast("Successfully stopped schedule!") + toast("Successfully stopped schedule!"); } - }) + + setTimeout(handleGetAllTriggers, 1000); + + }), ) .catch((error) => { console.log("Error in userdata: ", error); }); }; + const startSchedule = (trigger) => { + if (trigger.name.length <= 0) { + toast("Error: name can't be empty"); + return; + } - if (userdata.support === true && selectedOrganization.id !== "" && selectedOrganization.id !== undefined && selectedOrganization.id !== null && selectedOrganization.id !== userdata.active_org.id) { - toast("Refreshing window to fix org support access") - window.location.reload() - return null - } + toast("Creating schedule"); + const data = { + name: trigger.name, + frequency: trigger.frequency, + execution_argument: trigger.argument, + environment: trigger.environment, + id: trigger.id, + start: trigger.start_node, + }; + + fetch(`${globalUrl}/api/v1/workflows/${trigger.workflow_id}/schedule`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(data), + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for stream results :O!"); + } + + return response.json(); + }) + .then((responseJson) => { + if (!responseJson.success) { + toast("Failed to set schedule: " + responseJson.reason); + } else { + toast("Successfully created schedule"); + } + setTimeout(handleGetAllTriggers, 1000); + }) + .catch((error) => { + //toast(error.toString()); + console.log("Get schedule error: ", error.toString()); + }); + }; + + const deleteWebhook = (trigger) => { + if (trigger === undefined) { + return; + } + + fetch(globalUrl + "/api/v1/hooks/" + trigger.id + "/delete", { + method: "DELETE", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for stream results :O!"); + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success) { + toast("Successfully stopped webhook"); + } else { + if (responseJson.reason !== undefined) { + toast("Failed stopping webhook: " + responseJson.reason); + } + } + setTimeout(handleGetAllTriggers, 1000); + }) + .catch((error) => { + toast( + "Delete webhook error. Contact support or check logs if this persists.", + ); + }); + }; + + const startWebHook = (trigger) => { + const hookname = trigger.info.name; + if (hookname.length === 0) { + toast("Missing name"); + return; + } + + if (trigger.id.length !== 36) { + toast("Missing id"); + return; + } + + toast("Starting webhook"); + + const data = { + name: hookname, + type: "webhook", + id: trigger.id, + workflow: trigger.workflows[0], + start: trigger.start, + environment: trigger.environment, + auth: trigger.auth, + custom_response: trigger.custom_response, + version: trigger.version, + version_timeout: 15, + }; + + console.log("Trigger data: ", data); + + fetch(globalUrl + "/api/v1/hooks/new", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(data), + credentials: "include", + }) + .then((response) => response.json()) + .then((responseJson) => { + if (responseJson.success) { + // Set the status + toast("Successfully started webhook"); + } else { + toast("Failed starting webhook: " + responseJson.reason); + } + setTimeout(handleGetAllTriggers, 1000); + }) + .catch((error) => { + //console.log(error.toString()); + console.log("New webhook error: ", error.toString()); + }); + }; + + const changePipelineState = (pipeline, state) => { + if (state.trim() === "") { + toast("state is not defined"); + return; + } + + const data = { + name: pipeline.name, + type: state, + environment: pipeline.environment, + workflow_id: pipeline.workflow_id, + trigger_id: pipeline.trigger_id, + }; + + if (state === "start") toast("starting the pipeline"); + else toast("stopping the pipeline"); + + const url = `${globalUrl}/api/v1/triggers/pipeline`; + fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(data), + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for stream results :O!"); + toast("Failed to update the pipeline state"); + } + + return response.json(); + }) + .then((responseJson) => { + if (!responseJson.success) { + toast("Failed to update the pipeline: " + responseJson.reason); + } else { + if (state === "start") toast("Successfully created pipeline"); + else toast("Sucessfully stopped the pipeline"); + } + }) + .catch((error) => { + //toast(error.toString()); + console.log("Get schedule error: ", error.toString()); + }); + }; + + if ( + userdata.support === true && + selectedOrganization.id !== "" && + selectedOrganization.id !== undefined && + selectedOrganization.id !== null && + selectedOrganization.id !== userdata.active_org.id + ) { + toast("Refreshing window to fix org support access"); + window.location.reload(); + return null; + } const handleVerify2FA = (userId, code) => { const data = { @@ -648,7 +1121,7 @@ If you're interested, please let me know a time that works for you, or set up a } else { console.log("Cloud sync fail?"); toast( - "Failed stopping sync. Try again, and contact support if this persists." + "Failed stopping sync. Try again, and contact support if this persists.", ); } @@ -715,7 +1188,7 @@ If you're interested, please let me know a time that works for you, or set up a } else { toast("Cloud Syncronization successfully set up!"); setOrgSyncResponse( - "Successfully started syncronization. Cloud features you now have access to can be seen below." + "Successfully started syncronization. Cloud features you now have access to can be seen below.", ); } @@ -750,22 +1223,21 @@ If you're interested, please let me know a time that works for you, or set up a .then((response) => response.json().then((responseJson) => { if (responseJson["success"] === false) { - // Check if .reason exists - if (responseJson.reason !== undefined) { - toast("Failed changing authentication: " + responseJson.reason); - } else { - toast("Failed changing authentication"); - } + // Check if .reason exists + if (responseJson.reason !== undefined) { + toast("Failed changing authentication: " + responseJson.reason); + } else { + toast("Failed changing authentication"); + } } else { //toast("Successfully password!") setSelectedUserModalOpen(false); getAppAuthentication(); - - setSelectedAuthentication({}); - setSelectedAuthenticationModalOpen(false); + setSelectedAuthentication({}); + setSelectedAuthenticationModalOpen(false); } - }) + }), ) .catch((error) => { toast("Err: " + error.toString()); @@ -779,9 +1251,8 @@ If you're interested, please let me know a time that works for you, or set up a image, defaults, sso_config, - lead_info, + lead_info, ) => { - const data = { name: name, description: description, @@ -789,7 +1260,7 @@ If you're interested, please let me know a time that works for you, or set up a image: image, defaults: defaults, sso_config: sso_config, - lead_info: lead_info, + lead_info: lead_info, }; const url = globalUrl + `/api/v1/orgs/${selectedOrganization.id}`; @@ -809,11 +1280,15 @@ If you're interested, please let me know a time that works for you, or set up a if (responseJson["success"] === false) { toast("Failed updating org: ", responseJson.reason); } else { - if (lead_info === undefined || lead_info === null || lead_info === []) { - toast("Successfully edited org!"); - } + if ( + lead_info === undefined || + lead_info === null || + lead_info === [] + ) { + toast("Successfully edited org!"); + } } - }) + }), ) .catch((error) => { toast("Err: " + error.toString()); @@ -823,8 +1298,11 @@ If you're interested, please let me know a time that works for you, or set up a const editAuthenticationConfig = (id, parentAction) => { const data = { id: id, - action: parentAction !== undefined && parentAction !== null ? parentAction : "assign_everywhere", - } + action: + parentAction !== undefined && parentAction !== null + ? parentAction + : "assign_everywhere", + }; const url = globalUrl + "/api/v1/apps/authentication/" + id + "/config"; @@ -850,7 +1328,7 @@ If you're interested, please let me know a time that works for you, or set up a getAppAuthentication(); }, 1000); } - }) + }), ) .catch((error) => { toast("Err: " + error.toString()); @@ -882,9 +1360,7 @@ If you're interested, please let me know a time that works for you, or set up a toast("Failed creating suborg. Please try again"); } } else { - toast( - "Successfully created suborg. Reloading in 3 seconds!" - ); + toast("Successfully created suborg. Reloading in 3 seconds!"); setSelectedUserModalOpen(false); setTimeout(() => { @@ -894,7 +1370,7 @@ If you're interested, please let me know a time that works for you, or set up a setOrgName(""); setModalOpen(false); - }) + }), ) .catch((error) => { toast("Err: " + error.toString()); @@ -928,7 +1404,7 @@ If you're interested, please let me know a time that works for you, or set up a toast("Successfully updated password!"); setSelectedUserModalOpen(false); } - }) + }), ) .catch((error) => { toast("Err: " + error.toString()); @@ -957,6 +1433,10 @@ If you're interested, please let me know a time that works for you, or set up a .then((responseJson) => { if (!responseJson.success && responseJson.reason !== undefined) { toast("Failed to deactivate user: " + responseJson.reason); + } else if (responseJson.success === false) { + toast( + "Failed to deactivate user. Please contact support@shuffler.io if this persists.", + ); } else { toast("Changed activation for user " + data.id); } @@ -967,26 +1447,31 @@ If you're interested, please let me know a time that works for you, or set up a }); }; - - const handleGetOrg = (orgId) => { - - if (serverside !== true && window.location.search !== undefined && window.location.search !== null) { - const urlSearchParams = new URLSearchParams(window.location.search); - const params = Object.fromEntries(urlSearchParams.entries()); - const foundorgid = params["org_id"]; - if (foundorgid !== undefined && foundorgid !== null) { - orgId = foundorgid; - } - } + if ( + serverside !== true && + window.location.search !== undefined && + window.location.search !== null + ) { + const urlSearchParams = new URLSearchParams(window.location.search); + const params = Object.fromEntries(urlSearchParams.entries()); + const foundorgid = params["org_id"]; + if (foundorgid !== undefined && foundorgid !== null) { + orgId = foundorgid; + } + } if (orgId.length === 0) { - toast("Organization ID not defined. Please contact us on https://shuffler.io if this persists logout."); + toast( + "Organization ID not defined. Please contact us on https://shuffler.io if this persists logout.", + ); return; } // Just use this one? - + localStorage.setItem("globalUrl", ""); + localStorage.setItem("getting_started_sidebar", "open"); + fetch(`${globalUrl}/api/v1/orgs/${orgId}`, { method: "GET", credentials: "include", @@ -996,13 +1481,19 @@ If you're interested, please let me know a time that works for you, or set up a }) .then((response) => { if (response.status === 401) { - } + } else { + localStorage.removeItem("apps") + localStorage.removeItem("workflows") + localStorage.removeItem("userinfo") + } return response.json(); }) .then((responseJson) => { if (responseJson["success"] === false) { - toast("Failed getting your org. If this persists, please contact support."); + toast( + "Failed getting your org. If this persists, please contact support.", + ); } else { if ( responseJson.sync_features === undefined || @@ -1011,64 +1502,67 @@ If you're interested, please let me know a time that works for you, or set up a responseJson.sync_features = {}; } - if (responseJson.lead_info !== undefined && responseJson.lead_info !== null) { - var leads = [] - if (responseJson.lead_info.contacted) { - leads.push("contacted") - } + if ( + responseJson.lead_info !== undefined && + responseJson.lead_info !== null + ) { + var leads = []; + if (responseJson.lead_info.contacted) { + leads.push("contacted"); + } - if (responseJson.lead_info.customer) { - leads.push("customer") - } + if (responseJson.lead_info.customer) { + leads.push("customer"); + } - if (responseJson.lead_info.old_customer) { - leads.push("old customer") - } + if (responseJson.lead_info.old_customer) { + leads.push("old customer"); + } - if (responseJson.lead_info.old_lead) { - leads.push("old lead") - } + if (responseJson.lead_info.old_lead) { + leads.push("old lead"); + } - if (responseJson.lead_info.tech_partner) { - leads.push("tech partner") - } + if (responseJson.lead_info.tech_partner) { + leads.push("tech partner"); + } - if (responseJson.lead_info.creator) { - leads.push("creator") - } + if (responseJson.lead_info.creator) { + leads.push("creator"); + } - if (responseJson.lead_info.opensource) { - leads.push("open source") - } + if (responseJson.lead_info.opensource) { + leads.push("open source"); + } - if (responseJson.lead_info.demo_done) { - leads.push("demo done") - } + if (responseJson.lead_info.demo_done) { + leads.push("demo done"); + } - if (responseJson.lead_info.pov) { - leads.push("pov") - } + if (responseJson.lead_info.pov) { + leads.push("pov"); + } - if (responseJson.lead_info.lead) { - leads.push("lead") - } + if (responseJson.lead_info.lead) { + leads.push("lead"); + } - if (responseJson.lead_info.student) { - leads.push("student") - } + if (responseJson.lead_info.student) { + leads.push("student"); + } - if (responseJson.lead_info.internal) { - leads.push("internal") - } + if (responseJson.lead_info.internal) { + leads.push("internal"); + } - if (responseJson.lead_info.sub_org) { - leads.push("sub_org") - } + if (responseJson.lead_info.sub_org) { + leads.push("sub_org"); + } - setSelectedStatus(leads) - } + setSelectedStatus(leads); + } - setSelectedOrganization(responseJson) + setSelectedOrganization(responseJson); var lists = { active: { triggers: [], @@ -1098,6 +1592,100 @@ If you're interested, please let me know a time that works for you, or set up a }); }; + const handleGetSubOrgs = (orgId) => { + if (orgId.length === 0) { + toast( + "Organization ID not defined. Please contact us on https://shuffler.io if this persists logout.", + ); + return; + } + + fetch(`${globalUrl}/api/v1/orgs/${orgId}/suborgs`, { + method: "GET", + credentials: "include", + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => { + if (!response.ok) { + throw new Error("Failed to fetch sub organizations"); + } + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === false) { + //toast("Failed getting your org. If this persists, please contact support."); + } else { + const { subOrgs, parentOrg } = responseJson; + setSubOrgs(subOrgs); + setParentOrg(parentOrg); + } + }) + .catch((error) => { + console.log("Error getting sub orgs: ", error); + //toast("Error getting sub organizations"); + }); + }; + + const handleClickChangeOrg = (orgId) => { + // Don't really care about the logout + //name: org.name, + //orgId = "asd" + const data = { + org_id: orgId, + }; + + localStorage.setItem("globalUrl", ""); + localStorage.setItem("getting_started_sidebar", "open"); + + fetch(`${globalUrl}/api/v1/orgs/${orgId}/change`, { + mode: "cors", + credentials: "include", + crossDomain: true, + method: "POST", + body: JSON.stringify(data), + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then(function (response) { + if (response.status !== 200) { + console.log("Error in response"); + } else { + localStorage.removeItem("apps") + localStorage.removeItem("workflows") + localStorage.removeItem("userinfo") + } + + return response.json(); + }) + .then(function (responseJson) { + if (responseJson.success === true) { + if ( + responseJson.region_url !== undefined && + responseJson.region_url !== null && + responseJson.region_url.length > 0 + ) { + localStorage.setItem("globalUrl", responseJson.region_url); + //globalUrl = responseJson.region_url + } + + setTimeout(() => { + window.location.reload(); + }, 2000); + toast("Successfully changed active organization - refreshing!"); + } else { + toast("Failed changing org: " + responseJson.reason); + } + }) + .catch((error) => { + console.log("error changing: ", error); + //removeCookie("session_token", {path: "/"}) + }); + }; + const inviteUser = (data) => { //console.log("INPUT: ", data); setLoginInfo(""); @@ -1123,21 +1711,27 @@ If you're interested, please let me know a time that works for you, or set up a response.json().then((responseJson) => { if (responseJson["success"] === false) { setLoginInfo("Error: " + responseJson.reason); - toast("Failed to send email (2). Please try again and contact support if this persists.") + toast( + "Failed to send email (2). Please try again and contact support if this persists.", + ); } else { setLoginInfo(""); setModalOpen(false); setTimeout(() => { getUsers(); }, 1000); - - toast("Invite sent! They will show up in the list when they have accepted the invite.") + + toast( + "Invite sent! They will show up in the list when they have accepted the invite.", + ); } - }) + }), ) .catch((error) => { console.log("Error in userdata: ", error); - toast("Failed to send email. Please try again and contact support if this persists.") + toast( + "Failed to send email. Please try again and contact support if this persists.", + ); }); }; @@ -1169,7 +1763,7 @@ If you're interested, please let me know a time that works for you, or set up a getUsers(); }, 1000); } - }) + }), ) .catch((error) => { console.log("Error in userdata: ", error); @@ -1179,7 +1773,7 @@ If you're interested, please let me know a time that works for you, or set up a // Horrible frontend fix for environments const setDefaultEnvironment = (environment) => { // FIXME - add more checks to this - toast("Setting default env to " + environment.name); + toast("Changing default env") var newEnv = []; for (var key in environments) { if (environments[key].id == environment.id) { @@ -1223,7 +1817,7 @@ If you're interested, please let me know a time that works for you, or set up a getEnvironments(); }, 1500); } - }) + }), ) .catch((error) => { console.log("Error in backend data: ", error); @@ -1250,7 +1844,7 @@ If you're interested, please let me know a time that works for you, or set up a setModalOpen(false); getEnvironments(); } - }) + }), ) .catch((error) => { console.log("Error when deleting: ", error); @@ -1258,14 +1852,11 @@ If you're interested, please let me know a time that works for you, or set up a }; const rerunCloudWorkflows = (environment) => { - toast("Starting execution reruns. This can run in the background.") - fetch( - `${globalUrl}/api/v1/environments/${environment.id}/rerun`, - { - method: "GET", - credentials: "include", - } - ) + toast("Starting execution reruns. This can run in the background."); + fetch(`${globalUrl}/api/v1/environments/${environment.id}/rerun`, { + method: "GET", + credentials: "include", + }) .then((response) => { if (response.status !== 200) { console.log("Status not 200 for apps :O!"); @@ -1289,13 +1880,16 @@ If you're interested, please let me know a time that works for you, or set up a const abortEnvironmentWorkflows = (environment) => { //console.log("Aborting all workflows started >10 minutes ago, not finished"); + toast( + "Clearing the queue - this may take some time. A new will show up when finished.", + ); fetch( `${globalUrl}/api/v1/environments/${environment.id}/stop?deleteall=true`, { method: "GET", credentials: "include", - } + }, ) .then((response) => { if (response.status !== 200) { @@ -1303,7 +1897,9 @@ If you're interested, please let me know a time that works for you, or set up a toast("Failed aborting dangling workflows"); return; } else { - toast("Aborted all dangling workflows"); + toast("Successfully cleared the queue"); + + getEnvironments(); } return response.json(); @@ -1383,7 +1979,7 @@ If you're interested, please let me know a time that works for you, or set up a setModalOpen(false); getEnvironments(); } - }) + }), ) .catch((error) => { console.log("Error when deleting: ", error); @@ -1418,7 +2014,7 @@ If you're interested, please let me know a time that works for you, or set up a setModalOpen(false); getEnvironments(); } - }) + }), ) .catch((error) => { console.log("Error in userdata: ", error); @@ -1427,6 +2023,46 @@ If you're interested, please let me know a time that works for you, or set up a var localData = ""; + const handleDeleteAccount = (userID) => { + if (userID === undefined || userID === null || userID === "") { + return; + } + + const url = `${globalUrl}/api/v1/users/${userID}/remove`; + fetch(url, { + mode: "cors", + method: "DELETE", + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => response.json()) + .then((data) => { + if (data.success) { + toast.success( + "Deleted their account. Would reload users in a few seconds.", + ); + + setTimeout(() => { + getUsers(); + }); + } else { + toast.error(`${data.reason}`); + } + }) + .catch((error) => { + console.error( + "There was a problem with deleting the account. Please try again:", + error, + ); + toast.error( + "There was a problem with the delete request. Please try again", + ); + }); + }; const getSchedules = () => { fetch(globalUrl + "/api/v1/workflows/schedules", { @@ -1453,6 +2089,36 @@ If you're interested, please let me know a time that works for you, or set up a }); }; + const getAppAuthenticationGroups = () => { + console.log("DEBUG: Skipping app auth group loading") + return + + fetch(globalUrl + "/api/v1/apps/authentication/group", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for apps :O!"); + return; + } + + return response.json(); + }) + .then((responseJson) => { + if (responseJson.success === true) { + setAppAuthenticationGroups(responseJson.data); + } + }) + .catch((error) => { + toast(error.toString()); + }); + }; + const getAppAuthentication = () => { fetch(globalUrl + "/api/v1/apps/authentication", { method: "GET", @@ -1504,18 +2170,30 @@ If you're interested, please let me know a time that works for you, or set up a .then((responseJson) => { setEnvironments(responseJson); - // Helper info for users in case they have a large queue and don't know about queue flushing - if (responseJson !== undefined && responseJson !== null && responseJson.length > 0) { - for (var i = 0; i < responseJson.length; i++) { - const env = responseJson[i]; + // Helper info for users in case they have a large queue and don't know about queue flushing + if ( + responseJson !== undefined && + responseJson !== null && + responseJson.length > 0 + ) { + for (var i = 0; i < responseJson.length; i++) { + const env = responseJson[i]; - // Check if queuesize is too large - if (env.queue !== undefined && env.queue !== null && env.queue > 100) { - toast("Queue size for " + env.name + " is very large. We recommend you to reduce it by flushing the queue before continuing."); - break - } - } - } + // Check if queuesize is too large + if ( + env.queue !== undefined && + env.queue !== null && + env.queue > 100 + ) { + toast( + "Queue size for " + + env.name + + " is very large. We recommend you to reduce it by flushing the queue before continuing.", + ); + break; + } + } + } }) .catch((error) => { toast(error.toString()); @@ -1614,7 +2292,7 @@ If you're interested, please let me know a time that works for you, or set up a const admin_views = { 0: "organization", 1: "cloud_sync", - 2: "priorities", + 2: "priorities", 3: "billing", 4: "branding", }; @@ -1629,6 +2307,7 @@ If you're interested, please let me know a time that works for you, or set up a } else if (newValue === 2) { document.title = "Shuffle - admin - app authentication"; getAppAuthentication(); + getAppAuthenticationGroups(); } else if (newValue === 3) { document.title = "Shuffle - admin - Files"; } else if (newValue === 4) { @@ -1654,7 +2333,7 @@ If you're interested, please let me know a time that works for you, or set up a navigate(`/admin?tab=${views[newValue]}`); setModalUser({}); - } + }; if (firstRequest) { setFirstRequest(false); @@ -1677,26 +2356,26 @@ If you're interested, please let me know a time that works for you, or set up a const adminTab = params["admin_tab"]; if (adminTab !== null && adminTab !== undefined) { - for (var key in Object.keys(admin_views)) { + for (var key in Object.keys(admin_views)) { const value = admin_views[key]; if (value === adminTab) { - setAdminTab(parseInt(key)); + setAdminTab(parseInt(key)); setConfig("", 0); break; } } - } else { - const foundTab = params["tab"]; - if (foundTab !== null && foundTab !== undefined) { - for (var key in Object.keys(views)) { - const value = views[key]; - if (value === foundTab) { - setConfig("", key); - break; - } - } - } - } + } else { + const foundTab = params["tab"]; + if (foundTab !== null && foundTab !== undefined) { + for (var key in Object.keys(views)) { + const value = views[key]; + if (value === foundTab) { + setConfig("", key); + break; + } + } + } + } } } @@ -1748,9 +2427,11 @@ If you're interested, please let me know a time that works for you, or set up a .then((responseJson) => { if (!responseJson.success && responseJson.reason !== undefined) { toast("Failed setting user: " + responseJson.reason); + } else if (responseJson.success === false) { + toast("Failed to update user"); } else { //toast("Set the user field " + field + " to " + value); - toast("Successfully updated user field " + field) + toast("Successfully updated user field " + field); if (field !== "suborgs") { setSelectedUserModalOpen(false); @@ -1766,7 +2447,7 @@ If you're interested, please let me know a time that works for you, or set up a const userId = user.id; const data = { user_id: userId }; - toast("Generating new API key") + toast("Generating new API key"); var fetchdata = { method: "POST", @@ -1775,13 +2456,13 @@ If you're interested, please let me know a time that works for you, or set up a Accept: "application/json", }, credentials: "include", - } + }; if (userId === userdata.id) { - fetchdata.method = "GET" - } else { - fetchdata.body = JSON.stringify(data) - } + fetchdata.method = "GET"; + } else { + fetchdata.body = JSON.stringify(data); + } fetch(globalUrl + "/api/v1/generateapikey", fetchdata) .then((response) => { @@ -1823,54 +2504,80 @@ If you're interested, please let me know a time that works for you, or set up a > - Edit authentication for {selectedAuthentication.app.name.replaceAll("_", " ")} ( + Edit authentication for{" "} + {selectedAuthentication.app.name.replaceAll("_", " ")} ( {selectedAuthentication.label}) - - You can not see the previous values for an authentication while editing. This is to keep your data secure. You can overwrite one- or multiple fields at a time. - + + You can not see the previous values for an authentication while + editing. This is to keep your data secure. You can overwrite one- or + multiple fields at a time. + - - Authentication Label - - { - selectedAuthentication.label = e.target.value - }} - /> + + Authentication Label + + { + selectedAuthentication.label = e.target.value; + }} + /> - - {selectedAuthentication.type === "oauth" || selectedAuthentication.type === "oauth2" || selectedAuthentication.type === "oauth2-app" ? -
- - Only the name of the auth can be modified for Oauth2. Please remake the authentication to change the fields like Client ID, Secret, Scopes etc. - -
- : - selectedAuthentication.fields.map((data, index) => { - var fieldname = data.key.replaceAll("_", " ") - if (fieldname.endsWith(" basic")) { - fieldname = fieldname.substring(0, fieldname.length - 6) - } + + {selectedAuthentication.type === "oauth" || + selectedAuthentication.type === "oauth2" || + selectedAuthentication.type === "oauth2-app" ? ( +
+ + Only the name and url can be modified for Oauth2/OpenID connect. + Please remake the authentication if you want to change the other + fields like Client ID, Secret, Scopes etc. + +
+ ) : null} + + {selectedAuthentication.fields.map((data, index) => { + var fieldname = data.key.replaceAll("_", " "); + if (fieldname.endsWith(" basic")) { + fieldname = fieldname.substring(0, fieldname.length - 6); + } + + if ( + selectedAuthentication.type === "oauth" || + selectedAuthentication.type === "oauth2" || + selectedAuthentication.type === "oauth2-app" + ) { + if (selectedAuthentication.fields[index].key !== "url") { + return null; + } + } //console.log("DATA: ", data, selectedAuthentication) return ( @@ -1919,15 +2626,15 @@ If you're interested, please let me know a time that works for you, or set up a style={{ borderRadius: "0px" }} onClick={() => { var error = false; - var fails = 0 + var fails = 0; for (var key in authenticationFields) { const item = authenticationFields[key]; if (item.value.length === 0) { - fails += 1 + fails += 1; console.log("ITEM: ", item); //var currentnode = cy.getElementById(data.id) var textfield = document.getElementById( - `authentication-${key}` + `authentication-${key}`, ); if (textfield !== null && textfield !== undefined) { console.log("HANDLE ERROR FOR KEY ", key); @@ -1936,13 +2643,17 @@ If you're interested, please let me know a time that works for you, or set up a } } - if (selectedAuthentication.type === "oauth" || selectedAuthentication.type === "oauth2" || selectedAuthentication.type === "oauth2-app") { - selectedAuthentication.fields = [] - } + if ( + selectedAuthentication.type === "oauth" || + selectedAuthentication.type === "oauth2" || + selectedAuthentication.type === "oauth2-app" + ) { + selectedAuthentication.fields = []; + } if (error && fails === authenticationFields.length) { - toast("Updating auth with new name only") - saveAuthentication(selectedAuthentication); + toast("Updating auth with new name only"); + saveAuthentication(selectedAuthentication); } else { toast("Saving new version of this authentication"); selectedAuthentication.fields = authenticationFields; @@ -2011,20 +2722,16 @@ If you're interested, please let me know a time that works for you, or set up a ) : null; - - - - const editUserModal = ( { setSelectedUserModalOpen(false); - setImage2FA(""); - setValue2FA(""); - setSecret2FA(""); - setShow2faSetup(false); + setImage2FA(""); + setValue2FA(""); + setSecret2FA(""); + setShow2faSetup(false); }} PaperProps={{ style: { @@ -2172,6 +2879,80 @@ If you're interested, please let me know a time that works for you, or set up a ? "Disable 2FA" : "Enable 2FA"} + + {isCloud && userdata.support && selectedUser.id != userdata.id ? ( + + ) : null} + + {showDeleteAccountTextbox ? ( + { + setDeleteAccountText(e.target.value); + }} + /> + ) : null}
{show2faSetup ? (
{ const [expanded, setExpanded] = React.useState(false); - const [showEdit, setShowEdit] = React.useState(false); - const [newValue, setNewValue] = React.useState(-100); + const [showEdit, setShowEdit] = React.useState(false); + const [newValue, setNewValue] = React.useState(-100); const primary = props.data.primary; const secondary = props.data.secondary; const primaryIcon = props.data.icon; - const secondaryIcon = props.data.active ? + const secondaryIcon = props.data.active ? ( - : + ) : ( + ); - const submitFeatureEdit = (sync_features) => { - if (!userdata.support) { - console.log("User does not have support access and can't edit features"); - return - } + const submitFeatureEdit = (sync_features) => { + if (!userdata.support) { + console.log( + "User does not have support access and can't edit features", + ); + return; + } - sync_features.editing = true - const data = { - org_id: selectedOrganization.id, - sync_features: sync_features, - }; + sync_features.editing = true; + const data = { + org_id: selectedOrganization.id, + sync_features: sync_features, + }; - const url = globalUrl + `/api/v1/orgs/${selectedOrganization.id}`; - fetch(url, { - mode: "cors", - method: "POST", - body: JSON.stringify(data), - credentials: "include", - crossDomain: true, - withCredentials: true, - headers: { - "Content-Type": "application/json; charset=utf-8", - }, - }) - .then((response) => - response.json().then((responseJson) => { - if (responseJson["success"] === false) { - toast("Failed updating org: ", responseJson.reason); - } else { - toast("Successfully edited org!"); - } - }) - ) - .catch((error) => { - toast("Err: " + error.toString()); - }); - } + const url = globalUrl + `/api/v1/orgs/${selectedOrganization.id}`; + fetch(url, { + mode: "cors", + method: "POST", + body: JSON.stringify(data), + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then((response) => + response.json().then((responseJson) => { + if (responseJson["success"] === false) { + toast("Failed updating org: ", responseJson.reason); + } else { + toast("Successfully edited org!"); + } + }), + ) + .catch((error) => { + toast("Err: " + error.toString()); + }); + }; - const enableFeature = () => { - console.log("Enabling "+primary) + const enableFeature = () => { + console.log("Enabling " + primary); - console.log(selectedOrganization.sync_features) - // Check if primary is in sync_features - var tmpprimary = primary.replaceAll(" ", "_") - if (!(tmpprimary in selectedOrganization.sync_features)) { - console.log("Primary not in sync_features: "+tmpprimary) - return - } + console.log(selectedOrganization.sync_features); + // Check if primary is in sync_features + var tmpprimary = primary.replaceAll(" ", "_"); + if (!(tmpprimary in selectedOrganization.sync_features)) { + console.log("Primary not in sync_features: " + tmpprimary); + return; + } - if (props.data.active) { - selectedOrganization.sync_features[tmpprimary].active = false - } else { - selectedOrganization.sync_features[tmpprimary].active = true - } + if (props.data.active) { + selectedOrganization.sync_features[tmpprimary].active = false; + } else { + selectedOrganization.sync_features[tmpprimary].active = true; + } - setSelectedOrganization(selectedOrganization) - forceUpdate(Math.random()) - submitFeatureEdit(selectedOrganization.sync_features) - } - - const submitEdit = (e) => { - e.preventDefault(); - e.stopPropagation(); + setSelectedOrganization(selectedOrganization); + forceUpdate(Math.random()); + submitFeatureEdit(selectedOrganization.sync_features); + }; - // Check if primary is in sync_features - var tmpprimary = primary.replaceAll(" ", "_") - if (!(tmpprimary in selectedOrganization.sync_features)) { - console.log("Primary not in sync_features: "+tmpprimary) - return - } + const submitEdit = (e) => { + e.preventDefault(); + e.stopPropagation(); - // Make it into a number - var tmp = parseInt(newValue) - if (isNaN(tmp)) { - console.log("Not a number: "+newValue) - return - } + // Check if primary is in sync_features + var tmpprimary = primary.replaceAll(" ", "_"); + if (!(tmpprimary in selectedOrganization.sync_features)) { + console.log("Primary not in sync_features: " + tmpprimary); + return; + } - selectedOrganization.sync_features[tmpprimary].limit = tmp + // Make it into a number + var tmp = parseInt(newValue); + if (isNaN(tmp)) { + console.log("Not a number: " + newValue); + return; + } - setSelectedOrganization(selectedOrganization) - forceUpdate(Math.random()) - submitFeatureEdit(selectedOrganization.sync_features) - } + selectedOrganization.sync_features[tmpprimary].limit = tmp; + + setSelectedOrganization(selectedOrganization); + forceUpdate(Math.random()); + submitFeatureEdit(selectedOrganization.sync_features); + }; return ( - + { - setExpanded(!expanded); - }} - > + style={{ cursor: "pointer" }} + onClick={() => { + setExpanded(!expanded); + }} + > {primaryIcon} - {isCloud && userdata.support === true ? - - { - e.preventDefault(); - e.stopPropagation(); + {isCloud && userdata.support === true ? ( + + { + e.preventDefault(); + e.stopPropagation(); - if (showEdit) { - setShowEdit(false) - return - } + if (showEdit) { + setShowEdit(false); + return; + } - console.log("Edit") - - setShowEdit(true) - }} - /> - - : null} - - { - if (!isCloud || userdata.support !== true) { - return - } + console.log("Edit"); - e.preventDefault(); - e.stopPropagation(); + setShowEdit(true); + }} + /> + + ) : null} + + { + if (!isCloud || userdata.support !== true) { + return; + } - enableFeature() - }} - > - {secondaryIcon} - - + e.preventDefault(); + e.stopPropagation(); + + enableFeature(); + }} + > + {secondaryIcon} + + - {expanded ? + {expanded ? (
Usage:  @@ -2440,50 +3223,59 @@ If you're interested, please let me know a time that works for you, or set up a "Unlimited" ) : ( - {props.data.usage} / {props.data.limit === "" ? "Unlimited" : props.data.limit} + {props.data.usage} /{" "} + {props.data.limit === "" ? "Unlimited" : props.data.limit} )} {/* Data sharing: {props.data.data_collection} */} - Description: {secondary} + + Description: {secondary} +
- : null} + ) : null} - - {showEdit ? - { - console.log("Submit") - submitEdit(e) - }}> - - - { - setNewValue(event.target.value) - }} - /> - - - - : null} + {showEdit ? ( + { + console.log("Submit"); + submitEdit(e); + }} + > + + { + setNewValue(event.target.value); + }} + /> + + + + ) : null}
); @@ -2534,7 +3326,7 @@ If you're interested, please let me know a time that works for you, or set up a
cloud sync @@ -2578,7 +3370,7 @@ If you're interested, please let me know a time that works for you, or set up a enableCloudSync( cloudSyncApikey, selectedOrganization, - selectedOrganization.cloud_sync + selectedOrganization.cloud_sync, ); }} color="primary" @@ -2614,25 +3406,37 @@ If you're interested, please let me know a time that works for you, or set up a ); var regiontag = "eu"; - if (userdata.region_url !== undefined && userdata.region_url !== null && userdata.region_url.length > 0) { + if ( + userdata.region_url !== undefined && + userdata.region_url !== null && + userdata.region_url.length > 0 + ) { const regionsplit = userdata.region_url.split("."); if (regionsplit.length > 2 && !regionsplit[0].includes("shuffler")) { - const namesplit = regionsplit[0].split("/"); - - regiontag = namesplit[namesplit.length - 1]; + const namesplit = regionsplit[0].split("/"); + + regiontag = namesplit[namesplit.length - 1]; + if (regiontag === "california") { + regiontag = "us"; + } } } - + const organizationView = curTab === 0 && selectedOrganization.id !== undefined ? (
) : (
- {/* + {/* */} - {userdata.support === true ? - - {/**/} - - - Status - - - - : null} - {isCloud ? - - { - toast("Region change is not implemented yet for users. Please contact support.") - }} - > - {regiontag} - - - : null} + {userdata.support === true ? ( + + {/**/} + + + Status + + + + ) : null} + {isCloud ? ( + + { + if (userdata.support === false) { + toast( + "Region change is not directly implemented yet, and requires support help.", + ); + + if (window.drift !== undefined) { + window.drift.api.startInteraction({ + interactionId: 386411, + }); + } + } else { + // Show region change modal + console.log("Should open region change modal"); + setRegionChangeModalOpen(true); + } + }} + > + {regiontag} + + + ) : null} + + - {selectedOrganization.defaults !== undefined && selectedOrganization.defaults.documentation_reference !== undefined && selectedOrganization.defaults.documentation_reference !== null && selectedOrganization.defaults.documentation_reference.includes("http") ? - - - - - - - - : null} + aria-label={"Open org docs"} + > + + + + + + + ) : null} {selectedOrganization.name.length > 0 ? ( ) : (
)} - { - const newValue = parseInt(inputValue); - setAdminTab(newValue); + { + const newValue = parseInt(inputValue); + setAdminTab(newValue); - //const setConfig = (event, inputValue) => { - navigate(`/admin?admin_tab=${admin_views[newValue]}`); - }} - aria-label="disabled tabs example" - > - - Edit Details - - /> - - Cloud Synchronization - - /> - - Priorities - - /> - - Billing & Stats - - /> - - Branding (Beta) - - /> - + //const setConfig = (event, inputValue) => { + navigate(`/admin?admin_tab=${admin_views[newValue]}`); + }} + aria-label="disabled tabs example" + > + Edit Details /> + Cloud Synchronization /> + Priorities /> + Billing & Stats /> + Branding (Beta) /> + - {adminTab === 0 ? ( - - ) - : adminTab === 1 ? ( -
- - Cloud syncronization - - What does{" "} - - cloud sync - {" "} - do? Cloud syncronization is a way of getting more out of Shuffle. - Shuffle will ALWAYS make every option open source, but - features relying on other users can't be done without a - collaborative approach. - {isCloud ? ( -
-
- - Currently syncronizing:{" "} - {selectedOrganization.cloud_sync_active === true - ? "True" - : "False"} - - {selectedOrganization.cloud_sync_active ? ( - - Syncronization interval:{" "} - {selectedOrganization.sync_config.interval === 0 - ? "60" - : selectedOrganization.sync_config.interval} - - ) : null} - - Your Apikey - -
- - { - setShowApiKey(!showApiKey) - }} - > - {showApiKey ? : } - - - ) - }} - required - fullWidth={true} - disabled={true} - autoComplete="cloud apikey" - id="apikey_field" - margin="normal" - placeholder="Cloud Apikey" - variant="outlined" - defaultValue={userSettings.apikey} - type={!isCloud || showApiKey ? "text" : "password"} - /> - {selectedOrganization.cloud_sync_active ? ( - - ) : null} -
-
-
- ) : ( -
-
- { - setCloudSyncApikey(event.target.value); - }} - /> - -
- {orgSyncResponse.length > 0 ? ( - - Message from Shuffle Cloud: {orgSyncResponse} - - ) : null} -
- )} - - Features - - - Features and Limitations that are currently available to you in your Cloud or Hybrid Organization. App Executions (App Runs) reset monthly. If the organization is a customer or in a trial, these features limitations are not always enforced. - - + {adminTab === 0 ? ( + + ) : adminTab === 1 ? ( +
+ + Cloud syncronization + + What does{" "} + + cloud sync + {" "} + do? Cloud syncronization is a way of getting more out of + Shuffle. Shuffle will ALWAYS make every option open + source, but features relying on other users can't be done + without a collaborative approach. + {isCloud ? ( +
+
+ + Currently syncronizing:{" "} + {selectedOrganization.cloud_sync_active === true + ? "True" + : "False"} + + {selectedOrganization.cloud_sync_active ? ( + + Syncronization interval:{" "} + {selectedOrganization.sync_config.interval === 0 + ? "60" + : selectedOrganization.sync_config.interval} + + ) : null} + + Your Apikey + +
+ + { + setShowApiKey(!showApiKey); + }} + > + {showApiKey ? ( + + ) : ( + + )} + + + ), + }} + required + fullWidth={true} + disabled={true} + autoComplete="cloud apikey" + id="apikey_field" + margin="normal" + placeholder="Cloud Apikey" + variant="outlined" + defaultValue={userSettings.apikey} + type={!isCloud || showApiKey ? "text" : "password"} + /> + {selectedOrganization.cloud_sync_active ? ( + + ) : null} +
+
+
+ ) : ( +
+
+ { + setCloudSyncApikey(event.target.value); + }} + /> + +
+ {orgSyncResponse.length > 0 ? ( + + Message from Shuffle Cloud: {orgSyncResponse} + + ) : null} +
+ )} + + Features + + + Features and Limitations that are currently available to you + in your Cloud or Hybrid Organization. App Executions (App + Runs) reset monthly. If the organization is a customer or in a + trial, these features limitations are not always enforced. + + + {selectedOrganization.sync_features === undefined || + selectedOrganization.sync_features === null + ? null + : Object.keys(selectedOrganization.sync_features).map( + function (key, index) { + if ( + key === "schedule" || + key === "apps" || + key === "updates" || + key === "editing" + ) { + return null; + } - {selectedOrganization.sync_features === undefined || - selectedOrganization.sync_features === null - ? null - : Object.keys(selectedOrganization.sync_features).map(function ( - key, - index - ) { + const item = selectedOrganization.sync_features[key]; + if (item === null || item === undefined) { + return null; + } - if (key === "schedule" || key === "apps" || key === "updates" || key === "editing") { - return null; - } + const newkey = key.replaceAll("_", " "); + // Name rewrites as these are structs + var newname = ""; + if (newkey.toLowerCase() === "shuffle gpt") { + newname = "Shuffle AI"; + } - const item = selectedOrganization.sync_features[key]; - if (item === null) { - return null - } + const griditem = { + primary: newkey, + secondary: + item.description === undefined || + item.description === null || + item.description.length === 0 + ? "Not defined yet" + : item.description, + limit: item.limit, + usage: + item.usage === undefined || item.usage === null + ? 0 + : item.usage, + data_collection: "None", + active: item.active, + icon: , - const newkey = key.replaceAll("_", " "); - const griditem = { - primary: newkey, - secondary: - item.description === undefined || - item.description === null || - item.description.length === 0 - ? "Not defined yet" - : item.description, - limit: item.limit, - usage: item.usage === undefined || - item.usage === null ? 0 : item.usage, - data_collection: "None", - active: item.active, - icon: , - }; + newname: newname, + }; - return ( - - - - ); - })} - -
- ) - : adminTab === 2 ? - - : adminTab === 3 ? - - : adminTab === 4 ? - - : null - } + return ( + + + + ); + }, + )} +
+
+ ) : adminTab === 2 ? ( + + ) : adminTab === 3 ? ( + + ) : adminTab === 4 ? ( + + ) : null} - -
)}
@@ -3150,7 +4021,11 @@ If you're interested, please let me know a time that works for you, or set up a > - {curTab === 1 ? "Add user" : curTab === 7 ? "Add Sub-Organization" : "Add environment"} + {curTab === 1 + ? "Add user" + : curTab === 7 + ? "Add Sub-Organization" + : "Add environment"} @@ -3311,7 +4186,7 @@ If you're interested, please let me know a time that works for you, or set up a Add, edit, block or change passwords.{" "} @@ -3343,6 +4218,130 @@ If you're interested, please let me know a time that works for you, or set up a backgroundColor: theme.palette.inputColor, }} /> + + {logsViewModal ? ( + { + setLogsViewModal(false); + }} + PaperProps={{ + style: { + backgroundColor: theme.palette.surfaceColor, + color: "white", + minWidth: "1200px", + minHeight: "320px", + }, + }} + > + + User Logs + + + {/* ask user for which IP they want to see logs for by iterating of user.login_info */} + + + User IP + + + + + {logsLoading && ipSelected.length !== 0 ? ( +
+ + Loading logs +
+ ) : null} + + + {logs.map((data, index) => ( + // redirect user to logs + // using request id or trace id + + + + + + ))} + +
+
+ ) : null} + {users === undefined || users === null @@ -3401,27 +4400,51 @@ If you're interested, please let me know a time that works for you, or set up a bgColor = "#1f2023"; } - const timeNow = new Date().getTime(); - - // Get the highest timestamp in data.login_info - var lastLogin = "N/A" - if (data.login_info !== undefined && data.login_info !== null) { - var loginInfo = 0 - for (var i = 0; i < data.login_info.length; i++) { - if (data.login_info[i].timestamp > loginInfo) { - loginInfo = data.login_info[i].timestamp - } - } + const timeNow = new Date().getTime(); - if (loginInfo > 0) { - lastLogin = new Date(loginInfo * 1000).toISOString().slice(0, 10) + " (" + data.login_info.length + ")" - } - } + // Get the highest timestamp in data.login_info + var lastLogin = "N/A"; + if (data.login_info !== undefined && data.login_info !== null) { + var loginInfo = 0; + for (var i = 0; i < data.login_info.length; i++) { + if (data.login_info[i].timestamp > loginInfo) { + loginInfo = data.login_info[i].timestamp; + } + } + + if (loginInfo > 0) { + lastLogin = + new Date(loginInfo * 1000).toISOString().slice(0, 10) + + " (" + + data.login_info.length + + ")"; + } + } + + var userData = data.username; + if (userdata.support === true) { + userData = ( +
{ + setLogsViewModal(true); + setUserLogViewing(data); + }} + > + {data.username} + + ); + } return ( ) : null} - + { setSelectedUserModalOpen(true); @@ -3588,7 +4613,7 @@ If you're interested, please let me know a time that works for you, or set up a for (var key in userdata.orgs) { const found = selectedOrganization.child_orgs.find( - (item) => item.id === userdata.orgs[key].id + (item) => item.id === userdata.orgs[key].id, ); if (found !== null && found !== undefined) { if ( @@ -3599,7 +4624,7 @@ If you're interested, please let me know a time that works for you, or set up a } const subfound = data.orgs.find( - (item) => item === found.id + (item) => item === found.id, ); if ( subfound !== null && @@ -3617,21 +4642,22 @@ If you're interested, please let me know a time that works for you, or set up a {/**/} + onClick={() => { + generateApikey(data) + }} + disabled={data.role === "admin" && data.username !== userdata.username} + variant="outlined" + color="primary" + > + New apikey + */} + + + - - ); })} @@ -3652,18 +4678,18 @@ If you're interested, please let me know a time that works for you, or set up a //setShow2faSetup(true); }; + const filesView = + curTab !== 3 ? null : ( + + ); - - const filesView = curTab !== 3 ? null : - - - const schedulesView = + const schedulesView = curTab === 5 ? (
@@ -3672,7 +4698,7 @@ If you're interested, please let me know a time that works for you, or set up a Schedules used in Workflows. Makes locating and control easier.{" "} @@ -3687,85 +4713,357 @@ If you're interested, please let me know a time that works for you, or set up a backgroundColor: theme.palette.inputColor, }} /> - - - - - - - - - - {schedules === undefined || schedules === null - ? null - : schedules.map((schedule, index) => { - var bgColor = "#27292d"; - if (index % 2 === 0) { - bgColor = "#1f2023"; - } - - return ( - - 0 ? - schedule.frequency - : - {schedule.seconds} seconds - } - /> - - - {schedule.workflow_id} - - } - /> - - - - - - ); - })} - + {schedule.workflow_id} + + } + /> + + + + + + ); + })} + + )} + +
+

WebHooks

+
+ + + {webHooks === undefined || webHooks === null || webHooks.length === 0 ? ( +
+ No webhooks found. +
+ ) : ( + + + + + + + + + {webHooks.map((webhook, index) => { + var bgColor = "#27292d"; + if (index % 2 === 0) { + bgColor = "#1f2023"; + } + + return ( + + + + + {webhook.workflows[0]} + + } + /> + + + { + const elementName = "copy_element_shuffle"; + 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(webhook.info.url); + copyText.select(); + copyText.setSelectionRange( + 0, + 99999, + ); /* For mobile devices */ + + /* Copy the text inside the text field */ + document.execCommand("copy"); + + toast("URL copied to clipboard"); + } + }} + > + + + + ) + } + /> + + + + + + ); + })} + + )} + + {/*
+

Tenzir Pipelines

+ + Controls a pipeline to run things.{" "} + + Learn more + + +
+ + + {pipelines === undefined || + pipelines === null || + pipelines.length === 0 ? ( +
+ No pipelines found. +
+ ) : ( + + + + + + + + {pipelines.map((pipeline, index) => { + var bgColor = "#27292d"; + if (index % 2 === 0) { + bgColor = "#1f2023"; + } + + return ( + + + + + {pipeline.workflow_id} + + } + /> + + + + + ); + })} + + )}*/}
) : null; @@ -3867,18 +5165,155 @@ If you're interested, please let me know a time that works for you, or set up a setAuthenticationFields(newfields); }; + + const handleAppAuthGroupCheckbox = (data) => { + let appOrginal = data.app + if (appsForAppAuthGroup.includes(appOrginal.id)) { + return; + } + + setAppsForAppAuthGroup([...appsForAppAuthGroup, data.id]); + console.log("Apps for app auth group: ", appsForAppAuthGroup); + }; + const authenticationView = curTab === 2 ? ( + <> + {/* (appAuthenticationGroupModalOpen : { */} + {appAuthenticationGroupModalOpen && ( + { + setAppAuthenticationGroupModalOpen(false); + }} + PaperProps={{ + style: { + backgroundColor: theme.palette.surfaceColor, + color: "white", + minWidth: "1200px", + minHeight: "320px", + }, + }} + > + + App Authentication Groups + + + +
+ { + setAppAuthenticationGroupName(event.target.value); + }} + /> +
+
+ { + setAppAuthenticationGroupDescription(event.target.value); + }} + /> +
+ +
+ {/* Show a check box list of all app authentications to add to the auth group */} +
+ {authentication.map((data, index) => ( +
+ +
+ + { + handleAppAuthGroupCheckbox(data) + }} + name={data.label} + disabled={data.app.id in appsForAppAuthGroup} + /> +
+ + } + label={data.label} + /> +
+ ))} +
+ +
+ + +
+ +
+
+
+ )} + +

App Authentication

- Control the authentication options for individual apps. + Control the authentication options for individual apps.   @@ -3922,15 +5357,16 @@ If you're interested, please let me know a time that works for you, or set up a */} - + {authentication === undefined || authentication === null @@ -3963,7 +5399,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; + const isDistributed = + data.suborg_distributed === true ? true : false; return ( @@ -4031,7 +5468,8 @@ 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", + overflow: "auto", + marginRight: 10, }} /> { updateAppAuthentication(data); }} - disabled={data.org_id !== selectedOrganization.id ? true : false} + disabled={ + data.org_id !== selectedOrganization.id ? true : false + } > @@ -4059,14 +5499,17 @@ If you're interested, please let me know a time that works for you, or set up a > { editAuthenticationConfig(data.id); }} > - + ) : ( @@ -4078,17 +5521,21 @@ If you're interested, please let me know a time that works for you, or set up a {}} - disabled={data.org_id !== selectedOrganization.id ? true : false} + disabled={ + data.org_id !== selectedOrganization.id + ? true + : false + } > - + )} { deleteAuthentication(data); }} @@ -4097,41 +5544,219 @@ If you're interested, please let me know a time that works for you, or set up a - {selectedOrganization.id !== undefined && data.org_id !== selectedOrganization.id ? - - - - : - - { - changeDistribution(data, !isDistributed) - }} - /> - - } + {selectedOrganization.id !== undefined && + data.org_id !== selectedOrganization.id ? ( + + + + ) : ( + + { + changeDistribution(data, !isDistributed); + }} + /> + + )} ); })}
+ + {/*
+
+

App Authentication Groups

+ + Groups of authentication options for subflows.{" "} +
+ Learn more about App Authentication Groups + + + + + + + + + + + + + + {appAuthenticationGroups.map((data, index) => { + var bgColor = "#27292d"; + if (index % 2 === 0) { + bgColor = "#1f2023"; + } + return ( + + + + + {data.app_auths.map((appAuth, index) => ( + + {appAuth.app.name} + + ))} +
+ } + style={{ minWidth: 250, maxWidth: 250 }} + /> + + + { + }} + disabled={true} + > + + + { + // deleteAppAuthenticationGroup(data); + }} + disabled={true} + > + + +
+ } + style={{ minWidth: 150, maxWidth: 150 }} + /> + + + ); + } + )} + + + + +
+
*/} + ) : null; - const changeRecommendation = (recommendation, action) => { + const getLogs = async (ip, userId) => { + setLogsLoading(true); + console.log("logs loading: ", logsLoading); + fetch(`${globalUrl}/api/v1/users/${userId}/audit?user_ip=${ip}`, { + mode: "cors", + method: "GET", + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then((response) => { + return response.json(); + }) + .then((responseJson) => { + console.log("ResponseJSON: ", responseJson); + if (responseJson.success === true) { + setLogs(responseJson.logs); + } else { + if ( + responseJson.success === false || + responseJson.reason !== undefined + ) { + console.log("Reason given: ", responseJson.reason); + toast("Failed getting logs: " + responseJson.reason); + setLogs([]); + } else { + toast("Failed getting logs"); + } + } + console.log("logs loading now: ", logsLoading); + setLogsLoading(false); + }) + .catch((error) => { + console.log("Error: ", error); + toast("Failed getting logs. Please contact: ", error); + console.log("logs loading now: ", logsLoading); + setLogsLoading(false); + }); + }; + + const changeRecommendation = (recommendation, action) => { const data = { action: action, name: recommendation.name, @@ -4157,22 +5782,27 @@ If you're interested, please let me know a time that works for you, or set up a }) .then((responseJson) => { if (responseJson.success === true) { - if (checkLogin !== undefined) { - checkLogin() - getEnvironments() - } + if (checkLogin !== undefined) { + checkLogin(); + getEnvironments(); + } } else { - if (responseJson.success === false && responseJson.reason !== undefined) { - toast("Failed change recommendation: ", responseJson.reason) - } else { - toast("Failed change recommendation"); - } + if ( + responseJson.success === false && + responseJson.reason !== undefined + ) { + toast("Failed change recommendation: ", responseJson.reason); + } else { + toast("Failed change recommendation"); + } } }) .catch((error) => { - toast("Failed dismissing alert. Please contact support@shuffler.io if this persists."); + toast( + "Failed dismissing alert. Please contact support@shuffler.io if this persists.", + ); }); - } + }; const environmentView = curTab === 6 ? ( @@ -4180,11 +5810,12 @@ If you're interested, please let me know a time that works for you, or set up a

Environments

- Decides what Orborus environment to execute an action in a workflow - in.{" "} + Decides what Orborus environment to run your workflow actions. If + you have scale problems, talk to our team: + support@shuffler.io.  @@ -4223,34 +5854,42 @@ If you're interested, please let me know a time that works for you, or set up a }} /> - + + + 0) { - foundIndex = userdata.priorities.findIndex(prio => prio.name.includes("CPU") && prio.active === true) + // Check if there's a notification for it in userdata.priorities + var showCPUAlert = false; + var foundIndex = -1; + if ( + userdata !== undefined && + userdata !== null && + userdata.priorities !== undefined && + userdata.priorities !== null && + userdata.priorities.length > 0 + ) { + foundIndex = userdata.priorities.findIndex( + (prio) => prio.name.includes("CPU") && prio.active === true, + ); - if (foundIndex >= 0 && userdata.priorities[foundIndex].name.endsWith(environment.Name)) { - showCPUAlert = true - } - } + if ( + foundIndex >= 0 && + userdata.priorities[foundIndex].name.endsWith( + environment.Name, + ) + ) { + showCPUAlert = true; + } + } - //console.log("Show CPU alert: ", showCPUAlert) - - const queueSize = environment.queue !== undefined && environment.queue !== null ? environment.queue < 0 ? 0 : environment.queue > 1000 ? ">1000" : environment.queue : 0 + const queueSize = + environment.queue !== undefined && environment.queue !== null + ? environment.queue < 0 + ? 0 + : environment.queue > 1000 + ? ">1000" + : environment.queue + : 0; return ( - - - - - Not running -
- : environment.running_ip.split(":")[0] - : "N/A" - } - style={{ - minWidth: 200, - maxWidth: 200, - overflow: "hidden", - }} - /> + + + + + + ) : environment.run_type === "docker" ? ( + + + + ) : environment.run_type === "k8s" ? ( + + + + ) : ( + + + + ) + } + style={{ + minWidth: 50, + maxWidth: 50, + overflow: "hidden", + }} + /> + + + + ) : ( + + + + + + ) + } + style={{ + minWidth: 85, + maxWidth: 85, + overflow: "hidden", + }} + /> + + Not running
+ ) : ( + environment.running_ip.split(":")[0] + ) + ) : ( + "N/A" + ) + } + style={{ + minWidth: 150, + maxWidth: 150, + overflow: "hidden", + }} + /> - - { - if (environment.Type === "cloud") { - toast("No Orborus necessary for environment cloud. Create and use a different environment to run executions on-premises.") - return - } + + { + if (environment.Type === "cloud") { + toast( + "No Orborus necessary for environment cloud. Create and use a different environment to run executions on-premises.", + ); + return; + } - if (props.userdata.active_org === undefined || props.userdata.active_org === null) { - toast("No active organization yet. Are you logged in?") - return - } + if ( + props.userdata.active_org === undefined || + props.userdata.active_org === null + ) { + toast( + "No active organization yet. Are you logged in?", + ); + return; + } - const elementName = "copy_element_shuffle"; - const auth = environment.auth === "" ? 'cb5st3d3Z!3X3zaJ*Pc' : environment.auth - const newUrl = globalUrl === "https://shuffler.io" ? "https://shuffle-backend-stbuwivzoq-nw.a.run.app" : globalUrl + const elementName = "copy_element_shuffle"; + const auth = + environment.auth === "" + ? "cb5st3d3Z!3X3zaJ*Pc" + : environment.auth; + const newUrl = + globalUrl === "https://shuffler.io" + ? "https://shuffle-backend-stbuwivzoq-nw.a.run.app" + : globalUrl; - const commandData = `docker run --volume "/var/run/docker.sock:/var/run/docker.sock" -e ENVIRONMENT_NAME="${environment.Name}" -e 'AUTH=${auth}' -e ORG="${props.userdata.active_org.id}" -e DOCKER_API_VERSION=1.40 -e BASE_URL="${newUrl}" --name="shuffle-orborus" -d ghcr.io/shuffle/shuffle-orborus:latest` - var copyText = document.getElementById(elementName); - if (copyText !== null && copyText !== undefined) { - const clipboard = navigator.clipboard; - if (clipboard === undefined) { - toast("Can only copy over HTTPS (port 3443)"); - return; - } + const commandData = `docker run --restart=always --volume "/var/run/docker.sock:/var/run/docker.sock" -e ENVIRONMENT_NAME="${environment.Name}" -e 'AUTH=${auth}' -e ORG="${props.userdata.active_org.id}" -e DOCKER_API_VERSION=1.40 -e BASE_URL="${newUrl}" --name="shuffle-orborus" -d ghcr.io/shuffle/shuffle-orborus:latest`; + var copyText = + document.getElementById(elementName); + if ( + copyText !== null && + copyText !== undefined + ) { + const clipboard = navigator.clipboard; + if (clipboard === undefined) { + toast( + "Can only copy over HTTPS (port 3443)", + ); + return; + } - navigator.clipboard.writeText(commandData); - copyText.select(); - copyText.setSelectionRange( - 0, - 99999 - ); /* For mobile devices */ + navigator.clipboard.writeText(commandData); + copyText.select(); + copyText.setSelectionRange( + 0, + 99999, + ); /* For mobile devices */ - /* Copy the text inside the text field */ - document.execCommand("copy"); + /* Copy the text inside the text field */ + document.execCommand("copy"); - toast("Orborus command copied to clipboard"); - } - }} - > - - - - } - /> + toast("Orborus command copied to clipboard"); + } + }} + > + + + + } + /> - - + + + {environment.default ? null : ( + + )} + + +
+ + + - )} - - -
- - - - - -
-
- - - {showCPUAlert === false ? null : - -
-
- - 90% CPU the server(s) hosting the Shuffle App Runner (Orborus) was found. - - - Need help with High Availability and Scale? Read documentation and Get in touch. - -
-
- -
-
-
- } - + if ( + isCloud && + environment.Name.toLowerCase() === "cloud" + ) { + rerunCloudWorkflows(environment); + } else { + abortEnvironmentWorkflows(environment); + } + }} + color="primary" + > + {isCloud && + environment.Name.toLowerCase() === "cloud" + ? "Rerun" + : "Clear"} + +
+
+
+ + + {showCPUAlert === false ? null : ( + +
+
+ + 90% CPU the server(s) hosting the Shuffle App + Runner (Orborus) was found. + + + Need help with High Availability and Scale?{" "} + + Read documentation + {" "} + and{" "} + + Get in touch + + . + +
+
+ +
+
+
+ )} + ); })} - {/**/} + {/**/}
) : null; + const imagesize = 40; + const imageStyle = { + width: imagesize, + height: imagesize, + pointerEvents: "none", + }; + const organizationsTab = curTab === 7 ? (

Organizations

- Control sub organizations (tenants)! {isCloud ? "You can only make a sub organization if you are a customer of shuffle or running a POC of the platform. Please contact support@shuffler.io to try it out." : ""}. Learn more + Control sub organizations (tenants)!{" "} + {isCloud + ? "You can only make a sub organization if you are a customer of shuffle or running a POC of the platform. Please contact support@shuffler.io to try it out." + : ""} + .{" "} + + Learn more +
+ + {parentOrg ? ( + + + +
+

+ {" "} + Your Parent Organization +

+
+ + {(() => { + const image = + parentOrg.image === "" ? ( + {parentOrg.name} + ) : ( + {parentOrg.name} + ); + const bgColor = "#27292d"; + + return ( + + + + + + + + + + + + + + + ); + })()} +
+ ) : null} + + {subOrgs.length > 0 ? ( + + +
+

+ Sub Organizations of the Current Organization ({subOrgs.length}) +

+
+ + + + + + + + + + + {subOrgs.map((data, index) => { + const image = + data.image === "" ? ( + {data.name} + ) : ( + {data.name} + ); + + var bgColor = "#27292d"; + if (index % 2 === 0) { + bgColor = "#1f2023"; + } + + return ( + + + + + + + + ); + })} + + +
+ ) : null} + + + +
+

+ All Your Organizations +

+
+ - - {userdata.orgs !== undefined && @@ -4554,15 +6627,9 @@ If you're interested, please let me know a time that works for you, or set up a props.userdata.active_org.id === undefined ? "False" : props.userdata.active_org.id === data.id - ? "True" - : "False"; + ? "True" + : "False"; - const imagesize = 40; - const imageStyle = { - width: imagesize, - height: imagesize, - pointerEvents: "none", - }; const image = data.image === "" ? ( - - { - setCloudSyncModalOpen(true); - setSelectedOrganization(data); - console.log("INVERT CLOUD SYNC"); - }} - /> - style={{ minWidth: 150, maxWidth: 150 }} - /> - - ); + style={{ minWidth: 200, maxWidth: 200 }} + primary={ + + } + /> + + ) })} ) : null} @@ -4673,13 +6736,10 @@ If you're interested, please let me know a time that works for you, or set up a
) : null; - const cacheOrgView = + const cacheOrgView = curTab === 4 ? (
- +
) : null; @@ -4720,7 +6780,7 @@ If you're interested, please let me know a time that works for you, or set up a - App Auth + App Auth /> - Datastore + Datastore /> - Schedules + Triggers /> window.removeEventListener("resize", updateSize); @@ -416,7 +408,7 @@ const AngularWorkflow = (defaultprops) => { const [subworkflow, setSubworkflow] = React.useState({}); const [subworkflowStartnode, setSubworkflowStartnode] = React.useState(""); const [leftViewOpen, setLeftViewOpen] = React.useState(isMobile ? false : true); - const [leftBarSize, setLeftBarSize] = React.useState(isMobile ? 0 : 350); + const [leftBarSize, setLeftBarSize] = React.useState(isMobile ? 0 : 325) const [creatorProfile, setCreatorProfile] = React.useState({}); const [usecases, setUsecases] = React.useState([]); const [files, setFiles] = React.useState({ @@ -493,7 +485,7 @@ const AngularWorkflow = (defaultprops) => { const [apps, setApps] = React.useState([]); const [filteredApps, setFilteredApps] = React.useState([]); - const [prioritizedApps, setPrioritizedApps] = React.useState([]); + const [prioritizedApps, setPrioritizedApps] = React.useState([]) const [environments, setEnvironments] = React.useState([]); const [established, setEstablished] = React.useState(false); @@ -505,7 +497,10 @@ const AngularWorkflow = (defaultprops) => { const [selectedAction, setSelectedAction] = React.useState({}); const [selectedActionEnvironment, setSelectedActionEnvironment] = React.useState({}); - const [streamDisabled, setStreamDisabled] = React.useState(false); + // Disabled streaming for now + const [streamDisabled, setStreamDisabled] = React.useState(true) + + const [executionRequest, setExecutionRequest] = React.useState({}); const [executionRunning, setExecutionRunning] = React.useState(false); @@ -527,8 +522,14 @@ const AngularWorkflow = (defaultprops) => { const [workflowExecutionCount, setWorkflowExecutionCount] = React.useState(0); const [defaultEnvironmentIndex, setDefaultEnvironmentIndex] = React.useState(0); const [workflowRecommendations, setWorkflowRecommendations] = React.useState(undefined); + const [showErrors, setShowErrors] = React.useState(true); + const [highlightedApp, setHighlightedApp] = React.useState("") const [listCache, setListCache] = React.useState([]); + + const [selectedOption, setSelectedOption] = React.useState(""); + const [tenzirConfigModalOpen, setTenzirConfigModalOpen] = React.useState(false); + const [suggestionBox, setSuggestionBox] = React.useState({ "position": { "top": 500, @@ -536,7 +537,108 @@ const AngularWorkflow = (defaultprops) => { }, "open": false, "attachedTo": "", - }); + }) + + // New for generated stuff + const integrationApps = [{ + "id": "integration", + "name": "Integration Framework", + "type": "ACTION", + "app_version": "1.0.0", + "loop_versions": ["1.0.0"], + "authentication": { + "type": "", + }, + "description": "Support-use only", + "actions": [{ + "name": "Cases", + "description": "Available actions for case management", + "label": "Cases", + "parameters": [{ + "name": "action", + "value": "list_tickets", + "options": [ + "list_tickets", + "get_ticket", + "create_ticket", + ], + "required": true, + }, + { + "name": "fields", + "value": "", + "required": false, + "multiline": true, + }, + /*{ + "name": "options", + "value": "deduplicate,enrich", + "required": false, + "multiselect": true, + "options": [ + "deduplicate", + "enrich", + ] + }*/ + ] + },{ + "name": "Communication", + "description": "Available actions for communication", + "label": "Communication", + "parameters": [{ + "name": "action", + "value": "list_messages", + "options": [ + "list_messages", + "send_message", + ], + "required": true, + }, + { + "name": "fields", + "value": "", + "required": false, + "multiline": true, + }] + }, + { + "name": "IAM", + "description": "Available actions for IAM", + "label": "IAM", + "parameters": [{ + "name": "action", + "value": "get_kms_key", + "options": [ + "get_kms_key", + ], + "required": true, + }, + { + "name": "fields", + "value": "", + "required": false, + "multiline": true, + }] + }, + ] + }] + + /* + { + "name": "Email", + "label": "Email", + "parameters": [{ + "name": "action", + "value": "list_email", + "options": [ + "list_email", + "send_mail", + ], + "required": true, + }], + }] + }] + */ // For code editor const [codeEditorModalOpen, setCodeEditorModalOpen] = React.useState(false); @@ -549,6 +651,127 @@ const AngularWorkflow = (defaultprops) => { "field_id": "", }) + const [loadedApps, setLoadedApps] = React.useState([]) + + const loadAppConfig = (appId, select) => { + if (appId === undefined || appId === null || appId.length === 0) { + return + } + + if (loadedApps.includes(appId)) { + return + } + + loadedApps.push(appId) + setLoadedApps(loadedApps) + + const appUrl = `${globalUrl}/api/v1/apps/${appId}/config?openapi=false` + fetch(appUrl, { + headers: { + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + return response.json() + }) + .then((responseJson) => { + console.log("Loaded app config: ", responseJson) + + if (responseJson.success === true && responseJson.app !== undefined && responseJson.app !== null && responseJson.app.length > 0) { + // Base64 decode into json + const foundapp = JSON.parse(atob(responseJson.app)) + console.log("Checked app: ", foundapp) + + const selectedAppActions = selectedApp.actions === undefined || selectedApp.actions === null ? [] : selectedApp.actions + if (foundapp.actions !== undefined && foundapp.actions !== null && foundapp.actions.length > selectedAppActions.length) { + + if (select) { + setSelectedApp(foundapp) + } + + if (apps === undefined || apps === null || apps.length === 0) { + console.log("LOAD APPS!") + } + + for (var i = 0; i < apps.length; i++) { + if (apps[i].id !== foundapp.id) { + continue + } + + apps[i] = foundapp + setApps(apps) + setFilteredApps(apps) + + // Update the local storage + localStorage.setItem("apps", JSON.stringify(apps)) + break + } + } + + // FIXME: Add it to the existing list AND update the selected app + } + }) + .catch((error) => { + console.log(`Failed side-loading app ${appId}: ${error}`) + }) + } + + // Event for making sure app is correct + useEffect(() => { + if (selectedApp === undefined || selectedApp === null && selectedApp.app_name === undefined) { + return + } + + if (apps === undefined || apps === null || apps.length === 0) { + return + } + + // Handle the activation case, as they are NOT in the event management system yet + if (selectedApp.actions === undefined || selectedApp.actions === null || selectedApp.actions.length > 1) { + return + } else { + if (selectedApp.id !== undefined && selectedApp.id !== null && selectedApp.id.length > 0) { + loadAppConfig(selectedApp.id, true) + } + } + + for (let appkey in apps) { + const curapp = apps[appkey] + if (curapp.name !== selectedApp.name) { + continue + } + + if (curapp.actions !== undefined && curapp.actions !== null && curapp.actions.length > selectedApp.actions.length) { + var foundActionIndex = -1 + for (let actionkey in curapp.actions) { + const curaction = curapp.actions[actionkey] + + // First action with a label, as they are most used (typically) + if (curaction.category_label !== undefined && curaction.category_label !== null && curaction.category_label.length > 0) { + foundActionIndex = actionkey + break + } + } + + if (foundActionIndex >= 0) { + var newaction = curapp.actions[foundActionIndex] + + setNewSelectedAction({ + "target": { + "value": newaction.name + }, + }) + } + + setSelectedApp(curapp) + } + + break + } + + }, [selectedApp]) + const [executionArgumentModalOpen, setExecutionArgumentModalOpen] = React.useState(false); // This should all be set once, not on every iteration @@ -559,42 +782,36 @@ const AngularWorkflow = (defaultprops) => { props.userdata.active_org !== undefined ? props.userdata.active_org.cloud_sync === true : false; - const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; - - useEffect(() => { - return () => { - console.log("UNMOUNTING USER!") - sendStreamRequest({ - "item": "workflow", - "type": "leave", - "id": workflow.id, - }) - } - }, []) - /* - useEffect(() => { - console.log("In useeffect for workflow: ", workflow) - if (cy === undefined || cy === null) { - return - } - - if (workflow === undefined || workflow === null || workflow.actions === undefined || workflow.actions === null) { - return - } - - // Check if actions of original vs new are changed - fetchRecommendations(workflow) - }, [workflow]) - */ + const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io" || window.location.host === "migration.shuffler.io"; const appBarSize = isCloud ? 75 : 72; const triggerEnvironments = isCloud ? ["cloud"] : ["onprem", "cloud"]; const unloadText = "Are you sure you want to leave without saving (CTRL+S)?"; const classes = useStyles(); - const [bodyWidth, bodyHeight] = useWindowSize() - //console.log("Mobile: ", isMobile, bodyWidth, bodyHeight) + var [bodyWidth, bodyHeight] = useWindowSize() const cytoscapeWidth = isMobile ? bodyWidth - leftBarSize : bodyWidth - leftBarSize - 25 + + /* + // Zoom testing to try autofixing for small screens + if (document !== undefined && document !== null && !isMobile) { + const currentZoom = document.body.style.zoom; + + if (bodyWidth < 1367 || bodyHeight < 769) { + console.log("LOWER ZOOM") + document.body.style.zoom = "80%" + bodyWidth = bodyWidth*0.8 + bodyHeight = bodyHeight*0.8 + } else { + console.log("RESET ZOOM") + document.body.style.zoom = "100%" + } + } + + console.log("Width, height: ", bodyWidth, bodyHeight) + */ + + //console.log("Mobile: ", isMobile, bodyWidth, bodyHeight) const [elements, setElements] = useState([]); const [loopRunning, setLoopRunning] = useState(false) @@ -613,7 +830,6 @@ const AngularWorkflow = (defaultprops) => { useEffect(() => { // Current variable + future state controlled // This is so that the loop can stop itself as well - console.log("In useeffect for loopRunning: ", loopRunning, loopRunning2) if (loopRunning && loopRunning2) { const intervalId = setInterval(() => { if (!loopRunning) { @@ -843,48 +1059,6 @@ const AngularWorkflow = (defaultprops) => { }); }; - function OuterLink(props) { - if (props.href.includes("http") || props.href.includes("mailto")) { - return ( - - {props.children} - - ); - } - return ( - - {props.children} - - ); - } - - function Img(props) { - return {props.alt}; - } - - function CodeHandler(props) { - return ( -
-        {props.value}
-      
- ); - } - function Heading(props) { const element = React.createElement( `h${props.level}`, @@ -985,18 +1159,30 @@ const AngularWorkflow = (defaultprops) => { }) .then((responseJson) => { if (!responseJson.success) { - toast("Error: " + responseJson.reason); + // Remove the timeout. Has to be clicked + toast.error("Error: " + responseJson.reason, { + "autoClose": false, + }) + + + } else { if (refresh === true) { - getAppAuthentication(true, true, true); + getAppAuthentication(true, true, true) } else { - getAppAuthentication(true, false); + getAppAuthentication(true, false) } - setAuthenticationModalOpen(false); - + setAuthenticationModalOpen(false) // Needs a refresh with the new authentication.. //toast("Successfully saved new app auth") + if (configureWorkflowModalOpen === true) { + setConfigureWorkflowModalOpen(false) + + setTimeout(() => { + setConfigureWorkflowModalOpen(true) + }, 1000) + } } }) .catch((error) => { @@ -1022,8 +1208,6 @@ const AngularWorkflow = (defaultprops) => { return response.json(); }) .then((responseJson) => { - console.log("GOT A RESPONSE??") - // getWorkflowExecutionCount(id); if (responseJson !== undefined && responseJson !== null && responseJson.executions !== undefined && responseJson.executions !== null) { // - means it's opposite @@ -1036,13 +1220,10 @@ const AngularWorkflow = (defaultprops) => { tmpView = execution_id; } - console.log("EXECUTION ID: ", tmpView) - // Compare with currently selected item if (tmpView !== undefined && tmpView !== null && tmpView.length > 0) { // Don't clean up if it's already open if (executionModalOpen === true) { - console.log("Execution modal already open, not cleaning up") return } @@ -1136,7 +1317,6 @@ const AngularWorkflow = (defaultprops) => { return response.json(); }) .then((responseJson) => { - //console.log("RESPONSE: ", responseJson) handleUpdateResults(responseJson, executionRequest); }) .catch((error) => { @@ -1171,6 +1351,56 @@ const AngularWorkflow = (defaultprops) => { }); }; + const handleKafkaSubmit = (trigger) => { + if (trigger.trigger_type !== "PIPELINE") { + toast("Unable to save the configuration"); + return; + } + + trigger.parameters = []; + + const topic = document.getElementById('topic')?.value; + const bootstrapServers = document.getElementById('bootstrap_servers')?.value; + const groupId = document.getElementById('group_id')?.value; + const autoOffsetReset = document.getElementById('auto_offset_reset')?.value; + + if(topic) { + trigger.parameters.push({ + name: "topic", + value: topic + }); + } else { + toast("please enter the topic name"); + return; + } + + if (bootstrapServers) { + trigger.parameters.push({ + name: "bootstrap_servers", + value: bootstrapServers + }); + } else { + toast("please enter bootstrap server details"); + return; + } + + if (groupId) { + trigger.parameters.push({ + name: "group_id", + value: groupId + }); + } + + if (autoOffsetReset) { + trigger.parameters.push({ + name: "auto_offset_reset", + value: autoOffsetReset + }); + } + + setTenzirConfigModalOpen(false); + }; + const handleColoring = (actionId, status, label) => { if (cy === undefined) { return @@ -1389,12 +1619,10 @@ const AngularWorkflow = (defaultprops) => { const sendStreamRequest = (body) => { //console.log("Stream not activated yet.") if (!isCloud) { - console.log("Stream not activated yet for onprem") return } if (streamDisabled) { - console.log("Stream disabled - send") return } @@ -1405,7 +1633,7 @@ const AngularWorkflow = (defaultprops) => { //const url = ${globalUrl}/api/v1/workflows/${props.match.params.key}/stream //const streamUrl = "http://localhost:5002" - console.log("Stream request: ", body) + //console.log("Stream request: ", body) const streamUrl = "https://stream.shuffler.io" const url = `${streamUrl}/api/v1/workflows/${props.match.params.key}/stream` @@ -1437,7 +1665,7 @@ const AngularWorkflow = (defaultprops) => { return response.json(); }) .then((responseJson) => { - console.log("Stream resp: ", responseJson) + //console.log("Stream resp: ", responseJson) }) .catch((error) => { console.log("Stream send error: ", error.toString()) @@ -1450,11 +1678,21 @@ const AngularWorkflow = (defaultprops) => { var success = false; if (isCloud && !isLoggedIn) { - console.log("Should redirect to register with redirect."); - window.location.href = `/register?view=/workflows/${props.match.params.key}&message=You need sign up to use workflows with Shuffle`; - return; + console.log("Should redirect to register with redirect.") + window.location.href = `/register?view=/workflows/${props.match.params.key}&message=You need sign up to use workflows with Shuffle` + return } + if (curworkflow === undefined || curworkflow === null) { + console.log("No workflow during save") + return + } + + if (curworkflow.actions === undefined || curworkflow.actions === null || curworkflow.actions.length === 0) { + console.log("Can't save without actions") + return + } + setSavingState(2); // This might not be the right course of action, but seems logical, as items could be running already @@ -1465,7 +1703,13 @@ const AngularWorkflow = (defaultprops) => { useworkflow = curworkflow; } - var cyelements = cy.elements(); + + + var cyelements = [] + if (cy !== undefined && cy !== null) { + cyelements = cy.elements() + } + var newActions = []; var newTriggers = []; var newBranches = []; @@ -1518,34 +1762,38 @@ const AngularWorkflow = (defaultprops) => { curworkflowAction.position = cyelements[cyelementsKey].position(); // workaround to fix some edgecases - if ( - curworkflowAction.parameters === "" || - curworkflowAction.parameters === null - ) { - curworkflowAction.parameters = []; - } + if ( + curworkflowAction.parameters === "" || + curworkflowAction.parameters === null + ) { + curworkflowAction.parameters = []; + } - if ( - curworkflowAction.example === undefined || - curworkflowAction.example === "" || - curworkflowAction.example === null - ) { - if (cyelements[cyelementsKey].data().example !== undefined) { - curworkflowAction.example = cyelements[cyelementsKey].data().example; - } - } + if ( + curworkflowAction.example === undefined || + curworkflowAction.example === "" || + curworkflowAction.example === null + ) { + if (cyelements[cyelementsKey].data().example !== undefined) { + curworkflowAction.example = cyelements[cyelementsKey].data().example; + } + } // Override just in this place curworkflowAction.errors = []; curworkflowAction.isValid = true; - // Cleans up OpenAPI items - var newparams = []; - for (let parametersKey in curworkflowAction.parameters) { - const thisitem = curworkflowAction.parameters[parametersKey]; - if (thisitem.name.startsWith("${") && thisitem.name.endsWith("}")) { - continue; - } + // Cleans up OpenAPI items + var newparams = []; + for (let parametersKey in curworkflowAction.parameters) { + const thisitem = curworkflowAction.parameters[parametersKey]; + if (thisitem.name.startsWith("${") && thisitem.name.endsWith("}")) { + continue; + } + + if (thisitem.value !== undefined && thisitem.value !== null && Array.isArray(thisitem.value)) { + thisitem.value = thisitem.value.join(",") + } newparams.push(thisitem); } @@ -1557,17 +1805,17 @@ const AngularWorkflow = (defaultprops) => { useworkflow.triggers = []; } - var curworkflowTrigger = useworkflow.triggers.find( - (a) => a.id === cyelements[cyelementsKey].data()["id"] - ); - if (curworkflowTrigger === undefined) { - curworkflowTrigger = cyelements[cyelementsKey].data(); - } + var curworkflowTrigger = useworkflow.triggers.find( + (a) => a.id === cyelements[cyelementsKey].data()["id"] + ); + if (curworkflowTrigger === undefined) { + curworkflowTrigger = cyelements[cyelementsKey].data(); + } - curworkflowTrigger.position = cyelements[cyelementsKey].position(); - if (curworkflowTrigger.canConnect === false) { - continue - } + curworkflowTrigger.position = cyelements[cyelementsKey].position(); + if (curworkflowTrigger.canConnect === false) { + continue + } newTriggers.push(curworkflowTrigger); } else if (type === "COMMENT") { @@ -1657,6 +1905,10 @@ const AngularWorkflow = (defaultprops) => { } } + if (useworkflow.id === undefined || useworkflow.id === null || useworkflow.id.length === 0) { + useworkflow.id = props.match.params.key + } + setLastSaved(true); fetch(`${globalUrl}/api/v1/workflows/${useworkflow.id}`, { method: "PUT", @@ -1821,7 +2073,6 @@ const AngularWorkflow = (defaultprops) => { } } - console.log("FOUNDMISSING: ", foundmissing) if (foundmissing) { //toast("This workflow contains a node that requires an execution argument. Please provide one.") setExecutionRequestStarted(false) @@ -2063,6 +2314,7 @@ const AngularWorkflow = (defaultprops) => { const pretend_apps = [{ "name": "TBD", + "id": "TBD", "app_name": "TBD", "app_version": "TBD", "description": "TBD", @@ -2071,9 +2323,9 @@ const AngularWorkflow = (defaultprops) => { }] setAppsLoaded(true) - setApps(pretend_apps) setFilteredApps(pretend_apps) - setPrioritizedApps(pretend_apps); + setApps(Array.prototype.concat.apply(pretend_apps, triggers)) + setPrioritizedApps(pretend_apps) if (isLoggedIn) { toast("Something went wrong while loading apps. Please refresh the window to try again.") @@ -2082,13 +2334,16 @@ const AngularWorkflow = (defaultprops) => { return } - return response.json(); + console.log("Apps loaded. JSON decoding next") + + return response.json() }) .then((responseJson) => { if (responseJson === null) { console.log("No response") const pretend_apps = [{ "name": "TBD", + "id": "TBD", "app_name": "TBD", "app_version": "TBD", "description": "TBD", @@ -2096,10 +2351,10 @@ const AngularWorkflow = (defaultprops) => { "large_image": "", }] - setAppsLoaded(true) - setApps(pretend_apps) + setAppsLoaded(true) setFilteredApps(pretend_apps) - setPrioritizedApps(pretend_apps); + setApps(Array.prototype.concat.apply(pretend_apps, triggers)) + setPrioritizedApps(pretend_apps) return } @@ -2107,41 +2362,53 @@ const AngularWorkflow = (defaultprops) => { return } - // Used for e.g. Liquid testing - const foundTools = responseJson.find((app) => app.name === "Shuffle Tools") - if (foundTools !== undefined && foundTools !== null) { - setToolsApp(foundTools) - } + // Used for e.g. Liquid testing + const foundTools = responseJson.find((app) => app.name === "Shuffle Tools") + if (foundTools !== undefined && foundTools !== null) { + setToolsApp(foundTools) + } - setApps(responseJson); + // Set localstorage for the apps in the "apps" key + setApps(Array.prototype.concat.apply(responseJson, triggers)) + if (responseJson !== undefined && responseJson !== null && responseJson.length > 0) { + try { + localStorage.setItem("apps", JSON.stringify(responseJson)) + } catch (e) { + console.log("Failed to set apps in localstorage: ", e) + } + } + + var handledPrioritizedApps = responseJson.filter((app) => internalIds.includes(app.name.toLowerCase())); + handledPrioritizedApps = [].concat(integrationApps, handledPrioritizedApps) if (isCloud) { - setFilteredApps(responseJson.filter((app) => !internalIds.includes(app.name.toLowerCase()))); - setPrioritizedApps(responseJson.filter((app) => internalIds.includes(app.name.toLowerCase()))); + setFilteredApps(responseJson.filter((app) => !internalIds.includes(app.name.toLowerCase()))) + setPrioritizedApps(handledPrioritizedApps) } else { - //setFilteredApps( - // responseJson.filter( - // (app) => - // !internalIds.includes(app.name) && - // !(!app.activated && app.generated) - // ) - //); - - var tmpFiltered = responseJson.filter((app) => !internalIds.includes(app.name.toLowerCase())) - //tmpFiltered = sortByKey(tmpFiltered, "activated") + const tmpFiltered = responseJson.filter((app) => !internalIds.includes(app.name.toLowerCase())) setFilteredApps(tmpFiltered) - - //!(!app.activated && app.generated) - setPrioritizedApps(responseJson.filter((app) => internalIds.includes(app.name.toLowerCase()))); + setPrioritizedApps(handledPrioritizedApps) } - setAppsLoaded(true) + setAppsLoaded(true) + + // Remove all cytoscape triggers first? + if (cy !== undefined && cy !== null) { + cy.removeListener("select") + } + + // Re-adding cytoscape triggers + if (cy !== undefined && cy !== null) { + cy.on("select", "node", (e) => { + onNodeSelect(e, appAuthentication) + }) + } }) .catch((error) => { - console.log("App loading error: " + error.toString()); + console.log("App loading error: " + error.toString()) setAppsLoaded(true) - //toast("App loading error: "+error.toString()); + //toast("App loading error: "+error.toString()) }); }; @@ -2550,7 +2817,6 @@ const AngularWorkflow = (defaultprops) => { const node = cy.getElementById(chunkJson.id) if (node !== undefined && node !== null) { - console.log("Node already exists: ", node) return } @@ -2702,7 +2968,7 @@ const AngularWorkflow = (defaultprops) => { //console.log('got chunk of', chunk.length, 'bytes. Value: ', chunk) text += chunk; - //console.log('text so far is', text.length, 'bytes\n'); + //console.log('text so far is', text.length, 'bytes if (result.done) { console.log('returning') return text; @@ -2773,6 +3039,48 @@ const AngularWorkflow = (defaultprops) => { } } + const [usedSubflowApps, setUsedSubflowApps] = React.useState([]); + + const getWorkflowApps = (workflow_id) => { + let apps = [] + + if (workflow_id === "") { + console.log("workflow_id is empty"); + return {}; + } + + fetch(`${globalUrl}/api/v1/workflows/${workflow_id}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for workflows :O!"); + } + + return response.json(); + }) + .then((responseJson) => { + for (let index in responseJson.actions) { + apps.push(responseJson.actions[index]); + } + + console.log("Setting used subflow apps: ", apps) + setUsedSubflowApps(apps); + + return apps + }) + .catch((error) => { + console.log("Get workflow apps error: ", error); + }); + + return apps + }; + const getWorkflow = (workflow_id, sourcenode) => { fetch(`${globalUrl}/api/v1/workflows/${workflow_id}`, { method: "GET", @@ -2789,16 +3097,22 @@ const AngularWorkflow = (defaultprops) => { if (response.status >= 500) { toast("Something went wrong while loading the workflow. Please reload.") } else { - toast("You don't access to this workflow or loading failed. Redirecting to workflows in a few seconds..") - setTimeout(() => { - window.location.pathname = "/workflows"; - }, 2000); + // Check for execution_id in URL + // don't redirect if it exists + const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; + var execFound = new URLSearchParams(cursearch).get("execution_id"); + if (execFound === null) { + toast(`You don't access to this workflow or loading failed. Redirecting to workflows in a few seconds..`) + setTimeout(() => { + window.location.pathname = "/workflows"; + }, 2000); + } } } - // Read text from stream - //return response.text(); + // Read text from stream + //return response.text(); return response.json(); }) .then((responseJson) => { @@ -2823,6 +3137,10 @@ const AngularWorkflow = (defaultprops) => { if (responseJson.triggers === undefined || responseJson.triggers === null) { responseJson.triggers = []; } + + if (responseJson.org_id !== undefined && responseJson.org_id !== null) { + listOrgCache(responseJson.org_id) + } // Wait for this to finish fetchRecommendations(responseJson) @@ -3039,8 +3357,9 @@ const AngularWorkflow = (defaultprops) => { responseJson.errors.length > 0 ) { console.log("Setting configure Modal to open") - setConfigureWorkflowModalOpen(true); } + + setConfigureWorkflowModalOpen(true) } } }) @@ -3133,17 +3452,20 @@ const AngularWorkflow = (defaultprops) => { ReactDOM.unstable_batchedUpdates(() => { setSelectedAction({}); setSelectedApp({}); - setSelectedTrigger({}); setSelectedComment({}) setSelectedEdge({}); + setSelectedEdge({}) setSelectedActionEnvironment({}) setTriggerAuthentication({}) - setSelectedTriggerIndex(-1) setTriggerFolders([]) setLocalFirstrequest(true) + setSelectedTrigger({}); + setSelectedTriggerIndex(-1) + setUpdate(Math.random()) + // Can be used for right side view setRightSideBarOpen(false); setScrollConfig({ @@ -3178,9 +3500,7 @@ const AngularWorkflow = (defaultprops) => { ) { toast("This edge can't be edited."); } else { - //console.log("DATA: ", event.target.data()) const destinationId = event.target.data("target"); - //console.log("DATA: ", event.target.data()) const curaction = workflow.actions.find((a) => a.id === destinationId); //console.log("ACTION: ", curaction) if (curaction !== undefined && curaction !== null) { @@ -3260,28 +3580,28 @@ const AngularWorkflow = (defaultprops) => { return; } - const connected = event.target.connectedEdges().jsons() + const connected = event.target.connectedEdges().jsons() if (connected.length > 0 && connected !== undefined) { - for (let connectkey in connected) { - const edge = connected[connectkey] - //console.log("EDGE:", edge) + for (let connectkey in connected) { + const edge = connected[connectkey] + //console.log("EDGE:", edge) - //const edge = edgeBase.json() + //const edge = edgeBase.json() - const sourcenode = cy.getElementById(edge.data.source) - const destinationnode = cy.getElementById(edge.data.target) - if (sourcenode === undefined || sourcenode === null || destinationnode === undefined || destinationnode === null) { - continue - } + const sourcenode = cy.getElementById(edge.data.source) + const destinationnode = cy.getElementById(edge.data.target) + if (sourcenode === undefined || sourcenode === null || destinationnode === undefined || destinationnode === null) { + continue + } - const edgeCurve = calculateEdgeCurve(sourcenode.position(), destinationnode.position()) - const currentedge = cy.getElementById(edge.data.id) - if (currentedge !== undefined && currentedge !== null) { - currentedge.style('control-point-distance', edgeCurve.distance) - currentedge.style('control-point-weight', edgeCurve.weight) - } + const edgeCurve = calculateEdgeCurve(sourcenode.position(), destinationnode.position()) + const currentedge = cy.getElementById(edge.data.id) + if (currentedge !== undefined && currentedge !== null) { + currentedge.style('control-point-distance', edgeCurve.distance) + currentedge.style('control-point-weight', edgeCurve.weight) } } + } if (styledElements.length === 1) { console.log( @@ -3349,6 +3669,7 @@ const AngularWorkflow = (defaultprops) => { ((nodedata.app_name !== "Shuffle Tools" && nodedata.app_name !== "Testing" && nodedata.app_name !== "Shuffle Workflow" && + nodedata.app_name !== "Integration Framework" && nodedata.app_name !== "User Input") || nodedata.isStartNode) ) { @@ -3369,7 +3690,7 @@ const AngularWorkflow = (defaultprops) => { if (nodedata.app_name === "Webhook" || nodedata.app_name === "Schedule" || nodedata.app_name === "Gmail" || nodedata.app_name === "Office365") { console.log("Found triggers. Add!") - if (!found) { + if (!found) { console.log("Find amount of executions for the specific nodetype: ", nodedata.app_name, "Executions: ", workflowExecutions) // Find how many executions it has var executions = 0 @@ -3423,9 +3744,9 @@ const AngularWorkflow = (defaultprops) => { cy.add(decoratorNode).unselectify(); } else { - console.log("Node already exists - don't add descriptor node"); + //console.log("Node already exists - don't add descriptor node"); } - } + } } originalLocation = { @@ -3586,8 +3907,96 @@ const AngularWorkflow = (defaultprops) => { const onNodeSelect = (event, newAppAuth) => { // Forces all states to update at the same time, // Otherwise everything is SUPER slow - const data = event.target.data(); - if (data.isSuggestion === true) { + + //const data = JSON.parse(JSON.stringify(event.target.data())) + const data = event.target.data() + + console.log("NODE SELECT: ", data) + + if (data.app_name === "Shuffle Workflow") { + console.log("Shuffle Workflow selected") + if (data.parameters[0].value !== undefined && data.parameters[0].value !== null && data.parameters[0].value.length > 0) { + console.log("Get workflow apps calling") + getWorkflowApps(data.parameters[0].value) + } + } + + if (data.buttonType == "ACTIONSUGGESTION") { + const attachedToId = data.attachedTo + + const parentitemRaw = cy.getElementById(data.attachedTo) + const parentitem = parentitemRaw.data() + if (parentitem !== null && parentitem !== undefined) { + setTimeout(() => { + parentitemRaw.select() + + const allNodes = cy.nodes().jsons() + for (var _key in allNodes) { + const currentNode = allNodes[_key] + + if (currentNode.data.buttonType === "ACTIONSUGGESTION") { + cy.getElementById(currentNode.data.id).remove() + } + } + }, 100) + + const findaction = data.label + console.log("CLICKED: ", findaction, apps.length) + + for (let appkey in apps) { + const curapp = apps[appkey] + if (curapp.name !== parentitem.app_name) { + continue + } + + if (curapp.actions === undefined || curapp.actions === null) { + continue + } + + for (let actionkey in curapp.actions) { + const curaction = curapp.actions[actionkey] + + if (curaction.category_label !== undefined && curaction.category_label !== null && curaction.category_label.length > 0) { + if (curaction.category_label[0].toLowerCase() === findaction.toLowerCase()) { + console.log("FOUND: ", curaction) + + // Update the action itself + // Find the action index, and update: + // - label + // - description + // - parameters + // - name + + var foundindex = -1 + for (let wfactionkey in workflow.actions) { + const wfaction = workflow.actions[wfactionkey] + if (wfaction.id === data.attachedTo) { + foundindex = wfactionkey + break + } + } + + console.log("Updating action: ", foundindex, findaction) + if (foundindex >= 0) { + workflow.actions[foundindex].label = findaction + workflow.actions[foundindex].description = curaction.description + workflow.actions[foundindex].parameters = curaction.parameters + workflow.actions[foundindex].name = curaction.name + + setWorkflow(workflow) + } + break + } + } + } + + break + } + + return + } + + } else if (data.isSuggestion === true) { console.log("Suggestion! Replace with a real action.") const attachedToId = data.attachedTo @@ -3823,42 +4232,50 @@ const AngularWorkflow = (defaultprops) => { workflow.actions.push(newNodeData); - const sourcebranches = workflow.branches.filter( - (foundbranch) => foundbranch.source_id === parentNode.data("id") - ); - const destinationbranches = workflow.branches.filter( - (foundbranch) => - foundbranch.destination_id === parentNode.data("id") - ); + const sourcebranches = workflow.branches.filter((foundbranch) => foundbranch.source_id === parentNode.data("id")) + + + const destinationbranches = workflow.branches.filter((foundbranch) => foundbranch.destination_id === parentNode.data("id")) + for (var sourceBranchesKey in sourcebranches) { var newbranch = JSON.parse(JSON.stringify(sourcebranches[sourceBranchesKey])); - newbranch.id = uuidv4(); - newbranch.source_id = newNodeData.id; - newbranch._id = newbranch.id; - newbranch.source = newbranch.source_id; - newbranch.target = newbranch.destination_id; - cy.add({ - group: "edges", - data: newbranch, - }); + newbranch.id = uuidv4() + newbranch.source_id = newNodeData.id + + newbranch._id = newbranch.id + newbranch.source = newbranch.source_id + newbranch.target = newbranch.destination_id + cy.add({ + group: "edges", + data: newbranch, + }) } for (var destinationBranchesKey in destinationbranches) { - var newbranch = JSON.parse( - JSON.stringify(destinationbranches[destinationBranchesKey]) - ); - newbranch.id = uuidv4(); - newbranch.destination_id = newNodeData.id; + var newbranch = JSON.parse(JSON.stringify(destinationbranches[destinationBranchesKey])) - newbranch._id = newbranch.id; - newbranch.source = newbranch.source_id; - newbranch.target = newbranch.destination_id; - cy.add({ - group: "edges", - data: newbranch, - }); + const sourcenode = cy.getElementById(newbranch.source_id) + if (sourcenode !== null && sourcenode !== undefined) { + const sourcedata = sourcenode.data() + + if (sourcedata.trigger_type !== "SUBFLOW" && sourcedata.trigger_type !== "USERINPUT") { + continue + } + + } + + newbranch.id = uuidv4() + newbranch.destination_id = newNodeData.id + + newbranch._id = newbranch.id + newbranch.source = newbranch.source_id + newbranch.target = newbranch.destination_id + cy.add({ + group: "edges", + data: newbranch, + }) } //event.target.unselect(); @@ -3917,7 +4334,7 @@ const AngularWorkflow = (defaultprops) => { ) if (curapp === undefined || curapp === null) { - console.log("Couldn't find ID - checking with name & version") + console.log("Couldn't find app with that ID - checking with name & version") curapp = newapps.find((a) => a.name === curaction.app_name && @@ -3927,66 +4344,109 @@ const AngularWorkflow = (defaultprops) => { ) } + if (curapp === undefined || curapp === null) { + curapp = integrationApps.find((a) => + a.name === curaction.app_name && + (a.app_version === curaction.app_version || + (a.loop_versions !== null && + a.loop_versions.includes(curaction.app_version))) + ) + } + if (curaction.template === true && curaction.name !== undefined) { //newapps. const parsedname = curaction.name.replaceAll(" ", "_").toLowerCase() console.log("FIND AN ACTION AMONG THE APPS THAT MATCHES NAME: ", parsedname) - curaction.matching_actions = [] - for (var newAppskey in newapps) { - for (let actionsSubkey in newapps[newAppskey].actions) { - const tmpaction = newapps[newAppskey].actions[actionsSubkey] - if (tmpaction.name.replaceAll(" ", "_").toLowerCase() === parsedname) { - console.log("MATCH!: ", newapps[newAppskey]) - curaction.matching_actions.push({ - "app_name": newapps[newAppskey].name, - "app_version": newapps[newAppskey].app_version, - "app_id": newapps[newAppskey].id, - "action": tmpaction, - "large_image": newapps[newAppskey].large_image, - "app_index": newAppskey, - "action_index": actionsSubkey, - }) + curaction.matching_actions = [] + for (var newAppskey in newapps) { + for (let actionsSubkey in newapps[newAppskey].actions) { + const tmpaction = newapps[newAppskey].actions[actionsSubkey] + if (tmpaction.name.replaceAll(" ", "_").toLowerCase() === parsedname) { + console.log("MATCH!: ", newapps[newAppskey]) + curaction.matching_actions.push({ + "app_name": newapps[newAppskey].name, + "app_version": newapps[newAppskey].app_version, + "app_id": newapps[newAppskey].id, + "action": tmpaction, + "large_image": newapps[newAppskey].large_image, + "app_index": newAppskey, + "action_index": actionsSubkey, + }) + } + } + } + } + + if (!curapp || curapp === undefined) { + // Check local storage has it + const foundapps = localStorage.getItem("apps") + if (foundapps !== null && foundapps !== undefined) { + try { + const parsedapps = JSON.parse(foundapps) + if (parsedapps !== null && parsedapps !== undefined && parsedapps.length > 0) { + for (let appkey in parsedapps) { + if (parsedapps[appkey].name === curaction.app_name) { + curapp = parsedapps[appkey] + break } } } + } catch (e) { + console.log("Problem with parsing apps from local storage", e) } - if (!curapp || curapp === undefined) { - console.log("APPS - couldn't find it: ", newapps) - //toast(`App ${curaction.app_name}:${curaction.app_version} not found. Is it activated?`); + } else { + console.log("No apps found in local storage") + } + } + /* + if (curapp && curapp.app_id !== undefined && curapp.app_id !== null && curapp.app_id.length > 0 &&curapp.actions.length <= 1) { + toast(`Side-loading app ${curapp.name} to get actions.`) + } + */ + + if (!curapp || curapp === undefined) { const tmpapp = { name: curaction.app_name, app_name: curaction.app_name, app_version: curaction.app_version, id: curaction.app_id, actions: [curaction], - }; + } - setSelectedApp(tmpapp); - setSelectedAction(curaction); + setSelectedApp(tmpapp) + setSelectedAction(curaction) } else { - //if (curapp.id !== curaction.id) { - // curaction.app_id = curapp.id - // //.valueOf() - //} + curaction.app_id = curapp.id - setAuthenticationType( - curapp.authentication.type === "oauth2" && curapp.authentication.redirect_uri !== undefined && curapp.authentication.redirect_uri !== null ? { - type: "oauth2", - redirect_uri: curapp.authentication.redirect_uri, - refresh_uri: curapp.authentication.refresh_uri, - token_uri: curapp.authentication.token_uri, - scope: curapp.authentication.scope, - client_id: curapp.authentication.client_id, - client_secret: curapp.authentication.client_secret, - grant_type: curapp.authentication.grant_type, - } : { - type: "", - } - ) + if (curapp.authentication === undefined || curapp.authentication === null) { + setAuthenticationType({ + type: "", + }) + + curapp.authentication = { + type: "", + required: false, + } + } else { + setAuthenticationType( + curapp.authentication.type === "oauth2-app" || (curapp.authentication.type === "oauth2" && curapp.authentication.redirect_uri !== undefined && curapp.authentication.redirect_uri !== null) ? { + type: curapp.authentication.type, + redirect_uri: curapp.authentication.redirect_uri, + refresh_uri: curapp.authentication.refresh_uri, + token_uri: curapp.authentication.token_uri, + scope: curapp.authentication.scope, + client_id: curapp.authentication.client_id, + client_secret: curapp.authentication.client_secret, + grant_type: curapp.authentication.grant_type, + } : { + type: "", + } + ) + } const requiresAuth = curapp.authentication.required; //&& ((curapp.authentication.parameters !== undefined && curapp.authentication.parameters !== null) || (curapp.authentication.type === "oauth2" && curapp.authentication.redirect_uri !== undefined && curapp.authentication.redirect_uri !== null)) setRequiresAuthentication(requiresAuth); @@ -4116,7 +4576,6 @@ const AngularWorkflow = (defaultprops) => { //} //curaction.parameters = newparams - console.log("ACTION CLICK: ", curaction) setSelectedApp(curapp); setSelectedAction(curaction); @@ -4151,24 +4610,12 @@ const AngularWorkflow = (defaultprops) => { (a) => a.id === data.id ); - //console.log("Trigger: ", data, trigger_index) if (trigger_index === -1) { workflow.triggers.push(data) trigger_index = workflow.triggers.length - 1 setWorkflow(workflow) } - //console.log("Trigger2: ", data, trigger_index) - //if (data.id !== undefined && data.app_name !== undefined) { - // //newapps.push(data) - // workflow.actions.push(data) - // curaction = data - //} else { - // toast("Action not found. Please remake it."); - // event.target.remove(); - // return; - //} - if (data.app_name === "Shuffle Workflow" || data.app_name === "User Input") { // Check if public workflow @@ -4180,7 +4627,7 @@ const AngularWorkflow = (defaultprops) => { } } else if (data.app_name === "Webhook") { if (workflow.triggers[trigger_index].parameters !== undefined && workflow.triggers[trigger_index].parameters !== null && workflow.triggers[trigger_index].parameters.length > 0) { - console.log("Can set params here!") + console.log("Can set params here!") workflow.triggers[trigger_index].parameters[0] = { name: "url", value: referenceUrl + "webhook_" + workflow.triggers[trigger_index].id, @@ -4194,11 +4641,32 @@ const AngularWorkflow = (defaultprops) => { }) } } - } + } else if (data.app_name === "Pipeline") { - setSelectedTriggerIndex(trigger_index); - setSelectedTrigger(data); - setSelectedActionEnvironment(data.env); + // Check if environment is set + if (data.environment === undefined || data.environment === null || data.environment === "" || data.environment.toLowerCase() === "cloud") { + for (var envKey in environments) { + if (environments[envKey].archived === true) { + continue + } + + if (environments[envKey].Name.toLowerCase() === "cloud") { + continue + } + + workflow.triggers[trigger_index].environment = environments[envKey].Name + data.environment = environments[envKey].Name + //setSelectedTrigger(data) + break + } + } + } + + setTimeout(() => { + setSelectedTriggerIndex(trigger_index); + setSelectedTrigger(data) + setSelectedActionEnvironment(data.env) + }, 25) } else if (data.type === "COMMENT") { setSelectedComment(data); } else { @@ -4222,6 +4690,7 @@ const AngularWorkflow = (defaultprops) => { "open": false, "attachedTo": "", }); + sendStreamRequest({ "item": "node", "type": "select", @@ -4256,8 +4725,10 @@ const AngularWorkflow = (defaultprops) => { toast("Failed to auto-activate the app. Go to /apps and activate it.") } else { if (refresh === true) { - //toast("App activated for your organization! Refresh the page to use the app.") + setHighlightedApp(appid) + //toast("App activated for your organisation! Refresh the page to use the app.") getApps() + } } }) @@ -4484,7 +4955,6 @@ const AngularWorkflow = (defaultprops) => { const foundresult = GetParamMatch(paramname, exampledata, ""); if (foundresult.length > 0) { - console.log("FOUND ReS for field: ", dstdata.parameters[dstdataParamKey].name, foundresult) if (dstdata.parameters[dstdataParamKey].value.length === 0) { dstdata.parameters[dstdataParamKey].value = `$${parentlabel}${foundresult}`; dstdata.parameters[dstdataParamKey].autocompleted = true @@ -4567,7 +5037,6 @@ const AngularWorkflow = (defaultprops) => { if (sourcenode.data("app_name") !== "Shuffle Workflow" && sourcenode.data("app_name") !== "User Input") { setTimeout(() => { const alledges = cy.edges().jsons() - console.log("edges: ", alledges, edge) var targetedge = alledges.findIndex( (data) => data.data.source === edge.source && data.data.id !== edge.id ) @@ -4577,7 +5046,7 @@ const AngularWorkflow = (defaultprops) => { event.target.remove() //console.log("Found branch already!") - toast("Triggers can have exactly one target node") + toast.error("Triggers can have exactly one target node") return @@ -4593,7 +5062,6 @@ const AngularWorkflow = (defaultprops) => { const edgeCurve = calculateEdgeCurve(sourcenode.position(), destinationnode.position()) const currentedge = cy.getElementById(edge.id) if (currentedge !== undefined && currentedge !== null) { - console.log("Setting edge curve: ", edgeCurve) currentedge.style('control-point-distance', edgeCurve.distance) currentedge.style('control-point-weight', edgeCurve.weight) } @@ -4604,7 +5072,6 @@ const AngularWorkflow = (defaultprops) => { (data) => data.id === edge.target ); if (targetnode !== -1) { - console.log("TARGETNODE: ", targetnode); if (workflow.triggers[targetnode].app_name === "User Input" || workflow.triggers[targetnode].app_name === "Shuffle Workflow" || workflow.triggers[targetnode].app_name === "Shuffle Subflow") { } else { toast("Can't have triggers as target of branch"); @@ -4706,7 +5173,6 @@ const AngularWorkflow = (defaultprops) => { /* targetnode = workflow.triggers.findIndex(data => data.id === edge.target) if (targetnode !== -1) { - console.log("TARGETNODE: ", targetnode) if (workflow.triggers[targetnode].app_name === "User Input" || workflow.triggers[targetnode].app_name === "Shuffle Workflow") { } else { toast("Can't have triggers as target of branch") @@ -4731,7 +5197,7 @@ const AngularWorkflow = (defaultprops) => { newdst !== null ) { const dstdata = RunAutocompleter(newdst.data()); - console.log("DST Autocompleter: ", dstdata); + //console.log("DST Autocompleter: ", dstdata); } var newbranch = { @@ -4900,7 +5366,6 @@ const AngularWorkflow = (defaultprops) => { // toast("Recommendations to show??") //} - console.log("Added to workflow!!") setWorkflow(workflow); fetchRecommendations(workflow) } else if (nodedata.type === "TRIGGER") { @@ -4992,7 +5457,7 @@ const AngularWorkflow = (defaultprops) => { data: newdata, }) - toast("You must STOP the trigger before deleting its branches") + toast.error("You must STOP the trigger before deleting its branches") } catch (e) { console.log("Failed re-adding edge: ", e) } @@ -5091,10 +5556,10 @@ const AngularWorkflow = (defaultprops) => { } setWorkflow(workflow); - if (data.type === "TRIGGER") { - saveWorkflow(workflow); - } - }; + //if (data.type === "TRIGGER") { + // saveWorkflow(workflow); + //} + } //var previouskey = 0 const handleKeyDown = (event) => { @@ -5386,7 +5851,6 @@ const AngularWorkflow = (defaultprops) => { } if (nodedata.finished === false) { - console.log("NODE UNFINISHED HOVEROUT: ", nodedata) // Should just be 1, so this should be fast enough :3 const incomingEdges = event.target.incomers("edge").jsons() @@ -5561,57 +6025,140 @@ const AngularWorkflow = (defaultprops) => { }); }; + const addActionSuggestions = (nodedata, event) => { + console.log("App Action suggestions being added") + if (nodedata.type !== "ACTION") { + return + } + + var parentNode = cy.$("#" + event.target.data("id")) + if (parentNode.data("isButton") || parentNode.data("buttonId")) { + return + } + + const px = parentNode.position("x") + 0; + const py = parentNode.position("y") + 100; + + const parentlabel = parentNode.data("label").toLowerCase().replace(" ", "_") + const parentname = parentNode.data("app_name").toLowerCase().replace(" ", "_") + if (!parentlabel.startsWith(parentname)) { + console.log("Bad startname to start with: ", parentname, parentlabel) + return + } + + const iconInfo = { + icon: "M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm-1 4l6 6v10c0 1.1-.9 2-2 2H7.99C6.89 23 6 22.1 6 21l.01-14c0-1.1.89-2 1.99-2h7zm-1 7h5.5L14 6.5V12z", + iconColor: buttonColor, + iconBackgroundColor: buttonBackgroundColor, + }; + + const svg_pin = ``; + const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); + + // 1. Find the app + // 2. Loop the apps' actions + // 3. Find actions based on category label IF it exists + + console.log("Fidning app match for: ", parentname) + var added = 0 + for (let appKey in apps) { + const curapp = apps[appKey] + if (curapp.name.toLowerCase().replace(" ", "_") !== parentname) { + continue + } + + if (curapp.actions === undefined || curapp.actions === null || curapp.actions.length === 0) { + continue + } + + console.log("Found matching: ", curapp.name, parentname, curapp.actions.length) + for (let actionKey in curapp.actions) { + const curaction = curapp.actions[actionKey] + + // Check if this is the current action already + if (parentNode.data("name") == curaction.name) { + continue + } + + if (curaction.category_label !== undefined && curaction.category_label !== null && curaction.category_label.length > 0) { + console.log("IN NODE ADD") + + cy.add({ + group: "nodes", + data: { + weight: 30, + id: uuidv4(), + label: curaction.category_label[0], + attachedTo: event.target.data("id"), + is_valid: true, + buttonType: "ACTIONSUGGESTION", + }, + position: { + x: px, + y: py + (added * 50), + }, + }) + + added += 1 + if (added >= 3) { + break + } + } + } + + break + } + } + const addSuggestionButtons = (nodedata, event) => { //console.log("Skipping Adding suggestion buttons") //return - // Skipping add for now. Should Re-enable + // Skipping add for now. Should Re-enable - // Add a button for autocompletion based on input - if (nodedata.type === "ACTION") { - /* - const color = "#34a853" + // Add a button for autocompletion based on input + if (nodedata.type === "ACTION") { + /* + const color = "#34a853" - // Fix icon - const iconInfo = { - icon: "M7.5 5.6 10 7 8.6 4.5 10 2 7.5 3.4 5 2l1.4 2.5L5 7zm12 9.8L17 14l1.4 2.5L17 19l2.5-1.4L22 19l-1.4-2.5L22 14zM22 2l-2.5 1.4L17 2l1.4 2.5L17 7l2.5-1.4L22 7l-1.4-2.5zm-7.63 5.29a.9959.9959 0 0 0-1.41 0L1.29 18.96c-.39.39-.39 1.02 0 1.41l2.34 2.34c.39.39 1.02.39 1.41 0L16.7 11.05c.39-.39.39-1.02 0-1.41l-2.33-2.35zm-1.03 5.49-2.12-2.12 2.44-2.44 2.12 2.12-2.44 2.44z", - iconColor: buttonColor, - iconBackgroundColor: buttonBackgroundColor, - }; + // Fix icon + const iconInfo = { + icon: "M7.5 5.6 10 7 8.6 4.5 10 2 7.5 3.4 5 2l1.4 2.5L5 7zm12 9.8L17 14l1.4 2.5L17 19l2.5-1.4L22 19l-1.4-2.5L22 14zM22 2l-2.5 1.4L17 2l1.4 2.5L17 7l2.5-1.4L22 7l-1.4-2.5zm-7.63 5.29a.9959.9959 0 0 0-1.41 0L1.29 18.96c-.39.39-.39 1.02 0 1.41l2.34 2.34c.39.39 1.02.39 1.41 0L16.7 11.05c.39-.39.39-1.02 0-1.41l-2.33-2.35zm-1.03 5.49-2.12-2.12 2.44-2.44 2.12 2.12-2.44 2.44z", + iconColor: buttonColor, + iconBackgroundColor: buttonBackgroundColor, + }; - const svg_pin = ``; - const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); + const svg_pin = ``; + const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); - const decoratorNode = { - position: { - x: event.target.position().x + 0, - y: event.target.position().y + 65, - }, - locked: true, - data: { - isButton: true, - isValid: true, - is_valid: true, - //label: "+", - attachedTo: nodedata.id, - imageColor: color, - buttonType: "suggestion", - icon: svgpin_Url, - iconBackground: iconInfo.iconBackgroundColor, - }, - }; + const decoratorNode = { + position: { + x: event.target.position().x + 0, + y: event.target.position().y + 65, + }, + locked: true, + data: { + isButton: true, + isValid: true, + is_valid: true, + //label: "+", + attachedTo: nodedata.id, + imageColor: color, + buttonType: "suggestion", + icon: svgpin_Url, + iconBackground: iconInfo.iconBackgroundColor, + }, + }; - cy.add(decoratorNode); - */ - } + cy.add(decoratorNode); + */ + } - console.log("RECS: ", workflowRecommendations) + if (workflowRecommendations === undefined || workflowRecommendations === null || workflowRecommendations.length === 0) { + return + } - if (workflowRecommendations === undefined || workflowRecommendations === null || workflowRecommendations.length === 0) { - return - } - - var parentNode = cy.$("#" + event.target.data("id")); - if (parentNode.data("isButton") || parentNode.data("buttonId")) return; + var parentNode = cy.$("#" + event.target.data("id")); + if (parentNode.data("isButton") || parentNode.data("buttonId")) return; const px = parentNode.position("x") + 0; const py = parentNode.position("y") + 200; @@ -5858,7 +6405,6 @@ const AngularWorkflow = (defaultprops) => { currentNode.data.isDescriptor ) { found = true; - console.log("FOUND THE NODE!"); break; } } @@ -5896,7 +6442,8 @@ const AngularWorkflow = (defaultprops) => { for (var _key in allNodes) { const currentNode = allNodes[_key]; // console.log("CURRENT NODE: ", currentNode) - if ((currentNode.data.isButton || currentNode.data.isSuggestion) && currentNode.data.attachedTo !== nodedata.id) { + + if ((currentNode.data.buttonType === "ACTIONSUGGESTION" || currentNode.data.isButton || currentNode.data.isSuggestion) && currentNode.data.attachedTo !== nodedata.id) { cy.getElementById(currentNode.data.id).remove(); } @@ -5930,6 +6477,8 @@ const AngularWorkflow = (defaultprops) => { // autocomplete // right click // suggestions + addActionSuggestions(nodedata, event); + if (workflow.actions.length < 4) { addSuggestionButtons(nodedata, event); } else { @@ -5938,6 +6487,7 @@ const AngularWorkflow = (defaultprops) => { } } + var parsedStyle = { "border-width": "7px", "border-opacity": ".7", @@ -5945,6 +6495,10 @@ const AngularWorkflow = (defaultprops) => { //"cursor": "pointer", } + if (nodedata.buttonType === "ACTIONSUGGESTION") { + parsedStyle["font-size"] = "18px" + } + const typeIds = cy.elements('node:selected').jsons(); for (var idkey in typeIds) { const item = typeIds[idkey] @@ -6001,7 +6555,7 @@ const AngularWorkflow = (defaultprops) => { if (incomingEdges.length > 0) { outgoingEdges.addClass("hover-highlight"); } - }; + } const onEdgeHoverOut = (event) => { if (event === null || event === undefined || event.target === null || event.target === undefined) { @@ -6197,7 +6751,6 @@ const AngularWorkflow = (defaultprops) => { if (inputworkflow.actions !== undefined && inputworkflow.actions !== null && inputworkflow.actions.length > 0) { cy.remove('*') } - console.log("INPUT: ", inputworkflow) } if (inputworkflow.actions === undefined || inputworkflow.actions === null) { @@ -6224,7 +6777,7 @@ const AngularWorkflow = (defaultprops) => { const node = {}; if (!action.isStartNode && action.app_name === "Shuffle Tools") { - const iconInfo = GetIconInfo(action); + const iconInfo = GetIconInfo(action) const svg_pin = ``; const svgpin_Url = encodeURI("data:image/svg+xml;utf-8," + svg_pin); action.large_image = svgpin_Url; @@ -6239,7 +6792,15 @@ const AngularWorkflow = (defaultprops) => { } else { action.iconBackground = iconInfo.iconBackgroundColor; } - } + } else if (action.app_name === "Integration Framework") { + const iconInfo = GetIconInfo(action) + console.log("FOUND INTEGRATION: iconInfo: ", iconInfo) + if (iconInfo !== undefined && iconInfo !== null) { + action.fillGradient = iconInfo.fillGradient + action.iconBackground = iconInfo.iconBackgroundColor + action.fillstyle = "linear-gradient" + } + } node.position = action.position; node.data = action; @@ -6271,9 +6832,11 @@ const AngularWorkflow = (defaultprops) => { const decoratorNodes = inputworkflow.actions.map((action) => { if (!action.isStartNode) { if (action.app_name === "Testing") { - return null; + return null } else if (action.app_name === "Shuffle Tools") { - return null; + return null + } else if (action.app_name === "Integration Framework") { + return null } } @@ -6417,19 +6980,16 @@ const AngularWorkflow = (defaultprops) => { return edge; }); - if ( - inputworkflow.visual_branches !== undefined && - inputworkflow.visual_branches !== null && - inputworkflow.visual_branches.length > 0 - ) { + console.log("VISUAL BRANCHES: ", inputworkflow.visual_branches) + if (inputworkflow.visual_branches !== undefined && inputworkflow.visual_branches !== null && inputworkflow.visual_branches.length > 0) { const visualedges = inputworkflow.visual_branches.map((branch, index) => { const edge = {}; if (inputworkflow.branches[index] === undefined) { - return {}; + return {} } - var conditions = inputworkflow.branches[index].conditions; + var conditions = inputworkflow.branches[index].conditions if (conditions === undefined || conditions === null) { conditions = []; } @@ -6442,12 +7002,12 @@ const AngularWorkflow = (defaultprops) => { target: branch.destination_id, label: label, decorator: true, - }; + } - return edge; + return edge }); - edges = edges.concat(visualedges); + edges = edges.concat(visualedges) } @@ -6484,7 +7044,7 @@ const AngularWorkflow = (defaultprops) => { } console.log("Setupgraph done 2!") - }; + } const removeNode = (nodeId) => { const selectedNode = cy.getElementById(nodeId); @@ -6499,8 +7059,7 @@ const AngularWorkflow = (defaultprops) => { // Get selected node if (selectedNode.data().type === "TRIGGER") { - console.log("Should remove trigger!"); - console.log(selectedNode.data()); + const triggerindex = workflow.triggers.findIndex( (data) => data.id === selectedNode.data().id ); @@ -6515,6 +7074,20 @@ const AngularWorkflow = (defaultprops) => { } else if (selectedNode.data().trigger_type === "EMAIL") { setSelectedTrigger(selectedNode.data()); stopMailSub(selectedTrigger, triggerindex); + } else if (selectedNode.data().trigger_type === "PIPELINE") { + setSelectedTrigger(selectedNode.data()); + + const pipelineConfig = { + command: "", + name: selectedNode.data().label, + type: "delete", + environment: selectedNode.data().environment, + workflow_id: workflow.id, + trigger_id: selectedNode.data().id, + start_node: "", + }; + + submitPipeline(selectedNode.data(), triggerindex, pipelineConfig); } } @@ -6644,8 +7217,6 @@ const AngularWorkflow = (defaultprops) => { } const getRevisionHistory = (workflow_id) => { - console.log("Loading revisions for workflow ID ", workflow_id) - fetch(`${globalUrl}/api/v1/workflows/${workflow_id}/revisions`, { method: "GET", headers: { @@ -6687,7 +7258,7 @@ const AngularWorkflow = (defaultprops) => { setFirstrequest(false); getWorkflow(props.match.params.key, {}); getRevisionHistory(props.match.params.key) - getApps(); + getApps() fetchUsecases() const cursearch = typeof window === "undefined" || window.location === undefined ? "" : window.location.search; @@ -6771,6 +7342,7 @@ const AngularWorkflow = (defaultprops) => { cy.edgehandles({ handleNodes: (el) => { if (el.isNode() && + el.data("buttonType") != "ACTIONSUGGESTION" && !el.data("isButton") && !el.data("isDescriptor") && !el.data("isSuggestion") && @@ -6810,7 +7382,6 @@ const AngularWorkflow = (defaultprops) => { cy.on("boxstart", (e) => { console.log("START"); - //cy.removeListener("select"); }); cy.on("boxend", (e) => { @@ -6821,6 +7392,10 @@ const AngularWorkflow = (defaultprops) => { } }); + cy.on('grab', 'edge', (e) => { + console.log("Edge grabbed: ", e.target.data()) + }) + cy.on("select", "node", (e) => { onNodeSelect(e, appAuthentication); }); @@ -6889,11 +7464,91 @@ const AngularWorkflow = (defaultprops) => { saveWorkflow(workflow); }) .catch((error) => { - //toast(error.toString()); - console.log("Stop schedule error: ", error.toString()); + console.log("Stop schedule error: ", error.toString()) + }) + } + + const submitPipeline = (trigger, triggerindex, usecase) => { + if (trigger.name.length <= 0) { + toast("Error: name can't be empty"); + return; + } + + var mappedStartnode = ""; + const alledges = cy.edges().jsons(); + if (alledges !== undefined && alledges !== null && alledges.length > 0) { + for (let edgekey in alledges) { + const tmp = alledges[edgekey]; + console.log("TMP: ", tmp, tmp.data.source); + if (tmp.data.source === trigger.id) { + mappedStartnode = tmp.data.target; + break; + } + } + } + const data = usecase; + data.start_node = mappedStartnode + + if (data.type === "create") { + toast("Creating pipeline"); + } else if (data.type === "stop") { + toast("stopping pipeline"); + } + + const url = `${globalUrl}/api/v1/triggers/pipeline`; + fetch(url, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(data), + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for stream results :O!"); + } + + return response.json(); + }) + .then((responseJson) => { + if (!responseJson.success) { + if (responseJson.reason !== undefined) { + toast("Failed to set pipeline: " + responseJson.reason); + } else { + toast.error("Failed to stop pipeline") + } + } else { + if (data.type === "create") { + toast("Pipeline will be created!"); + } else if (data.type === "stop") { + toast("Pipeline will be stopped!"); + } else { + toast("Pipeline deleted!") + return + } + + trigger.parameters.push({ + name: data.name, + value: data.command, + }); + + if (data.type === "stop") trigger.status = "stopped"; + else trigger.status = "running"; + workflow.triggers[triggerindex] = trigger; + + setSelectedTrigger(trigger); + setWorkflow(workflow); + console.log("Should set the status to running and save"); + saveWorkflow(workflow); + } + }) + .catch((error) => { + console.log("Get pipeline error: ", error.toString()); }); }; - + const submitSchedule = (trigger, triggerindex) => { if (trigger.name.length <= 0) { toast("Error: name can't be empty"); @@ -6951,7 +7606,6 @@ const AngularWorkflow = (defaultprops) => { trigger.status = "running"; setSelectedTrigger(trigger); setWorkflow(workflow); - console.log("Should set the status to running and save"); saveWorkflow(workflow); } }) @@ -6961,19 +7615,20 @@ const AngularWorkflow = (defaultprops) => { }); }; + const parsedHeight = isMobile ? bodyHeight - appBarSize * 4 : bodyHeight - appBarSize - 50 const appViewStyle = { marginLeft: 5, marginRight: 5, display: "flex", flexDirection: "column", - minHeight: isMobile ? bodyHeight - appBarSize * 4 : "100%", - maxHeight: isMobile ? bodyHeight - appBarSize * 4 : "100%", + minHeight: isMobile ? bodyHeight - appBarSize * 4 : parsedHeight, + maxHeight: isMobile ? bodyHeight - appBarSize * 4 : parsedHeight, }; const paperAppStyle = { borderRadius: theme.palette.borderRadius, - minHeight: isMobile ? 50 : 100, - maxHeight: isMobile ? 50 : 100, + minHeight: isMobile ? 50 : 70, + maxHeight: isMobile ? 50 : 70, minWidth: isMobile ? 50 : "100%", maxWidth: isMobile ? 50 : "100%", marginTop: "5px", @@ -7289,7 +7944,6 @@ const AngularWorkflow = (defaultprops) => { marginRight: 5, }; - const parsedHeight = isMobile ? bodyHeight - appBarSize * 4 : bodyHeight - appBarSize - 50 return (
{ > {thisview}
- + { {isMobile ? null : Triggers} } - style={tabStyle} + style={{ + maxWidth: isMobile ? leftBarSize : leftBarSize / 3, + minWidth: isMobile ? leftBarSize : leftBarSize / 3, + flex: 1, + textTransform: "none", + padding: 0, + }} /> { />
- ); - }; + ) + } const TriggersView = () => { const triggersViewStyle = { - marginLeft: "10px", - marginRight: "10px", + marginLeft: 10, + marginRight: 10, display: "flex", flexDirection: "column", - }; + } // Predefined hurr - return (
{triggers.map((trigger, index) => { - const imagesize = isMobile ? 40 : trigger.large_image.includes("svg") ? 80 : 80 - var imageline = trigger.large_image.length === 0 ? + + /* + if (trigger.trigger_type === "PIPELINE") { + if (userdata.support !== true) { + return null + } + } + */ + + // Hiding since March 2024 + if (trigger.trigger_type === "EMAIL") { + return null + } + + const imagesize = isMobile ? 40 : trigger.large_image.includes("svg") ? 50 : 50 + var imageline = trigger.large_image.length === 0 ? : { return ( {title.length > 0 ? - + {title} : null} @@ -7420,7 +8093,7 @@ const AngularWorkflow = (defaultprops) => { >
{imageline} @@ -7430,17 +8103,18 @@ const AngularWorkflow = (defaultprops) => { style={{ display: "flex", flexDirection: "column", - marginLeft: "20px", - overflow: "hidden", + marginLeft: 20, }} > -

+ {trigger.name} -

+
- {trigger.description} + + {trigger.description} +
} @@ -7636,18 +8310,25 @@ const AngularWorkflow = (defaultprops) => { parsedApp = {}; }; + const barHeight = bodyHeight - appBarSize - 50; const appScrollStyle = { overflow: "scroll", - maxHeight: isMobile ? bodyHeight - appBarSize * 4 : bodyHeight - appBarSize - 55 - 50, - minHeight: isMobile ? bodyHeight - appBarSize * 4 : bodyHeight - appBarSize - 55 - 50, + maxHeight: isMobile ? bodyHeight - appBarSize * 4 : barHeight, + minHeight: isMobile ? bodyHeight - appBarSize * 4 : barHeight, marginTop: 1, overflowY: "auto", overflowX: "hidden", - }; + } const handleAppDrag = (e, app) => { const cycontainer = cy.container(); + + if (app.type === "TRIGGER") { + handleTriggerDrag(e, app) + return + } + //console.log("e: ", e) //console.log("Offset: ", cycontainer) @@ -7668,26 +8349,25 @@ const AngularWorkflow = (defaultprops) => { return; } - currentnode[0].renderedPosition("x", e.pageX - cycontainer.offsetLeft); - currentnode[0].renderedPosition("y", e.pageY - cycontainer.offsetTop); + currentnode[0].renderedPosition("x", e.pageX - cycontainer.offsetLeft) + currentnode[0].renderedPosition("y", e.pageY - cycontainer.offsetTop) } else { if (workflow.public) { - console.log("workflow is public - not adding"); + console.log("workflow is public - not adding") return; } - if ( - app.actions === undefined || - app.actions === null || - app.actions.length === 0 - ) { - toast( - "App " + - app.name + - " currently has no actions to perform. Please go to https://shuffler.io/apps to edit it." - ); - return; + if (app.actions === undefined || app.actions === null) { + app.actions = [] + } + + /* + if (app.actions === undefined || app.actions === null || app.actions.length === 0) { + toast("App " + app.name + " currently has no actions to perform. Please go to https://shuffler.io/apps to edit it.") + + return } + */ newNodeId = uuidv4(); const actionType = "ACTION"; @@ -7754,7 +8434,6 @@ const AngularWorkflow = (defaultprops) => { 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 } @@ -7762,7 +8441,6 @@ const AngularWorkflow = (defaultprops) => { } } - console.log("Discovered environment: ", parsedEnvironments) const newAppData = { name: app.actions[actionIndex].name, label: actionLabel, @@ -7829,11 +8507,15 @@ const AngularWorkflow = (defaultprops) => { const AppView = (props) => { const { allApps, prioritizedApps, filteredApps, extraApps } = props; + //extraApps, const [visibleApps, setVisibleApps] = React.useState( Array.prototype.concat.apply( prioritizedApps, - filteredApps.filter((innerapp) => !internalIds.includes(innerapp.id.toLowerCase())) + Array.prototype.concat.apply( + filteredApps.filter((innerapp) => !internalIds.includes(innerapp.id.toLowerCase())), + triggers + ) ) ) @@ -7844,32 +8526,38 @@ const AngularWorkflow = (defaultprops) => { const app = props.app; const [hover, setHover] = React.useState(false); - if (app.id === "" || app.name === "") { - return null - } + if (app.type !== "TRIGGER" && (app.id === "" || app.name === "")) { + return null + } - const maxlen = 24; - var newAppname = app.name; - newAppname = newAppname.charAt(0).toUpperCase() + newAppname.substring(1); + const maxlen = 35 + var newAppname = app.name + newAppname = newAppname.charAt(0).toUpperCase() + newAppname.substring(1) if (newAppname.length > maxlen) { - newAppname = newAppname.slice(0, maxlen) + ".."; + newAppname = newAppname.slice(0, maxlen) + ".." } - newAppname = newAppname.replaceAll("_", " "); + newAppname = newAppname.replaceAll("_", " ") - if (app.large_image === undefined || app.large_image === null || app.large_image === "") { - app.large_image = theme.palette.defaultImage - } + if (app.large_image === undefined || app.large_image === null || app.large_image === "") { + app.large_image = theme.palette.defaultImage + } const image = app.large_image !== undefined && app.large_image !== null && app.large_image !== "" ? app.large_image : theme.palette.defaultImage - const newAppStyle = JSON.parse(JSON.stringify(paperAppStyle)); + const newAppStyle = JSON.parse(JSON.stringify(paperAppStyle)) const pixelSize = !hover ? "2px" : "4px"; //newAppStyle.borderLeft = app.is_valid && app.actions !== null && app.actions !== undefined && app.actions.length > 0 && !(app.activated && app.generated) newAppStyle.borderLeft = app.is_valid && app.actions !== null && app.actions !== undefined && app.actions.length > 0 ? `${pixelSize} solid ${green}` : `${pixelSize} solid ${yellow}`; + if (app.id == highlightedApp && app.id !== "") { + //console.log("Found correct appid to highlight: ", app.id) + + newAppStyle.border = "3px solid " + green + } + if (!app.activated && app.generated) { newAppStyle.borderLeft = `${pixelSize} solid ${yellow}`; } @@ -7892,8 +8580,17 @@ const AngularWorkflow = (defaultprops) => { { - setHover(true); + onMouseOver={(e) => { + e.preventDefault() + e.stopPropagation() + + setHover(true) + + if (app.actions !== undefined && app.actions !== null && app.actions.length === 1) { + console.log("HOVERING: ", app.id) + loadAppConfig(app.id, false) + } + }} onMouseOut={() => { setHover(false); @@ -7989,7 +8686,7 @@ const AngularWorkflow = (defaultprops) => { > { userDrag: "none", userSelect: "none", borderRadius: theme.palette.borderRadius, - height: isMobile ? 40 : 80, - width: isMobile ? 40 : 80, + height: isMobile ? 40 : 55, + width: isMobile ? 40 : 55, }} /> @@ -8020,29 +8717,16 @@ const AngularWorkflow = (defaultprops) => { 20 ? -1 : 12, + fontSize: 19, + }} > {newAppname} - - - Version: {app.app_version} - - - - - {app.description} - - }
@@ -8052,32 +8736,39 @@ const AngularWorkflow = (defaultprops) => { }; const runSearch = (value) => { + value = value.trim().toLowerCase() if (value.length > 0) { - var newApps = allApps.filter( + // Dedup based on name + const preppedApps = Array.prototype.concat.apply(allApps, triggers).filter((app, index, self) => { + return index === self.findIndex((t) => ( + t.name === app.name + )) + }) + + var newApps = preppedApps.filter( (app) => - app.name.toLowerCase().includes(value.trim().toLowerCase()) + app.name.toLowerCase().includes(value) || - app.description.toLowerCase().includes(value.trim().toLowerCase()) + app.description.toLowerCase().includes(value) ) // Extend search if (newApps.length === 0) { - const searchvalue = value.trim().toLowerCase(); newApps = allApps.filter((app) => { - if (app.actions !== undefined && app.actions !== null) { - for (let actionkey in app.actions) { - const inneraction = app.actions[actionkey]; - if (inneraction.name.toLowerCase().includes(searchvalue)) { - return true; - } - } - } + if (app.actions !== undefined && app.actions !== null) { + for (let actionkey in app.actions) { + const inneraction = app.actions[actionkey] + if (inneraction.name.toLowerCase().includes(value)) { + return true; + } + } + } return false; - }); + }) } - setVisibleApps(newApps); + setVisibleApps(newApps) } else { setVisibleApps( prioritizedApps.concat( @@ -8085,23 +8776,19 @@ const AngularWorkflow = (defaultprops) => { (innerapp) => !internalIds.includes(innerapp.id.toLowerCase()) ) ) - ); + ) } }; const SearchBox = ({ currentRefinement, refine, isSearchStalled, }) => { - - useEffect(() => { - if (document !== undefined) { - const appsearchValue = document.getElementById("appsearch") - if (appsearchValue !== undefined && appsearchValue !== null) { - if (appsearchValue.value !== undefined && appsearchValue.value !== null && appsearchValue.value.length > 0) { - refine(appsearchValue.value) - } - } - //} - } - }, []) + if (document !== undefined) { + const appsearchValue = document.getElementById("appsearch") + if (appsearchValue !== undefined && appsearchValue !== null) { + if (appsearchValue.value !== undefined && appsearchValue.value !== null && appsearchValue.value.length > 0) { + refine(appsearchValue.value) + } + } + } return (