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 ? (
+
+ ) : 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,