Many small fixes :)

This commit is contained in:
Frikky
2025-08-18 17:03:54 +02:00
parent 5ba5d2fb3d
commit 45ca47c282
7 changed files with 529 additions and 116 deletions
+1 -1
View File
@@ -5,7 +5,7 @@ go 1.24.0
toolchain go1.24.3 toolchain go1.24.3
//replace github.com/frikky/schemaless => ../../../schemaless //replace github.com/frikky/schemaless => ../../../schemaless
//replace github.com/shuffle/shuffle-shared => ../../../shuffle-shared replace github.com/shuffle/shuffle-shared => ../../../shuffle-shared
//replace github.com/frikky/kin-openapi => ../../../../git/kin-openapi //replace github.com/frikky/kin-openapi => ../../../../git/kin-openapi
+2 -1
View File
@@ -4099,8 +4099,9 @@ func runInitEs(ctx context.Context) {
time.Sleep(30 * time.Second) 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") schedules, err := shuffle.GetAllSchedules(ctx, "ALL")
if err != nil { if err != nil {
log.Printf("[WARNING] Failed getting schedules during service init: %s", err) log.Printf("[WARNING] Failed getting schedules during service init: %s", err)
+324 -77
View File
@@ -63,6 +63,7 @@ import {
Add as AddIcon, Add as AddIcon,
Remove as RemoveIcon, Remove as RemoveIcon,
EditNote as EditNoteIcon, EditNote as EditNoteIcon,
AutoAwesome as AutoAwesomeIcon
} from "@mui/icons-material"; } from "@mui/icons-material";
const EditWorkflow = (props) => { const EditWorkflow = (props) => {
@@ -72,6 +73,7 @@ const EditWorkflow = (props) => {
const {themeMode, brandColor} = useContext(Context) const {themeMode, brandColor} = useContext(Context)
const theme = getTheme(themeMode, brandColor) const theme = getTheme(themeMode, brandColor)
const [submitLoading, setSubmitLoading] = React.useState(false); const [submitLoading, setSubmitLoading] = React.useState(false);
const [aiGenerateLoading, setAiGenerateLoading] = React.useState(false);
const [showMoreClicked, setShowMoreClicked] = React.useState(isEditing !== false ? true : false); const [showMoreClicked, setShowMoreClicked] = React.useState(isEditing !== false ? true : false);
const [innerWorkflow, setInnerWorkflow] = React.useState(workflow) const [innerWorkflow, setInnerWorkflow] = React.useState(workflow)
@@ -220,7 +222,7 @@ const EditWorkflow = (props) => {
<div style={{ display: "flex" }}> <div style={{ display: "flex" }}>
<div style={{ flex: 1, color: theme.palette.textColor }}> <div style={{ flex: 1, color: theme.palette.textColor }}>
<div style={{ display: "flex" }}> <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 {newWorkflow ? "New" : "Editing"} Workflow
</Typography> </Typography>
@@ -298,93 +300,316 @@ const EditWorkflow = (props) => {
paddingLeft: 30, paddingLeft: 30,
backgroundColor: themeMode === "dark" ? "#262626" : theme.palette.DialogStyle.backgroundColor, backgroundColor: themeMode === "dark" ? "#262626" : theme.palette.DialogStyle.backgroundColor,
}}> }}>
<Button {newWorkflow === true ? (
variant="contained" <div style={{ display: "flex", gap: 10 }}>
style={{}}
id="save_workflow_button"
disabled={name.length === 0 || submitLoading === true}
onClick={() => {
setSubmitLoading(true)
// Loop inputfields
var validfields = [] <Button
for (var i = 0; i < inputQuestions.length; i++) { variant="contained"
if (inputQuestions[i].deleted === true) { style={{}}
continue 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={{}}
id="save_workflow_button"
disabled={name.length === 0 || submitLoading === 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])
} }
if (inputQuestions[i].value.length === 0) { innerWorkflow.input_questions = validfields
continue
if (innerWorkflow.form_control === undefined || innerWorkflow.form_control === null) {
innerWorkflow.form_control = {}
} }
validfields.push(inputQuestions[i]) 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.input_questions = validfields innerWorkflow.name = name
innerWorkflow.description = description
if (innerWorkflow.form_control === undefined || innerWorkflow.form_control === null) { if (newWorkflowTags.length > 0) {
innerWorkflow.form_control = {} innerWorkflow.tags = newWorkflowTags
} } else {
innerWorkflow.tags = []
}
innerWorkflow.form_control.input_markdown = inputMarkdown if (selectedUsecases.length > 0) {
innerWorkflow.form_control.output_yields = selectedYieldActions innerWorkflow.usecase_ids = selectedUsecases
innerWorkflow.form_control.form_width = formWidth } else {
innerWorkflow.form_control.cleanup_actions = selectedCleanupActions innerWorkflow.usecase_ids = []
}
innerWorkflow.name = name if (dueDate > 0) {
innerWorkflow.description = description innerWorkflow.due_date = new Date(`${dueDate["$y"]}-${dueDate["$M"] + 1}-${dueDate["$D"]}`).getTime() / 1000
}
if (newWorkflowTags.length > 0) { if (saveWorkflow !== undefined) {
innerWorkflow.tags = newWorkflowTags saveWorkflow(innerWorkflow)
} else {
innerWorkflow.tags = []
}
if (selectedUsecases.length > 0) { if (setWorkflow !== undefined) {
innerWorkflow.usecase_ids = selectedUsecases setWorkflow(innerWorkflow)
} else { }
innerWorkflow.usecase_ids = [] } else if (setNewWorkflow !== undefined) {
} setNewWorkflow(
innerWorkflow.name,
if (dueDate > 0) { innerWorkflow.description,
innerWorkflow.due_date = new Date(`${dueDate["$y"]}-${dueDate["$M"] + 1}-${dueDate["$D"]}`).getTime() / 1000 innerWorkflow.tags,
} innerWorkflow.default_return_value,
innerWorkflow,
if (saveWorkflow !== undefined) { newWorkflow,
saveWorkflow(innerWorkflow) innerWorkflow.usecase_ids,
innerWorkflow.blogpost,
if (setWorkflow !== undefined) { innerWorkflow.status,
workflowAsCode
)
setWorkflow({})
} else {
setWorkflow(innerWorkflow) 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) setSubmitLoading(true)
// If new workflow, don't close it // If new workflow, don't close it
if (isEditing) { if (isEditing) {
setModalOpen(false) setModalOpen(false)
} }
}} }}
color="primary" color="primary"
> >
{submitLoading ? <CircularProgress color="secondary" /> : "Save Changes"} {submitLoading ? <CircularProgress color="secondary" /> : "Save Changes"}
</Button> </Button>
)}
</div> </div>
<DialogContent style={{ paddingTop: 10, display: "flex", minHeight: 300, zIndex: 1001, paddingBottom: 400, paddingLeft: 50, }}> <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" 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, }}> <div style={{ display: "flex", marginTop: 10, }}>
{usecases !== null && usecases !== undefined && usecases.length > 0 ? {usecases !== null && usecases !== undefined && usecases.length > 0 ?
<FormControl style={{ flex: 1, marginRight: 5, }}> <FormControl style={{ flex: 1, marginRight: 5, }}>
@@ -1285,7 +1533,7 @@ const EditWorkflow = (props) => {
</DialogContent> </DialogContent>
{newWorkflow === true ? {/*newWorkflow === true ?
<span style={{ paddingTop: 30 }}> <span style={{ paddingTop: 30 }}>
<Typography variant="h6" style={{ marginLeft: 30, paddingBottom: 0, }}> <Typography variant="h6" style={{ marginLeft: 30, paddingBottom: 0, }}>
Relevant Workflows Relevant Workflows
@@ -1311,9 +1559,8 @@ const EditWorkflow = (props) => {
} }
</span> </span>
: null} : null*/}
{/*newWorkflow === true && name.length > 2 ?
{/*newWorkflow === true && name.length > 2 ?
<div style={{marginLeft: 30, }}> <div style={{marginLeft: 30, }}>
<WorkflowGrid <WorkflowGrid
maxRows={1} maxRows={1}
+29 -14
View File
@@ -396,10 +396,8 @@ const EnvironmentTab = memo((props) => {
environment.auth === "" environment.auth === ""
? "cb5st3d3Z!3X3zaJ*Pc" ? "cb5st3d3Z!3X3zaJ*Pc"
: environment.auth : environment.auth
// Escape exclamation marks for copying
// Escape exclamation marks for copying auth = auth.replace("\\!", "!").replace(/!/g, "\\!")
auth = auth.replace("\\!", "!").replace(/!/g, "\\!")
const newUrl = const newUrl =
globalUrl === "https://shuffler.io" globalUrl === "https://shuffler.io"
? "https://shuffle-backend-stbuwivzoq-nw.a.run.app" ? "https://shuffle-backend-stbuwivzoq-nw.a.run.app"
@@ -428,12 +426,30 @@ const EnvironmentTab = memo((props) => {
-e SHUFFLE_SWARM_CONFIG=run \\ -e SHUFFLE_SWARM_CONFIG=run \\
-e SHUFFLE_LOGS_DISABLED=true \\ -e SHUFFLE_LOGS_DISABLED=true \\
-e BASE_URL="${newUrl}" \\${addProxy ? ` -e BASE_URL="${newUrl}" \\${addProxy ? `
-e HTTPS_PROXY=IP:PORT \\` : ""}${skipPipeline ? ` -e HTTPS_PROXY=IP:PORT \\` : ""}${skipPipeline ? `
-e SHUFFLE_SKIP_PIPELINES=true \\` : ""} -e SHUFFLE_SKIP_PIPELINES=true \\` : ""}
ghcr.io/shuffle/shuffle-orborus:latest ghcr.io/shuffle/shuffle-orborus:latest
`) `)
} else if (installationTab === 2) { } 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 \\ const commandData = `docker rm shuffle-orborus --force; \\\ndocker run -d \\
@@ -445,12 +461,13 @@ const EnvironmentTab = memo((props) => {
-e ENVIRONMENT_NAME="${environment.Name}" \\ -e ENVIRONMENT_NAME="${environment.Name}" \\
-e ORG="${props.userdata.active_org.id}" \\ -e ORG="${props.userdata.active_org.id}" \\
-e BASE_URL="${newUrl}" \\${addProxy ? ` -e BASE_URL="${newUrl}" \\${addProxy ? `
-e HTTPS_PROXY=IP:PORT \\` : ""}${skipPipeline ? ` -e HTTPS_PROXY=IP:PORT \\` : ""}${skipPipeline ? `
-e SHUFFLE_SKIP_PIPELINES=true \\` : ""} -e SHUFFLE_SKIP_PIPELINES=true \\` : ""}
ghcr.io/shuffle/shuffle-orborus:latest` ghcr.io/shuffle/shuffle-orborus:latest`
return commandData return commandData
}; };
const submitEnvironment = (data) => { const submitEnvironment = (data) => {
// FIXME - add some check here ROFL // FIXME - add some check here ROFL
@@ -1475,7 +1492,7 @@ const EnvironmentTab = memo((props) => {
<Typography variant="body1" color="textSecondary" style={{marginTop: 15, }}> <Typography variant="body1" color="textSecondary" style={{marginTop: 15, }}>
{installationTab === 2 ? {installationTab === 2 ?
<Typography variant='body2' color="textSecondary"> <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>
: :
<Typography variant='body2' color="textSecondary"> <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"} "2. Run this command on the server you want to run workflows or store Pipeline data on"}
</Typography> </Typography>
{installationTab === 2 ? null :
<div <div
style={{ style={{
marginTop: 10, marginTop: 10,
@@ -1571,8 +1587,7 @@ const EnvironmentTab = memo((props) => {
}} }}
/> />
</div> </div>
</div> </div>
}
<Typography variant="body1" color="textSecondary" style={{marginTop: 15, }}> <Typography variant="body1" color="textSecondary" style={{marginTop: 15, }}>
{installationTab === 2 ? null : {installationTab === 2 ? null :
+2 -2
View File
@@ -741,7 +741,7 @@ const PartnerHeader = (props) => {
<div > <div >
<Button <Button
style={{ fontSize: 16, textTransform: 'capitalize', boxShadow: "none", borderRadius: 4, width: 128, height: 40 }} style={{ fontSize: 16, textTransform: 'capitalize', boxShadow: "none", borderRadius: 4, width: 128, height: 40 }}
variant="contained" variant="outlined"
color="primary" color="primary"
disabled={isDisabled || isPublishing} disabled={isDisabled || isPublishing}
onClick={() => { onClick={() => {
@@ -812,7 +812,7 @@ const PartnerHeader = (props) => {
<div > <div >
<Button <Button
style={{ fontSize: 16, textTransform: 'capitalize', boxShadow: "none", borderRadius: 4, width: 128, height: 40 }} style={{ fontSize: 16, textTransform: 'capitalize', boxShadow: "none", borderRadius: 4, width: 128, height: 40 }}
variant="contained" variant="outlined"
color="primary" color="primary"
disabled={isDisabled || isPublishing} disabled={isDisabled || isPublishing}
onClick={() => { onClick={() => {
+82 -10
View File
@@ -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: { MuiTab: {
@@ -412,39 +484,39 @@ export const getTheme = (themeMode, brandColor) =>
MuiCssBaseline: { MuiCssBaseline: {
styleOverrides: ` styleOverrides: `
@font-face { @font-face {
font-family: 'Roboto'; font-family: 'Inter';
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
font-weight: 300; font-weight: 300;
src: local('Roboto Light'), local('Roboto-Light'); src: local('Inter Light'), local('Inter-Light');
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: 'Inter';
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
font-weight: 400; font-weight: 400;
src: local('Roboto'), local('Roboto-Regular'); src: local('Inter Regular'), local('Inter-Regular');
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: 'Inter';
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
font-weight: 500; font-weight: 500;
src: local('Roboto Medium'), local('Roboto-Medium'); src: local('Inter Medium'), local('Inter-Medium');
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: 'Inter';
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
font-weight: 600; font-weight: 600;
src: local('Roboto SemiBold'), local('Roboto-SemiBold'); src: local('Inter SemiBold'), local('Inter-SemiBold');
} }
@font-face { @font-face {
font-family: 'Roboto'; font-family: 'Inter';
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
font-weight: 700; font-weight: 700;
src: local('Roboto Bold'), local('Roboto-Bold'); src: local('Inter Bold'), local('Inter-Bold');
} }
`, `,
}, },
+82 -4
View File
@@ -5,6 +5,7 @@ import { useNavigate, Link, useParams } from "react-router-dom";
import { isMobile } from "react-device-detect"; import { isMobile } from "react-device-detect";
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useInterval } from "react-powerhooks"; import { useInterval } from "react-powerhooks";
import ReactGA from 'react-ga4';
import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined'; import VisibilityOutlinedIcon from '@mui/icons-material/VisibilityOutlined';
import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined'; import VisibilityOffOutlinedIcon from '@mui/icons-material/VisibilityOffOutlined';
import { import {
@@ -157,7 +158,7 @@ const FreePlanCard = ({ classes }) => {
); );
}; };
const MarketplaceCard = ({ classes }) => { const MarketplaceCard = ({ classes, isCloud }) => {
const marketplaceOptions = [ const marketplaceOptions = [
{ {
name: "Open Source Install", name: "Open Source Install",
@@ -216,6 +217,15 @@ const MarketplaceCard = ({ classes }) => {
> >
<button <button
onClick={() => { onClick={() => {
// Track marketplace click
if (isCloud) {
ReactGA.event({
category: "authentication",
action: "click_marketplace_option",
label: option.name,
});
}
if (option.valid === true) { if (option.valid === true) {
window.open(option.link, "_blank") window.open(option.link, "_blank")
} }
@@ -512,6 +522,15 @@ const LoginPage = props => {
const onSubmit = (e) => { const onSubmit = (e) => {
//toast("Testing from login page") //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("") setMessage("")
setLoginLoading(true) setLoginLoading(true)
e.preventDefault() e.preventDefault()
@@ -556,6 +575,14 @@ const LoginPage = props => {
setLoginLoading(false) setLoginLoading(false)
if (responseJson["success"] === 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"]) setLoginInfo(responseJson["reason"])
} else { } else {
if (responseJson?.region_url !== undefined && responseJson?.region_url !== null && responseJson?.region_url !== "") { if (responseJson?.region_url !== undefined && responseJson?.region_url !== null && responseJson?.region_url !== "") {
@@ -583,6 +610,14 @@ const LoginPage = props => {
return 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...") setLoginInfo("Successful login! Redirecting you in 3 seconds...")
for (var key in responseJson["cookies"]) { for (var key in responseJson["cookies"]) {
@@ -643,6 +678,14 @@ const LoginPage = props => {
.then(response => .then(response =>
response.json().then(responseJson => { response.json().then(responseJson => {
if (responseJson["success"] === false) { if (responseJson["success"] === false) {
// Track failed registration event
if (isCloud) {
ReactGA.event({
category: "authentication",
action: "register_failed",
label: "Email Registration Failed",
});
}
setLoginInfo(responseJson["reason"]) setLoginInfo(responseJson["reason"])
} else { } else {
if (responseJson["reason"] === "shuffle_account") { if (responseJson["reason"] === "shuffle_account") {
@@ -659,6 +702,16 @@ const LoginPage = props => {
} }
setLoginLoading(false) 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...") setLoginInfo("Successful registration! Redirecting in 3 seconds...")
@@ -707,15 +760,40 @@ const LoginPage = props => {
} }
const HandleLoginWithSSO = () => { const HandleLoginWithSSO = () => {
// Track SSO login attempt
if (isCloud) {
ReactGA.event({
category: "authentication",
action: "click_sso_login",
label: "SSO Login Attempt",
});
}
setPassword("") setPassword("")
setLoginInfo("") setLoginInfo("")
setLoginWithSSO(true) setLoginWithSSO(true)
} }
var formtitle = register ? <div>Welcome Back!</div> : <div>Create your account</div> 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", }}>Dont 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", }}>Dont 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> //<Link to={`/login${parsedsearch}`} style={hrefStyle}><div>Click here to Login</div></Link>
@@ -1025,7 +1103,7 @@ const LoginPage = props => {
<div className={classes.divider}> <div className={classes.divider}>
<span>OR</span> <span>OR</span>
</div> </div>
<MarketplaceCard classes={classes} /> <MarketplaceCard classes={classes} isCloud={isCloud} />
</> </>
)} )}
</div> </div>