From 11830fd38bcea659da84138ba8dbb82a54e78c94 Mon Sep 17 00:00:00 2001 From: Frikky Date: Mon, 20 Oct 2025 12:11:04 +0200 Subject: [PATCH] Fixed debouncecallback ref --- backend/go-app/go.sum | 2 + .../src/components/AuthenticationModal.jsx | 891 ++++++++++++++++++ frontend/src/components/DiscordChat.jsx | 2 +- frontend/src/components/LineChartWrapper.jsx | 168 +++- frontend/src/utils/useDebouncedCallback.jsx | 25 + frontend/src/views/AngularWorkflow.jsx | 2 +- 6 files changed, 1071 insertions(+), 19 deletions(-) create mode 100644 frontend/src/components/AuthenticationModal.jsx create mode 100644 frontend/src/utils/useDebouncedCallback.jsx diff --git a/backend/go-app/go.sum b/backend/go-app/go.sum index b520ef42..8d99773a 100644 --- a/backend/go-app/go.sum +++ b/backend/go-app/go.sum @@ -365,6 +365,8 @@ github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/shuffle/shuffle-shared v0.9.30 h1:3CYvNyD7sTxdxoZjTVrtaDqFvSWQRKAFGaga6rPGf8A= github.com/shuffle/shuffle-shared v0.9.30/go.mod h1:PhDEizuz4SmJaSmy0+yrFWwD1mXVUsy8/knKlrqF1qw= +github.com/shuffle/shuffle-shared v0.9.31 h1:BMoDO4Sgz4+I12aHhc3cWHiCuAQ827XIxajPqCJ9cXA= +github.com/shuffle/shuffle-shared v0.9.31/go.mod h1:vfI2QDGphZGrcwuUPQ1yI/Hgc8aseFro5+2k36irfkQ= github.com/shuffle/singul v0.0.16 h1:dW+0Mln9R1aUJ0fjikpWcxbjoQWqJHxe4kSxh2tQN5E= github.com/shuffle/singul v0.0.16/go.mod h1:LYkp320A6gsoPlYbXUM+WvEPUVAuutlSsqnVKyRy4gs= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= diff --git a/frontend/src/components/AuthenticationModal.jsx b/frontend/src/components/AuthenticationModal.jsx new file mode 100644 index 00000000..62f15e7f --- /dev/null +++ b/frontend/src/components/AuthenticationModal.jsx @@ -0,0 +1,891 @@ +import React, { useState, useEffect, useContext, memo } from "react"; +import { CodeHandler, Img, OuterLink, } from '../views/Docs.jsx' +import { getTheme } from "../theme.jsx"; +import { isMobile } from "react-device-detect" +import Markdown from "react-markdown"; +import { Context } from '../context/ContextApi.jsx'; +import PaperComponent from "../components/PaperComponent.jsx"; +import { toast } from "react-toastify"; +import { v4 as uuidv4} from "uuid"; +import AuthenticationOauth2 from "../components/Oauth2Auth.jsx"; + +import { + Edit as EditIcon, + DragIndicator as DragIndicatorIcon, + Close as CloseIcon, + LockOpen as LockOpenIcon, +} from "@mui/icons-material"; + +import { + Button, + Typography, + Dialog, + DialogContent, + DialogTitle, + DialogActions, + MenuItem, + Select, + TextField, + IconButton, + Tooltip, + Divider, +} from "@mui/material"; + +const AuthenticationModal = (props) => { + const { + globalUrl, + userdata, + + selectedAppData, + getAppAuthentication, + appAuthentication, + setSelectedAction, + + selectedMeta, + setSelectedMeta, + + setAppAuthentication, + } = props; + + const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io" || window.location.host === "migration.shuffler.io"; + const [selectedAuthentication, setSelectedAuthentication] = React.useState({}); + const [authenticationModalOpen, setAuthenticationModalOpen] = React.useState(false); + const [authenticationType, setAuthenticationType] = React.useState({}) + + const [appid, setAppId] = useState("") + + //const [appAuthentication, setAppAuthentication] = useState([]); + + const { themeMode, supportEmail, brandColor } = useContext(Context) + const theme = getTheme(themeMode, brandColor) + + useEffect(() => { + if (selectedAppData === undefined || selectedAppData === null || Object.getOwnPropertyNames(selectedAppData).length === 0) { + return + } + + if (selectedAppData.authentication === undefined || selectedAppData.authentication === null) { + setAuthenticationType({ + type: "", + }) + + selectedAppData.authentication = { + type: "", + required: false, + } + } else { + setAuthenticationType( + selectedAppData.authentication.type === "oauth2-app" || (selectedAppData.authentication.type === "oauth2" && selectedAppData.authentication.redirect_uri !== undefined && selectedAppData.authentication.redirect_uri !== null) ? { + type: selectedAppData.authentication.type, + redirect_uri: selectedAppData.authentication.redirect_uri, + refresh_uri: selectedAppData.authentication.refresh_uri, + token_uri: selectedAppData.authentication.token_uri, + scope: selectedAppData.authentication.scope, + client_id: selectedAppData.authentication.client_id, + client_secret: selectedAppData.authentication.client_secret, + grant_type: selectedAppData.authentication.grant_type, + } : { + type: "", + } + ) + } + }, [selectedAppData]) + + if (selectedAppData === undefined || selectedAppData === null || Object.getOwnPropertyNames(selectedAppData).length === 0) { + console.log("No app data for authentication modal"); + return null + } + + if (authenticationModalOpen === false) { + return ( + + ) + } + + function Heading(props) { + const element = React.createElement( + `h${props.level}`, + { style: { marginTop: 40 } }, + props.children + ); + return ( + + {props.level !== 1 ? ( + + ) : null} + {element} + + ); + } + + const UpdateAppAuthentication = (data) => { + if (data === undefined || data === null) { + return; + } + + const filteredData = data.filter((appAuth) => appAuth?.app?.id === appid); + if (filteredData.length === 0) { + setAppAuthentication([]); + setSelectedAuthentication({}); + } else { + setAppAuthentication(filteredData); + setSelectedAuthentication(filteredData[0]); + } + }; + + const HandleAppAuthentication = () => { + + const url = `${globalUrl}/api/v1/apps/authentication`; + + fetch(url, { + method: "GET", + headers: { + "Content-Type": "application/json", + }, + credentials: "include", + }).then((response) => { + if (response.status !== 200) { + return; + } + return response.json(); + }).then((responseJson) => { + if (responseJson.success === true) { + UpdateAppAuthentication(responseJson.data); + } else { + toast.error("Failed to get app authentication data"); + } + }).catch((error) => { + console.error("error for app is :", error); + }); + } + + const AuthenticationData = (props) => { + const selectedApp = props.app; + + const [authenticationOption, setAuthenticationOptions] = React.useState({ + app: JSON.parse(JSON.stringify(selectedApp)), + fields: {}, + label: "", + usage: [ + { + // workflow_id: workflow.id, + }, + ], + id: uuidv4(), + active: true, + }); + + if ( + selectedApp.authentication === undefined || + selectedApp.authentication.parameters === null || + selectedApp.authentication.parameters === undefined || + selectedApp.authentication.parameters.length === 0 + ) { + return ( + + + {selectedApp.name} does not require authentication + + + ); + } + + authenticationOption.app.actions = []; + + for (let paramkey in selectedApp.authentication.parameters) { + if ( + authenticationOption.fields[ + selectedApp.authentication.parameters[paramkey].name + ] === undefined + ) { + authenticationOption.fields[ + selectedApp.authentication.parameters[paramkey].name + ] = ""; + } + } + + const setNewAppAuth = (appAuthData, refresh) => { + setSelectedAuthentication(appAuthData); + var headers = { + "Content-Type": "application/json", + "Accept": "application/json", + } + + headers["Org-Id"] = userdata?.active_org?.id + + fetch(globalUrl + "/api/v1/apps/authentication", { + method: "PUT", + headers: headers, + body: JSON.stringify(appAuthData), + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + console.log("Status not 200 for setting app auth :O!"); + + if (response.status === 400) { + toast.error("Failed setting new auth. Please try again", { + "autoClose": true, + }) + } + } + + return response.json(); + }) + .then((responseJson) => { + if (!responseJson.success) { + toast.error("Error: " + responseJson.reason, { + "autoClose": false, + }) + + } else { + HandleAppAuthentication() + setAuthenticationModalOpen(false) + getAppAuthentication() + } + }) + .catch((error) => { + console.log("New auth error: ", error.toString()); + }); + }; + + const handleSubmitCheck = () => { + if (authenticationOption.label.length === 0) { + authenticationOption.label = `Auth for ${selectedApp.name}`; + } + for (let paramkey in selectedApp.authentication.parameters) { + if ( + authenticationOption.fields[ + selectedApp.authentication.parameters[paramkey].name + ].length === 0 + ) { + if ( + selectedApp.authentication.parameters[paramkey].value !== undefined && + selectedApp.authentication.parameters[paramkey].value !== null && + selectedApp.authentication.parameters[paramkey].value.length > 0 + ) { + authenticationOption.fields[ + selectedApp.authentication.parameters[paramkey].name + ] = selectedApp.authentication.parameters[paramkey].value; + } else { + if ( + selectedApp.authentication.parameters[paramkey].schema.type === "bool" + ) { + authenticationOption.fields[ + selectedApp.authentication.parameters[paramkey].name + ] = "false"; + } else { + toast( + "Field " + + selectedApp.authentication.parameters[paramkey].name + + " can't be empty" + ); + return; + } + } + } + } + + var newAuthOption = JSON.parse(JSON.stringify(authenticationOption)); + var newFields = []; + for (let authkey in newAuthOption.fields) { + const value = newAuthOption.fields[authkey]; + newFields.push({ + "key": authkey, + "value": value, + }); + } + + newAuthOption.fields = newFields + setNewAppAuth(newAuthOption) + } + + if (authenticationOption.label === null || authenticationOption.label === undefined) { + authenticationOption.label = selectedApp.name + " authentication"; + } + + return ( +
+ +
+ Authentication for {selectedApp.name.replaceAll("_", " ", -1)} +
+
+ + + What is app authentication? + +
+ These are required fields for authenticating with {selectedApp.name} +
+ Label for you to remember + { + authenticationOption.label = event.target.value; + }} + /> + +
+ {selectedApp.authentication.parameters.map((data, index) => { + if (data.value === "" || data.value === null || data.value === undefined || data.name === "url") { + } + + + return ( +
+ + {data.name} + + {data.schema !== undefined && + data.schema !== null && + data.schema.type === "bool" ? ( + + ) : ( + { + authenticationOption.fields[data.name] = + event.target.value; + }} + /> + )} +
+ ); + })} + + + + + +
+ ); + }; + + const authenticationModal = authenticationModalOpen ? ( + {setSelectedMeta(undefined)}} + PaperProps={{ + style: { + pointerEvents: "auto", + color: theme.palette.textColor, + minWidth: 1100, + minHeight: 800, + maxHeight: 800, + padding: 15, + overflow: "hidden", + zIndex: 10012, + border: theme.palette.defaultBorder, + }, + }} + > +
+ { selectedAppData.reference_info === undefined || + selectedAppData.reference_info === null || + selectedAppData.reference_info.github_url === undefined || + selectedAppData.reference_info.github_url === null || + selectedAppData.reference_info.github_url.length === 0 ? ( + + + {`Documentation + + ) : ( + + {`Documentation + + )} +
+ + + + + + { + setAuthenticationModalOpen(false); + }} + > + + +
+
+ {authenticationType?.type === "oauth2" || authenticationType?.type === "oauth2-app" ? + + : + + } +
+
+ {selectedAppData?.documentation === undefined || + selectedAppData?.documentation === null || + selectedAppData?.documentation.length === 0 ? ( + +
+ + {selectedAppData?.description} + +
+ + +
+ + There is no Shuffle-specific documentation for this app yet outside of the general description above. Documentation is written for each api, and is a community effort. We hope to see your contribution! + + +
+ + + Want to help the making of, or improve this app?{" "} +
+ + Join the community on Discord! + +
+ + + Want to help change this app directly? + + {selectedAppData.reference_info === undefined || + selectedAppData.reference_info === null || + selectedAppData.reference_info.github_url === undefined || + selectedAppData.reference_info.github_url === null || + selectedAppData.reference_info.github_url.length === 0 ? ( + + + + Check it out on Github! + + + + ) : ( + + + + Check it out on Github! + + + + )} +
+ ) : ( +
+ {selectedMeta !== undefined && selectedMeta !== null && Object.getOwnPropertyNames(selectedMeta).length > 0 && selectedMeta.name !== undefined && selectedMeta.name !== null ? +
+
+ {isMobile ? null : ( + + + + + + )} + {isMobile ? null : ( +
+ )} + + {selectedMeta.read_time} minute + {selectedMeta.read_time === 1 ? "" : "s"} to read + +
+
+ {isMobile || + selectedMeta.contributors === undefined || + selectedMeta.contributors === null ? ( + "" + ) : ( +
+ {selectedMeta.contributors.slice(0, 7).map((data, index) => { + return ( + + + {data.url} + + + ); + })} +
+ )} +
+
+ : null} + + + {selectedAppData.documentation} + +
+ )} +
+
+
+ ) : null; + + return authenticationModal +} + +export default AuthenticationModal; diff --git a/frontend/src/components/DiscordChat.jsx b/frontend/src/components/DiscordChat.jsx index 5d1a9d04..07394852 100644 --- a/frontend/src/components/DiscordChat.jsx +++ b/frontend/src/components/DiscordChat.jsx @@ -16,7 +16,7 @@ import { ListItemText, } from '@mui/material'; import { Search as SearchIcon } from '@mui/icons-material'; -import useDebouncedCallback from '../utils/useDebouncedCallback.js'; +import useDebouncedCallback from '../utils/useDebouncedCallback.jsx'; const searchClient = algoliasearch("JNSS5CFDZZ", "1e5f29b1550939855de5915eac3bf5f7"); diff --git a/frontend/src/components/LineChartWrapper.jsx b/frontend/src/components/LineChartWrapper.jsx index 95aa8024..307b7a62 100644 --- a/frontend/src/components/LineChartWrapper.jsx +++ b/frontend/src/components/LineChartWrapper.jsx @@ -1,6 +1,7 @@ import React, { useState, useEffect, useContext, memo, useMemo } from 'react' import {getTheme} from '../theme.jsx'; import { Context } from '../context/ContextApi.jsx'; +import { toast } from "react-toastify"; import { Typography, @@ -14,19 +15,104 @@ import { GridlineSeries, Gridline, - TooltipArea, ChartTooltip, TooltipTemplate, + TooltipArea, } from 'reaviz'; +export const LoadStats = (globalUrl, cachekey) => { + if (globalUrl === undefined) { + console.log("Error: Global URL is undefined") + return + } + + if (cachekey === undefined) { + console.log("Error: Cachekey is undefined") + return + } + + var basedata = { + "key": cachekey, + "total": 0, + "available_keys": [], + "labels": [], + "datasets": [ + { + "label": "", + "data": [], + "backgroundColor": [], + "barThickness": 15, + } + ] + } + + //const url = `${globalUrl}/api/v1/stats/app_executions_test2` + //cachekey = cachekey.replace(" ", "_", -1) + const url = `${globalUrl}/api/v1/stats/${cachekey}` + return fetch(url, { + method: "GET", + credentials: "include", + }) + .then((resp) => { + return resp.json() + }).then((respJson) => { + const selectedIndex = 0 + + //console.log("Stats response: ", respJson) + + if (respJson.success === true) { + for (let entryKey in respJson.entries) { + const entry = respJson.entries[entryKey] + basedata.labels.push(entry.date) + + basedata.datasets[0].data.push(entry.value) + basedata.datasets[0].backgroundColor.push(entry.value > 0 ? "rgba(255,255,255,0.4)" : "red") + } + + basedata.available_keys = respJson.available_keys + basedata.total = respJson.total + + return basedata + } else { + console.log("Failed to get stats") + return basedata + } + }) + .catch((err) => { + toast("Failed to get stats") + return basedata + }) +} + const LineChartWrapper = (props) => { - const {keys, inputname, height, width, border} = props + const {keys, inputname, height, width, border, color} = props const [hovered, setHovered] = useState(""); const {themeMode} = useContext(Context) const theme = getTheme(themeMode) - var inputdata = keys.data === undefined ? keys : keys.data + // Correct format: + /* keys={[ + { + key: "2025-07-23T00:00:09.409718Z", + data: 24, + }, + { + key: "2025-07-24T00:00:09.409718Z", + data: 50, + }, + { + key: "2025-07-25T00:00:09.409718Z", + data: 75, + }, + { + key: "2025-07-26T00:00:09.409718Z", + data: 42, + }, + ]} + */ + + var inputdata = keys?.data === undefined ? keys : keys.data var newname = inputname === undefined || inputname === null ? "" : inputname.trim().replaceAll("_", " ") newname = newname.charAt(0).toUpperCase() + newname.slice(1) @@ -35,7 +121,6 @@ const LineChartWrapper = (props) => { var tmpdata = inputdata?.datasets[0] if (tmpdata?.data !== undefined && tmpdata?.data !== null && tmpdata?.data.length > 0 && inputdata?.labels?.length === tmpdata?.data?.length) { - console.log("Fix it!") var newarray = [] for (var key in tmpdata.data) { var entry = { @@ -48,26 +133,25 @@ const LineChartWrapper = (props) => { inputdata = newarray } - } if (inputdata === undefined || inputdata === null) { - return ( + return null /*( Invalid linegraph data format - ) + )*/ } var defaultStyle = { color: "white", - padding: 30, + padding: "5px 5px 10px 5px", marginTop: 15, overflow: "hidden", border: "1px solid rgba(255,255,255,0.3)", borderRadius: theme.palette?.borderRadius, - backgroundColor: theme.palette.platformColor, + //backgroundColor: theme.palette.platformColor, } if (border === false) { @@ -76,11 +160,64 @@ const LineChartWrapper = (props) => { defaultStyle.backgroundColor = "transparent" } + // Check if it's a list or not + if (!Array.isArray(inputdata) || inputdata.length === 0 || (inputdata.length > 0 && (inputdata[0].key === undefined && inputdata[0].data === undefined))) { + console.log("Invalid graph data format: ", inputdata) + console.log("Expected format: [{key: 'label1', data: 10}, {key: 'label2', data: 20}]") + //inputdata = inputdata?.datasets[0]?.data + return null + } + + //console.log("FORMAT: ", inputdata) + + const tooltip = ( +
+ {data?.x ?? ''} + {data?.y ?? ''} +
+ )} + /> + } + />; + + const selectedColor = color === undefined || color === null || color === "" ? "" : color + const barseries = color === undefined || color === null || color === "" ? + + } + tooltip={tooltip} + /> + : + + } + tooltip={tooltip} + /> + return (
- - {newname} - + {newname !== "" && + + {newname} + + } { data={inputdata} series={ - - } - /> + barseries + } gridlines={ } /> diff --git a/frontend/src/utils/useDebouncedCallback.jsx b/frontend/src/utils/useDebouncedCallback.jsx new file mode 100644 index 00000000..aafa23c9 --- /dev/null +++ b/frontend/src/utils/useDebouncedCallback.jsx @@ -0,0 +1,25 @@ +import { useRef, useEffect, useCallback } from "react"; + +export const useDebouncedCallback = (callback, delay = 300) => { + const timeoutRef = useRef(null); + const savedCallback = useRef(callback); + + useEffect(() => { + savedCallback.current = callback; + }, [callback]); + + useEffect(() => () => { + if (timeoutRef.current) clearTimeout(timeoutRef.current); + }, []); + + return useCallback((...args) => { + if (timeoutRef.current) clearTimeout(timeoutRef.current); + timeoutRef.current = setTimeout(() => { + savedCallback.current(...args); + }, delay); + }, [delay]); +}; + +export default useDebouncedCallback; + + diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index 88adac1b..61b3a3c3 100755 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -22,7 +22,7 @@ import { CodeHandler, Img, OuterLink, } from "../views/Docs.jsx"; import { InstantSearch, Configure, connectSearchBox, connectHits, Index } from 'react-instantsearch-dom'; import algoliasearch from 'algoliasearch/lite'; -import useDebouncedCallback from "../utils/useDebouncedCallback.js"; +import useDebouncedCallback from "../utils/useDebouncedCallback.jsx"; import { Zoom, Fade,