diff --git a/.github/install-guide.md b/.github/install-guide.md
index bc7f855d..aa763642 100755
--- a/.github/install-guide.md
+++ b/.github/install-guide.md
@@ -60,7 +60,7 @@ 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
```
diff --git a/backend/app_sdk/app_base.py b/backend/app_sdk/app_base.py
index 54b238e3..d5194909 100755
--- a/backend/app_sdk/app_base.py
+++ b/backend/app_sdk/app_base.py
@@ -1127,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}")
@@ -2524,7 +2526,7 @@ 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
@@ -2864,8 +2866,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?
@@ -2879,26 +2882,24 @@ class AppBase:
# 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~.")
pass
@@ -3435,196 +3436,101 @@ class AppBase:
handled = False
# Has a loop without a variable used inside
- if len(actualitem[0]) > 2 and actualitem[0][1] == "SHUFFLE_NO_SPLITTER":
-
- 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]
-
- #json_replacement = tmpitem.replace(actualitem[index][0], replacement, 1)
- 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)
-
- 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)
+ 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:
- 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:
- pass
-
- new_replacement.append(newvalue)
-
-
- # FIXME: Should this use new_replacement?
- tmpitem = tmpitem.replace(actualitem[index][0], replacement, 1)
# This code handles files.
- resultarray = []
isfile = False
try:
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("(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)
resultarray.append(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:
- params[parameter["name"]] = tmpitem
- multi_parameters[parameter["name"]] = new_replacement
- else:
- 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:
- # 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.
- 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"]):
- file_value = self.get_file(tmp_file_split)
- resultarray.append(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
- 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)
@@ -3653,8 +3559,7 @@ 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))
@@ -3932,7 +3837,8 @@ class AppBase:
# 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, {})
diff --git a/backend/go-app/main.go b/backend/go-app/main.go
index fd5ec88e..5ca8bc9c 100755
--- a/backend/go-app/main.go
+++ b/backend/go-app/main.go
@@ -3583,8 +3583,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
}
@@ -3668,7 +3668,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")
@@ -4780,6 +4780,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")
@@ -4908,8 +4909,8 @@ func initHandlers() {
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/triggers/pipeline/save", shuffle.HandleSavePipelineInfo).Methods("PUT", "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")
diff --git a/frontend/Dockerfile b/frontend/Dockerfile
index 2a45fb5f..e9d6d683 100755
--- a/frontend/Dockerfile
+++ b/frontend/Dockerfile
@@ -1,18 +1,17 @@
# Build environment
-FROM node:18 as builder
+FROM node:21 as builder
RUN mkdir /usr/src/app
-
WORKDIR /usr/src/app
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/
@@ -22,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/package.json b/frontend/package.json
index c45a9a9f..c8cb2465 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",
@@ -27,7 +27,7 @@
"codemirror": "^6.0.1",
"cpx": "^1.5.0",
"create-react-app": "^5.0.1",
- "cytoscape": "^3.23.0",
+ "cytoscape": "^3.29.2",
"cytoscape-clipboard": "^2.2.1",
"cytoscape-cxtmenu": "^3.4.0",
"cytoscape-edgehandles": "^3.5.1",
diff --git a/frontend/src/components/AppFramework.jsx b/frontend/src/components/AppFramework.jsx
index a73be239..d119e708 100644
--- a/frontend/src/components/AppFramework.jsx
+++ b/frontend/src/components/AppFramework.jsx
@@ -31,8 +31,8 @@ import {
Delete as DeleteIcon,
} from "@mui/icons-material";
-import * as edgehandles from "cytoscape-edgehandles";
-import * as cytoscape from "cytoscape";
+import edgehandles from "cytoscape-edgehandles";
+import cytoscape from "cytoscape";
import { toast } from 'react-toastify';
cytoscape.use(edgehandles);
diff --git a/frontend/src/components/AppGrid.jsx b/frontend/src/components/AppGrid.jsx
index 5bb08119..e84bbf35 100644
--- a/frontend/src/components/AppGrid.jsx
+++ b/frontend/src/components/AppGrid.jsx
@@ -1,289 +1,1913 @@
-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 } 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,
+} 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 noImage from "../no_image.png"
-import aa from 'search-insights'
+import CircularProgress from '@mui/material/CircularProgress';
-import {
- Zoom,
- Grid,
- Paper,
- TextField,
- ButtonBase,
- InputAdornment,
- Typography,
- Button,
- Tooltip
-} from '@mui/material';
+import algoliasearch from "algoliasearch/lite";
+import {
+ InstantSearch,
+ Configure,
+ connectSearchBox,
+ connectHits,
+ connectHitInsights,
+ RefinementList,
+ ClearRefinements,
+ connectStateResults
+} from "react-instantsearch-dom";
-const searchClient = algoliasearch("JNSS5CFDZZ", "db08e40265e2941b9a7d8f644b6e5240")
+import aa from "search-insights";
+
+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."
- 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 submitContact = (email, message) => {
+ const data = {
+ firstname: "",
+ lastname: "",
+ title: "",
+ companyname: "",
+ email: email,
+ phone: "",
+ message: message,
+ };
- setFormMail("")
- setMessage("")
+ 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),
})
- .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 (
-
- )
- }
+ var [searchQuery, setSearchQuery] = useState("");
- 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 (
+
+ );
+ };
- 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);
- 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 += " "
- }
- }
+ const handleTabChange = (event, newValue) => {
+ setCurrTab(newValue);
+ };
- 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 [isLoggedIn, setIsLoggedIn] = useState(false);
+ const [userInfo, setUserInfo] = useState([]);
- //const searchClient = algoliasearch("L55H18ZINA", "a19be455e7e75ee8f20a93d26b9fc6d6")
- console.log(searchClient)
- aa('init', {
- appId: searchClient.appId,
- apiKey: searchClient.transporter.queryParameters["x-algolia-api-key"]
- })
+ 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) {
+ setIsLoggedIn(true);
+ setUserInfo(responseJson);
+ }
+ })
+ .catch(error => {
+ console.log("Failed login check: ", error);
+ });
+ }, []);
- 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,
- }
- ])
+ //Component to fetch all app from the algolia
+ const Hits = ({
+ hits,
+ insights,
+ setIsAnyAppActivated
+ }) => {
+ const [mouseHoverIndex, setMouseHoverIndex] = useState(-1);
+ var counted = 0;
+ const [hoverEffect, setHoverEffect] = useState(-1);
- }}>
-
-
-
-
- {index === mouseHoverIndex || showName === true ?
- parsedname
- :
- null
- }
- {data.generated ?
-
- {data.invalid ?
-
- :
-
- }
-
- :
-
-
-
- }
-
-
-
-
- )
- })}
-
- )
- }
+ const normalizedString = (name) => {
+ if (typeof name === 'string') {
+ return name.replace(/_/g, ' ');
+ } else {
+ return name;
+ }
+ };
- const CustomSearchBox = connectSearchBox(SearchBox)
- const CustomHits = connectHits(Hits)
- //const CustomHits = connectHitInsights(aa)(Hits)
- const selectButtonStyle = {
- minWidth: 150,
- maxWidth: 150,
- minHeight: 50,
- }
+ const [allActivatedAppIds, setAllActivatedAppIds] = useState(() => {
+ const storedApps = isLoggedIn && localStorage.getItem('allActivatedAppIds');
+ return storedApps ? JSON.parse(storedApps) : userInfo.active_apps;
+ });
+ const [isAppActivated, setIsAppActivated] = useState(false);
+ const [isActivateAppSuccess, setIsActivateAppSuccess] = useState(false);
- return (
-
- {/*
+ //Function for activation and deactivation of app
+ const handleActivateButton = (event, data, type) => {
+ 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);
+ }
+ setIsActivateAppSuccess(prev => !prev);
+ }
+ })
+ .catch(error => {
+ console.log("app error: ", error.toString());
+ });
+ }
+
+ useEffect(() => {
+ isLoggedIn && localStorage.setItem('allActivatedAppIds', JSON.stringify(allActivatedAppIds));
+ }, [allActivatedAppIds]);
+
+
+ const memoizedHits = useMemo(() => {
+ return hits.map((data, index) => {
+ 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',
+ backgroundColor: "rgba(26, 26, 26, 1)",
+ };
+
+ const appUrl =
+ isCloud
+ ? `/apps/${data.objectID}?queryID=${data.__queryID}`
+ : `https://shuffler.io/apps/${data.objectID}?queryID=${data.__queryID}`;
+
+ //check if appExist in userInfo.active_app or not.
+ return (
+
+
+
+ {
+ setMouseHoverIndex(index);
+ }}
+ onMouseOut={() => {
+ setMouseHoverIndex(-1);
+ }}
+ >
+
+
+
+
+ {(allActivatedAppIds && allActivatedAppIds.includes(data.objectID)) && }
+ {normalizedString(data.name)}
+
+
+
+ {data.categories !== null
+ ? normalizedString(data.categories).join(", ")
+ : "NA"}
+
+
+
+ {mouseHoverIndex === index ? (
+
+ {data.tags && (
+
+
+ {data.tags.slice(0, 1).map((tag, tagIndex) => (
+
+ {normalizedString(tag)}
+ {tagIndex < 1 ? ", " : ""}
+
+ ))}
+
+
+ )}
+
+ ) : (
+
+ {data.tags &&
+ data.tags.map((tag, tagIndex) => (
+
+ {normalizedString(tag)}
+ {tagIndex < data.tags.length - 1 ? ", " : ""}
+
+ ))}
+
+ )}
+
+
+ {mouseHoverIndex === index && (
+
+ {allActivatedAppIds && allActivatedAppIds.includes(data.objectID) ? (
+ {
+ handleActivateButton(event, data, "deactivate");
+ }}>
+ Deactivate
+
+ ) : (
+ {
+ handleActivateButton(event, data, "activate");
+ }}
+ >
+ Activate
+
+ )}
+
+ )}
+
+
+
+
+
+
+
+
+ );
+ });
+ }, [hits, mouseHoverIndex, isActivateAppSuccess, allActivatedAppIds]);
+
+ return (
+
+
+ {memoizedHits}
+
+
+ );
+ };
+
+ 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 [isRefinementListExpanded, setIsRefinementListExpanded] =
+ useState(true);
+
+ const toggleRefinementList = () => {
+ setIsRefinementListExpanded((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 (
+
+
+ Category
+ {isRefinementListExpanded ? (
+
+ ) : (
+
+ )}
+
+
+ {isRefinementListExpanded && (
+ <>
+
+
+ >
+ )}
+
+ );
+ };
+
+ //Component to filter all apps base on Action label
+ const FilterByActionLabel = () => {
+ const [isActionLabelExpanded, setIsActionLabelExpanded] = useState(false);
+ 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 (
+
+
+ Labels
+ {isActionLabelExpanded ? (
+
+ ) : (
+
+ )}
+
+
+ {isActionLabelExpanded && (
+ <>
+
+
+ >
+ )}
+
+ );
+ };
+
+ //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 (
+
+
+ Created With
+ {isCreatedWithExpanded ? : }
+
+
+ {isCreatedWithExpanded && (
+ <>
+
+
+ >
+ )}
+
+ );
+ };
+
+ 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 (
+
+
+ Created By
+ {isCreatedByExpanded ? (
+
+ ) : (
+
+ )}
+
+
+ {isCreatedByExpanded && (
+ <>
+ {/* */}
+
+ {/* */}
+
+ Clear All
+
+ >
+ )}
+
+ );
+ };
+
+ const FilterApps = () => {
+ 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 }) => {
+
+ return (
+
+
+
+
+ );
+ };
+
+ //Search box for the orgs and users apps
+ const SearchBoxForOrgsAndUsersApp = ({ searchQuery, setSearchQuery }) => {
+
+ return (
+
+ )
+ }
+
+
+
+ const [selectedCategoryForUsersAndOgsApps, setselectedCategoryForUsersAndOgsApps] = useState([]);
+ const [selectedTagsForUserAndOrgApps, setSelectedTagsForUserAndOrgApps] = useState([]);
+ const [isCategoreListExpanded, setIsCategoryListExpanded] = useState(true);
+
+ const toogleCategoryList = () => {
+ setIsCategoryListExpanded((prevState) => !prevState);
+ };
+
+ //Component to display category List for User and Orgs app
+ const FilterUsersAndOrgsAppByCategory = ({ userAndOrgsApp }) => {
+
+ //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(userAndOrgsApp)) {
+ userAndOrgsApp.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, 9);
+
+ 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)"
+ }
+ return (
+
+
+ Category
+ {isCategoreListExpanded ? (
+
+ ) : (
+
+ )}
+
+ {isCategoreListExpanded && topCategories && topCategories.length > 0 && (
+
+ {topCategories.map((data, index) => (
+ handleCheckboxChange(data.category)}
+ >
+ handleCheckboxChange(data.category)}
+ style={{ marginRight: 5 }}
+ />
+ {data.category}
+
+
+ ))}
+
+
+ Clear All
+
+
+ )}
+
+ );
+ };
+
+ const [isActionLabelExpanded, setIsActionLabelExpanded] = useState(false);
+ const toogleActionLabel = () => {
+ setIsActionLabelExpanded((prevState) => !prevState);
+ };
+
+ const FilterUsersAndOrgsAppByActionLabel = ({ userAndOrgsApp }) => {
+
+ const findTopTags = () => {
+ const tagCountMap = {};
+
+ // Check if userAndOrgsApp is an array before iterating over it and Find top 10 tags from the apps
+ if (Array.isArray(userAndOrgsApp)) {
+ userAndOrgsApp.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, 9);
+
+ return topTags;
+ };
+
+ const topTags = findTopTags();
+ const [selectedCategories, setSelectedCategories] = useState([]);
+
+ 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",
+ flexDirection: "row",
+ width: "100%",
+ height: 30,
+ textTransform: 'none',
+ marginBottom: isActionLabelExpanded && 16,
+ fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)"
+ }
+
+ return (
+
+
+ Labels
+ {isActionLabelExpanded ? (
+
+ ) : (
+
+ )}
+
+
+ {isActionLabelExpanded && topTags && topTags.length > 0 && (
+ <>
+ {topTags.map((data, index) => (
+ handleCheckboxChange(index)}
+ >
+ handleCheckboxChange(index)}
+ style={{ marginRight: 5 }}
+ />
+ {data.tag}
+
+
+ ))}
+
+
+ Clear All
+
+ >
+ )}
+
+ );
+ };
+
+ const [selectedOptionOfCreatedWith, setSelectedOptionOfCreatedWith] = useState([]);
+ const [isCreatedWithExpanded, setIsCreatedWithExpanded] = useState(false);
+
+ const toogleCreatedWith = () => {
+ setIsCreatedWithExpanded((prevState) => !prevState);
+ };
+ const FilterUsersAndOrgsAppByCreatedWith = () => {
+
+ 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 (
+
+
+ Created With
+ {isCreatedWithExpanded ? : }
+
+
+ {isCreatedWithExpanded && (
+ <>
+ {AppCreatedWithOptions.map((data, index) => (
+ handleCheckboxChange(index)}
+ >
+ handleCheckboxChange(index)}
+ style={{ marginRight: 5 }}
+ />
+ {data}
+
+
+ ))}
+
+
+ Clear All
+
+ >
+ )}
+
+ );
+ };
+
+ 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 (
+
+
+ Created By
+ {isCreatedByExpanded ? (
+
+ ) : (
+
+ )}
+
+
+ {isCreatedByExpanded && (
+ <>
+
+
+ Clear All
+
+ >
+ )}
+
+ );
+ };
+
+ const FilterUserAndOrgApps = () => {
+
+ const [userAndOrgsApp, setUserAndOrgsApp] = useState([]);
+
+ useEffect(() => {
+ if (currTab === 2) {
+ const baseUrl = globalUrl;
+ const userAppsUrl = `${baseUrl}/api/v1/users/apps`;
+ fetch(userAppsUrl, {
+ method: "GET",
+ credentials: "include",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ })
+ .then((response) => response.json())
+ .then((data) => {
+ setUserAndOrgsApp(data);
+ })
+ .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) => {
+ setUserAndOrgsApp(data);
+ })
+ .catch((err) => {
+ console.error("Error fetching apps:", err);
+ });
+ }
+ }, [currTab]);
+
+
+ return (
+
+ {isLoggedIn === true && (
+
+
+ Filter By
+
+
+
+
+
+
+ )}
+
+ )
+ }
+ const [isLoading, setIsLoading] = useState(false)
+ useEffect(() => {
+ if (currTab) {
+ setselectedCategoryForUsersAndOgsApps([]);
+ setSelectedTagsForUserAndOrgApps([]);
+ setSelectedOptionOfCreatedWith([]);
+ setIsCategoryListExpanded(true);
+ setIsActionLabelExpanded(false);
+ setIsCreatedWithExpanded(false);
+ }
+ if (currTab === 1 || currTab === 2) {
+ setIsLoading(true);
+ }
+
+ }, [currTab])
+
+ //Component to fetch all apps created by user and Org
+ const UserAndOrgApps = () => {
+
+ const [searchQuery, setSearchQuery] = useState("");
+ const [userAndOrgAppData, setUserAndOrgAppData] = useState([])
+
+ const allActivatedAppIdsString = localStorage.getItem('allActivatedAppIds');
+ const allActivatedAppIds = allActivatedAppIdsString ? JSON.parse(allActivatedAppIdsString) : [];
+ const latestActivatedAppId = allActivatedAppIds.length > 0 ? allActivatedAppIds[allActivatedAppIds.length - 1] : null;
+
+ useEffect(() => {
+ if (currTab === 2 && isLoggedIn != undefined && isLoggedIn != null && isLoggedIn === true) {
+ const baseUrl = globalUrl;
+ const URL = `${baseUrl}/api/v1/users/apps`;
+ fetch(URL, {
+ method: "GET",
+ credentials: "include",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ })
+ .then((response) => {
+ return response.json();
+ })
+ .then((data) => {
+ setUserAndOrgAppData(data)
+ setIsLoading(false)
+ })
+ .catch((err) => {
+ console.error("Error fetching user apps:", err);
+ });
+ }
+ else if (currTab === 1 && isLoggedIn != undefined && isLoggedIn != null && isLoggedIn === true) {
+ const baseUrl = globalUrl;
+ const URL = `${baseUrl}/api/v1/apps`;
+ fetch(URL, {
+ method: "GET",
+ credentials: "include",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ })
+ .then((response) => {
+ return response.json();
+ })
+ .then((data) => {
+ setUserAndOrgAppData(data)
+ setIsLoading(false);
+ })
+ .catch((err) => {
+ console.error("Error fetching user apps:", err);
+ });
+ }
+ }, [])
+
+ //Search app base on app name, category and tag
+ const filteredUserAppdata = Array.isArray(userAndOrgAppData) ? userAndOrgAppData.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 memoizedHits = useMemo(() => {
+ return filteredUserAppdata.map((data, index) => {
+ const isMouseOverOnCloudIcon = false;
+ const xs = 12;
+ const rowHandler = 12;
+ const searchClient = {};
+ const userdata = {};
+
+ const paperStyle = {
+ backgroundColor: "#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 === false
+ ? `/apps/${data.id}`
+ : `https://shuffler.io/apps/${data.id}`;
+
+ return (
+
+
+
+ {
+ setMouseHoverIndex(index);
+ }}
+ onMouseOut={() => {
+ setMouseHoverIndex(-1);
+ }}
+ >
+
+
+
+
+ {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 ? ", " : ""}
+
+ ))}
+
+ {/* )} */}
+
+
+
+
+
+
+ );
+ });
+ }, [filteredUserAppdata, latestActivatedAppId]);
+
+
+ return (
+
+ {isLoggedIn ? (
+
+ {isLoading ?
: (
+
+
+
+
+ {memoizedHits}
+
+
+
+ )}
+
+ ) : (
+
+
Please login to your account first to view {`${currTab === 1 ? "Organization" : "My"}`} Apps.
+ Or signup to create a new account.
+
+ )}
+
+ );
+ };
+
+
+ const AppTab = () => {
+
+ 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);
+ return (
+
+ {/*
{
*/}
-
-
-
-
-
-
-
-
- {showSuggestion === true ?
-
-
- Can't find what you're looking for?
-
-
- setFormMail(e.target.value)}
- />
- setMessage(e.target.value)}
- />
-
-
{
- submitContact(formMail, message)
- }}
- >
- Submit
-
-
{formMessage}
-
- : null
- }
-
-
-
- Search by
-
-
-
-
-
-
-
- )
-}
+
+
+
+ {currTab === 0 ?
:
}
+
+
+ {/* */}
+
+
+ {showSuggestion === true ? (
+
+
+ Can't find what you're looking for?
+
+
+ setFormMail(e.target.value)}
+ />
+ setMessage(e.target.value)}
+ />
+
+
{
+ submitContact(formMail, message);
+ }}
+ >
+ Submit
+
+
+ {formMessage}
+
+
+ ) : null}
+
+
+ );
+};
export default AppGrid;
diff --git a/frontend/src/components/Billing.jsx b/frontend/src/components/Billing.jsx
index c3687489..8602911c 100644
--- a/frontend/src/components/Billing.jsx
+++ b/frontend/src/components/Billing.jsx
@@ -43,7 +43,7 @@ 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();
@@ -970,21 +970,29 @@ const Billing = (props) => {
const isChildOrg = userdata.active_org.creator_org !== "" && userdata.active_org.creator_org !== undefined && userdata.active_org.creator_org !== null
return (
-
+
{addDealModal}
+ {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 ?
"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."
}
-
+ }
{userdata.support === true ?
-
+
For sales: Create
New Cloud Contract
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 4d4d02fe..22500fe2 100644
--- a/frontend/src/components/CacheView.jsx
+++ b/frontend/src/components/CacheView.jsx
@@ -1,10 +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,
+ Typography,
Tooltip,
Divider,
TextField,
@@ -22,8 +22,8 @@ import {
} from "@mui/material";
import {
- Link as LinkIcon,
- AutoFixHigh as AutoFixHighIcon,
+ Link as LinkIcon,
+ AutoFixHigh as AutoFixHighIcon,
Edit as EditIcon,
FileCopy as FileCopyIcon,
SelectAll as SelectAllIcon,
@@ -46,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,
@@ -64,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("");
@@ -79,7 +79,7 @@ const CacheView = (props) => {
const [dataValue, setDataValue] = React.useState({});
const [editCache, setEditCache] = React.useState(false);
const [show, setShow] = useState({});
-
+
useEffect(() => {
listOrgCache(orgId);
}, []);
@@ -115,62 +115,25 @@ const CacheView = (props) => {
});
};
- // 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) => {
@@ -255,20 +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 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),
@@ -316,21 +279,21 @@ const CacheView = (props) => {
/>
-
-
- Value - ({isValidJson.valid === true ? "Valid" : "Invalid"} JSON)
-
-
- {
- autoFixJson(value)
- }}
- >
-
-
-
-
+
+
+ Value - ({isValidJson.valid === true ? "Valid" : "Invalid"} JSON)
+
+
+ {
+ autoFixJson(value)
+ }}
+ >
+
+
+
+
{
id="Valuefield"
margin="normal"
variant="outlined"
- multiline
- minRows={4}
- maxRows={12}
+ multiline
+ minRows={4}
+ maxRows={12}
//defaultValue={editCache ? dataValue.value : ""}
- value={value}
+ value={value}
onChange={(e) => setValue(e.target.value)}
/>
{
- setModalOpen(false)
- setValue("")
- setDataValue({})
- }}
+ onClick={() => {
+ setModalOpen(false)
+ setValue("")
+ setDataValue({})
+ }}
color="primary"
>
Cancel
@@ -372,9 +335,9 @@ const CacheView = (props) => {
style={{ borderRadius: "0px" }}
onClick={() => {
{editCache ? editOrgCache(orgId) : addOrgCache(orgId)}
-
- setValue("")
- setDataValue({})
+
+ setValue("")
+ setDataValue({})
}}
color="primary"
>
@@ -386,116 +349,117 @@ const CacheView = (props) => {
return (
-
+
{modalView}
-
-
Shuffle Datastore
-
- Datastore is a permanent key-value database for storing data that can be used cross-workflow. You can store anything from lists of IPs to complex configurations.
+
+
Shuffle Datastore
+
+ Datastore is a permanent key-value database for storing data that can be used cross-workflow. You can store anything from lists of IPs to complex configurations.
Learn more
{
setEditCache(false)
setModalOpen(true)
-
- setValue("")
- }}
+
+ setValue("")
+ }}
>
Add Cache
listOrgCache(orgId)}
>
-
-
-
+ />}
+
+
{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);
+
+ 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
- })
- setValue(data.value)
+ setDataValue({
+ "key": data.key,
+ "value":data.value
+ })
+ setValue(data.value)
setModalOpen(true)
}}
>
@@ -525,18 +489,18 @@ const CacheView = (props) => {
{
- window.open(`${globalUrl}/api/v1/orgs/${orgId}/cache/${data.key}?type=text&authorization=${data.public_authorization}`, "_blank");
- }}
+ window.open(`${globalUrl}/api/v1/orgs/${orgId}/cache/${data.key}?type=text&authorization=${data.public_authorization}`, "_blank");
+ }}
>
-
+
diff --git a/frontend/src/components/Files.jsx b/frontend/src/components/Files.jsx
index 106cd18d..441c4963 100644
--- a/frontend/src/components/Files.jsx
+++ b/frontend/src/components/Files.jsx
@@ -41,7 +41,7 @@ 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");
@@ -617,16 +617,16 @@ const Files = (props) => {
style={{
maxWidth: window.innerWidth > 1366 ? 1366 : 1200,
margin: "auto",
- padding: 20,
+ padding: isSelectedFiles ? null : 20,
}}
onDrop={uploadFile}
>
-
+
setLoadFileModalOpen(true)}
>
@@ -636,15 +636,15 @@ const Files = (props) => {
{fileDownloadModal}
-
-
Files
-
+
+
Files
+
Files from Workflows are a way to store as well as edit files.{" "}
Learn more
@@ -659,6 +659,7 @@ const Files = (props) => {
onClick={() => {
upload.click();
}}
+ style={{backgroundColor: isSelectedFiles?'rgba(255, 132, 68, 0.2)':null, color:isSelectedFiles?"#FF8444":null, borderRadius:isSelectedFiles?200:null, width:isSelectedFiles?162:null, height:isSelectedFiles?40:null, boxShadow: isSelectedFiles?'none':null,}}
>
Upload files
@@ -678,7 +679,7 @@ const Files = (props) => {
}}
/>
getFiles()}
@@ -783,20 +784,20 @@ const Files = (props) => {
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(".")
@@ -854,8 +855,8 @@ 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,
}}
/>
@@ -1063,7 +1068,7 @@ const Files = (props) => {
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/NewHeader.jsx b/frontend/src/components/NewHeader.jsx
index 75184bab..def025c2 100644
--- a/frontend/src/components/NewHeader.jsx
+++ b/frontend/src/components/NewHeader.jsx
@@ -103,7 +103,8 @@ const Header = (props) => {
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
@@ -616,7 +617,7 @@ const Header = (props) => {
- Version: 1.3.4
+ Version: 1.4.0
diff --git a/frontend/src/components/Priorities.jsx b/frontend/src/components/Priorities.jsx
index bd6e5fa8..e73374bd 100644
--- a/frontend/src/components/Priorities.jsx
+++ b/frontend/src/components/Priorities.jsx
@@ -20,7 +20,7 @@ import Priority from "../components/Priority.jsx";
//import { useAlert
const Priorities = (props) => {
- const { globalUrl, userdata, serverside, billingInfo, stripeKey, checkLogin, setAdminTab, setCurTab, notifications, setNotifications, } = props;
+ const { globalUrl, userdata,clickedFromOrgTab, serverside, billingInfo, stripeKey, checkLogin, setAdminTab, setCurTab, notifications, setNotifications, } = props;
const [showDismissed, setShowDismissed] = React.useState(false);
const [showRead, setShowRead] = React.useState(false);
const [appFramework, setAppFramework] = React.useState({});
@@ -203,7 +203,7 @@ const Priorities = (props) => {
{
}
return (
-
-
Suggestions
-
- Suggestions are tasks identified by Shuffle to help you discover ways to protect your and customers' company. These range from simple configurations in Shuffle to Usecases you may have missed.
+
+
Suggestions
+
+ Suggestions are tasks identified by Shuffle to help you discover ways to protect your and customers' company. These range from simple configurations in Shuffle to Usecases you may have missed.
Learn more
@@ -348,6 +348,7 @@ const Priorities = (props) => {
globalUrl={globalUrl}
priority={priority}
checkLogin={checkLogin}
+ clickedFromOrgTab={true}
setAdminTab={setAdminTab}
setCurTab={setCurTab}
appFramework={appFramework}
@@ -355,15 +356,15 @@ const Priorities = (props) => {
)
})
}
-
- Notifications
-
+ {clickedFromOrgTab?null: }
+ Notifications
+
Notifications help you find potential problems with your workflows and apps.
Learn more
diff --git a/frontend/src/components/Priority.jsx b/frontend/src/components/Priority.jsx
index 60e4bce2..67d56af7 100644
--- a/frontend/src/components/Priority.jsx
+++ b/frontend/src/components/Priority.jsx
@@ -22,7 +22,7 @@ import {
//import { useAlert
const Priority = (props) => {
- const { globalUrl, userdata, serverside, priority, checkLogin, setAdminTab, setCurTab, appFramework, } = props;
+ const { globalUrl, clickedFromOrgTab,userdata, serverside, priority, checkLogin, setAdminTab, setCurTab, appFramework, } = props;
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
let navigate = useNavigate();
@@ -114,7 +114,7 @@ const Priority = (props) => {
const srcSize = realignedSrc ? 35 : 30
const dstSize = realignedDst ? 35 : 30
return (
-
+
{priority.type === "usecase" || priority.type == "apps" ? : null}
@@ -147,7 +147,7 @@ const Priority = (props) => {
}
-
{
+ {
if (isCloud) {
ReactGA.event({
@@ -173,7 +173,7 @@ const Priority = (props) => {
Explore
{priority.active === true ?
- {
+ {
// dismiss -> get envs
changeRecommendation(priority, "dismiss")
}}>
diff --git a/frontend/src/views/Admin.jsx b/frontend/src/views/Admin.jsx
index c8d012e0..a02f0930 100755
--- a/frontend/src/views/Admin.jsx
+++ b/frontend/src/views/Admin.jsx
@@ -201,7 +201,9 @@ 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] =
@@ -214,22 +216,32 @@ const Admin = (props) => {
getUsers();
setTimeout(() => {
- if (adminTab === 3) {
- window.scroll({
- top: 450,
- left: 0,
- behavior: "smooth",
- });
- }
+ 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(() => {
- window.scroll({
- top: 450,
- left: 0,
- behavior: "smooth",
- });
+ 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(() => {
@@ -247,7 +259,11 @@ const Admin = (props) => {
) {
handleGetSubOrgs(userdata.active_org.id);
} else console.log("error in user data");
- }, [userdata]);
+ }, [userdata]);
+
+ useEffect(() => {
+ handleGetAllTriggers()
+ }, []);
const isCloud =
window.location.host === "localhost:3002" ||
@@ -731,6 +747,32 @@ If you're interested, please let me know a time that works for you, or set up a
});
};
+ 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);
@@ -756,11 +798,11 @@ 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) => {
@@ -768,6 +810,189 @@ If you're interested, please let me know a time that works for you, or set up a
});
};
+ const startSchedule = (trigger) => {
+ if (trigger.name.length <= 0) {
+ toast("Error: name can't be empty");
+ return;
+ }
+
+ 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 !== "" &&
@@ -3127,7 +3352,7 @@ If you're interested, please let me know a time that works for you, or set up a
color="textSecondary"
style={{ marginLeft: 0 }}
>
- On this page organization admins can configure organisations, and
+ On this page organization admins can configure organizations, and
sub-orgs (MSSP).{" "}
);
- const schedulesView =
+ const schedulesView =
curTab === 5 ? (
@@ -4408,88 +4633,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}
-
- }
- />
-
-
- deleteSchedule(schedule)}
+ {allSchedules === undefined ||
+ allSchedules === null ||
+ allSchedules.length === 0 ? (
+
+ No schedules found.
+
+ ) : (
+
+
+
+
+
+
+
+
+
+ {allSchedules.map((schedule, index) => {
+ var bgColor = "#27292d";
+ if (index % 2 === 0) {
+ bgColor = "#1f2023";
+ }
+
+ return (
+
+ 0 ? (
+ schedule.frequency
+ ) : (
+ {schedule.seconds} seconds
+ )
+ }
+ />
+
+
- Stop schedule
-
-
-
- );
- })}
-
+ {schedule.workflow_id}
+
+ }
+ />
+
+
+ {
+ if (schedule.status === "running") {
+ deleteSchedule(schedule);
+ } else startSchedule(schedule);
+ }}
+ >
+ {schedule.status === "running"
+ ? "Stop Schedule"
+ : "Start Schedule"}
+
+
+
+ );
+ })}
+
+ )}
+
+
+
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");
+ }
+ }}
+ >
+
+
+
+ )
+ }
+ />
+
+
+ {
+ if (webhook.status === "running") {
+ deleteWebhook(webhook);
+ } else startWebHook(webhook);
+ }}
+ >
+ {webhook.status === "running"
+ ? "Stop webhook"
+ : "Start Webhook"}
+
+
+
+ );
+ })}
+
+ )}
+
+ {/*
+
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}
+
+ }
+ />
+
+ {
+ if (pipeline.status === "running") {
+ changePipelineState(pipeline, "stop");
+ } else changePipelineState(pipeline, "start");
+ }}
+ >
+ {pipeline.status === "running"
+ ? "Stop pipeline"
+ : "Start pipeline"}
+
+
+
+ );
+ })}
+
+ )}*/}
) : null;
diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx
index bf5b21e1..ecaf4e6a 100755
--- a/frontend/src/views/AngularWorkflow.jsx
+++ b/frontend/src/views/AngularWorkflow.jsx
@@ -120,9 +120,12 @@ import {
Add as AddIcon,
} from "@mui/icons-material";
-import * as cytoscape from "cytoscape";
-import * as edgehandles from "cytoscape-edgehandles";
+//import * as cytoscape from "cytoscape";
+import cytoscape from "cytoscape";
+
+import edgehandles from "cytoscape-edgehandles";
import CytoscapeComponent from "react-cytoscapejs";
+
import Draggable from "react-draggable";
import cytoscapestyle from "../defaultCytoscapeStyle.jsx";
import ShuffleCodeEditor from "../components/ShuffleCodeEditor1.jsx";
@@ -137,6 +140,8 @@ import ExtraApps from "../components/ExtraApps.jsx"
import EditWorkflow from "../components/EditWorkflow.jsx"
// import AppStats from "../components/AppStats.jsx";
+cytoscape.use(edgehandles);
+
export const triggers = [
{
name: "Webhook",
@@ -236,15 +241,7 @@ export const triggers = [
},
];
-// http://apps.cytoscape.org/apps/yfileslayoutalgorithms
-cytoscape.use(edgehandles);
-//cytoscape.use(clipboard);
-//cytoscape.use(undoRedo);
-//cytoscape.use(cxtmenu);
-
// Adds specific text to items
-//import popper from 'cytoscape-popper';
-//cytoscape.use(popper);
// https://stackoverflow.com/questions/19014250/rerender-view-on-browser-resize-with-react
function useWindowSize() {
@@ -592,7 +589,27 @@ const AngularWorkflow = (defaultprops) => {
]
}*/
]
- }]
+ },{
+ "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,
+ }]
+ },
+ ]
}]
/*
@@ -685,7 +702,7 @@ const AngularWorkflow = (defaultprops) => {
props.userdata.active_org !== undefined
? props.userdata.active_org.cloud_sync === true
: false;
- const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
+ const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io" || window.location.host === "migration.shuffler.io";
const appBarSize = isCloud ? 75 : 72;
const triggerEnvironments = isCloud ? ["cloud"] : ["onprem", "cloud"];
@@ -3996,42 +4013,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();
@@ -4783,7 +4808,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
@@ -5194,7 +5219,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)
}
@@ -13130,7 +13155,7 @@ const AngularWorkflow = (defaultprops) => {
if (trigger.id === undefined) {
return;
}
-
+
fetch(globalUrl + "/api/v1/hooks/" + trigger.id + "/delete", {
method: "DELETE",
headers: {
@@ -13143,32 +13168,34 @@ const AngularWorkflow = (defaultprops) => {
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 stop webhook: " + responseJson.reason);
+ }
+ } else {
+ toast("Successfully stopped webhook");
+ }
if (workflow.triggers[triggerindex] !== undefined) {
workflow.triggers[triggerindex].status = "stopped";
}
-
- if (responseJson.success) {
- // Set the status
- saveWorkflow(workflow);
- } else {
- if (responseJson.reason !== undefined) {
- toast("Failed stopping webhook: " + responseJson.reason);
- }
- }
-
trigger.status = "stopped";
- setWorkflow(workflow);
setSelectedTrigger(trigger);
+ setWorkflow(workflow);
+ saveWorkflow(workflow);
+
})
.catch((error) => {
//toast(error.toString());
- toast("Delete webhook error. Contact support or check logs if this persists.")
+ toast(
+ "Delete webhook error. Contact support or check logs if this persists.",
+ );
});
};
+
// POST to /api/v1/workflows
const createWorkflow = (workflow, trigger_index) => {
@@ -17221,6 +17248,10 @@ const AngularWorkflow = (defaultprops) => {
return "Run this workflow in a local environment to increase the timeout. Go to https://shuffler.io/admin?tab=environments to create an environment to connect to"
}
+ if (stringjson.toLowerCase().includes("invalid header")) {
+ return "A header or authentication token in the app is invalid. Check the app's configuration"
+ }
+
return ""
}
diff --git a/frontend/src/views/AppCreator.jsx b/frontend/src/views/AppCreator.jsx
index 354a60c3..209407ea 100755
--- a/frontend/src/views/AppCreator.jsx
+++ b/frontend/src/views/AppCreator.jsx
@@ -1037,55 +1037,41 @@ const AppCreator = (defaultprops) => {
"schema"
] !== null
) {
- try {
- if (
- methodvalue["requestBody"]["content"]["application/xml"][
- "schema"
- ]["properties"] !== undefined
- ) {
- var tmpobject = {};
- for (let [prop, propvalue] of Object.entries(methodvalue["requestBody"]["content"]["application/xml"]["schema"]["properties"])) {
-
- tmpobject[prop] = `\$\{${prop}\}`;
- }
+ try {
+ if (
+ methodvalue["requestBody"]["content"]["application/xml"][
+ "schema"
+ ]["properties"] !== undefined
+ ) {
+ var tmpobject = {};
+ for (let [prop, propvalue] of Object.entries(methodvalue["requestBody"]["content"]["application/xml"]["schema"]["properties"])) {
+
+ tmpobject[prop] = `\$\{${prop}\}`;
+ }
- for (let [subkey,subkeyval] in Object.entries(methodvalue["requestBody"]["content"]["application/xml"]["schema"]["required"])) {
- const tmpitem =
- methodvalue["requestBody"]["content"][
- "application/xml"
- ]["schema"]["required"][subkey];
- tmpobject[tmpitem] = `\$\{${tmpitem}\}`;
- }
+ for (let [subkey,subkeyval] in Object.entries(methodvalue["requestBody"]["content"]["application/xml"]["schema"]["required"])) {
+ const tmpitem =
+ methodvalue["requestBody"]["content"][
+ "application/xml"
+ ]["schema"]["required"][subkey];
+ tmpobject[tmpitem] = `\$\{${tmpitem}\}`;
+ }
- //console.log("OBJ XML: ", tmpobject)
- //newaction["body"] = XML.stringify(tmpobject, null, 2)
- }
- } catch (e) {
- console.log("RequestBody xml error: ", e, path)
- }
+ //console.log("OBJ XML: ", tmpobject)
+ //newaction["body"] = XML.stringify(tmpobject, null, 2)
+ }
+ } catch (e) {
+ console.log("RequestBody xml error: ", e, path)
+ }
}
} else {
- if (
- methodvalue["requestBody"]["content"]["example"] !== undefined
- ) {
- if (
- methodvalue["requestBody"]["content"]["example"][
- "example"
- ] !== undefined
- ) {
- newaction["body"] =
- methodvalue["requestBody"]["content"]["example"][
- "example"
- ];
- //JSON.stringify(tmpobject, null, 2)
+ if (methodvalue["requestBody"]["content"]["example"] !== undefined) {
+ if (methodvalue["requestBody"]["content"]["example"]["example"] !== undefined) {
+ newaction["body"] = methodvalue["requestBody"]["content"]["example"]["example"]
}
}
-
- if (
- methodvalue["requestBody"]["content"][
- "multipart/form-data"
- ] !== undefined
- ) {
+
+ if (methodvalue["requestBody"]["content"]["multipart/form-data"] !== undefined) {
if (
methodvalue["requestBody"]["content"][
"multipart/form-data"
@@ -1553,8 +1539,10 @@ const AppCreator = (defaultprops) => {
} else if (parameter.in === "body") {
// FIXME: Add tracking for components
// E.G: https://raw.githubusercontent.com/owentl/Shuffle/master/gosecure.yaml
- if (parameter.example !== undefined) {
- newaction.body = parameter.example;
+ if (parameter.example !== undefined && parameter.example !== null) {
+ if (newaction.body === undefined || newaction.body === null || newaction.body.length < 5) {
+ newaction.body = parameter.example
+ }
}
} else if (parameter.in === "header") {
newaction.headers += `${parameter.name}=${parameter.example}\n`;
@@ -1566,6 +1554,13 @@ const AppCreator = (defaultprops) => {
}
}
+ // Check if body is valid JSON.
+ if (newaction.body !== undefined && newaction.body !== null && newaction.body.length > 0) {
+ // Trim starting / ending newlines, spaces and tabs
+ newaction.body = newaction.body.trim()
+ }
+
+
if (newaction.name === "" || newaction.name === undefined) {
// Find a unique part of the string
// FIXME: Looks for length between /, find the one where they differ
@@ -1920,11 +1915,11 @@ const AppCreator = (defaultprops) => {
setProjectCategories(all_categories);
- // Rearrange them by which has action_label
- const firstActions = newActions.filter(data => data.action_label !== undefined && data.action_label !== null && data.action_label !== "No Label")
- console.log("First actions: ", firstActions)
- const secondActions = newActions.filter(data => data.action_label === undefined || data.action_label === null || data.action_label === "No Label")
- newActions = firstActions.concat(secondActions)
+ // Rearrange them by which has action_label
+ const firstActions = newActions.filter(data => data.action_label !== undefined && data.action_label !== null && data.action_label !== "No Label")
+ console.log("First actions: ", firstActions)
+ const secondActions = newActions.filter(data => data.action_label === undefined || data.action_label === null || data.action_label === "No Label")
+ newActions = firstActions.concat(secondActions)
setActions(newActions);
//data.paths[item.url][item.method.toLowerCase()]["x-label"] = item.action_label
@@ -3078,7 +3073,10 @@ const AppCreator = (defaultprops) => {
Refresh-token URL for Oauth2 (Optional)
{
margin="normal"
variant="outlined"
value={refreshUrl}
+ helperText={!refreshUrl.startsWith("http") || refreshUrl.includes("//shuffler.")? "Must start with http(s):// and can not contain shuffler.io" : ""}
onChange={(e) => setRefreshUrl(e.target.value)}
- onBlur={(event) => {
+ onBlur={(event) => {
var tmpstring = event.target.value.trim();
if (
@@ -3153,7 +3152,7 @@ const AppCreator = (defaultprops) => {
- Key
+ Key
{
value={parameterName}
helperText={
- Can't be empty or contain any of the following: !#$%&'^"+-._~|]+$
+ Can't be empty or contain any of the following: !#$%&'^"+-._~|]+$:=
}
onChange={(e) => {
setParameterName(e.target.value);
}}
+ onBlur={(event) => {
+ var tmpstring = event.target.value.trim()
+
+ // Check if tmpstring has any of the illegal characters in it
+ }}
InputProps={{
classes: {
notchedOutline: classes.notchedOutline,
diff --git a/frontend/src/views/EditWorkflow.jsx b/frontend/src/views/EditWorkflow.jsx
index 29066fef..bf30f62f 100755
--- a/frontend/src/views/EditWorkflow.jsx
+++ b/frontend/src/views/EditWorkflow.jsx
@@ -207,7 +207,7 @@ const EditWorkflow = (props) => {
const cyDummy = cytoscape();
if (!cyDummy.edgehandles) {
- cytoscape.use(edgehandles);
+ //cytoscape.use(edgehandles);
}
});
diff --git a/frontend/src/views/Search.jsx b/frontend/src/views/Search.jsx
index b19050c7..687e392f 100644
--- a/frontend/src/views/Search.jsx
+++ b/frontend/src/views/Search.jsx
@@ -1,206 +1,316 @@
-import React, { useState, useEffect } from "react";
+import React, { useState, useEffect, useMemo } from "react";
-import theme from '../theme.jsx';
+import theme from "../theme.jsx";
import { isMobile } from "react-device-detect";
-import AppGrid from "../components/AppGrid.jsx"
-import WorkflowGrid from "../components/WorkflowGrid.jsx"
-import CreatorGrid from "../components/CreatorGrid.jsx"
-import DocsGrid from "../components/DocsGrid.jsx"
-import DiscordChat from "../components/DiscordChat.jsx";
+import AppGrid from "../components/AppGrid.jsx";
+import WorkflowGrid from "../components/WorkflowGrid.jsx";
+import CreatorGrid from "../components/CreatorGrid.jsx";
+import DocsGrid from "../components/DocsGrid.jsx";
import { useNavigate } from "react-router-dom";
+import Typography from "@material-ui/core/Typography";
+import { Tabs, Tab, setRef } from "@mui/material";
+import { styled } from "@mui/material/styles";
+import { makeStyles } from '@mui/styles';
+import DiscordChat from "../components/DiscordChat.jsx";
import {
- Tabs,
- Tab,
-} from "@mui/material";
-
-import {
- Apps as AppsIcon,
- Code as CodeIcon,
- Chat as ChatIcon,
- EmojiObjects as EmojiObjectsIcon,
- Description as DescriptionIcon,
+ Apps as AppsIcon,
+ Code as CodeIcon,
+ EmojiObjects as EmojiObjectsIcon,
+ Chat as ChatIcon,
+ BorderBottom,
} from "@mui/icons-material";
+import PeopleAltOutlinedIcon from '@mui/icons-material/PeopleAltOutlined';
+import DescriptionOutlinedIcon from '@mui/icons-material/DescriptionOutlined';
// Should be different if logged in :|
const Search = (props) => {
- const { globalUrl, isLoaded, serverside, userdata, hidemargins, isHeader } = props;
- let navigate = useNavigate();
+ const { globalUrl, isLoaded, serverside, userdata, hidemargins, isHeader } =
+ props;
+ let navigate = useNavigate();
- const [curTab, setCurTab] = useState(0);
- const iconStyle = { marginRight: isHeader ? null : 10 };
+ const [curTab, setCurTab] = useState(0);
+ const iconStyle = { marginRight: isHeader ? null : 10 };
- useEffect(() => {
- if (serverside !== true && window.location.search !== undefined && window.location.search !== null) {
- const urlSearchParams = new URLSearchParams(window.location.search)
- const params = Object.fromEntries(urlSearchParams.entries())
- const foundTab = params["tab"]
- if (foundTab !== null && foundTab !== undefined) {
- for (var key in Object.keys(views)) {
- const value = views[key]
- console.log(key, value)
- if (value === foundTab) {
- setConfig("", key)
- break
- }
- }
- }
- }
- }, [])
+ useEffect(() => {
+ if (
+ serverside !== true &&
+ window.location.search !== undefined &&
+ window.location.search !== null
+ ) {
+ const urlSearchParams = new URLSearchParams(window.location.search);
+ const params = Object.fromEntries(urlSearchParams.entries());
+ const foundTab = params["tab"];
+ if (foundTab !== null && foundTab !== undefined) {
+ for (var key in Object.keys(views)) {
+ const value = views[key];
+ console.log(key, value);
+ if (value === foundTab) {
+ setConfig("", key);
+ break;
+ }
+ }
+ }
+ }
+ }, []);
- if (serverside === true) {
- return null
- }
+ //Stop unnecessariry re-rendering of the component to improve performace
+ const MemoizedAppGrid = useMemo(() => , [curTab]);
- const bodyDivStyle = {
- margin: "auto",
- maxWidth: 1024,
- scrollX: "hidden",
- overflowX: "hidden",
- justifyContent: isHeader ? "center" : null,
- }
+const MemoizedWorkflowGrid = useMemo(() => , [curTab]);
- 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,
- paddingTop: hidemargins === true ? 0 : isHeader ? null : 30,
- display: "flex",
- flexDirection: "column",
- overflowX: "hidden",
- minHeight: 400,
- }
+const MemoizedDocsGrid = useMemo(() => , [curTab]);
- const views = {
- 0: "apps",
- 1: "workflows",
- 2: "docs",
- 3: "creators",
- 4: "discord",
- }
+const MemoizedCreatorGrid = useMemo(() => , [curTab]);
- const setConfig = (event, inputValue) => {
- const newValue = parseInt(inputValue)
+ const MemoizedDiscordChat = useMemo(() => )
- setCurTab(newValue)
- if (newValue === 0) {
- document.title = "Shuffle - search - apps";
- } else if (newValue === 1) {
- document.title = "Shuffle - search - workflows";
- } else if (newValue === 2) {
- document.title = "Shuffle - search - documentation";
- } else if (newValue === 3) {
- document.title = "Shuffle - search - creators";
- } else if (newValue === 4) {
- document.title = "Shuffle - search - Discord Chat";
- }else {
- document.title = "Shuffle - search";
- }
+const useStyles = makeStyles({
+ hideIndicator: {
+ display: 'none',
+ },
+ customTab: {
+ justifyContent: 'center',
+ gap: '46px',
+ }
+});
+const classes = useStyles();
+ if (serverside === true) {
+ return null;
+ }
- const urlSearchParams = new URLSearchParams(window.location.search)
- const params = Object.fromEntries(urlSearchParams.entries())
- const foundQuery = params["q"]
- var extraQ = ""
- if (foundQuery !== null && foundQuery !== undefined) {
- extraQ = "&q=" + foundQuery
- }
+ const bodyDivStyle = {
+ margin: "auto",
+ maxWidth: "100%",
+ scrollX: "hidden",
+ overflowX: "hidden",
+ justifyContent: isHeader ? "center" : null,
+ };
+ 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,
+ paddingTop: hidemargins === true ? 0 : isHeader ? null : 30,
+ display: "flex",
+ flexDirection: "column",
+ overflowX: "hidden",
+ width: "100%",
+ minHeight: 400,
+ };
- if ((serverside === false || serverside === undefined) && window.location.pathname.includes("/search")) {
- navigate(`/search?tab=${views[newValue]}` + extraQ)
- }
- }
+ const views = {
+ 0: "apps",
+ 1: "workflows",
+ 2: "docs",
+ 3: "creators",
+ };
+ const setConfig = (event, inputValue) => {
+ const newValue = parseInt(inputValue);
- if (isLoaded === false) {
- return null
- }
+ setCurTab(newValue);
+ if (newValue === 0) {
+ document.title = "Shuffle - search - apps";
+ } else if (newValue === 1) {
+ document.title = "Shuffle - search - workflows";
+ } else if (newValue === 2) {
+ document.title = "Shuffle - search - documentation";
+ } else if (newValue === 3) {
+ document.title = "Shuffle - search - creators";
+ } else {
+ document.title = "Shuffle - search";
+ }
+ const urlSearchParams = new URLSearchParams(window.location.search);
+ const params = Object.fromEntries(urlSearchParams.entries());
+ const foundQuery = params["q"];
+ var extraQ = "";
+ if (foundQuery !== null && foundQuery !== undefined) {
+ extraQ = "&q=" + foundQuery;
+ }
- // Random names for type & autoComplete. Didn't research :^)
- const landingpageDataBrowser =
-
-
-
-
- Apps
-
- />
-
- Workflows
-
- />
-
- Docs
-
- />
-
- Creators
-
- />
-
- Discord Chat
-
- />
+ if (
+ (serverside === false || serverside === undefined) &&
+ window.location.pathname.includes("/search")
+ ) {
+ navigate(`/search?tab=${views[newValue]}` + extraQ);
+ }
+ };
-
- {curTab === 0 ?
-
- :
- curTab === 1 ?
- window.location.pathname === "/search" ?
-
- :
-
- :
- curTab === 2 ?
-
- :
- curTab === 3 ?
-
- :
- curTab === 4 ?
-
-:
+ if (isLoaded === false) {
+ return null;
+ }
- null}
-
-
- //{/*alternativeView={true} />*/}
+ const StyledTab = styled(Tab)(({ theme }) => ({
+ width: 151,
+ height: 51,
+ padding: "10px 20px",
+ borderRadius: 8,
+ fontWeight: 600,
+ textTransform: "none",
+ border: 'none',
+ "&.Mui-selected": {
+ backgroundColor: theme.palette.primary.main,
+ color: theme.palette.common.white,
+ "& .MuiSvgIcon-root": {
+ color: theme.palette.common.white,
+ },
+ },
+ }));
- const loadedCheck = isLoaded ?
-
-
{landingpageDataBrowser}
-
- :
-
-
+ const tabSpanStyling = {
+ display: 'flex',
+ flexDirection: 'row',
+ alignItems: 'center'
+ }
- // #1f2023?
- return (
-
- {loadedCheck}
-
- )
-}
+ const tabTextStyling = {
+ marginLeft: '5px',
+ color: 'white'
+ }
+
+
+ // Random names for type & autoComplete. Didn't research :^)
+ const landingpageDataBrowser = (
+
+
+
+
+
+ App
+
+ }
+ />
+
+
+ Workflow
+
+ }
+ />
+
+
+ Docs
+
+ }
+ />
+
+
+ Creators
+
+ }
+ />
+
+
+ Discord Chat
+
+ }
+ />
+
+ {curTab === 0 && MemoizedAppGrid}
+ {curTab === 1 && MemoizedWorkflowGrid}
+ {curTab === 2 && MemoizedDocsGrid}
+ {curTab === 3 && MemoizedCreatorGrid}
+ {curTab === 4 && MemoizedDiscordChat}
+
+
+ );
+ //{/*alternativeView={true} />*/}
+
+ const loadedCheck = isLoaded ? (
+
+
{landingpageDataBrowser}
+
+ ) : (
+
+ );
+
+ // #1f2023?
+ return {loadedCheck}
;
+};
export default Search;