diff --git a/backend/Dockerfile b/backend/Dockerfile
index 9e27ffbe..16ffeff0 100755
--- a/backend/Dockerfile
+++ b/backend/Dockerfile
@@ -27,7 +27,7 @@ FROM alpine:latest as certs
RUN apk add --update ca-certificates
# Sets up the final image
-FROM alpine:3.21.2
+FROM alpine:3.22.1
# FIXME: Install cgo because CGO_ENABLED=1 during build
RUN apk add --no-cache libc6-compat
diff --git a/backend/go-app/go.mod b/backend/go-app/go.mod
index 2861454a..7c857d01 100644
--- a/backend/go-app/go.mod
+++ b/backend/go-app/go.mod
@@ -24,7 +24,7 @@ require (
github.com/gorilla/mux v1.8.1
github.com/h2non/filetype v1.1.3
github.com/satori/go.uuid v1.2.0
- github.com/shuffle/shuffle-shared v0.8.96
+ github.com/shuffle/shuffle-shared v0.9.0
github.com/shuffle/singul v0.0.15
golang.org/x/crypto v0.38.0
google.golang.org/api v0.236.0
diff --git a/backend/go-app/go.sum b/backend/go-app/go.sum
index 51d36d6e..163f7931 100644
--- a/backend/go-app/go.sum
+++ b/backend/go-app/go.sum
@@ -359,8 +359,8 @@ github.com/sendgrid/sendgrid-go v3.16.1+incompatible h1:zWhTmB0Y8XCDzeWIm2/BIt1G
github.com/sendgrid/sendgrid-go v3.16.1+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
-github.com/shuffle/shuffle-shared v0.8.91 h1:ZcxSKhns96w1GQG/EjEJSLBvuEA3KVjvORAetV3W4bM=
-github.com/shuffle/shuffle-shared v0.8.91/go.mod h1:PFLG4eaxz8zQc9Lbc0Ok/nG8rNsaBkZ0TNar+VU+J/g=
+github.com/shuffle/shuffle-shared v0.9.0 h1:hUNtW3qHkji2cUVyR1cIXAz6qxpAHHP5hBmSAe3pIhs=
+github.com/shuffle/shuffle-shared v0.9.0/go.mod h1:PFLG4eaxz8zQc9Lbc0Ok/nG8rNsaBkZ0TNar+VU+J/g=
github.com/shuffle/singul v0.0.15 h1:a5Qro1BKs9a+mWE289oYTkDeVFoKEd/H9qu9XJGsVxo=
github.com/shuffle/singul v0.0.15/go.mod h1:z1RRZsymTYxsB8WMQwwswypruy4j5EugGPFEZspTEA4=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
diff --git a/backend/go-app/main.go b/backend/go-app/main.go
index 11d4473d..f57dcdcd 100755
--- a/backend/go-app/main.go
+++ b/backend/go-app/main.go
@@ -4100,8 +4100,9 @@ func runInitEs(ctx context.Context) {
time.Sleep(30 * time.Second)
}
- // FIXME: This should ONLY run on one backend instance
+ shuffle.InitOpensearchIndexes()
+ // FIXME: This should ONLY run on one backend instance. This may cause interference.
schedules, err := shuffle.GetAllSchedules(ctx, "ALL")
if err != nil {
log.Printf("[WARNING] Failed getting schedules during service init: %s", err)
diff --git a/frontend/src/components/EditWorkflow.jsx b/frontend/src/components/EditWorkflow.jsx
index dbd8d3ea..00b9608d 100644
--- a/frontend/src/components/EditWorkflow.jsx
+++ b/frontend/src/components/EditWorkflow.jsx
@@ -63,6 +63,7 @@ import {
Add as AddIcon,
Remove as RemoveIcon,
EditNote as EditNoteIcon,
+ AutoAwesome as AutoAwesomeIcon
} from "@mui/icons-material";
const EditWorkflow = (props) => {
@@ -72,6 +73,7 @@ const EditWorkflow = (props) => {
const {themeMode, brandColor} = useContext(Context)
const theme = getTheme(themeMode, brandColor)
const [submitLoading, setSubmitLoading] = React.useState(false);
+ const [aiGenerateLoading, setAiGenerateLoading] = React.useState(false);
const [showMoreClicked, setShowMoreClicked] = React.useState(isEditing !== false ? true : false);
const [innerWorkflow, setInnerWorkflow] = React.useState(workflow)
@@ -220,7 +222,7 @@ const EditWorkflow = (props) => {
-
+
{newWorkflow ? "New" : "Editing"} Workflow
@@ -298,93 +300,316 @@ const EditWorkflow = (props) => {
paddingLeft: 30,
backgroundColor: themeMode === "dark" ? "#262626" : theme.palette.DialogStyle.backgroundColor,
}}>
-
+ )}
@@ -409,6 +634,29 @@ const EditWorkflow = (props) => {
id="Enter-Workflow-Name"
/>
+ {newWorkflow === true ?
+ {
+ innerWorkflow.default_return_value = event.target.value
+ setInnerWorkflow(innerWorkflow)
+ setUpdate(Math.random())
+ }}
+ InputProps={{
+ style: {
+ color: "white",
+ },
+ }}
+ color="primary"
+ defaultValue={innerWorkflow.default_return_value}
+ placeholder="Please describe your workflow below so the AI can generate it."
+ rows="3"
+ multiline
+ label="Description"
+ margin="dense"
+ fullWidth
+ />
+ : null}
+
{usecases !== null && usecases !== undefined && usecases.length > 0 ?
@@ -1285,7 +1533,7 @@ const EditWorkflow = (props) => {
- {newWorkflow === true ?
+ {/*newWorkflow === true ?
Relevant Workflows
@@ -1311,9 +1559,8 @@ const EditWorkflow = (props) => {
}
- : null}
-
- {/*newWorkflow === true && name.length > 2 ?
+ : null*/}
+ {/*newWorkflow === true && name.length > 2 ?
{
//toast("No Orborus necessary for environment cloud. Create and use a different environment to run executions on-premises.",)
return
}
-
+
if (
props.userdata.active_org === undefined ||
props.userdata.active_org === null
) {
return;
}
-
+
const elementName = "copy_element_shuffle";
var auth =
environment.auth === ""
? "cb5st3d3Z!3X3zaJ*Pc"
: environment.auth
-
- // Escape exclamation marks for copying
- auth = auth.replace("\\!", "!").replace(/!/g, "\\!")
-
+ // Escape exclamation marks for copying
+ auth = auth.replace("\\!", "!").replace(/!/g, "\\!")
const newUrl =
globalUrl === "https://shuffler.io"
? "https://shuffle-backend-stbuwivzoq-nw.a.run.app"
: globalUrl;
-
+
var skipPipeline = false
if (commandController.pipelines === true) {
skipPipeline = true
}
-
+
var addProxy = false
if (commandController.proxies === true) {
addProxy = true
}
-
+
if (installationTab === 1) {
return (`docker run -d \\
--restart=always \\
@@ -428,14 +426,32 @@ const EnvironmentTab = memo((props) => {
-e SHUFFLE_SWARM_CONFIG=run \\
-e SHUFFLE_LOGS_DISABLED=true \\
-e BASE_URL="${newUrl}" \\${addProxy ? `
- -e HTTPS_PROXY=IP:PORT \\` : ""}${skipPipeline ? `
- -e SHUFFLE_SKIP_PIPELINES=true \\` : ""}
+ -e HTTPS_PROXY=IP:PORT \\` : ""}${skipPipeline ? `
+ -e SHUFFLE_SKIP_PIPELINES=true \\` : ""}
ghcr.io/shuffle/shuffle-orborus:latest
`)
} else if (installationTab === 2) {
- return `https://shuffler.io/docs/configuration#kubernetes`
+ return `helm install shuffle-orborus oci://ghcr.io/shuffle/charts/shuffle \\
+ --namespace shuffle --create-namespace \\
+ --set opensearch.enabled=false \\
+ --set backend.replicaCount=0 \\
+ --set frontend.replicaCount=0 \\
+ --set 'orborus.extraEnvVars[0].name=AUTH' \\
+ --set 'orborus.extraEnvVars[0].value=${auth}' \\
+ --set 'orborus.extraEnvVars[1].name=BASE_URL' \\
+ --set 'orborus.extraEnvVars[1].value=${newUrl}' \\
+ --set 'orborus.extraEnvVars[2].name=ENVIRONMENT_NAME' \\
+ --set 'orborus.extraEnvVars[2].value=${environment.Name}' \\
+ --set 'orborus.extraEnvVars[3].name=ORG' \\
+ --set 'orborus.extraEnvVars[3].value=${environment.org_id}' \\
+ --set shuffle.org="${environment.org_id}"${addProxy ? ` \\
+ --set 'orborus.extraEnvVars[4].name=HTTPS_PROXY' \\
+ --set 'orborus.extraEnvVars[4].value=IP:PORT'` : ""}${skipPipeline ? ` \\
+ --set 'orborus.extraEnvVars[${addProxy ? 5 : 4}].name=SHUFFLE_SKIP_PIPELINES' \\
+ --set 'orborus.extraEnvVars[${addProxy ? 5 : 4}].value=true'` : ""} \\
+ --set persistence.storageClass=standard`
}
-
+
const commandData = `docker rm shuffle-orborus --force; \\\ndocker run -d \\
--restart=always \\
--name="shuffle-orborus" \\
@@ -445,12 +461,13 @@ const EnvironmentTab = memo((props) => {
-e ENVIRONMENT_NAME="${environment.Name}" \\
-e ORG="${props.userdata.active_org.id}" \\
-e BASE_URL="${newUrl}" \\${addProxy ? `
- -e HTTPS_PROXY=IP:PORT \\` : ""}${skipPipeline ? `
- -e SHUFFLE_SKIP_PIPELINES=true \\` : ""}
+ -e HTTPS_PROXY=IP:PORT \\` : ""}${skipPipeline ? `
+ -e SHUFFLE_SKIP_PIPELINES=true \\` : ""}
ghcr.io/shuffle/shuffle-orborus:latest`
-
+
return commandData
- };
+ };
+
const submitEnvironment = (data) => {
// FIXME - add some check here ROFL
@@ -1475,7 +1492,7 @@ const EnvironmentTab = memo((props) => {
{installationTab === 2 ?
- Check our Kubernetes documentation for more information on how to run Shuffle on Kubernetes. The status of the node will change when connected.
+ Simply connect to your Kubernetes cluster and run the following command:
:
@@ -1489,7 +1506,6 @@ const EnvironmentTab = memo((props) => {
"2. Run this command on the server you want to run workflows or store Pipeline data on"}
- {installationTab === 2 ? null :
{
}}
/>
-
- }
+
{installationTab === 2 ? null :
diff --git a/frontend/src/components/HealthBarChart.jsx b/frontend/src/components/HealthBarChart.jsx
index a4f05273..a5d1dec4 100644
--- a/frontend/src/components/HealthBarChart.jsx
+++ b/frontend/src/components/HealthBarChart.jsx
@@ -1,22 +1,93 @@
-import React from 'react';
-import { Bar } from 'react-chartjs-2';
+import React, { useState } from 'react';
const HealthBarChart = (props) => {
- const { globalUrl, filteredData, options, onBarClick } = props;
-
- return (
- {
- if (elements && elements.length > 0) {
- onBarClick(elements);
- }
+ const { filteredData, onBarClick } = props;
+ const [hoveredBar, setHoveredBar] = useState(null);
+
+ if (!filteredData || !Array.isArray(filteredData)) {
+ console.error('Invalid chart data format');
+ return null;
+ }
+
+ const formatData = () => {
+ return filteredData.map((item) => ({
+ label: item.date,
+ value: item.avgRunFinished,
+ color: item.color,
+ executionIds: item.executionIds,
+ }));
+ };
+
+ const handleBarClick = (data) => {
+ if (onBarClick) {
+ onBarClick(data);
+ }
+ };
+
+ const chartData = formatData();
+
+ const barWidth = `calc((100% - ${(chartData.length - 1) * 5}px) / ${chartData.length})`;
+ const barGap = '5px';
+
+ return (
+
+ {/* Chart Container */}
+
+ {chartData.map((item, index) => (
+
- );
+ onMouseEnter={() => setHoveredBar(item)}
+ onMouseLeave={() => setHoveredBar(null)}
+ onClick={() => handleBarClick(item)}
+ />
+ ))}
+
+
+ {/* Tooltip */}
+ {hoveredBar && (
+
+
Date: {hoveredBar.label}
+
Uptime: {hoveredBar.value}%
+
Executions: {hoveredBar.executionIds.length}
+
+ )}
+
+ );
};
export default HealthBarChart;
diff --git a/frontend/src/components/HealthPage.jsx b/frontend/src/components/HealthPage.jsx
index fcadb776..660719e0 100644
--- a/frontend/src/components/HealthPage.jsx
+++ b/frontend/src/components/HealthPage.jsx
@@ -1,31 +1,35 @@
import React, { useEffect, useState, useCallback } from 'react';
-import { Bar } from 'react-chartjs-2';
-//import healthData1 from '../healthstats1.json';
-import { toast } from "react-toastify"
-import CheckOutlinedIcon from '@mui/icons-material/CheckOutlined';
+import { toast } from "react-toastify";
+import {
+ CheckOutlined as CheckOutlinedIcon,
+} from '@mui/icons-material';
+
import {
Button,
ButtonGroup,
Typography,
+ LinearProgress,
} from "@mui/material";
+
import HealthBarChart from '../components/HealthBarChart.jsx';
+import LiveExecutionsChart from '../components/LiveExecutionsGraph.jsx';
const HealthPage = (props) => {
- const { globalUrl, userdata } = props;
+ const { userdata, globalUrl } = props;
const [healthData, setHealthData] = useState(null);
const [selectedRange, setSelectedRange] = useState('30d');
+ const [liveExecutionsData, setLiveExecutionsData] = useState([]);
const [filteredData, setFilteredData] = useState([]);
const [averageUptime, setAverageUptime] = useState(0);
+ const [liveExecutionsRange, setLiveExecutionsRange] = useState('1h'); // Default to 1h
+ const [isHealthLoading, setIsHealthLoading] = useState(false); // Loading state for HealthBarChart
+ const [isLiveExecutionsLoading, setIsLiveExecutionsLoading] = useState(false); // Loading state for LiveExecutionsChart
- const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
- //const globalUrl = `https://shuffler.io`
-
- console.log("HEALTHPAGE 1")
+ const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true");
const fetchHealthStats = useCallback(async () => {
- //const after = new Date().getTime() - 90 * 24 * 60 * 60
+ setIsHealthLoading(true); // Start loading for HealthBarChart
try {
- //const response = await fetch(`${globalUrl}/api/v1/health/stats?after=${after}`, {
const response = await fetch(`${globalUrl}/api/v1/health/stats`, {
method: "GET",
headers: {
@@ -42,31 +46,118 @@ const HealthPage = (props) => {
} catch (error) {
console.error("Error fetching health stats:", error);
toast.error("Failed loading health stats");
+ } finally {
+ setIsHealthLoading(false); // Stop loading for HealthBarChart
+ }
+ }, [globalUrl]);
+
+ const fetchLiveExecutions = useCallback(async (range = '1h') => {
+ setIsLiveExecutionsLoading(true); // Start loading for LiveExecutionsChart
+ try {
+ const fetchOptions = {
+ method: "GET",
+ credentials: "include",
+ };
+
+ if (window.location.host !== "localhost:3002") {
+ fetchOptions.headers = {
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ };
+ }
+
+ const now = Math.floor(Date.now() / 1000);
+ // let after = now - 3600; // Default to 1h
+ let mode = ""
+
+ switch (range) {
+ case '1h':
+ mode = "1h"
+ break;
+ case '7h':
+ mode = "7h"
+ break;
+ case '1d':
+ mode = "1d"
+ break;
+ case '7d':
+ mode = "7d"
+ break;
+ case 'month':
+ mode = "month"
+ break;
+ default:
+ mode = "1h"
+ }
+
+ if (!userdata.support_access) {
+ return
+ }
+
+ const response = await fetch(
+ `${globalUrl}/api/v1/health/executions/live?mode=${mode}`,
+ fetchOptions
+ );
+
+ if (!response.ok) {
+ throw new Error("Failed to fetch live executions");
+ }
+
+ const data = await response.json();
+ console.log("Raw live executions data:", data);
+
+ if (Array.isArray(data)) {
+ const formattedData = data
+ .map(item => ({
+ ...item,
+ // failed: Number(item.failed) || 0,
+ executing: Number(item.executing) || 0,
+ finished: Number(item.finished) || 0,
+ aborted: Number(item.aborted) || 0,
+ created_at: Number(item.created_at) || 0
+ }))
+ .sort((a, b) => a.created_at - b.created_at);
+
+ console.log("Formatted live executions data:", formattedData);
+ setLiveExecutionsData(formattedData);
+ } else {
+ console.error("Received invalid data format:", data);
+ setLiveExecutionsData([]);
+ }
+ } catch (error) {
+ console.error("Error fetching live executions:", error);
+ toast.error("Failed loading live executions data");
+ } finally {
+ setIsLiveExecutionsLoading(false); // Stop loading for LiveExecutionsChart
}
}, [globalUrl]);
useEffect(() => {
fetchHealthStats();
- }, [fetchHealthStats]);
+ fetchLiveExecutions(liveExecutionsRange);
+
+ const interval = setInterval(() => fetchLiveExecutions(liveExecutionsRange), 60000);
+ return () => clearInterval(interval);
+ }, [fetchHealthStats, fetchLiveExecutions, liveExecutionsRange]);
const extractRunFinished = (data, range) => {
if (!data || !Array.isArray(data)) return [];
-
+
const currentDate = new Date().getTime();
const rangeInMillis = {
'24hr': 24 * 60 * 60 * 1000,
'7day': 7 * 24 * 60 * 60 * 1000,
'30d': 30 * 24 * 60 * 60 * 1000,
'90d': 90 * 24 * 60 * 60 * 1000
- }
- const filteredData = data.filter(item => currentDate - item.updated * 1000 <= rangeInMillis[range])
-
- const aggregatedData = new Map()
-
+ };
+ const filteredData = data.filter(item => currentDate - item.updated * 1000 <= rangeInMillis[range]);
+
+ const aggregatedData = new Map();
+
filteredData.forEach(item => {
const timestamp = item.updated * 1000; // Convert Unix timestamp to milliseconds
let key;
-
+
switch (range) {
case '24hr':
const date = new Date(timestamp);
@@ -83,7 +174,7 @@ const HealthPage = (props) => {
default:
key = new Date(timestamp).toLocaleDateString();
}
-
+
// Check if date already exists in the map
if (aggregatedData.has(key)) {
// Update aggregated values
@@ -100,13 +191,13 @@ const HealthPage = (props) => {
});
}
});
-
+
// Calculate averages and assign colors
const result = Array.from(aggregatedData.entries()).map(([key, { totalEntries, totalRunFinished, executionIds }]) => {
const avg = totalEntries > 0 ? totalRunFinished / totalEntries : 0;
const FinalAvg = avg * 100;
let color;
-
+
if (FinalAvg >= 100) {
color = '#00F670';
} else if (FinalAvg >= 98.50 && FinalAvg <= 99.99) {
@@ -114,7 +205,7 @@ const HealthPage = (props) => {
} else if (FinalAvg <= 98.49) {
color = '#FF354C';
}
-
+
return {
date: range === '24hr' ? `${key}:00` : key,
avgRunFinished: FinalAvg,
@@ -122,11 +213,10 @@ const HealthPage = (props) => {
executionIds
};
});
-
+
return result;
};
-
-
+
useEffect(() => {
if (healthData) {
const newData = extractRunFinished(healthData, selectedRange);
@@ -160,19 +250,19 @@ const HealthPage = (props) => {
const labels = filteredData.map((value, i) => {
if (selectedRange === '24hr') {
const [datePart, hourPart] = value.date.split(' ');
- const [day, month, year] = datePart.split('/');
+ const [month, day, year] = datePart.split('/');
const monthIndex = parseInt(month, 10) - 1;
const date = new Date(year, monthIndex, day);
let formattedDate = `${date.toLocaleString('en-US', { month: 'short', day: '2-digit' })}`;
-
// Add hour part if available
if (hourPart) {
formattedDate += `, ${hourPart.split(':').slice(0, 2).join(':')}`;
}
+
return `${formattedDate} \nUptime: ${value.avgRunFinished.toFixed(2)}%`;
} else if (selectedRange === '7day') {
const [datePart, hourPart] = value.date.split(' ');
- const [day, month, year] = datePart.split('/');
+ const [month, day, year] = datePart.split('/');
const monthIndex = parseInt(month, 10) - 1;
const date = new Date(year, monthIndex, day);
let formattedDate = `${date.toLocaleString('en-US', { month: 'short', day: '2-digit' })}`;
@@ -185,12 +275,12 @@ const HealthPage = (props) => {
}
else {
const dateParts = value.date.split('/'); // Assuming the date format is "DD/MM/YYYY"
- const date = new Date(`${dateParts[2]}-${dateParts[1]}-${dateParts[0]}`); // Reformat the date string to "YYYY-MM-DD"
+ const date = new Date(`${dateParts[2]}-${dateParts[0]}-${dateParts[1]}`); // Reformat the date string to "YYYY-MM-DD"
+
return `${date.toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' })} \nUptime: ${value.avgRunFinished.toFixed(2)}%`;
}
});
-
if (selectedRange === '24hr') {
barThickness = 35;
}
@@ -200,12 +290,6 @@ const HealthPage = (props) => {
else if (selectedRange === '30d') {
barThickness = 25;
}
- // let width = 800; // Default chart width
- // if (selectedRange === '7day') {
- // width = 400; // Adjust chart width for 7 days
- // } else if (selectedRange === '30d') {
- // width = 600; // Adjust chart width for 30 days
- // }
const datasets = [{
label: "",
@@ -213,14 +297,11 @@ const HealthPage = (props) => {
backgroundColor: filteredData.map(item => item.color),
borderWidth: 1,
barThickness: barThickness,
- // barPercentage: barPercentage,
}];
return { labels, datasets };
};
- console.log("HEALTHPAGE 2")
-
const options = {
legend: {
display: false
@@ -253,12 +334,11 @@ const HealthPage = (props) => {
},
afterLabel: function (tooltipItem, data) {
const label = data.labels[tooltipItem.index];
- // console.log(label)
const uptime = label.match(/Uptime:\s*(\d+(?:\.\d+)?)/)[1]; // Extract uptime value using regex
- return `Success Rate: ${uptime}%`; // Customize the uptime display
+ return `Test-Workflow Health: ${uptime}%`; // Customize the uptime display
},
title: function () {
- return 'Fully Oprational'; // Hide the tooltip title
+ return 'Fully Operational'; // Hide the tooltip title
}
}
}
@@ -284,32 +364,118 @@ const HealthPage = (props) => {
}
};
+ const healthBarData = updateChartData()
+
return (
-
-
+
+ {/* Health Bar Chart Section */}
- filterDataByRange('24hr')}>24h
- filterDataByRange('7day')}>7d
- filterDataByRange('30d')}>30d
- filterDataByRange('90d')}>90d
- filterDataByRange('180d')}>180d
+ filterDataByRange('24hr')} disabled={isHealthLoading}>24h
+ filterDataByRange('7day')} disabled={isHealthLoading}>7d
+ filterDataByRange('30d')} disabled={isHealthLoading}>30d
+ filterDataByRange('90d')} disabled={isHealthLoading}>90d
+ filterDataByRange('180d')} disabled={isHealthLoading}>180d
-
-
-
-
-
Workflow Health
-
Operational
+ {/* Loading Bar for HealthBarChart */}
+ {isHealthLoading && (
+
+ )}
+
+
+
+
+
+
+ Workflow Health
+ Operational
+
-
-
{averageUptime.toFixed(2)}%
-
Success Rate
+
+ {averageUptime.toFixed(2)}%
+ Success Rate
-
+
+
+ {userdata.support_access && (
+
+
+ Live Executions
+
+ setLiveExecutionsRange('1h')}
+ disabled={isLiveExecutionsLoading}
+ >
+ 1h
+
+ setLiveExecutionsRange('7h')}
+ disabled={isLiveExecutionsLoading}
+ >
+ 7h
+
+ setLiveExecutionsRange('1d')}
+ disabled={isLiveExecutionsLoading}
+ >
+ 1d
+
+ setLiveExecutionsRange('7d')}
+ disabled={isLiveExecutionsLoading}
+ >
+ 7d
+
+ {/* setLiveExecutionsRange('month')} disabled={isLiveExecutionsLoading}>Month */}
+
+
+ {/* Loading Bar for LiveExecutionsChart */}
+ {isLiveExecutionsLoading && (
+
+ )}
+
+
+ )}
+
+
);
};
diff --git a/frontend/src/components/LiveExecutionsGraph.jsx b/frontend/src/components/LiveExecutionsGraph.jsx
new file mode 100644
index 00000000..ac572e84
--- /dev/null
+++ b/frontend/src/components/LiveExecutionsGraph.jsx
@@ -0,0 +1,7 @@
+import React from 'react';
+
+const LiveExecutionsGraph = ({ executions }) => {
+ return null
+}
+
+export default LiveExecutionsGraph
diff --git a/frontend/src/components/PartnerHeader.jsx b/frontend/src/components/PartnerHeader.jsx
index 8cb84c0a..3a6a910b 100644
--- a/frontend/src/components/PartnerHeader.jsx
+++ b/frontend/src/components/PartnerHeader.jsx
@@ -741,7 +741,7 @@ const PartnerHeader = (props) => {
{
@@ -812,7 +812,7 @@ const PartnerHeader = (props) => {
{
diff --git a/frontend/src/theme.jsx b/frontend/src/theme.jsx
index 4835e4f1..8772ff23 100644
--- a/frontend/src/theme.jsx
+++ b/frontend/src/theme.jsx
@@ -391,6 +391,78 @@ export const getTheme = (themeMode, brandColor) =>
},
},
},
+ {
+ props: { variant: 'aiButton' },
+ style: {
+ background: 'linear-gradient(90deg, #ff8544 0%, #ec517c 50%, #9c5af2 100%)',
+ color: '#ffffff',
+ borderRadius: '4px',
+ whiteSpace: "nowrap",
+ textWrap: "normal",
+ border: 'none',
+ boxShadow: 'none',
+ transition: 'all 0.2s ease-in-out',
+ '&:hover': {
+ color: '#ffffff',
+ textShadow: '0 0 1px currentColor',
+ },
+ '&:active': {
+ background: 'linear-gradient(90deg, #e6743a 0%, #d4456e 50%, #8a4de8 100%)',
+ },
+ '&:disabled': {
+ background: themeMode === "dark" ? '#494949' : '#C9C9C9',
+ color: themeMode === "dark" ? '#9E9E9E' : '#616161',
+ },
+ },
+ },
+ {
+ props: { variant: 'aiButtonGhost' },
+ style: {
+ background: 'transparent',
+ border: 'none',
+ borderRadius: '4px',
+ color: '#ffffff',
+ whiteSpace: "nowrap",
+ textWrap: "normal",
+ boxShadow: 'none',
+ transition: 'all 0.2s ease-in-out',
+ position: 'relative',
+ '&::before': {
+ content: '""',
+ position: 'absolute',
+ top: 0,
+ left: 0,
+ right: 0,
+ bottom: 0,
+ borderRadius: '4px',
+ padding: '2px',
+ background: 'linear-gradient(90deg, #ff8544 0%, #ec517c 50%, #9c5af2 100%)',
+ mask: 'linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)',
+ WebkitMask: 'linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)',
+ maskComposite: 'exclude',
+ WebkitMaskComposite: 'xor',
+ transition: 'opacity 0.2s ease-in-out',
+ zIndex: -1,
+ },
+ '&:hover': {
+ background: 'linear-gradient(90deg, #ff8544 0%, #ec517c 50%, #9c5af2 100%)',
+ color: '#ffffff',
+ '&::before': {
+ opacity: 0,
+ },
+ },
+ '&:active': {
+ background: 'linear-gradient(90deg, #e6743a 0%, #d4456e 50%, #8a4de8 100%)',
+ },
+ '&:disabled': {
+ background: 'transparent',
+ color: themeMode === "dark" ? '#9E9E9E' : '#616161',
+ '&::before': {
+ background: themeMode === "dark" ? '#494949' : '#C9C9C9',
+ },
+ },
+ },
+ },
],
},
MuiTab: {
@@ -412,39 +484,39 @@ export const getTheme = (themeMode, brandColor) =>
MuiCssBaseline: {
styleOverrides: `
@font-face {
- font-family: 'Roboto';
+ font-family: 'Inter';
font-style: normal;
font-display: swap;
font-weight: 300;
- src: local('Roboto Light'), local('Roboto-Light');
+ src: local('Inter Light'), local('Inter-Light');
}
@font-face {
- font-family: 'Roboto';
+ font-family: 'Inter';
font-style: normal;
font-display: swap;
font-weight: 400;
- src: local('Roboto'), local('Roboto-Regular');
+ src: local('Inter Regular'), local('Inter-Regular');
}
@font-face {
- font-family: 'Roboto';
+ font-family: 'Inter';
font-style: normal;
font-display: swap;
font-weight: 500;
- src: local('Roboto Medium'), local('Roboto-Medium');
+ src: local('Inter Medium'), local('Inter-Medium');
}
@font-face {
- font-family: 'Roboto';
+ font-family: 'Inter';
font-style: normal;
font-display: swap;
font-weight: 600;
- src: local('Roboto SemiBold'), local('Roboto-SemiBold');
+ src: local('Inter SemiBold'), local('Inter-SemiBold');
}
@font-face {
- font-family: 'Roboto';
+ font-family: 'Inter';
font-style: normal;
font-display: swap;
font-weight: 700;
- src: local('Roboto Bold'), local('Roboto-Bold');
+ src: local('Inter Bold'), local('Inter-Bold');
}
`,
},
diff --git a/frontend/src/views/LoginPage.jsx b/frontend/src/views/LoginPage.jsx
index b717b9f6..d697f6f2 100755
--- a/frontend/src/views/LoginPage.jsx
+++ b/frontend/src/views/LoginPage.jsx
@@ -5,6 +5,7 @@ import { useNavigate, Link, useParams } from "react-router-dom";
import { isMobile } from "react-device-detect";
import { toast } from 'react-toastify';
import { useInterval } from "react-powerhooks";
+import ReactGA from 'react-ga4';
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined';
import {
@@ -157,7 +158,7 @@ const FreePlanCard = ({ classes }) => {
);
};
-const MarketplaceCard = ({ classes }) => {
+const MarketplaceCard = ({ classes, isCloud }) => {
const marketplaceOptions = [
{
name: "Open Source Install",
@@ -216,6 +217,15 @@ const MarketplaceCard = ({ classes }) => {
>
{
+ // Track marketplace click
+ if (isCloud) {
+ ReactGA.event({
+ category: "authentication",
+ action: "click_marketplace_option",
+ label: option.name,
+ });
+ }
+
if (option.valid === true) {
window.open(option.link, "_blank")
}
@@ -512,6 +522,15 @@ const LoginPage = props => {
const onSubmit = (e) => {
//toast("Testing from login page")
+ // Track form submission attempt
+ if (isCloud) {
+ ReactGA.event({
+ category: "authentication",
+ action: register ? "submit_login_form" : "submit_register_form",
+ label: register ? (loginWithSSO ? "SSO Login Form" : "Email Login Form") : "Email Registration Form",
+ });
+ }
+
setMessage("")
setLoginLoading(true)
e.preventDefault()
@@ -556,6 +575,14 @@ const LoginPage = props => {
setLoginLoading(false)
if (responseJson["success"] === false) {
+ // Track failed login event
+ if (isCloud) {
+ ReactGA.event({
+ category: "authentication",
+ action: loginWithSSO ? "login_sso_failed" : "login_failed",
+ label: loginWithSSO ? "SSO Login Failed" : "Email Login Failed",
+ });
+ }
setLoginInfo(responseJson["reason"])
} else {
if (responseJson?.region_url !== undefined && responseJson?.region_url !== null && responseJson?.region_url !== "") {
@@ -583,6 +610,14 @@ const LoginPage = props => {
return
}
+ // Track successful login event
+ if (isCloud) {
+ ReactGA.event({
+ category: "authentication",
+ action: loginWithSSO ? "login_sso_success" : "login_success",
+ label: loginWithSSO ? "SSO Login" : "Email Login",
+ });
+ }
setLoginInfo("Successful login! Redirecting you in 3 seconds...")
for (var key in responseJson["cookies"]) {
@@ -643,6 +678,14 @@ const LoginPage = props => {
.then(response =>
response.json().then(responseJson => {
if (responseJson["success"] === false) {
+ // Track failed registration event
+ if (isCloud) {
+ ReactGA.event({
+ category: "authentication",
+ action: "register_failed",
+ label: "Email Registration Failed",
+ });
+ }
setLoginInfo(responseJson["reason"])
} else {
if (responseJson["reason"] === "shuffle_account") {
@@ -659,6 +702,16 @@ const LoginPage = props => {
}
setLoginLoading(false)
+
+ // Track successful registration event
+ if (isCloud) {
+ ReactGA.event({
+ category: "authentication",
+ action: "register_success",
+ label: "Email Registration",
+ });
+ }
+
setLoginInfo("Successful registration! Redirecting in 3 seconds...")
@@ -707,15 +760,40 @@ const LoginPage = props => {
}
const HandleLoginWithSSO = () => {
+ // Track SSO login attempt
+ if (isCloud) {
+ ReactGA.event({
+ category: "authentication",
+ action: "click_sso_login",
+ label: "SSO Login Attempt",
+ });
+ }
+
setPassword("")
setLoginInfo("")
setLoginWithSSO(true)
}
var formtitle = register ? Welcome Back!
: Create your account
- var formButton = !isCloud ? "" : register ? Don’t have an account yet?
Register here
: <>
+ var formButton = !isCloud ? "" : register ? Don’t have an account yet?
{
+ if (isCloud) {
+ ReactGA.event({
+ category: "authentication",
+ action: "click_register_link",
+ label: "Switch to Register",
+ });
+ }
+ }}>
Register here
: <>
- Already have an account?
Login here
+ Already have an account?
{
+ if (isCloud) {
+ ReactGA.event({
+ category: "authentication",
+ action: "click_login_link",
+ label: "Switch to Login",
+ });
+ }
+ }}>
Login here
>
//Click here to Login
@@ -1025,7 +1103,7 @@ const LoginPage = props => {
OR
-
+
>
)}
diff --git a/functions/onprem/orborus/go.mod b/functions/onprem/orborus/go.mod
index c34b4b32..8e32a56d 100644
--- a/functions/onprem/orborus/go.mod
+++ b/functions/onprem/orborus/go.mod
@@ -10,7 +10,7 @@ require (
github.com/docker/docker v28.2.2+incompatible
github.com/docker/go-connections v0.5.0
github.com/satori/go.uuid v1.2.0
- github.com/shuffle/shuffle-shared v0.8.96
+ github.com/shuffle/shuffle-shared v0.8.99
k8s.io/api v0.33.1
k8s.io/apimachinery v0.33.1
)
diff --git a/functions/onprem/worker/go.mod b/functions/onprem/worker/go.mod
index 083d46a1..7b39554b 100644
--- a/functions/onprem/worker/go.mod
+++ b/functions/onprem/worker/go.mod
@@ -10,7 +10,7 @@ require (
github.com/docker/docker v28.2.2+incompatible
github.com/gorilla/mux v1.8.1
github.com/satori/go.uuid v1.2.0
- github.com/shuffle/shuffle-shared v0.8.96
+ github.com/shuffle/shuffle-shared v0.8.99
k8s.io/api v0.33.1
k8s.io/apimachinery v0.33.1
k8s.io/client-go v0.33.1