Merge remote-tracking branch 'upstream/nightly' into nightly
This commit is contained in:
+1
-1
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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) => {
|
||||
<div style={{ display: "flex" }}>
|
||||
<div style={{ flex: 1, color: theme.palette.textColor }}>
|
||||
<div style={{ display: "flex" }}>
|
||||
<Typography variant="h4" style={{ flex: 9, marginTop: 25, }}>
|
||||
<Typography variant="h4" style={{ flex: 9, marginTop: newWorkflow ? 50 : 25, }}>
|
||||
{newWorkflow ? "New" : "Editing"} Workflow
|
||||
</Typography>
|
||||
|
||||
@@ -298,6 +300,228 @@ const EditWorkflow = (props) => {
|
||||
paddingLeft: 30,
|
||||
backgroundColor: themeMode === "dark" ? "#262626" : theme.palette.DialogStyle.backgroundColor,
|
||||
}}>
|
||||
{newWorkflow === true ? (
|
||||
<div style={{ display: "flex", gap: 10 }}>
|
||||
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
style={{}}
|
||||
id="save_workflow_button"
|
||||
disabled={name.length === 0 || submitLoading === true || aiGenerateLoading === true}
|
||||
onClick={() => {
|
||||
setSubmitLoading(true)
|
||||
|
||||
// Loop inputfields
|
||||
var validfields = []
|
||||
for (var i = 0; i < inputQuestions.length; i++) {
|
||||
if (inputQuestions[i].deleted === true) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (inputQuestions[i].value.length === 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
validfields.push(inputQuestions[i])
|
||||
}
|
||||
|
||||
innerWorkflow.input_questions = validfields
|
||||
|
||||
if (innerWorkflow.form_control === undefined || innerWorkflow.form_control === null) {
|
||||
innerWorkflow.form_control = {}
|
||||
}
|
||||
|
||||
innerWorkflow.form_control.input_markdown = inputMarkdown
|
||||
innerWorkflow.form_control.output_yields = selectedYieldActions
|
||||
innerWorkflow.form_control.form_width = formWidth
|
||||
innerWorkflow.form_control.cleanup_actions = selectedCleanupActions
|
||||
|
||||
innerWorkflow.name = name
|
||||
innerWorkflow.description = description
|
||||
|
||||
if (newWorkflowTags.length > 0) {
|
||||
innerWorkflow.tags = newWorkflowTags
|
||||
} else {
|
||||
innerWorkflow.tags = []
|
||||
}
|
||||
|
||||
if (selectedUsecases.length > 0) {
|
||||
innerWorkflow.usecase_ids = selectedUsecases
|
||||
} else {
|
||||
innerWorkflow.usecase_ids = []
|
||||
}
|
||||
|
||||
if (dueDate > 0) {
|
||||
innerWorkflow.due_date = new Date(`${dueDate["$y"]}-${dueDate["$M"] + 1}-${dueDate["$D"]}`).getTime() / 1000
|
||||
}
|
||||
|
||||
if (saveWorkflow !== undefined) {
|
||||
saveWorkflow(innerWorkflow)
|
||||
|
||||
if (setWorkflow !== undefined) {
|
||||
setWorkflow(innerWorkflow)
|
||||
}
|
||||
} else if (setNewWorkflow !== undefined) {
|
||||
setNewWorkflow(
|
||||
innerWorkflow.name,
|
||||
innerWorkflow.description,
|
||||
innerWorkflow.tags,
|
||||
innerWorkflow.default_return_value,
|
||||
innerWorkflow,
|
||||
newWorkflow,
|
||||
innerWorkflow.usecase_ids,
|
||||
innerWorkflow.blogpost,
|
||||
innerWorkflow.status,
|
||||
workflowAsCode
|
||||
)
|
||||
setWorkflow({})
|
||||
} else {
|
||||
setWorkflow(innerWorkflow)
|
||||
}
|
||||
|
||||
setSubmitLoading(true)
|
||||
|
||||
// If new workflow, don't close it
|
||||
if (isEditing) {
|
||||
setModalOpen(false)
|
||||
}
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
{submitLoading ? <CircularProgress color="secondary" /> : "Create from scratch"}
|
||||
</Button>
|
||||
|
||||
<Tooltip placement="top" arrow
|
||||
title={
|
||||
<Typography variant="body1" style={{padding: 10, }}>
|
||||
Generate using the name, description, usecases and tags provided. Required: Name + Description
|
||||
</Typography>
|
||||
}
|
||||
>
|
||||
<span>
|
||||
<Button
|
||||
disabled={name.length === 0 || innerWorkflow?.default_return_value?.length === 0 || aiGenerateLoading === true || submitLoading === true}
|
||||
variant="aiButton"
|
||||
onClick={async () => {
|
||||
// Check if description is provided in the visible field for new workflows
|
||||
if (!innerWorkflow.default_return_value || innerWorkflow.default_return_value.trim().length === 0) {
|
||||
toast.error("You need to describe what you want to generate so the AI can auto generate the entire workflow");
|
||||
return;
|
||||
}
|
||||
|
||||
setAiGenerateLoading(true);
|
||||
toast.info("Creating workflow...");
|
||||
|
||||
try {
|
||||
// Step 1: Create basic workflow WITHOUT using setNewWorkflow (to avoid modal closing)
|
||||
const workflowData = {
|
||||
name: name.length > 0 ? name : "AI Generated Workflow",
|
||||
description: innerWorkflow.default_return_value,
|
||||
tags: newWorkflowTags,
|
||||
default_return_value: innerWorkflow.default_return_value,
|
||||
usecase_ids: selectedUsecases,
|
||||
blogpost: innerWorkflow.blogpost || "",
|
||||
status: innerWorkflow.status || ""
|
||||
};
|
||||
|
||||
const workflowResponse = await fetch(`${globalUrl}/api/v1/workflows`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: JSON.stringify(workflowData),
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (workflowResponse.status !== 200) {
|
||||
toast.error("Failed to create workflow");
|
||||
setAiGenerateLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const workflowJson = await workflowResponse.json();
|
||||
|
||||
if (workflowJson.success === false) {
|
||||
toast.error("Failed to create workflow: " + (workflowJson.reason || "Unknown error"));
|
||||
setAiGenerateLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!workflowJson || !workflowJson.id) {
|
||||
toast.error("Failed to create workflow - no ID returned");
|
||||
setAiGenerateLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const workflowId = workflowJson.id;
|
||||
console.log("Created workflow with ID:", workflowId);
|
||||
|
||||
// Step 2: Generate AI content for the workflow
|
||||
const data = {
|
||||
query: innerWorkflow.default_return_value,
|
||||
workflow_id: workflowId,
|
||||
};
|
||||
|
||||
const aiResponse = await fetch(globalUrl + "/api/v2/workflows/generate/llm", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
const json = await aiResponse.json();
|
||||
|
||||
// Handle AI response and provide feedback
|
||||
if (aiResponse.status !== 200) {
|
||||
console.log("AI generation failed, but workflow created:", json.message || "Unexpected response");
|
||||
toast.warning("Workflow created successfully, but AI generation failed. Opening workflow editor...");
|
||||
} else if (json.success === true && typeof json.message === "string") {
|
||||
// AI "rejection" message
|
||||
console.log("AI rejected request:", json.message);
|
||||
toast.warning(`AI: ${json.message}. Opening empty workflow editor...`);
|
||||
} else if (json.success === false) {
|
||||
console.log("AI generation failed:", json.message || "Operation failed");
|
||||
toast.warning("AI generation failed. Opening empty workflow editor...");
|
||||
} else if (!json || Object.keys(json).length === 0) {
|
||||
console.log("Empty AI response");
|
||||
toast.warning("AI returned empty response. Opening workflow editor...");
|
||||
} else {
|
||||
toast.success("Workflow generated successfully! Opening editor...");
|
||||
console.log("AI generation successful");
|
||||
}
|
||||
|
||||
// Step 3: Now close modal and redirect (only after everything is complete)
|
||||
setTimeout(() => {
|
||||
setModalOpen(false);
|
||||
setAiGenerateLoading(false);
|
||||
window.location.href = `/workflows/${workflowId}`;
|
||||
}, 1500); // Give user time to read the final message
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error in AI workflow generation:", error);
|
||||
toast.error("Failed to generate. Please try again later: " + error.message);
|
||||
setAiGenerateLoading(false);
|
||||
// Don't close modal on error so user can try again
|
||||
}
|
||||
}}
|
||||
>
|
||||
{aiGenerateLoading ? (
|
||||
<CircularProgress color="secondary" size={20} style={{ marginRight: 8 }} />
|
||||
) : (
|
||||
<AutoAwesomeIcon style={{ marginRight: 8 }} />
|
||||
)}
|
||||
|
||||
AI Generate
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
) : (
|
||||
<Button
|
||||
variant="contained"
|
||||
style={{}}
|
||||
@@ -385,6 +609,7 @@ const EditWorkflow = (props) => {
|
||||
>
|
||||
{submitLoading ? <CircularProgress color="secondary" /> : "Save Changes"}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogContent style={{ paddingTop: 10, display: "flex", minHeight: 300, zIndex: 1001, paddingBottom: 400, paddingLeft: 50, }}>
|
||||
@@ -409,6 +634,29 @@ const EditWorkflow = (props) => {
|
||||
id="Enter-Workflow-Name"
|
||||
/>
|
||||
|
||||
{newWorkflow === true ?
|
||||
<TextField
|
||||
onBlur={(event) => {
|
||||
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}
|
||||
|
||||
<div style={{ display: "flex", marginTop: 10, }}>
|
||||
{usecases !== null && usecases !== undefined && usecases.length > 0 ?
|
||||
<FormControl style={{ flex: 1, marginRight: 5, }}>
|
||||
@@ -1285,7 +1533,7 @@ const EditWorkflow = (props) => {
|
||||
</DialogContent>
|
||||
|
||||
|
||||
{newWorkflow === true ?
|
||||
{/*newWorkflow === true ?
|
||||
<span style={{ paddingTop: 30 }}>
|
||||
<Typography variant="h6" style={{ marginLeft: 30, paddingBottom: 0, }}>
|
||||
Relevant Workflows
|
||||
@@ -1311,8 +1559,7 @@ const EditWorkflow = (props) => {
|
||||
}
|
||||
|
||||
</span>
|
||||
: null}
|
||||
|
||||
: null*/}
|
||||
{/*newWorkflow === true && name.length > 2 ?
|
||||
<div style={{marginLeft: 30, }}>
|
||||
<WorkflowGrid
|
||||
|
||||
@@ -396,10 +396,8 @@ const EnvironmentTab = memo((props) => {
|
||||
environment.auth === ""
|
||||
? "cb5st3d3Z!3X3zaJ*Pc"
|
||||
: environment.auth
|
||||
|
||||
// Escape exclamation marks for copying
|
||||
auth = auth.replace("\\!", "!").replace(/!/g, "\\!")
|
||||
|
||||
const newUrl =
|
||||
globalUrl === "https://shuffler.io"
|
||||
? "https://shuffle-backend-stbuwivzoq-nw.a.run.app"
|
||||
@@ -433,7 +431,25 @@ const EnvironmentTab = memo((props) => {
|
||||
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 \\
|
||||
@@ -452,6 +468,7 @@ const EnvironmentTab = memo((props) => {
|
||||
return commandData
|
||||
};
|
||||
|
||||
|
||||
const submitEnvironment = (data) => {
|
||||
// FIXME - add some check here ROFL
|
||||
environments.push({
|
||||
@@ -1475,7 +1492,7 @@ const EnvironmentTab = memo((props) => {
|
||||
<Typography variant="body1" color="textSecondary" style={{marginTop: 15, }}>
|
||||
{installationTab === 2 ?
|
||||
<Typography variant='body2' color="textSecondary">
|
||||
Check our <a href="https://docs.docker.com/get-started/get-docker/" target="_blank" rel="noopener noreferrer" style={{textDecoration: "none", color: "#f85a3e",}}>Kubernetes documentation</a> 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:
|
||||
</Typography>
|
||||
:
|
||||
<Typography variant='body2' color="textSecondary">
|
||||
@@ -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"}
|
||||
</Typography>
|
||||
|
||||
{installationTab === 2 ? null :
|
||||
<div
|
||||
style={{
|
||||
marginTop: 10,
|
||||
@@ -1572,7 +1588,6 @@ const EnvironmentTab = memo((props) => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<Typography variant="body1" color="textSecondary" style={{marginTop: 15, }}>
|
||||
{installationTab === 2 ? null :
|
||||
|
||||
@@ -1,21 +1,92 @@
|
||||
import React from 'react';
|
||||
import { Bar } from 'react-chartjs-2';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
const HealthBarChart = (props) => {
|
||||
const { globalUrl, filteredData, options, onBarClick } = props;
|
||||
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 (
|
||||
<Bar
|
||||
data={filteredData}
|
||||
options={options}
|
||||
height="35.5rem"
|
||||
width={filteredData.width}
|
||||
getElementAtEvent={(elements) => {
|
||||
if (elements && elements.length > 0) {
|
||||
onBarClick(elements);
|
||||
}
|
||||
<div style={{
|
||||
height: '5rem',
|
||||
width: '100%',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
alignItems: 'center', // Center the content vertically
|
||||
justifyContent: 'center', // Center the content horizontally
|
||||
padding: '0.5rem 0' // Add some padding for spacing
|
||||
}}>
|
||||
{/* Chart Container */}
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
height: '80%', // Take up 80% of the parent height
|
||||
width: '100%',
|
||||
alignItems: 'center', // Center the bars vertically
|
||||
gap: barGap,
|
||||
}}>
|
||||
{chartData.map((item, index) => (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
width: barWidth,
|
||||
height: '70%', // 70% of the container height
|
||||
backgroundColor: item.color,
|
||||
cursor: 'pointer',
|
||||
borderRadius: '2px',
|
||||
}}
|
||||
onMouseEnter={() => setHoveredBar(item)}
|
||||
onMouseLeave={() => setHoveredBar(null)}
|
||||
onClick={() => handleBarClick(item)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Tooltip */}
|
||||
{hoveredBar && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '-3rem', // Position above the chart
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
backgroundColor: '#333333',
|
||||
color: '#ffffff',
|
||||
padding: '8px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px',
|
||||
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.5)',
|
||||
zIndex: 1000,
|
||||
}}
|
||||
>
|
||||
<div>Date: {hoveredBar.label}</div>
|
||||
<div>Uptime: {hoveredBar.value}%</div>
|
||||
<div>Executions: {hoveredBar.executionIds.length}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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,12 +46,99 @@ 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 [];
|
||||
@@ -58,10 +149,10 @@ const HealthPage = (props) => {
|
||||
'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 filteredData = data.filter(item => currentDate - item.updated * 1000 <= rangeInMillis[range]);
|
||||
|
||||
const aggregatedData = new Map()
|
||||
const aggregatedData = new Map();
|
||||
|
||||
filteredData.forEach(item => {
|
||||
const timestamp = item.updated * 1000; // Convert Unix timestamp to milliseconds
|
||||
@@ -126,7 +217,6 @@ const HealthPage = (props) => {
|
||||
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,33 +364,119 @@ const HealthPage = (props) => {
|
||||
}
|
||||
};
|
||||
|
||||
const healthBarData = updateChartData()
|
||||
|
||||
|
||||
return (
|
||||
<div style={{ padding: 30, width: '100%', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
|
||||
|
||||
<div style={{ paddingTop: 30, width: '100%', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center' }}>
|
||||
{/* Health Bar Chart Section */}
|
||||
<ButtonGroup style={{ display: 'flex', margin: "auto", marginBottom: 10, width: 300, borderRadius: 30, background: "#000000" }}>
|
||||
<Button variant="contained" style={{ flex: 1, borderBottom: selectedRange === '24hr' ? '2px solid #FF8444' : 'none', background: "#000000", color: selectedRange === '24hr' ? '#FF8444' : '#cfd8dc', textTransform: 'none' }} onClick={() => filterDataByRange('24hr')}>24h</Button>
|
||||
<Button variant="contained" style={{ flex: 1, borderBottom: selectedRange === '7day' ? '2px solid #FF8444' : 'none', background: "#000000", color: selectedRange === '7day' ? '#FF8444' : '#cfd8dc', textTransform: 'none' }} onClick={() => filterDataByRange('7day')}>7d</Button>
|
||||
<Button variant="contained" style={{ flex: 1, borderBottom: selectedRange === '30d' ? '2px solid #FF8444' : 'none', background: "#000000", color: selectedRange === '30d' ? '#FF8444' : '#cfd8dc', textTransform: 'none' }} onClick={() => filterDataByRange('30d')}>30d</Button>
|
||||
<Button disabled variant="contained" style={{ flex: 1, borderBottom: selectedRange === '90d' ? '2px solid #FF8444' : 'none', background: "#000000", color: false === false ? "grey" : selectedRange === '90d' ? '#FF8444' : '#cfd8dc', textTransform: 'none' }} onClick={() => filterDataByRange('90d')}>90d</Button>
|
||||
<Button disabled variant="contained" style={{ flex: 1, borderBottom: selectedRange === '180d' ? '2px solid #FF8444' : 'none', background: "#000000", color: false === false ? "grey" : selectedRange === '180d' ? '#FF8444' : '#cfd8dc', textTransform: 'none' }} onClick={() => filterDataByRange('180d')}>180d</Button>
|
||||
<Button variant="contained" style={{ flex: 1, borderBottom: selectedRange === '24hr' ? '2px solid #FF8444' : 'none', background: "#000000", color: selectedRange === '24hr' ? '#FF8444' : '#cfd8dc', textTransform: 'none' }} onClick={() => filterDataByRange('24hr')} disabled={isHealthLoading}>24h</Button>
|
||||
<Button variant="contained" style={{ flex: 1, borderBottom: selectedRange === '7day' ? '2px solid #FF8444' : 'none', background: "#000000", color: selectedRange === '7day' ? '#FF8444' : '#cfd8dc', textTransform: 'none' }} onClick={() => filterDataByRange('7day')} disabled={isHealthLoading}>7d</Button>
|
||||
<Button variant="contained" style={{ flex: 1, borderBottom: selectedRange === '30d' ? '2px solid #FF8444' : 'none', background: "#000000", color: selectedRange === '30d' ? '#FF8444' : '#cfd8dc', textTransform: 'none' }} onClick={() => filterDataByRange('30d')} disabled={isHealthLoading}>30d</Button>
|
||||
<Button disabled variant="contained" style={{ flex: 1, borderBottom: selectedRange === '90d' ? '2px solid #FF8444' : 'none', background: "#000000", color: false === false ? "grey" : selectedRange === '90d' ? '#FF8444' : '#cfd8dc', textTransform: 'none' }} onClick={() => filterDataByRange('90d')} disabled={isHealthLoading}>90d</Button>
|
||||
<Button disabled variant="contained" style={{ flex: 1, borderBottom: selectedRange === '180d' ? '2px solid #FF8444' : 'none', background: "#000000", color: false === false ? "grey" : selectedRange === '180d' ? '#FF8444' : '#cfd8dc', textTransform: 'none' }} onClick={() => filterDataByRange('180d')} disabled={isHealthLoading}>180d</Button>
|
||||
</ButtonGroup>
|
||||
|
||||
<div style={{ margin: '0 auto', padding: 20, width: 1000, justifyContent: "center", color: '#ffffff', backgroundColor: '#000000', fontSize: '16px', borderRadius: '16px' }}>
|
||||
<div style={{ display: "flex", alignItems: "center" }}>
|
||||
<CheckOutlinedIcon style={{ borderRadius: 20, fontSize: 24, backgroundColor: '#00e600', marginLeft: 25 }} />
|
||||
{/* Loading Bar for HealthBarChart */}
|
||||
{isHealthLoading && (
|
||||
<LinearProgress style={{ width: '100%', marginBottom: 10 }} />
|
||||
)}
|
||||
|
||||
<div style={{ margin: '0 auto', padding: 10, width: 800, backgroundColor: '#000000', borderRadius: '16px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<CheckOutlinedIcon style={{ borderRadius: 20, fontSize: 24, backgroundColor: '#00e600', marginRight: 10 }} />
|
||||
<div>
|
||||
<Typography style={{ marginLeft: 10 }}>Workflow Health</Typography>
|
||||
<Typography style={{ marginLeft: 10, fontWeight: 100, fontSize: 13, color: "#00FF00" }}>Operational</Typography>
|
||||
</div>
|
||||
<div style={{ marginLeft: 720 }}>
|
||||
<Typography style={{}}>{averageUptime.toFixed(2)}%</Typography>
|
||||
<Typography style={{ fontWeight: 100, fontSize: 13, textAlign: "end" }}>Success Rate</Typography>
|
||||
<Typography style={{ color: '#ffffff', fontSize: 16 }}>Workflow Health</Typography>
|
||||
<Typography style={{ color: '#00FF00', fontSize: 12, fontWeight: 100 }}>Operational</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<HealthBarChart filteredData={updateChartData()} options={options} onBarClick={handleBarClick} />
|
||||
<div>
|
||||
<Typography style={{ color: '#ffffff', fontSize: 16, textAlign: 'end' }}>{averageUptime.toFixed(2)}%</Typography>
|
||||
<Typography style={{ color: '#ffffff', fontSize: 12, fontWeight: 100 }}>Success Rate</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<HealthBarChart
|
||||
filteredData={filteredData}
|
||||
onBarClick={handleBarClick}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{userdata.support_access && (
|
||||
<div style={{ margin: '20px auto', padding: 20, width: 1000, justifyContent: "center", color: '#ffffff', backgroundColor: '#000000', fontSize: '16px', borderRadius: '16px' }}>
|
||||
<div style={{ display: "flex", alignItems: "center", marginBottom: 20 }}>
|
||||
<Typography variant="h6" style={{ marginLeft: 10 }}>Live Executions</Typography>
|
||||
<ButtonGroup style={{ marginLeft: 'auto', borderRadius: 30, background: "#000000" }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
style={{
|
||||
flex: 1,
|
||||
borderBottom: liveExecutionsRange === '1h' ? '2px solid #FF8444' : 'none',
|
||||
background: "#000000",
|
||||
color: liveExecutionsRange === '1h' ? '#FF8444' : '#cfd8dc',
|
||||
textTransform: 'none'
|
||||
}}
|
||||
onClick={() => setLiveExecutionsRange('1h')}
|
||||
disabled={isLiveExecutionsLoading}
|
||||
>
|
||||
1h
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
style={{
|
||||
flex: 1,
|
||||
borderBottom: liveExecutionsRange === '7h' ? '2px solid #FF8444' : 'none',
|
||||
background: "#000000",
|
||||
color: liveExecutionsRange === '7h' ? '#FF8444' : '#cfd8dc',
|
||||
textTransform: 'none'
|
||||
}}
|
||||
onClick={() => setLiveExecutionsRange('7h')}
|
||||
disabled={isLiveExecutionsLoading}
|
||||
>
|
||||
7h
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
style={{
|
||||
flex: 1,
|
||||
borderBottom: liveExecutionsRange === '1d' ? '2px solid #FF8444' : 'none',
|
||||
background: "#000000",
|
||||
color: liveExecutionsRange === '1d' ? '#FF8444' : '#cfd8dc',
|
||||
textTransform: 'none'
|
||||
}}
|
||||
onClick={() => setLiveExecutionsRange('1d')}
|
||||
disabled={isLiveExecutionsLoading}
|
||||
>
|
||||
1d
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
style={{
|
||||
flex: 1,
|
||||
borderBottom: liveExecutionsRange === '7d' ? '2px solid #FF8444' : 'none',
|
||||
background: "#000000",
|
||||
color: liveExecutionsRange === '7d' ? '#FF8444' : '#cfd8dc',
|
||||
textTransform: 'none'
|
||||
}}
|
||||
onClick={() => setLiveExecutionsRange('7d')}
|
||||
disabled={isLiveExecutionsLoading}
|
||||
>
|
||||
7d
|
||||
</Button>
|
||||
{/* <Button variant="contained" style={{ flex: 1, borderBottom: liveExecutionsRange === 'month' ? '2px solid #FF8444' : 'none', background: "#000000", color: liveExecutionsRange === 'month' ? '#FF8444' : '#cfd8dc', textTransform: 'none' }} onClick={() => setLiveExecutionsRange('month')} disabled={isLiveExecutionsLoading}>Month</Button> */}
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
{/* Loading Bar for LiveExecutionsChart */}
|
||||
{isLiveExecutionsLoading && (
|
||||
<LinearProgress style={{ width: '100%', marginBottom: 10 }} />
|
||||
)}
|
||||
<LiveExecutionsChart data={liveExecutionsData} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
|
||||
const LiveExecutionsGraph = ({ executions }) => {
|
||||
return null
|
||||
}
|
||||
|
||||
export default LiveExecutionsGraph
|
||||
@@ -741,7 +741,7 @@ const PartnerHeader = (props) => {
|
||||
<div >
|
||||
<Button
|
||||
style={{ fontSize: 16, textTransform: 'capitalize', boxShadow: "none", borderRadius: 4, width: 128, height: 40 }}
|
||||
variant="contained"
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
disabled={isDisabled || isPublishing}
|
||||
onClick={() => {
|
||||
@@ -812,7 +812,7 @@ const PartnerHeader = (props) => {
|
||||
<div >
|
||||
<Button
|
||||
style={{ fontSize: 16, textTransform: 'capitalize', boxShadow: "none", borderRadius: 4, width: 128, height: 40 }}
|
||||
variant="contained"
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
disabled={isDisabled || isPublishing}
|
||||
onClick={() => {
|
||||
|
||||
+82
-10
@@ -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');
|
||||
}
|
||||
`,
|
||||
},
|
||||
|
||||
@@ -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 }) => {
|
||||
>
|
||||
<button
|
||||
onClick={() => {
|
||||
// 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 ? <div>Welcome Back!</div> : <div>Create your account</div>
|
||||
var formButton = !isCloud ? "" : register ? <div style={{ display: "flex" }}> <div style={{ fontSize: "14px", paddingRight: "7px", textDecoration: "none", }}>Don’t have an account yet?</div> <Link style={hrefStyle} to={`/register${parsedsearch}`}><div>Register here</div></Link></div> : <>
|
||||
var formButton = !isCloud ? "" : register ? <div style={{ display: "flex" }}> <div style={{ fontSize: "14px", paddingRight: "7px", textDecoration: "none", }}>Don’t have an account yet?</div> <Link style={hrefStyle} to={`/register${parsedsearch}`} onClick={() => {
|
||||
if (isCloud) {
|
||||
ReactGA.event({
|
||||
category: "authentication",
|
||||
action: "click_register_link",
|
||||
label: "Switch to Register",
|
||||
});
|
||||
}
|
||||
}}><div>Register here</div></Link></div> : <>
|
||||
|
||||
<div style={{ display: "flex", marginTop: 40, marginBottom: -10 }}> <div style={{ fontSize: "14px", paddingRight: "7px", textDecoration: "none", }}>Already have an account?</div> <Link style={hrefStyle} to={`/login${parsedsearch}`}><div>Login here</div></Link></div>
|
||||
<div style={{ display: "flex", marginTop: 40, marginBottom: -10 }}> <div style={{ fontSize: "14px", paddingRight: "7px", textDecoration: "none", }}>Already have an account?</div> <Link style={hrefStyle} to={`/login${parsedsearch}`} onClick={() => {
|
||||
if (isCloud) {
|
||||
ReactGA.event({
|
||||
category: "authentication",
|
||||
action: "click_login_link",
|
||||
label: "Switch to Login",
|
||||
});
|
||||
}
|
||||
}}><div>Login here</div></Link></div>
|
||||
</>
|
||||
//<Link to={`/login${parsedsearch}`} style={hrefStyle}><div>Click here to Login</div></Link>
|
||||
|
||||
@@ -1025,7 +1103,7 @@ const LoginPage = props => {
|
||||
<div className={classes.divider}>
|
||||
<span>OR</span>
|
||||
</div>
|
||||
<MarketplaceCard classes={classes} />
|
||||
<MarketplaceCard classes={classes} isCloud={isCloud} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user