Bumped shuffle-shared

This commit is contained in:
Frikky
2026-05-07 17:35:28 +02:00
58 changed files with 6058 additions and 3781 deletions
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="100" height="100" viewBox="0,0,256,256">
<g fill="none" fill-rule="nonzero" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal"><g transform="scale(5.33333,5.33333)"><path d="M13.527,21.529c0,0.597 0.064,1.08 0.176,1.435c0.128,0.355 0.287,0.742 0.511,1.161c0.08,0.129 0.112,0.258 0.112,0.371c0,0.161 -0.096,0.322 -0.303,0.484l-1.006,0.677c-0.144,0.097 -0.287,0.145 -0.415,0.145c-0.16,0 -0.319,-0.081 -0.479,-0.226c-0.224,-0.242 -0.415,-0.5 -0.575,-0.758c-0.16,-0.274 -0.319,-0.58 -0.495,-0.951c-1.245,1.483 -2.81,2.225 -4.694,2.225c-1.341,0 -2.411,-0.387 -3.193,-1.161c-0.782,-0.774 -1.181,-1.806 -1.181,-3.096c0,-1.37 0.479,-2.483 1.453,-3.321c0.974,-0.838 2.267,-1.258 3.911,-1.258c0.543,0 1.102,0.048 1.692,0.129c0.59,0.081 1.197,0.21 1.836,0.355v-1.177c0,-1.225 -0.255,-2.08 -0.75,-2.58c-0.511,-0.5 -1.373,-0.742 -2.602,-0.742c-0.559,0 -1.133,0.064 -1.724,0.21c-0.591,0.145 -1.165,0.322 -1.724,0.548c-0.255,0.113 -0.447,0.177 -0.559,0.21c-0.112,0.032 -0.192,0.048 -0.255,0.048c-0.224,0 -0.335,-0.161 -0.335,-0.5v-0.79c0,-0.258 0.032,-0.451 0.112,-0.564c0.08,-0.113 0.224,-0.226 0.447,-0.339c0.559,-0.29 1.229,-0.532 2.012,-0.726c0.782,-0.21 1.612,-0.306 2.49,-0.306c1.9,0 3.289,0.435 4.183,1.306c0.878,0.871 1.325,2.193 1.325,3.966v5.224h0.03zM7.045,23.979c0.527,0 1.07,-0.097 1.644,-0.29c0.575,-0.193 1.086,-0.548 1.517,-1.032c0.255,-0.306 0.447,-0.645 0.543,-1.032c0.096,-0.387 0.16,-0.855 0.16,-1.403v-0.677c-0.463,-0.113 -0.958,-0.21 -1.469,-0.274c-0.511,-0.064 -1.006,-0.097 -1.501,-0.097c-1.07,0 -1.852,0.21 -2.379,0.645c-0.527,0.435 -0.782,1.048 -0.782,1.854c0,0.758 0.192,1.322 0.591,1.709c0.383,0.404 0.942,0.597 1.676,0.597zM19.865,25.721c-0.287,0 -0.479,-0.048 -0.607,-0.161c-0.128,-0.097 -0.239,-0.322 -0.335,-0.629l-3.752,-12.463c-0.096,-0.322 -0.144,-0.532 -0.144,-0.645c0,-0.258 0.128,-0.403 0.383,-0.403h1.565c0.303,0 0.511,0.048 0.623,0.161c0.128,0.097 0.223,0.322 0.319,0.629l2.682,10.674l2.49,-10.674c0.08,-0.322 0.176,-0.532 0.303,-0.629c0.128,-0.097 0.351,-0.161 0.639,-0.161h1.277c0.303,0 0.511,0.048 0.639,0.161c0.128,0.097 0.239,0.322 0.303,0.629l2.522,10.803l2.762,-10.803c0.096,-0.322 0.208,-0.532 0.319,-0.629c0.128,-0.097 0.335,-0.161 0.623,-0.161h1.485c0.255,0 0.399,0.129 0.399,0.403c0,0.081 -0.016,0.161 -0.032,0.258c-0.016,0.097 -0.048,0.226 -0.112,0.403l-3.847,12.463c-0.096,0.322 -0.208,0.532 -0.335,0.629c-0.127,0.097 -0.335,0.161 -0.607,0.161h-1.373c-0.303,0 -0.511,-0.048 -0.639,-0.161c-0.128,-0.113 -0.239,-0.322 -0.303,-0.645l-2.474,-10.4l-2.458,10.384c-0.08,0.322 -0.176,0.532 -0.303,0.645c-0.128,0.113 -0.351,0.161 -0.639,0.161zM40.379,26.156c-0.83,0 -1.66,-0.097 -2.458,-0.29c-0.798,-0.193 -1.421,-0.403 -1.836,-0.645c-0.255,-0.145 -0.431,-0.306 -0.495,-0.451c-0.064,-0.145 -0.096,-0.306 -0.096,-0.451v-0.822c0,-0.339 0.128,-0.5 0.367,-0.5c0.096,0 0.192,0.016 0.287,0.048c0.096,0.032 0.239,0.097 0.399,0.161c0.543,0.242 1.133,0.435 1.756,0.564c0.639,0.129 1.261,0.193 1.9,0.193c1.006,0 1.788,-0.177 2.331,-0.532c0.543,-0.355 0.83,-0.871 0.83,-1.532c0,-0.451 -0.144,-0.822 -0.431,-1.129c-0.287,-0.306 -0.83,-0.58 -1.612,-0.838l-2.315,-0.726c-1.165,-0.371 -2.027,-0.919 -2.554,-1.645c-0.527,-0.709 -0.798,-1.499 -0.798,-2.338c0,-0.677 0.144,-1.274 0.431,-1.79c0.287,-0.516 0.671,-0.967 1.149,-1.322c0.479,-0.371 1.022,-0.645 1.66,-0.838c0.639,-0.192 1.309,-0.273 2.012,-0.273c0.351,0 0.718,0.016 1.07,0.064c0.367,0.048 0.702,0.113 1.038,0.177c0.319,0.081 0.623,0.161 0.91,0.258c0.287,0.097 0.511,0.193 0.671,0.29c0.224,0.129 0.383,0.258 0.479,0.403c0.096,0.129 0.144,0.306 0.144,0.532v0.758c0,0.339 -0.128,0.516 -0.367,0.516c-0.128,0 -0.335,-0.064 -0.607,-0.193c-0.91,-0.419 -1.932,-0.629 -3.065,-0.629c-0.91,0 -1.628,0.145 -2.123,0.451c-0.495,0.306 -0.75,0.774 -0.75,1.435c0,0.451 0.16,0.838 0.479,1.145c0.319,0.306 0.91,0.613 1.756,0.887l2.267,0.726c1.149,0.371 1.98,0.887 2.474,1.548c0.494,0.661 0.734,1.419 0.734,2.257c0,0.693 -0.144,1.322 -0.415,1.87c-0.287,0.548 -0.671,1.032 -1.165,1.419c-0.495,0.403 -1.086,0.693 -1.772,0.903c-0.721,0.226 -1.471,0.339 -2.285,0.339z" fill="#ffffff"></path><path d="M43.396,33.992c-5.252,3.918 -12.883,5.998 -19.445,5.998c-9.195,0 -17.481,-3.434 -23.739,-9.142c-0.495,-0.451 -0.048,-1.064 0.543,-0.709c6.769,3.966 15.118,6.369 23.755,6.369c5.827,0 12.229,-1.225 18.119,-3.741c0.879,-0.403 1.629,0.58 0.767,1.225zM45.583,31.477c-0.671,-0.871 -4.438,-0.419 -6.146,-0.21c-0.511,0.064 -0.591,-0.387 -0.128,-0.726c3.001,-2.128 7.934,-1.516 8.509,-0.806c0.575,0.726 -0.16,5.708 -2.969,8.094c-0.431,0.371 -0.846,0.177 -0.655,-0.306c0.639,-1.596 2.06,-5.192 1.389,-6.046z" fill="#ff9900"></path></g></g>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

+4
View File
@@ -142,6 +142,10 @@ const AdminNavBar = (props) => {
// navigate(`?tab=organization`, { replace: true }); // navigate(`?tab=organization`, { replace: true });
// } // }
// } // }
const adminTab = queryParams.get('admin_tab');
if (adminTab) {
setSelectedItem("Organization");
}
if (partnerTab) { if (partnerTab) {
setSelectedItem("Partner"); setSelectedItem("Partner");
} }
+1 -1
View File
@@ -1977,7 +1977,7 @@ const Action = memo((
const actionId = action.name.replace(/ /g, "-").replace(/_/g, "-"); const actionId = action.name.replace(/ /g, "-").replace(/_/g, "-");
window.history.pushState(null, "", `#${actionId}`); window.history.pushState(null, "", `#${actionId}`);
setExampleBody(action?.example_response); setExampleBody(action?.example_response);
document.getElementById(`action-list-${nextSelectedActionIndex}`).scrollIntoView({ behavior: "smooth", block: "center" }); document.getElementById(`action-list-${nextSelectedActionIndex}`)?.scrollIntoView({ behavior: "smooth", block: "center" });
} }
}, 300); }, 300);
} }
+54 -137
View File
@@ -20,6 +20,7 @@ import { isMobile } from "react-device-detect"
import PaperComponent from "../components/PaperComponent.jsx"; import PaperComponent from "../components/PaperComponent.jsx";
import { CodeHandler, Img, OuterLink, } from '../views/Docs.jsx' import { CodeHandler, Img, OuterLink, } from '../views/Docs.jsx'
import { v4 as uuidv4} from "uuid"; import { v4 as uuidv4} from "uuid";
import DeleteConfirmDialog from "./DeleteConfirmDialog.jsx";
import { import {
Divider, Divider,
@@ -63,10 +64,11 @@ import {
} from "react-instantsearch-dom"; } from "react-instantsearch-dom";
import aa from "search-insights"; import aa from "search-insights";
import { Context } from '../context/ContextApi.jsx'; import { Context } from '../context/ContextApi.jsx';
import SubOrgDistributionDialog from './SubOrgDistributionDialog.jsx';
const searchClient = algoliasearch( const searchClient = algoliasearch(
"JNSS5CFDZZ", "JNSS5CFDZZ",
"c8f882473ff42d41158430be09ec2b4e" "33e4e3564f4f060e96e0531957bed552"
) )
const AppAuthTab = memo((props) => { const AppAuthTab = memo((props) => {
@@ -89,9 +91,13 @@ const AppAuthTab = memo((props) => {
const [searchQuery, setSearchQuery] = React.useState(""); const [searchQuery, setSearchQuery] = React.useState("");
const [showAppModal, setShowAppModal] = useState(false) const [showAppModal, setShowAppModal] = useState(false)
const [selectedAuthId, setSelectedAuthId] = useState(""); const [selectedAuthId, setSelectedAuthId] = useState("");
const [selectedAuthName, setSelectedAuthName] = useState("");
const [showDistributionPopup, setShowDistributionPopup] = useState(false); const [showDistributionPopup, setShowDistributionPopup] = useState(false);
const [showAuthenticationLoader, setShowAuthenticationLoader] = useState(true) const [showAuthenticationLoader, setShowAuthenticationLoader] = useState(true)
const [showAppAuthGroupLoader, setShowAppAuthGroupLoader] = useState(true) const [showAppAuthGroupLoader, setShowAppAuthGroupLoader] = useState(true)
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
const [deleteConfirmTarget, setDeleteConfirmTarget] = useState(null);
const [distribOrgOrder, setDistribOrgOrder] = useState([]);
const { themeMode, supportEmail, brandColor } = useContext(Context) const { themeMode, supportEmail, brandColor } = useContext(Context)
const theme = getTheme(themeMode, brandColor) const theme = getTheme(themeMode, brandColor)
@@ -183,7 +189,7 @@ const AppAuthTab = memo((props) => {
}; };
const deleteAuthentication = (data) => { const deleteAuthentication = (data) => {
toast("Deleting auth " + data.label); toast("Deleting auth " + data?.label);
// Just use this one? // Just use this one?
const url = globalUrl + "/api/v1/apps/authentication/" + data.id; const url = globalUrl + "/api/v1/apps/authentication/" + data.id;
@@ -213,33 +219,6 @@ const AppAuthTab = memo((props) => {
}); });
}; };
const handleSelectSubOrg = (id, action) => {
if (action === "all") {
const childOrgs = userdata.orgs.filter(
(data) => data.creator_org === userdata.active_org.id
);
setSelectedSubOrg((prev) => {
if (prev.length === childOrgs.length) {
// If all child orgs are already selected, clear the selection
return [];
} else {
// Otherwise, select all child org IDs
return childOrgs.map((data) => data.id);
}
});
} else if (action === "none") {
setSelectedSubOrg([]);
} else {
setSelectedSubOrg((prev) => {
if (prev.includes(id)) {
return prev.filter((data) => data !== id);
} else {
return [...prev, id];
}
});
}
};
const editAuthenticationConfig = (id, parentAction, selectedSuborgs) => { const editAuthenticationConfig = (id, parentAction, selectedSuborgs) => {
const data = { const data = {
id: id, id: id,
@@ -285,99 +264,21 @@ const AppAuthTab = memo((props) => {
editAuthenticationConfig(id, "suborg_distribute", [...new Set(selectedSubOrg)]) editAuthenticationConfig(id, "suborg_distribute", [...new Set(selectedSubOrg)])
} }
const deleteConfirmDialog = (
<DeleteConfirmDialog
open={deleteConfirmOpen}
onClose={() => { setDeleteConfirmOpen(false); setDeleteConfirmTarget(null); }}
onConfirm={() => {
deleteAuthentication(deleteConfirmTarget);
setDeleteConfirmOpen(false);
setDeleteConfirmTarget(null);
}}
title="Delete Authentication?"
description={<>Are you sure you want to delete <b>{deleteConfirmTarget?.app?.name}</b> authentication?</>}
warningText="This cannot be undone. Any workflows using this authentication will lose access."
/>
);
const cacheDistributionModal = showDistributionPopup ? (
<Dialog
open={showDistributionPopup}
onClose={() => {setShowDistributionPopup(false);setSelectedAuthId("")}}
PaperProps={{
sx: {
borderRadius: theme?.palette?.DialogStyle?.borderRadius,
border: theme?.palette?.DialogStyle?.border,
fontFamily: theme?.typography?.fontFamily,
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
zIndex: 1000,
minWidth: "600px",
minHeight: "320px",
overflow: "auto",
'& .MuiDialogContent-root': {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
},
'& .MuiDialogTitle-root': {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
},
'& .MuiDialogActions-root': {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
},
},
}}
>
<DialogTitle>
<Typography variant="h5" color="textPrimary">
Select sub-org to distribute Datastore key
</Typography>
</DialogTitle>
<DialogContent style={{ color: "rgba(255,255,255,0.65)" }}>
<MenuItem value="none" onClick={()=> {handleSelectSubOrg(null, "none")}}>None</MenuItem>
<MenuItem value="all" onClick={()=> {handleSelectSubOrg(null, "all")}}>All</MenuItem>
{userdata.orgs.map((data, index) => {
if (data.creator_org !== userdata.active_org.id) {
return null;
}
const imagesize = 22;
const imageStyle = {
width: imagesize,
height: imagesize,
pointerEvents: "none",
marginRight: 10,
marginLeft: data.id === userdata.active_org.id ? 0 : 20,
};
const image = data.image === "" ? (
<img alt={data.name} src={theme.palette.defaultImage} style={imageStyle} />
) : (
<img alt={data.name} src={data.image} style={imageStyle} />
);
return (
<MenuItem
key={index}
value={data.id}
onClick={() => handleSelectSubOrg(data.id)}
style={{ display: "flex", alignItems: "center" }}
>
<Checkbox
checked={selectedSubOrg.includes(data.id)}
/>
{image}
<span style={{ marginLeft: 8 }}>{data.name}</span>
</MenuItem>
);
})}
<div style={{ display: "flex", marginTop: 20 }}>
<Button
style={{ borderRadius: "2px", textTransform: 'none', fontSize:16, color: theme.palette.primary.main }}
onClick={() => {setShowDistributionPopup(false); setSelectedAuthId("")}}
color="primary"
>
Cancel
</Button>
<Button
variant="contained"
style={{ borderRadius: "2px", textTransform: 'none', fontSize:16, marginLeft: 10 }}
onClick={() => {
changeDistribution(selectedAuthId, selectedSubOrg);
}}
color="primary"
>
Submit
</Button>
</div>
</DialogContent>
</Dialog>
) : null;
const editAuthenticationModal = selectedAuthenticationModalOpen ? ( const editAuthenticationModal = selectedAuthenticationModalOpen ? (
<Dialog <Dialog
@@ -985,7 +886,17 @@ const AppAuthTab = memo((props) => {
return ( return (
<div style={{width: "100%", minHeight: 1100, maxHeight: 1700, overflowY: "auto", scrollbarColor: theme.palette.scrollbarColorTransparent, scrollbarWidth: 'thin',boxSizing: 'border-box', padding: "27px 10px 19px 27px", height:"100%", backgroundColor: theme.palette.platformColor,borderTopRightRadius: '8px', borderBottomRightRadius: 8, borderLeft: theme.palette.defaultBorder, }}> <div style={{width: "100%", minHeight: 1100, maxHeight: 1700, overflowY: "auto", scrollbarColor: theme.palette.scrollbarColorTransparent, scrollbarWidth: 'thin',boxSizing: 'border-box', padding: "27px 10px 19px 27px", height:"100%", backgroundColor: theme.palette.platformColor,borderTopRightRadius: '8px', borderBottomRightRadius: 8, borderLeft: theme.palette.defaultBorder, }}>
{appModal} {appModal}
{cacheDistributionModal} <SubOrgDistributionDialog
open={showDistributionPopup}
onClose={() => { setShowDistributionPopup(false); setSelectedAuthId(""); setSelectedAuthName(""); }}
title="Distribute App Auth to Sub-Organizations"
extraInfo={selectedAuthName ? `Selected Auth: ${selectedAuthName}` : null}
orgs={distribOrgOrder.map(id => (userdata?.orgs || []).find(o => o.id === id)).filter(Boolean)}
selectedOrgIds={selectedSubOrg}
onSelectionChange={setSelectedSubOrg}
onSave={(ids) => { changeDistribution(selectedAuthId, ids); }}
/>
{deleteConfirmDialog}
<div style={{ height: "100%", width: "calc(100% - 20px)", scrollbarColor: theme.palette.scrollbarColorTransparent, scrollbarWidth: 'thin'}}> <div style={{ height: "100%", width: "calc(100% - 20px)", scrollbarColor: theme.palette.scrollbarColorTransparent, scrollbarWidth: 'thin'}}>
<div style={{ width: 'auto', display:'flex',}}> <div style={{ width: 'auto', display:'flex',}}>
<div style={{display: 'flex', flexDirection: 'column'}}> <div style={{display: 'flex', flexDirection: 'column'}}>
@@ -1362,10 +1273,10 @@ const AppAuthTab = memo((props) => {
</IconButton> </IconButton>
</Tooltip> </Tooltip>
<IconButton <IconButton
style={{ }}
disabled={data.org_id !== selectedOrganization.id} disabled={data.org_id !== selectedOrganization.id}
onClick={() => { onClick={() => {
deleteAuthentication(data); setDeleteConfirmTarget(data);
setDeleteConfirmOpen(true);
}} }}
> >
<img src='/icons/deleteIcon.svg' alt='delete icon' color="secondary" /> <img src='/icons/deleteIcon.svg' alt='delete icon' color="secondary" />
@@ -1400,21 +1311,27 @@ const AppAuthTab = memo((props) => {
color="secondary" color="secondary"
onClick={() => { onClick={() => {
setShowDistributionPopup(true) setShowDistributionPopup(true)
if(data?.suborg_distribution?.length > 0){ let initialSelected = [];
setSelectedSubOrg(data.suborg_distribution)
}else{
setSelectedSubOrg([])
}
setSelectedAuthId(data.id)
if (data?.suborg_distributed) { if (data?.suborg_distributed) {
const allSuborg = userdata?.orgs?.map((data, index) => { const allSuborg = userdata?.orgs?.map((d) => {
if (data.creator_org !== userdata.active_org.id) { if (d.creator_org !== userdata.active_org.id) return null;
return null; return d.id;
} });
return data.id; initialSelected = allSuborg.filter((d) => d !== null);
}) } else if (data?.suborg_distribution?.length > 0) {
setSelectedSubOrg(allSuborg.filter((data) => data !== null)) initialSelected = data.suborg_distribution;
} }
setSelectedSubOrg(initialSelected);
setSelectedAuthId(data.id);
setSelectedAuthName(data?.app?.name);
const suborgs = (userdata?.orgs || []).filter(o => o.creator_org === userdata?.active_org?.id);
const sorted = [...suborgs].sort((a, b) => {
const aS = initialSelected.includes(a.id);
const bS = initialSelected.includes(b.id);
if (aS !== bS) return bS - aS;
return a.name.localeCompare(b.name);
});
setDistribOrgOrder(sorted.map(o => o.id));
}} }}
/> />
+1 -1
View File
@@ -708,7 +708,7 @@ const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud, startOpenA
fontWeight: 500, fontWeight: 500,
fontFamily: theme?.typography?.fontFamily, fontFamily: theme?.typography?.fontFamily,
}}> }}>
Generate an app based on documentation (beta) Generate an app based on documentation
</Typography> </Typography>
<IconButton <IconButton
onClick={() => { onClick={() => {
+93 -140
View File
@@ -1,10 +1,9 @@
import React, { useEffect, useState, useRef } from "react"; import React, { useEffect, useState, useRef, useMemo } from "react";
import theme from "../theme.jsx"; import theme from "../theme.jsx";
import ReactGA from "react-ga4"; import ReactGA from "react-ga4";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { removeQuery } from "../components/ScrollToTop.jsx"; import { removeQuery } from "../components/ScrollToTop.jsx";
import { useMemo } from "react";
import { Tabs, Tab, Collapse } from "@mui/material"; import { Tabs, Tab, Collapse } from "@mui/material";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
@@ -27,7 +26,7 @@ import {
InstantSearch, InstantSearch,
Configure, Configure,
connectSearchBox, connectSearchBox,
connectHits, connectInfiniteHits,
connectHitInsights, connectHitInsights,
RefinementList, RefinementList,
ClearRefinements, ClearRefinements,
@@ -39,6 +38,7 @@ import aa from "search-insights";
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import "./FilterCSS.css"; import "./FilterCSS.css";
import SearchContactForm from "../components/SearchContactForm.jsx";
import { import {
Zoom, Zoom,
@@ -54,7 +54,7 @@ import {
const searchClient = algoliasearch( const searchClient = algoliasearch(
"JNSS5CFDZZ", "JNSS5CFDZZ",
"c8f882473ff42d41158430be09ec2b4e" "eb5fd80aa6ed5ab4730d836cff3ea283"
); );
//const searchClient = algoliasearch("L55H18ZINA", "a19be455e7e75ee8f20a93d26b9fc6d6") //const searchClient = algoliasearch("L55H18ZINA", "a19be455e7e75ee8f20a93d26b9fc6d6")
@@ -77,62 +77,13 @@ const AppGrid = (props) => {
const xs = const xs =
parsedXs === undefined || parsedXs === null ? (isMobile ? 6 : 3) : parsedXs; parsedXs === undefined || parsedXs === null ? (isMobile ? 6 : 3) : parsedXs;
const [formMail, setFormMail] = React.useState("");
const [message, setMessage] = React.useState("");
const [formMessage, setFormMessage] = React.useState("");
const [deactivatedIndexes, setDeactivatedIndexes] = React.useState([]); const [deactivatedIndexes, setDeactivatedIndexes] = React.useState([]);
const buttonStyle = {
borderRadius: 30,
height: 50,
width: 220,
margin: isMobile ? "15px auto 15px auto" : 20,
fontSize: 18,
};
const innerColor = "rgba(255,255,255,0.65)"; const innerColor = "rgba(255,255,255,0.65)";
const borderRadius = 3; const borderRadius = 3;
window.title = "Shuffle | Apps | Find and integrate any app"; window.title = "Shuffle | Apps | Find and integrate any app";
const noImage = "/public/no_image.png"; const noImage = "/public/no_image.png";
const submitContact = (email, message) => {
const data = {
firstname: "",
lastname: "",
title: "",
companyname: "",
email: email,
phone: "",
message: message,
};
const errorMessage =
"Something went wrong. Please contact frikky@shuffler.io directly.";
fetch(globalUrl + "/api/v1/contact", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
})
.then((response) => response.json())
.then((response) => {
if (response?.success === true) {
setFormMessage(response.reason);
//toast("Thanks for submitting!")
} else {
setFormMessage(errorMessage);
}
setFormMail("");
setMessage("");
})
.catch((error) => {
setFormMessage(errorMessage);
console.log(error);
});
};
const SearchBox = ({ currentRefinement, refine, isSearchStalled, searchQuery, setSearchQuery }) => { const SearchBox = ({ currentRefinement, refine, isSearchStalled, searchQuery, setSearchQuery }) => {
var defaultSearch = ""; var defaultSearch = "";
@@ -148,10 +99,11 @@ const AppGrid = (props) => {
const params = Object.fromEntries(urlSearchParams.entries()); const params = Object.fromEntries(urlSearchParams.entries());
const foundQuery = params["q"]; const foundQuery = params["q"];
if (foundQuery !== null && foundQuery !== undefined) { if (foundQuery !== null && foundQuery !== undefined) {
console.log("Got query: ", foundQuery);
refine(foundQuery); refine(foundQuery);
defaultSearch = foundQuery; defaultSearch = foundQuery;
searchQuery = foundQuery if (searchQuery !== foundQuery) {
setSearchQuery(foundQuery);
}
} }
} }
//}, []) //}, [])
@@ -234,7 +186,13 @@ const AppGrid = (props) => {
onChange={(event) => { onChange={(event) => {
const value = event.currentTarget.value; const value = event.currentTarget.value;
setSearchQuery(value); setSearchQuery(value);
removeQuery("q"); const urlSearchParams = new URLSearchParams(window.location.search);
if (value) {
urlSearchParams.set("q", value);
} else {
urlSearchParams.delete("q");
}
window.history.replaceState({}, '', `${window.location.pathname}?${urlSearchParams.toString()}`);
debouncedRefine(value); debouncedRefine(value);
}} }}
onKeyDown={(event) => { onKeyDown={(event) => {
@@ -252,6 +210,21 @@ const AppGrid = (props) => {
const [currTab, setCurrTab] = useState(0); const [currTab, setCurrTab] = useState(0);
const location = useLocation(); const location = useLocation();
const conditionalSearchClient = useMemo(() => ({
...searchClient,
search(requests) {
if (currTab !== 0) {
return Promise.resolve({
results: requests.map(() => ({
hits: [], nbHits: 0, page: 0, nbPages: 0, hitsPerPage: 0,
processingTimeMS: 0, exhaustiveNbHits: true, query: "", params: "",
})),
});
}
return searchClient.search(requests);
},
}), [currTab]);
useEffect(() => { useEffect(() => {
const queryParams = new URLSearchParams(location.search); const queryParams = new URLSearchParams(location.search);
const tabParam = queryParams.get('tab'); const tabParam = queryParams.get('tab');
@@ -269,7 +242,6 @@ const AppGrid = (props) => {
const newQueryParam = newTab === 0 ? 'all_apps' : newTab === 1 ? 'org_apps' : 'my_apps'; const newQueryParam = newTab === 0 ? 'all_apps' : newTab === 1 ? 'org_apps' : 'my_apps';
const queryParams = new URLSearchParams(location.search); const queryParams = new URLSearchParams(location.search);
queryParams.set('tab', newQueryParam); queryParams.set('tab', newQueryParam);
queryParams.delete('q');
window.history.replaceState({}, '', `${location.pathname}?${queryParams.toString()}`); window.history.replaceState({}, '', `${location.pathname}?${queryParams.toString()}`);
}; };
@@ -281,6 +253,8 @@ const AppGrid = (props) => {
// Component to fetch all public app from the algolia. // Component to fetch all public app from the algolia.
const Hits = ({ const Hits = ({
hits, hits,
hasMore,
refineNext,
insights, insights,
setIsAnyAppActivated, setIsAnyAppActivated,
searchQuery searchQuery
@@ -288,6 +262,32 @@ const AppGrid = (props) => {
const [mouseHoverIndex, setMouseHoverIndex] = useState(-1); const [mouseHoverIndex, setMouseHoverIndex] = useState(-1);
var counted = 0; var counted = 0;
const [hoverEffect, setHoverEffect] = useState(-1); const [hoverEffect, setHoverEffect] = useState(-1);
const [isLoadingMore, setIsLoadingMore] = useState(false);
const loadMoreRef = useRef(null);
const scrollContainerRef = useRef(null);
const isFetchingMore = useRef(false);
useEffect(() => {
isFetchingMore.current = false;
setIsLoadingMore(false);
}, [hits.length]);
useEffect(() => {
return; // infinite scroll disabled
if (!loadMoreRef.current || !scrollContainerRef.current) return;
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasMore && !isFetchingMore.current) {
isFetchingMore.current = true;
setIsLoadingMore(true);
refineNext();
}
},
{ root: scrollContainerRef.current, rootMargin: "200px" }
);
observer.observe(loadMoreRef.current);
return () => observer.disconnect();
}, [hasMore, refineNext]);
const normalizedString = (name) => { const normalizedString = (name) => {
if (typeof name === 'string') { if (typeof name === 'string') {
@@ -359,11 +359,11 @@ const AppGrid = (props) => {
} else { } else {
//toast.success(`App ${type}d Successfully!`); //toast.success(`App ${type}d Successfully!`);
if (type === 'activate') { if (type === 'activate') {
setAllActivatedAppIds(prev => [...prev, data.objectID]); setAllActivatedAppIds(prev => [...(prev || []), data.objectID]);
setIsAnyAppActivated(true); setIsAnyAppActivated(true);
} }
if (type === 'deactivate') { if (type === 'deactivate') {
const updatedIds = allActivatedAppIds.filter(id => id !== data.objectID); const updatedIds = (allActivatedAppIds || []).filter(id => id !== data.objectID);
setAllActivatedAppIds(updatedIds); setAllActivatedAppIds(updatedIds);
} }
} }
@@ -373,6 +373,16 @@ const AppGrid = (props) => {
}); });
} }
const sortedHits = useMemo(() => {
const list = [...(hits || [])];
if (!allActivatedAppIds?.length) return list;
return list.sort((a, b) => {
const aActive = allActivatedAppIds.includes(a.objectID) ? 1 : 0;
const bActive = allActivatedAppIds.includes(b.objectID) ? 1 : 0;
return bActive - aActive;
});
}, [hits, hits?.length, allActivatedAppIds]);
let workflowDelay = 0; let workflowDelay = 0;
const isHeader = true; const isHeader = true;
const paperStyle = { const paperStyle = {
@@ -404,6 +414,7 @@ const AppGrid = (props) => {
) : ( ) : (
<Grid item spacing={2} justifyContent="flex-start"> <Grid item spacing={2} justifyContent="flex-start">
<div <div
ref={scrollContainerRef}
style={{ style={{
gap: 16, gap: 16,
marginTop: 16, marginTop: 16,
@@ -418,7 +429,7 @@ const AppGrid = (props) => {
scrollbarColor: "#494949 #2f2f2f", scrollbarColor: "#494949 #2f2f2f",
}} }}
> >
{hits?.map((data, index) => { {sortedHits.map((data, index) => {
const appUrl = const appUrl =
isCloud === true ? isCloud === true ?
`/apps/${data.objectID}` `/apps/${data.objectID}`
@@ -629,6 +640,12 @@ const AppGrid = (props) => {
); );
}) })
} }
<div ref={loadMoreRef} style={{ width: "100%", height: 10 }} />
{isLoadingMore && (
<div style={{ width: "100%", display: "flex", justifyContent: "center", padding: 10 }}>
<CircularProgress size={24} />
</div>
)}
</div> </div>
</Grid > </Grid >
)} )}
@@ -926,7 +943,7 @@ const AppGrid = (props) => {
//Component to display all apps. //Component to display all apps.
const AllApps = ({ setIsAnyAppActivated }) => { const AllApps = ({ setIsAnyAppActivated }) => {
var [searchQuery, setSearchQuery] = useState(""); var [searchQuery, setSearchQuery] = useState(() => new URLSearchParams(window.location.search).get('q') || "");
return ( return (
<div <div
@@ -987,6 +1004,7 @@ const AppGrid = (props) => {
}} }}
onClick={() => { onClick={() => {
setSearchQuery(''); setSearchQuery('');
removeQuery("q");
}} }}
/> />
)} )}
@@ -1015,7 +1033,15 @@ const AppGrid = (props) => {
placeholder="Search your Activated or self-built apps" placeholder="Search your Activated or self-built apps"
id="shuffle_search_field" id="shuffle_search_field"
onChange={(event) => { onChange={(event) => {
setSearchQuery(event.currentTarget.value); const value = event.currentTarget.value;
setSearchQuery(value);
const urlSearchParams = new URLSearchParams(window.location.search);
if (value) {
urlSearchParams.set("q", value);
} else {
urlSearchParams.delete("q");
}
window.history.replaceState({}, '', `${window.location.pathname}?${urlSearchParams.toString()}`);
}} }}
onKeyDown={(event) => { onKeyDown={(event) => {
if(event.key === "Enter") { if(event.key === "Enter") {
@@ -1592,7 +1618,7 @@ const AppGrid = (props) => {
//Component to fetch all apps created by user and Org //Component to fetch all apps created by user and Org
const UserAndOrgApps = ({ selectedCategoryForUsersAndOgsApps, selectedTagsForUserAndOrgApps, selectedOptionOfCreatedWith, setselectedCategoryForUsersAndOgsApps, setSelectedTagsForUserAndOrgApps, setSelectedOptionOfCreatedWith }) => { const UserAndOrgApps = ({ selectedCategoryForUsersAndOgsApps, selectedTagsForUserAndOrgApps, selectedOptionOfCreatedWith, setselectedCategoryForUsersAndOgsApps, setSelectedTagsForUserAndOrgApps, setSelectedOptionOfCreatedWith }) => {
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState(() => new URLSearchParams(window.location.search).get('q') || "");
const [appsToShow, setAppsToShow] = useState([]); const [appsToShow, setAppsToShow] = useState([]);
useEffect(() => { useEffect(() => {
if (currTab === 1) { if (currTab === 1) {
@@ -2003,7 +2029,7 @@ const AppGrid = (props) => {
}; };
const CustomSearchBox = connectSearchBox(SearchBox); const CustomSearchBox = connectSearchBox(SearchBox);
const CustomHits = connectHits(Hits); const CustomHits = connectInfiniteHits(Hits);
const DisplayAllAppsTab = () => { const DisplayAllAppsTab = () => {
const [selectedCategoryForUsersAndOgsApps, setselectedCategoryForUsersAndOgsApps] = useState([]); const [selectedCategoryForUsersAndOgsApps, setselectedCategoryForUsersAndOgsApps] = useState([]);
@@ -2012,7 +2038,7 @@ const AppGrid = (props) => {
return ( return (
<div> <div>
<InstantSearch searchClient={searchClient} indexName="appsearch"> <InstantSearch key={currTab === 0 ? "active" : "inactive"} searchClient={conditionalSearchClient} indexName="appsearch">
<div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'center', paddingRight: 215 }}> <div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'center', paddingRight: 215 }}>
{currTab === 0 ? ( {currTab === 0 ? (
<FilterForAllApps /> <FilterForAllApps />
@@ -2037,7 +2063,7 @@ const AppGrid = (props) => {
/> />
</div> </div>
<Configure clickAnalytics /> {currTab === 0 && <Configure clickAnalytics hitsPerPage={20} />}
</InstantSearch> </InstantSearch>
</div> </div>
); );
@@ -2060,80 +2086,7 @@ const AppGrid = (props) => {
> >
<DisplayAllAppsTab /> <DisplayAllAppsTab />
{showSuggestion === true ? ( {showSuggestion === true ? (
<div <SearchContactForm globalUrl={globalUrl} isMobile={isMobile} tabName="apps" />
style={{
paddingTop: 0,
maxWidth: isMobile ? "100%" : "60%",
margin: "auto",
}}
>
<Typography variant="h6" style={{ color: "white", marginTop: 50 }}>
Can't find what you're looking for?
</Typography>
<div
style={{
flex: "1",
display: "flex",
flexDirection: "row",
textAlign: "center",
}}
>
<TextField
required
style={{
flex: "1",
marginRight: 15,
backgroundColor: theme.palette.inputColor,
}}
InputProps={{
style: {
color: "#ffffff",
},
}}
color="primary"
fullWidth={true}
placeholder="Email (optional)"
type="email"
id="email-handler"
autoComplete="email"
margin="normal"
variant="outlined"
onChange={(e) => setFormMail(e.target.value)}
/>
<TextField
required
style={{ flex: "1", backgroundColor: theme.palette.inputColor }}
InputProps={{
style: {
color: "#ffffff",
},
}}
color="primary"
fullWidth={true}
placeholder="What apps do you want to see?"
type=""
id="standard-required"
margin="normal"
variant="outlined"
autoComplete="off"
onChange={(e) => setMessage(e.target.value)}
/>
</div>
<Button
variant="contained"
color="primary"
style={buttonStyle}
disabled={message.length === 0}
onClick={() => {
submitContact(formMail, message);
}}
>
Submit
</Button>
<Typography style={{ color: "white" }} variant="body2">
{formMessage}
</Typography>
</div>
) : null} ) : null}
</div> </div>
</div> </div>
+1 -1
View File
@@ -35,7 +35,7 @@ import { Context } from '../context/ContextApi.jsx';
const searchClient = algoliasearch( const searchClient = algoliasearch(
"JNSS5CFDZZ", "JNSS5CFDZZ",
"c8f882473ff42d41158430be09ec2b4e" "33e4e3564f4f060e96e0531957bed552"
);; );;
const AppModal = ({ open, onClose, app, globalUrl, getApps}) => { const AppModal = ({ open, onClose, app, globalUrl, getApps}) => {
+1 -1
View File
@@ -13,7 +13,7 @@ import {
InputAdornment, InputAdornment,
Typography, Typography,
} from '@mui/material'; } from '@mui/material';
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e") const searchClient = algoliasearch("JNSS5CFDZZ", "33e4e3564f4f060e96e0531957bed552")
const Appsearch = props => { const Appsearch = props => {
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, newSelectedApp, setNewSelectedApp, defaultSearch, showSearch, ConfiguredHits, userdata, cy, isCreatorPage, actionImageList, setActionImageList, setUserSpecialzedApp, placeholder, const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, newSelectedApp, setNewSelectedApp, defaultSearch, showSearch, ConfiguredHits, userdata, cy, isCreatorPage, actionImageList, setActionImageList, setUserSpecialzedApp, placeholder,
+1 -1
View File
@@ -20,7 +20,7 @@ import {
} from '@mui/material'; } from '@mui/material';
import aa from 'search-insights' import aa from 'search-insights'
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e") const searchClient = algoliasearch("JNSS5CFDZZ", "33e4e3564f4f060e96e0531957bed552")
const Appsearch = props => { const Appsearch = props => {
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, newSelectedApp, setNewSelectedApp, defaultSearch, showSearch, ConfiguredHits, userdata, cy, isCreatorPage, actionImageList, setActionImageList, setUserSpecialzedApp, inputHeight, apps, } = props const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, newSelectedApp, setNewSelectedApp, defaultSearch, showSearch, ConfiguredHits, userdata, cy, isCreatorPage, actionImageList, setActionImageList, setUserSpecialzedApp, inputHeight, apps, } = props
const { themeMode } = useContext(Context) const { themeMode } = useContext(Context)
+393 -75
View File
@@ -52,6 +52,7 @@ import {
Cancel as CancelIcon, Cancel as CancelIcon,
Shield as ShieldIcon, Shield as ShieldIcon,
Cancel as XCircleIcon, Cancel as XCircleIcon,
LockOutlined as LockIcon,
FlashOn as ZapIcon, FlashOn as ZapIcon,
People as UsersIcon, People as UsersIcon,
FmdGoodOutlined as FmdGoodOutlinedIcon, FmdGoodOutlined as FmdGoodOutlinedIcon,
@@ -81,7 +82,7 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) =>
} }
const themeMode = theme.palette.mode; const themeMode = theme.palette.mode;
const workflowActive = selectedOrganization?.sync_features?.workflow_executions?.active; const workflowActive = selectedOrganization?.sync_features?.app_executions?.active;
const multiTenantActive = selectedOrganization?.sync_features?.multi_tenant?.active; const multiTenantActive = selectedOrganization?.sync_features?.multi_tenant?.active;
const multiEnvActive = selectedOrganization?.sync_features?.multi_env?.active; const multiEnvActive = selectedOrganization?.sync_features?.multi_env?.active;
const brandingActive = selectedOrganization?.sync_features?.branding?.active; const brandingActive = selectedOrganization?.sync_features?.branding?.active;
@@ -104,9 +105,9 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) =>
const features = [ const features = [
{ {
icon: ZapIcon, icon: ZapIcon,
label: 'Workflow Executions', label: 'App Runs',
licensed: `${selectedOrganization?.sync_features?.workflow_executions?.limit}/month limit`, licensed: `${selectedOrganization?.sync_features?.app_executions?.limit}/month limit`,
unlicensed: '10,000/month limit', unlicensed: '25,000/month limit',
isActive: workflowActive, isActive: workflowActive,
}, },
{ {
@@ -203,7 +204,7 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) =>
> >
{isProdStatusOn {isProdStatusOn
? 'Your organization has full access to all enterprise features and capabilities.' ? 'Your organization has full access to all enterprise features and capabilities.'
: 'View your current limits and available features. Upgrade to unlock enterprise capabilities.'} : 'Your organization is running on the open-source plan. Upgrade to Enterprise to remove limits and unlock advanced capabilities.'}
</Typography> </Typography>
{/* Features Grid */} {/* Features Grid */}
@@ -223,8 +224,8 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) =>
}) })
.map((feature, index) => { .map((feature, index) => {
const Icon = feature.icon; const Icon = feature.icon;
const isAvailable = isProdStatusOn && feature.isActive; const isAvailable = isProdStatusOn;
const statusColor = isAvailable ? colors.success : colors.disabled; const statusColor = isAvailable ? colors.success : colors.warning;
const bgColor = isAvailable ? themeMode === "dark" ? "#212121" : "#ffffff" : colors.disabledBg; const bgColor = isAvailable ? themeMode === "dark" ? "#212121" : "#ffffff" : colors.disabledBg;
return ( return (
@@ -235,19 +236,34 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) =>
alignItems: 'center', alignItems: 'center',
gap: 14, gap: 14,
padding: '14px 16px', padding: '14px 16px',
paddingLeft: !isAvailable ? 20 : 16,
borderRadius: 10, borderRadius: 10,
background: bgColor, background: bgColor,
border: `1px solid ${isAvailable ? colors.success + '40' : colors.border}`, border: `1px solid ${isAvailable ? colors.success + '40' : colors.border}`,
transition: 'all 0.2s', transition: 'all 0.2s',
position: 'relative',
overflow: 'hidden',
}} }}
> >
{!isAvailable && (
<div style={{
position: 'absolute',
left: 0,
top: 0,
bottom: 0,
width: 3,
background: colors.warning,
borderRadius: '10px 0 0 10px',
}} />
)}
{/* Icon */} {/* Icon */}
<div <div
style={{ style={{
width: 40, width: 40,
height: 40, height: 40,
borderRadius: 8, borderRadius: 8,
background: isAvailable ? colors.success + '20' : colors.disabledBg, background: isAvailable ? colors.success + '20' : `${colors.warning}20`,
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
justifyContent: 'center', justifyContent: 'center',
@@ -264,7 +280,7 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) =>
fontSize: 15, fontSize: 15,
fontWeight: 600, fontWeight: 600,
color: colors.textPrimary, color: colors.textPrimary,
marginBottom: 4, marginBottom: 3,
}} }}
> >
{feature.label} {feature.label}
@@ -272,7 +288,7 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) =>
<div <div
style={{ style={{
fontSize: 13, fontSize: 13,
color: colors.textSecondary, color: !isAvailable ? colors.warning : colors.textSecondary,
lineHeight: 1.4, lineHeight: 1.4,
}} }}
> >
@@ -285,8 +301,8 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) =>
style={{ color: colors.success, fontSize: 20, flexShrink: 0 }} style={{ color: colors.success, fontSize: 20, flexShrink: 0 }}
/> />
) : ( ) : (
<XCircleIcon <LockIcon
style={{ color: colors.disabled, fontSize: 20, flexShrink: 0 }} style={{ color: colors.warning, fontSize: 20, flexShrink: 0 }}
/> />
)} )}
</div> </div>
@@ -304,80 +320,323 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) =>
/> />
{!isProdStatusOn && ( {!isProdStatusOn && (
<div <div style={{
style={{
padding: 20,
borderRadius: 12, borderRadius: 12,
background: themeMode == "dark" ? "#212121" : "#ffffff", overflow: 'hidden',
border: `1px solid ${colors.border}`, border: `1px solid ${colors.accent}40`,
marginBottom: 20, marginBottom: 24,
}} }}>
> {/* Header */}
<div style={{ display: 'flex', gap: 12, marginBottom: 12 }}> <div style={{
<InfoIcon style={{ color: colors.accent, flexShrink: 0, marginTop: 2, fontSize: 20 }} /> background: `linear-gradient(135deg, ${colors.accent}14 0%, ${colors.accent}06 100%)`,
<div> padding: '16px 20px',
<Typography variant="h6" style={{ fontWeight: 600, margin: '0 0 8px 0' }}> borderBottom: `1px solid ${colors.accent}20`,
About Shuffle Enterprise display: 'flex',
</Typography> alignItems: 'center',
<Typography gap: 12,
variant="body2" }}>
color="textSecondary" <div style={{
style={{ width: 36,
margin: 0, height: 36,
lineHeight: 1.6, borderRadius: 8,
}} background: `${colors.accent}20`,
> display: 'flex',
Shuffle Enterprise is designed for organizations that require scalability, high alignItems: 'center',
availability, dedicated support, and robust infrastructure to run mission-critical justifyContent: 'center',
workflows in production environments. <a href="https://shuffler.io/articles/Shuffle_Open_Source" target="_blank" rel="noreferrer" style={{color: "#FF8544" }}>learn more</a> flexShrink: 0,
</Typography> }}>
<ShieldIcon style={{ color: colors.accent, fontSize: 20 }} />
</div>
<div>
<Typography variant="h6" style={{ fontWeight: 700, margin: 0, fontSize: 16 }}>
Unlock Shuffle Enterprise
</Typography>
<Typography variant="body2" color="textSecondary" style={{ margin: 0, fontSize: 12, lineHeight: 1.4 }}>
Scale your security operations without limits
</Typography>
</div>
</div> </div>
</div>
</div>)}
{/* CTA Section */} {/* Body */}
{!isProdStatusOn && ( <div style={{
<div padding: '16px 20px 20px',
style={{ background: themeMode === "dark" ? "#212121" : "#ffffff",
padding: 20, }}>
borderRadius: 12, <div style={{
background: `linear-gradient(135deg, #FF854415 0%, #FF854408 100%)`, display: 'grid',
border: `1px solid #FF854440`, gridTemplateColumns: '1fr 1fr',
marginBottom: 24, gap: 8,
}} marginBottom: 16,
> }}>
<div style={{ marginBottom: 16 }}> {[
<Typography variant="h6" style={{ fontWeight: 600, margin: '0 0 8px 0' }}> { icon: ZapIcon, text: 'Higher App Run Limits' },
Ready to upgrade? { icon: UsersIcon, text: 'Multi-Tenant Support' },
{ icon: ShieldIcon, text: 'Enterprise SLA' },
{ icon: FmdGoodOutlinedIcon, text: 'Multi-Location Deploy' },
].map((item, i) => {
const ItemIcon = item.icon;
return (
<div key={i} style={{
display: 'flex',
alignItems: 'center',
gap: 8,
padding: '8px 12px',
borderRadius: 8,
background: themeMode === "dark" ? `${colors.accent}08` : `${colors.accent}06`,
}}>
<ItemIcon style={{ color: colors.accent, fontSize: 16 }} />
<span style={{ fontSize: 13, color: colors.textPrimary, fontWeight: 500 }}>
{item.text}
</span>
</div>
);
})}
</div>
<Typography variant="body2" color="textSecondary" style={{
marginBottom: 16,
lineHeight: 1.6,
fontSize: 13,
}}>
Purpose-built for security teams that need scalability, high availability, and dedicated
expert support to run mission-critical workflows in production.{' '}
<a href="https://shuffler.io/articles/Shuffle_Open_Source" target="_blank" rel="noreferrer" style={{ color: colors.accent }}>
Learn more
</a>
</Typography> </Typography>
<Typography
variant="body2" <div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
color="textSecondary" <Button
style={{ href="https://shuffler.io/pricing?env=Self-Hosted"
margin: 0, target="_blank"
lineHeight: 1.6, rel="noreferrer"
}} variant="contained"
color="primary"
endIcon={<ArrowRightIcon sx={{ fontSize: 16 }} />}
>
Upgrade Now
</Button>
<Button
href="https://shuffler.io/contact?category=talk_to_sales"
target="_blank"
rel="noreferrer"
variant="outlined"
startIcon={<MailIcon sx={{ fontSize: 16 }} />}
> >
Contact our team to learn more about enterprise features and pricing. Talk to Sales
</Typography> </Button>
</div>
</div> </div>
<Button
href="https://shuffler.io/contact?category=talk_to_sales"
target="_blank"
rel="noreferrer"
variant="contained"
color="primary"
startIcon={<MailIcon sx={{ fontSize: 16}} />}
endIcon={<ArrowRightIcon sx={{ fontSize: 16 }} />}
>
Contact Sales
</Button>
</div> </div>
)} )}
</div> </div>
); );
}; };
const AppRunsQueueCard = memo(({ environment, isAirGapped, isCloudSynching, totalRuns, limit, theme, navigate }) => {
const usagePct = limit > 0 ? (totalRuns / limit) * 100 : 0;
const hardPauseLimit = limit * 2;
const hardPausePct = hardPauseLimit > 0 ? Math.min((totalRuns / hardPauseLimit) * 100, 100) : 0;
const mainBarPct = Math.min(usagePct, 100);
const queueSize = environment?.queue !== undefined && environment?.queue !== null
? Math.max(0, environment.queue)
: 0;
const isThrottled = isCloudSynching ? false : (isAirGapped ? hardPausePct >= 100 : usagePct >= 100);
let status, statusColor, statusBg;
if (isThrottled) {
status = 'Throttled';
statusColor = '#ef4444';
statusBg = 'rgba(239, 68, 68, 0.12)';
} else if (!isCloudSynching && usagePct >= 80) {
status = 'Warning';
statusColor = '#f59e0b';
statusBg = 'rgba(245, 158, 11, 0.12)';
} else {
status = 'Healthy';
statusColor = theme.palette.green;
statusBg = `${theme.palette.green}1f`;
}
const throttleRate = isThrottled ? '1/min' : '\u2014';
const estClearTime = isThrottled && queueSize > 0 ? `${queueSize} min` : '\u2014';
const mainBarColor = isThrottled ? '#ef4444' : usagePct >= 80 && !isCloudSynching ? '#f59e0b' : theme.palette.green;
const envTypeLabel = environment?.run_type === 'cloud' ? 'Cloud' : 'On-prem';
const envName = environment?.Name || environment?.name || 'Default';
const borderColor = theme.palette.slateGrayColor;
const trackBg = theme.palette.slateGrayColor;
const mutedText = theme.palette.text.secondary;
return (
<div style={{
border: `1px solid ${borderColor}`,
borderRadius: 12,
padding: '20px 24px',
backgroundColor: theme.palette.platformColor,
marginBottom: 16,
maxWidth: 800,
}}>
{/* Title row */}
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
<Typography variant="body2" style={{ color: mutedText, fontSize: 13, fontWeight: 500 }}>
{envTypeLabel} - {envName} · App runs / month
</Typography>
<div style={{
padding: '4px 14px',
borderRadius: 20,
backgroundColor: statusBg,
border: `1px solid ${statusColor}60`,
display: 'inline-flex',
alignItems: 'center',
gap: 6,
}}>
<span style={{ width: 7, height: 7, borderRadius: '50%', backgroundColor: statusColor, display: 'inline-block', boxShadow: `0 0 6px ${statusColor}` }} />
<span style={{ color: statusColor, fontWeight: 700, fontSize: 13 }}>{status}</span>
</div>
</div>
{/* Main number */}
<div style={{ display: 'flex', alignItems: 'baseline', gap: 6, marginBottom: 2 }}>
<span style={{ fontSize: 28, fontWeight: 700, color: theme.palette.text.primary }}>
{totalRuns.toLocaleString()}
</span>
<span style={{ fontSize: 18, color: mutedText }}>
/ {limit.toLocaleString()}
</span>
</div>
{isAirGapped && !isCloudSynching && (
<Typography variant="caption" style={{ color: mutedText, fontSize: 12, display: 'block', marginBottom: 14 }}>
No throttle until {hardPauseLimit.toLocaleString()} runs · 2× your plan limit
</Typography>
)}
{/* Main usage bar */}
<div style={{ position: 'relative', height: 8, borderRadius: 4, backgroundColor: trackBg, marginBottom: 4 }}>
<div style={{
position: 'absolute',
left: 0, top: 0, bottom: 0,
width: `${mainBarPct}%`,
borderRadius: 4,
backgroundColor: mainBarColor,
transition: 'width 0.4s ease',
}} />
{/* 80% threshold marker */}
<div style={{
position: 'absolute',
left: '80%',
top: -3,
bottom: -3,
width: 2,
backgroundColor: borderColor,
borderRadius: 1,
}} />
</div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 14 }}>
<Typography variant="caption" style={{ color: mutedText, fontSize: 11 }}>0</Typography>
<Typography variant="caption" style={{ color: mutedText, fontSize: 11 }}>80% threshold</Typography>
<Typography variant="caption" style={{ color: mutedText, fontSize: 11 }}>{limit.toLocaleString()}</Typography>
</div>
{/* Throttle limit row */}
{isAirGapped && (
<>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
<Typography variant="caption" style={{ color: mutedText, fontSize: 12 }}>
Burst throttle threshold (2× limit) · workflows throttle to 1/min above this
</Typography>
<Typography variant="caption" style={{ color: mutedText, fontSize: 12 }}>
{totalRuns.toLocaleString()} / {hardPauseLimit.toLocaleString()}
</Typography>
</div>
<div style={{ position: 'relative', height: 6, borderRadius: 3, backgroundColor: trackBg, marginBottom: 16 }}>
<div style={{
position: 'absolute',
left: 0, top: 0, bottom: 0,
width: `${hardPausePct}%`,
borderRadius: 3,
backgroundColor: '#ef4444',
transition: 'width 0.4s ease',
}} />
</div>
</>
)}
{/* Alert box for Warning / Throttled */}
{status !== 'Healthy' && (
<div style={{
borderRadius: 8,
padding: '14px 16px',
backgroundColor: 'rgba(245, 158, 11, 0.08)',
border: '1px solid rgba(245, 158, 11, 0.3)',
marginBottom: 16,
}}>
<Typography variant="body2" style={{ color: '#f59e0b', fontWeight: 700, marginBottom: 6, fontSize: 14 }}>
{isThrottled ? 'Running slow \u2014 workflows are still running' : 'Approaching your monthly limit'}
</Typography>
<Typography variant="body2" style={{ color: theme.palette.text.primary, fontSize: 13, lineHeight: 1.6, marginBottom: 12 }}>
{isThrottled
? isAirGapped
? `You've exceeded the burst threshold of ${hardPauseLimit.toLocaleString()} runs (2\u00d7 your plan limit). Your workflows are still running \u2014 there is no hard stop. Executions slow to 1 per minute until next month.`
: `You've exceeded your ${limit.toLocaleString()} monthly limit. Your instance keeps running \u2014 executions slow to 1 per minute until next month. Nothing is lost. You can view or clear the queue from the Locations tab.`
: isAirGapped
? `You've used ${totalRuns.toLocaleString()} of ${limit.toLocaleString()} app runs. Workflows run normally \u2014 slowdown only begins at ${hardPauseLimit.toLocaleString()} runs (2\u00d7 your plan limit). No action needed.`
: `You've used ${totalRuns.toLocaleString()} of ${limit.toLocaleString()} app runs (${Math.max(0, limit - totalRuns).toLocaleString()} remaining). If you reach 100%, executions continue at a reduced rate of 1 per minute, nothing stops or is lost.`
}
</Typography>
<Button
variant="outlined"
size="small"
sx={{
borderColor: '#f59e0b',
color: '#f59e0b',
textTransform: 'none',
fontSize: 13,
borderRadius: '6px',
padding: '5px 14px',
}}
onClick={() => {
if (isThrottled) navigate('/admin?tab=locations');
else window.open('https://shuffler.io/contact', '_blank', 'noopener,noreferrer');
}}
>
{isThrottled ? 'Manage queue' : 'Contact Us'}
</Button>
</div>
)}
{/* Stats row */}
<div style={{ display: 'flex', borderTop: `1px solid ${borderColor}`, paddingTop: 16 }}>
{[
{ label: 'Queued jobs', value: queueSize },
{ label: 'Throttle rate', value: throttleRate },
{ label: 'Est. clear time', value: estClearTime },
].map((stat, i) => (
<div key={i} style={{
flex: 1,
paddingRight: i < 2 ? 16 : 0,
borderRight: i < 2 ? `1px solid ${borderColor}` : 'none',
marginRight: i < 2 ? 16 : 0,
}}>
<Typography variant="caption" style={{ color: mutedText, fontSize: 12, display: 'block', marginBottom: 4 }}>
{stat.label}
</Typography>
<Typography style={{ fontWeight: 700, fontSize: 18, color: theme.palette.text.primary }}>
{stat.value}
</Typography>
</div>
))}
</div>
</div>
);
});
const Billing = memo((props) => { const Billing = memo((props) => {
const { globalUrl, userdata, serverside, billingInfo, stripeKey,isLoaded, selectedOrganization, handleGetOrg, clickedFromOrgTab, removeCookie} = props; const { globalUrl, userdata, serverside, billingInfo, stripeKey,isLoaded, selectedOrganization, handleGetOrg, clickedFromOrgTab, removeCookie} = props;
//const alert = useAlert(); //const alert = useAlert();
@@ -413,7 +672,7 @@ const Billing = memo((props) => {
const [statistics, setStatistics] = useState([]) const [statistics, setStatistics] = useState([])
const [monthlyAppRunsParent, setMonthlyAppRunsParent] = useState(0) const [monthlyAppRunsParent, setMonthlyAppRunsParent] = useState(0)
const [monthlyAllSuborgExecutions, setMonthlyAllSuborgExecutions] = useState(0) const [monthlyAllSuborgExecutions, setMonthlyAllSuborgExecutions] = useState(0)
const [billingEnvironments, setBillingEnvironments] = useState([])
useEffect(() => { useEffect(() => {
if (monthlyAppRunsParent > 0 || monthlyAllSuborgExecutions > 0) { if (monthlyAppRunsParent > 0 || monthlyAllSuborgExecutions > 0) {
const percentage = ((monthlyAppRunsParent + monthlyAllSuborgExecutions) / userdata.app_execution_limit) * 100; const percentage = ((monthlyAppRunsParent + monthlyAllSuborgExecutions) / userdata.app_execution_limit) * 100;
@@ -525,6 +784,29 @@ const Billing = memo((props) => {
}, []) }, [])
const getBillingEnvironments = () => {
fetch(globalUrl + "/api/v1/getenvironments", {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) return;
return response.json();
})
.then((responseJson) => {
if (responseJson && Array.isArray(responseJson)) {
setBillingEnvironments(responseJson);
}
})
.catch((error) => {
console.log("Error fetching environments for billing:", error);
});
};
const getStats = (orgid) => { const getStats = (orgid) => {
if (orgid === undefined || orgid === null) { if (orgid === undefined || orgid === null) {
@@ -563,6 +845,9 @@ const Billing = memo((props) => {
useEffect(() => { useEffect(() => {
if (selectedOrganization && selectedOrganization?.id?.length > 0) { if (selectedOrganization && selectedOrganization?.id?.length > 0) {
getStats(selectedOrganization.id); getStats(selectedOrganization.id);
if (!isCloud) {
getBillingEnvironments();
}
} }
}, [selectedOrganization]); }, [selectedOrganization]);
@@ -2387,6 +2672,17 @@ const Billing = memo((props) => {
const isChildOrg = userdata?.active_org?.creator_org !== "" && userdata?.active_org?.creator_org !== undefined && userdata?.active_org?.creator_org !== null const isChildOrg = userdata?.active_org?.creator_org !== "" && userdata?.active_org?.creator_org !== undefined && userdata?.active_org?.creator_org !== null
const activeQueueEnvs = Array.isArray(billingEnvironments) ? billingEnvironments.filter(env => env != null && !env.archived && env.Type !== 'cloud') : [];
const totalQueueSize = activeQueueEnvs.reduce((sum, env) => sum + Math.max(0, env?.queue || 0), 0);
const aggregatedQueueEnv = {
Name: `${activeQueueEnvs.length} Runtime Location${activeQueueEnvs.length !== 1 ? 's' : ''}`,
run_type: 'on-prem',
queue: totalQueueSize,
};
const appExecLimit = selectedOrganization?.sync_features?.app_executions?.limit ?? 0;
const isAirGapped = selectedOrganization != null && (selectedOrganization.cloud_sync_active === true || selectedOrganization.cloud_sync === true) ? false : appExecLimit < 300000 ? false : true;
const isCloudSynching = selectedOrganization != null && selectedOrganization.cloud_sync === true && appExecLimit >= 300000;
useEffect(() => { useEffect(() => {
if (isChildOrg && currentTab === 0) { if (isChildOrg && currentTab === 0) {
setCurrentTab(1); setCurrentTab(1);
@@ -2800,6 +3096,28 @@ const Billing = memo((props) => {
</div> </div>
) : null*/} ) : null*/}
{/* Queue Management */}
{!isCloud && activeQueueEnvs.length > 0 && !isChildOrg && (
<div style={{ maxWidth: 800, marginTop: 32, marginBottom: 8 }}>
<Typography variant="h6" style={{ marginBottom: 6, fontSize: 20, fontWeight: 600 }}>
Queue Management
</Typography>
<Typography variant="body2" color="textSecondary" style={{ marginBottom: 16, fontSize: 14 }}>
Real-time status of your app run usage and workflow queue across all runtime locations.
</Typography>
<AppRunsQueueCard
environment={aggregatedQueueEnv}
totalRuns={Number(monthlyAppRunsParent ?? 0) + Number(monthlyAllSuborgExecutions ?? 0)}
limit={selectedOrganization?.sync_features?.app_executions?.limit || 25000}
theme={theme}
navigate={navigate}
isAirGapped={isAirGapped}
isCloudSynching={isCloudSynching}
/>
</div>
)}
{!isChildOrg && isCloud && ( {!isChildOrg && isCloud && (
<div style={{ display: 'flex', flexDirection: 'column', marginTop: 50, maxWidth: 860 }} id="professional-services"> <div style={{ display: 'flex', flexDirection: 'column', marginTop: 50, maxWidth: 860 }} id="professional-services">
<Typography variant="h6" style={{ marginBottom: 5, fontSize: 24, fontWeight: 500 }}> <Typography variant="h6" style={{ marginBottom: 5, fontSize: 24, fontWeight: 500 }}>
+16
View File
@@ -92,6 +92,11 @@ const AppStats = (defaultprops) => {
const statKey = syncStats === true ? "onprem_stats" : "daily_statistics" const statKey = syncStats === true ? "onprem_stats" : "daily_statistics"
const dailyStats = inputdata[statKey] const dailyStats = inputdata[statKey]
if (dailyStats === undefined || dailyStats === null) { if (dailyStats === undefined || dailyStats === null) {
setAppruns(undefined)
setWorkflowRuns(undefined)
setSubflowRuns(undefined)
setChildOrgsAppRuns(undefined)
setApprunCosts(undefined)
return return
} }
@@ -378,6 +383,17 @@ const AppStats = (defaultprops) => {
return return
} }
if (syncStats && (statistics[statKey] === undefined || statistics[statKey] === null)) {
setOnpremAppRuns(0)
setFilteredStatistics(statistics)
setAppruns(undefined)
setWorkflowRuns(undefined)
setSubflowRuns(undefined)
setChildOrgsAppRuns(undefined)
setApprunCosts(undefined)
return
}
// Calculate month to date cost // Calculate month to date cost
var mtd_cost = 0 var mtd_cost = 0
for (let key in statistics[statKey]) { for (let key in statistics[statKey]) {
+100 -155
View File
@@ -1,6 +1,8 @@
import React, { useState, useEffect, useContext, memo } from "react"; import React, { useState, useEffect, useContext, memo } from "react";
import { makeStyles } from "@mui/styles"; import { makeStyles } from "@mui/styles";
import { getTheme } from "../theme.jsx"; import { getTheme } from "../theme.jsx";
import SubOrgDistributionDialog from "./SubOrgDistributionDialog.jsx";
import DeleteConfirmDialog from "./DeleteConfirmDialog.jsx";
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import ReactJson from "react-json-view-ssr"; import ReactJson from "react-json-view-ssr";
@@ -17,8 +19,6 @@ import {
Button, Button,
Tabs, Tabs,
Tab, Tab,
List,
ListItem,
ListItemText, ListItemText,
IconButton, IconButton,
Dialog, Dialog,
@@ -82,6 +82,7 @@ import {
Hub as HubIcon, Hub as HubIcon,
Key as KeyIcon, Key as KeyIcon,
FlashOn as FlashOnIcon, FlashOn as FlashOnIcon,
Search as SearchIcon,
} from "@mui/icons-material"; } from "@mui/icons-material";
import { Context } from "../context/ContextApi.jsx"; import { Context } from "../context/ContextApi.jsx";
@@ -127,6 +128,9 @@ const CacheView = memo((props) => {
const [showDistributionPopup, setShowDistributionPopup] = useState(false); const [showDistributionPopup, setShowDistributionPopup] = useState(false);
const [selectedSubOrg, setSelectedSubOrg] = useState([]); const [selectedSubOrg, setSelectedSubOrg] = useState([]);
const [selectedCacheKey, setSelectedCacheKey] = useState(""); const [selectedCacheKey, setSelectedCacheKey] = useState("");
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
const [deleteConfirmTarget, setDeleteConfirmTarget] = useState(null);
const [distribOrgOrder, setDistribOrgOrder] = useState([]);
const [totalAmount, setTotalAmount] = useState(0); const [totalAmount, setTotalAmount] = useState(0);
const [page, setPage] = useState(0); const [page, setPage] = useState(0);
const [pageSize, setPageSize] = useState(50) const [pageSize, setPageSize] = useState(50)
@@ -233,7 +237,7 @@ const CacheView = memo((props) => {
{ {
"name": "Enrich", "name": "Enrich",
"description": "Enriches the data. Only runs on valid JSON data AND if the 'enrichment' field does not exist.", "description": "Enriches the data. Uses regex keys and runs a workflow in the background. Added to the 'enrichments' key.",
"type": "singul", "type": "singul",
"options": [{ "options": [{
"key": "", "key": "",
@@ -331,6 +335,14 @@ const CacheView = memo((props) => {
// In order to make linking weird urls from workflow page work. // In order to make linking weird urls from workflow page work.
if (urlParams.get("src") == "workflow") { if (urlParams.get("src") == "workflow") {
if (categoryParam === "OCSF") {
const newParam = "shuffle-security incidents"
urlParams.set("category", newParam)
window.history.replaceState({}, '', `${window.location.pathname}?${urlParams.toString()}`)
categoryParam = newParam
}
if (categoryParam?.toLowerCase().startsWith("list")) { if (categoryParam?.toLowerCase().startsWith("list")) {
const newParam = categoryParam.substring(5).replaceAll("%20", "_") const newParam = categoryParam.substring(5).replaceAll("%20", "_")
@@ -570,17 +582,26 @@ const CacheView = memo((props) => {
.then((response) => { .then((response) => {
if (response.status === 200) { if (response.status === 200) {
if (refreshList === undefined || refreshList === null || refreshList === true) { if (refreshList === undefined || refreshList === null || refreshList === true) {
toast.success("Deleted datastore entry"); toast.success("Deleted datastore entry");
setTimeout(() => { setTimeout(() => {
listOrgCache(orgId, selectedCategory, 0, pageSize, page) listOrgCache(orgId, selectedCategory, 0, pageSize, page)
}, 1000); }, 1000);
} }
} else { } else {
if (refreshList === undefined || refreshList === null || refreshList === true) {
setTimeout(() => {
listOrgCache(orgId, selectedCategory, 0, pageSize, page)
}, 1000);
}
toast.error(`Failed deleting entry ${key} in category ${itemCategory || selectedCategory}. If this persists, please contact support@shuffler.io.`) toast.error(`Failed deleting entry ${key} in category ${itemCategory || selectedCategory}. If this persists, please contact support@shuffler.io.`)
} }
}) })
.catch((error) => { .catch((error) => {
if (refreshList === undefined || refreshList === null || refreshList === true) {
setTimeout(() => {
listOrgCache(orgId, selectedCategory, 0, pageSize, page)
}, 1000);
}
toast(error.toString()); toast(error.toString());
}); });
}; };
@@ -831,6 +852,11 @@ const CacheView = memo((props) => {
Category: {dataValue.category} Category: {dataValue.category}
</Typography> </Typography>
: null} : null}
{dataValue?.enrichments !== undefined && dataValue?.enrichments !== null && dataValue.enrichments.length > 0 ?
<Typography variant="body2" color="textSecondary" style={{ }}>
Enrichments: {dataValue.enrichments.length}
</Typography>
: null}
{dataValue?.tags !== undefined && dataValue?.tags !== null && dataValue?.tags?.length > 0 ? {dataValue?.tags !== undefined && dataValue?.tags !== null && dataValue?.tags?.length > 0 ?
<div style={{display: "flex", marginTop: 12, }}> <div style={{display: "flex", marginTop: 12, }}>
<Typography variant="body2" color="textSecondary" style={{ marginRight: 10, marginTop: 4, }}> <Typography variant="body2" color="textSecondary" style={{ marginRight: 10, marginTop: 4, }}>
@@ -900,33 +926,6 @@ const CacheView = memo((props) => {
</Dialog> </Dialog>
); );
const handleSelectSubOrg = (id, action) => {
if (action === "all") {
const childOrgs = userdata.orgs.filter(
(data) => data.creator_org === userdata.active_org.id
);
setSelectedSubOrg((prev) => {
if (prev.length === childOrgs.length) {
// If all child orgs are already selected, clear the selection
return [];
} else {
// Otherwise, select all child org IDs
return childOrgs.map((data) => data.id);
}
});
} else if (action === "none") {
setSelectedSubOrg([]);
} else {
setSelectedSubOrg((prev) => {
if (prev.includes(id)) {
return prev.filter((data) => data !== id);
} else {
return [...prev, id];
}
});
}
};
const changeDistribution = (id, selectedSubOrg) => { const changeDistribution = (id, selectedSubOrg) => {
editFileConfig(id, [...new Set(selectedSubOrg)], selectedCategory) editFileConfig(id, [...new Set(selectedSubOrg)], selectedCategory)
@@ -941,8 +940,6 @@ const CacheView = memo((props) => {
category: category === undefined || category === "" || category === "default" ? "" : category, category: category === undefined || category === "" || category === "default" ? "" : category,
} }
console.log("data: ", data);
const url = `${globalUrl}/api/v1/orgs/${orgId}/cache/config`; const url = `${globalUrl}/api/v1/orgs/${orgId}/cache/config`;
fetch(url, { fetch(url, {
@@ -975,98 +972,41 @@ const CacheView = memo((props) => {
}; };
const cacheDistributionModal = showDistributionPopup ? (
<Dialog
open={showDistributionPopup}
onClose={() => setShowDistributionPopup(false)}
PaperProps={{
sx: {
borderRadius: theme?.palette?.DialogStyle?.borderRadius,
border: theme?.palette?.DialogStyle?.border,
fontFamily: theme?.typography?.fontFamily,
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
zIndex: 1000,
minWidth: "600px",
minHeight: "320px",
overflow: "auto",
'& .MuiDialogContent-root': {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
},
'& .MuiDialogTitle-root': {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
},
'& .MuiDialogActions-root': {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
},
},
}}
>
<DialogTitle>
<Typography variant="h5" color="textPrimary">
Select sub-org to distribute Datastore key
</Typography>
</DialogTitle>
<DialogContent style={{ color: "rgba(255,255,255,0.65)" }}>
<MenuItem value="none" onClick={()=> {handleSelectSubOrg(null, "none")}}>None</MenuItem>
<MenuItem value="all" onClick={()=> {handleSelectSubOrg(null, "all")}}>All</MenuItem>
{userdata.orgs.map((data, index) => {
if (data.creator_org !== userdata.active_org.id) {
return null;
}
const imagesize = 22;
const imageStyle = {
width: imagesize,
height: imagesize,
pointerEvents: "none",
marginRight: 10,
marginLeft: data.id === userdata.active_org.id ? 0 : 20,
};
const image = data.image === "" ? ( const deleteConfirmDialog = (
<img alt={data.name} src={theme.palette.defaultImage} style={imageStyle} /> <DeleteConfirmDialog
) : ( open={deleteConfirmOpen}
<img alt={data.name} src={data.image} style={imageStyle} /> onClose={() => { setDeleteConfirmOpen(false); setDeleteConfirmTarget(null); }}
); onConfirm={() => {
if (deleteConfirmTarget?.bulk) {
const itemsToDelete = selectedRows.map(rowId =>
listCache.find(item => `${item.key}_${item.category || ""}` === rowId)
).filter(Boolean);
return ( const count = itemsToDelete.length;
<MenuItem setSelectedRows([]);
key={index} itemsToDelete.forEach(item => deleteEntry(orgId, item.key, item.category, false));
value={data.id}
onClick={() => handleSelectSubOrg(data.id)}
style={{ display: "flex", alignItems: "center" }}
>
<Checkbox
checked={selectedSubOrg.includes(data.id)}
/>
{image}
<span style={{ marginLeft: 8 }}>{data.name}</span>
</MenuItem>
);
})}
<div style={{ display: "flex", marginTop: 20 }}> setTimeout(() => {
<Button listOrgCache(orgId, selectedCategory, 0, pageSize, page);
style={{ borderRadius: "2px", textTransform: 'none', fontSize:16, color: theme.palette.primary.main }} toast.success("Deleted " + count + " keys from datastore");
onClick={() => setShowDistributionPopup(false)} }, 3000);
color="primary" } else {
> deleteEntry(orgId, deleteConfirmTarget.key, deleteConfirmTarget.category);
Cancel }
</Button> setDeleteConfirmOpen(false);
<Button setDeleteConfirmTarget(null);
variant="contained" }}
style={{ borderRadius: "2px", textTransform: 'none', fontSize:16, marginLeft: 10 }} title={deleteConfirmTarget?.bulk ? `Delete ${selectedRows?.length} Key${selectedRows?.length > 1 ? "s" : ""}?` : "Delete Key?"}
onClick={() => { description={
changeDistribution(selectedCacheKey, selectedSubOrg); deleteConfirmTarget?.bulk
}} ? <>Are you sure you want to delete <b>{selectedRows?.length} key{selectedRows?.length > 1 ? "s" : ""}</b>?</>
color="primary" : <>Are you sure you want to delete <b>{deleteConfirmTarget?.key}</b>?</>
> }
Submit warningText="This cannot be undone. Any workflows using these keys will lose access."
</Button> />
</div> );
</DialogContent>
</Dialog>
) : null;
const saveAutomation = (allAutomation, settings) => { const saveAutomation = (allAutomation, settings) => {
// Check if icon is a string. Otherwise make it empty. // Check if icon is a string. Otherwise make it empty.
@@ -1759,6 +1699,10 @@ const CacheView = memo((props) => {
enableClipboard={(copy) => { enableClipboard={(copy) => {
handleReactJsonClipboard(copy) handleReactJsonClipboard(copy)
}} }}
onSelect={(select) => {
//currentParams.set("category", selectedCategory);
//HandleJsonCopy(validate.result, select, "exec");
}}
collapseStringsAfterLength={theme.palette.jsonCollapseStringsAfterLength} collapseStringsAfterLength={theme.palette.jsonCollapseStringsAfterLength}
iconStyle={theme.palette.jsonIconStyle} iconStyle={theme.palette.jsonIconStyle}
displayDataTypes={false} displayDataTypes={false}
@@ -1935,6 +1879,7 @@ const CacheView = memo((props) => {
"workflow_id": data.workflow_id, "workflow_id": data.workflow_id,
"category": data.category, "category": data.category,
"tags": data.tags, "tags": data.tags,
"enrichments": data.enrichments,
}) })
setValue(newvalue) setValue(newvalue)
setModalOpen(true) setModalOpen(true)
@@ -2019,7 +1964,8 @@ const CacheView = memo((props) => {
onClick={(e) => { onClick={(e) => {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
deleteEntry(orgId, data.key, data.category) setDeleteConfirmTarget({ key: data.key, category: data.category })
setDeleteConfirmOpen(true)
}} }}
> >
<svg <svg
@@ -2094,13 +2040,21 @@ const CacheView = memo((props) => {
style={{ margin: "auto" }} style={{ margin: "auto" }}
color="secondary" color="secondary"
onClick={() => { onClick={() => {
setShowDistributionPopup(true) setShowDistributionPopup(true);
let initialSelected = [];
if(data?.suborg_distribution?.length > 0){ if(data?.suborg_distribution?.length > 0){
setSelectedSubOrg(data.suborg_distribution) initialSelected = data.suborg_distribution;
}else{
setSelectedSubOrg([])
} }
setSelectedCacheKey(data.key) setSelectedSubOrg(initialSelected);
setSelectedCacheKey(data.key);
const suborgs = (userdata?.orgs || []).filter(o => o.creator_org === userdata?.active_org?.id);
const sorted = [...suborgs].sort((a, b) => {
const aS = initialSelected.includes(a.id);
const bS = initialSelected.includes(b.id);
if (aS !== bS) return bS - aS;
return a.name.localeCompare(b.name);
});
setDistribOrgOrder(sorted.map(o => o.id));
}} }}
/> />
</Tooltip> </Tooltip>
@@ -2116,6 +2070,7 @@ const CacheView = memo((props) => {
var previousgroup = "" var previousgroup = ""
const isAutomating = categoryAutomations?.find((automation) => automation.enabled) !== undefined const isAutomating = categoryAutomations?.find((automation) => automation.enabled) !== undefined
const isAutomatingAccess = categoryConfig?.settings?.timeout >= 60 || categoryConfig?.settings?.public === true ? true : false
return ( return (
<div style={{ <div style={{
minHeight: 2000, minHeight: 2000,
@@ -2183,7 +2138,17 @@ const CacheView = memo((props) => {
apps={apps} apps={apps}
/> />
{cacheDistributionModal} <SubOrgDistributionDialog
open={showDistributionPopup}
onClose={() => { setShowDistributionPopup(false); setSelectedCacheKey(""); }}
title="Distribute Datastore Key to Sub-Organizations"
extraInfo={selectedCacheKey ? `Selected Key: ${selectedCacheKey}` : null}
orgs={distribOrgOrder.map(id => (userdata?.orgs || []).find(o => o.id === id)).filter(Boolean)}
selectedOrgIds={selectedSubOrg}
onSelectionChange={setSelectedSubOrg}
onSave={(ids) => { changeDistribution(selectedCacheKey, ids); }}
/>
{deleteConfirmDialog}
<div style={{height: "100%", overflowY: "auto", scrollbarColor: theme.palette.scrollbarColorTransparent, scrollbarWidth: 'thin'}}> <div style={{height: "100%", overflowY: "auto", scrollbarColor: theme.palette.scrollbarColorTransparent, scrollbarWidth: 'thin'}}>
<div style={{ height: "100%", width: "calc(100% - 20px)", scrollbarColor: theme.palette.scrollbarColorTransparent, scrollbarWidth: 'thin' }}> <div style={{ height: "100%", width: "calc(100% - 20px)", scrollbarColor: theme.palette.scrollbarColorTransparent, scrollbarWidth: 'thin' }}>
@@ -2204,7 +2169,6 @@ const CacheView = memo((props) => {
height: 35, height: 35,
textTransform: 'none', textTransform: 'none',
//border: isAutomating ? `1px solid ${theme.palette.primary.main}` : null,
}} }}
variant="outlined" variant="outlined"
color="secondary" color="secondary"
@@ -2272,12 +2236,12 @@ const CacheView = memo((props) => {
datastoreCategories !== null && datastoreCategories !== null &&
datastoreCategories.length > 1 ? ( datastoreCategories.length > 1 ? (
<FormControl style={{ minWidth: 250, maxWidth: 250, marginTop: 8, }}> <FormControl style={{ minWidth: 275, maxWidth: 275, marginTop: 8, }}>
<Autocomplete <Autocomplete
labelId="category-choice" labelId="category-choice"
style={{ style={{
minWidth: 250, minWidth: 275,
maxWidth: 250, maxWidth: 275,
}} }}
ListboxProps={{ ListboxProps={{
style: { style: {
@@ -2557,13 +2521,13 @@ const CacheView = memo((props) => {
marginLeft: 3, marginLeft: 3,
}} }}
variant="outlined" variant="outlined"
color="secondary" color={isAutomatingAccess ? "primary" : "secondary"}
disabled={selectedCategory === undefined || selectedCategory === "" || selectedCategory === "default"} disabled={selectedCategory === undefined || selectedCategory === "" || selectedCategory === "default"}
onClick={() => { onClick={() => {
setShowSettingsMenu(true) setShowSettingsMenu(true)
}} }}
> >
<SettingsIcon style={{color: theme.palette.secondary.main, }} /> <SettingsIcon style={{}} />
</Button> </Button>
</Tooltip> </Tooltip>
</ButtonGroup> </ButtonGroup>
@@ -2877,27 +2841,8 @@ const CacheView = memo((props) => {
<Button <Button
style={{ marginLeft: 50, }} style={{ marginLeft: 50, }}
onClick={() => { onClick={() => {
setCachedLoaded(false) setDeleteConfirmTarget({ bulk: true });
for (var key in selectedRows) { setDeleteConfirmOpen(true);
// Find the item and its category
var foundCategory = ""
for (var i = 0; i < listCache.length; i++) {
if (listCache[i].key === selectedRows[key]) {
foundCategory = listCache[i].category
break
}
}
deleteEntry(orgId, selectedRows[key], foundCategory, false)
}
setSelectedRows([])
setTimeout(() => {
// Refresh the list
listOrgCache(orgId, selectedCategory, 0, pageSize, page)
toast.success("Deleted " + selectedRows.length + " keys from datastore")
}, 2500)
}} }}
variant={"outlined"} variant={"outlined"}
color="secondary" color="secondary"
+4
View File
@@ -777,6 +777,10 @@ const CloudSyncTab = (props) => {
newname = "app runs" newname = "app runs"
} }
if (key === "onprem_app_executions" && userdata.support !== true) {
return null
}
const griditem = { const griditem = {
primary: newkey, primary: newkey,
secondary: secondary:
@@ -34,7 +34,7 @@ import {
} from '../views/AngularWorkflow.jsx' } from '../views/AngularWorkflow.jsx'
import algoliasearch from 'algoliasearch/lite'; import algoliasearch from 'algoliasearch/lite';
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e") const searchClient = algoliasearch("JNSS5CFDZZ", "33e4e3564f4f060e96e0531957bed552")
const CollectIngestModal = (props) => { const CollectIngestModal = (props) => {
const { globalUrl, open, setOpen, workflows, getWorkflows, apps, } = props; const { globalUrl, open, setOpen, workflows, getWorkflows, apps, } = props;
@@ -437,7 +437,7 @@ const CollectIngestModal = (props) => {
<Grid container> <Grid container>
<IngestItem type="Ingest Tickets" appCategory={"cases"} webhook={true} index={1} /> <IngestItem type="Ingest Tickets" appCategory={"cases"} webhook={true} index={1} />
<IngestItem type="Enable Threat feeds" index={2} /> <IngestItem type="Enable Threat feeds" index={2} webhook={true} />
<IngestItem type="Ingest Assets" appCategory={"assets"} index={2} /> <IngestItem type="Ingest Assets" appCategory={"assets"} index={2} />
<IngestItem type="Ingest Users " appCategory={"users"} index={2} /> <IngestItem type="Ingest Users " appCategory={"users"} index={2} />
<IngestItem type="Enable Search" index={2} /> <IngestItem type="Enable Search" index={2} />
@@ -633,7 +633,7 @@ const ConfigureWorkflow = (props) => {
if (aa !== undefined) { if (aa !== undefined) {
aa('init', { aa('init', {
appId: "JNSS5CFDZZ", appId: "JNSS5CFDZZ",
apiKey: "c8f882473ff42d41158430be09ec2b4e", apiKey: "33e4e3564f4f060e96e0531957bed552",
}) })
const timestamp = new Date().getTime() const timestamp = new Date().getTime()
+4 -99
View File
@@ -4,6 +4,7 @@ import ReactGA from 'react-ga4';
import {Link} from 'react-router-dom'; import {Link} from 'react-router-dom';
import theme from '../theme.jsx'; import theme from '../theme.jsx';
import { removeQuery } from '../components/ScrollToTop.jsx'; import { removeQuery } from '../components/ScrollToTop.jsx';
import SearchContactForm from '../components/SearchContactForm.jsx';
import { import {
SkipNext as SkipNextIcon, SkipNext as SkipNextIcon,
@@ -38,18 +39,14 @@ import {
} from "@mui/material" } from "@mui/material"
import { useDebouncedCallback } from "../utils/useDebouncedCallback.jsx"; import { useDebouncedCallback } from "../utils/useDebouncedCallback.jsx";
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e") const searchClient = algoliasearch("JNSS5CFDZZ", "33e4e3564f4f060e96e0531957bed552")
const CreatorGrid = props => { const CreatorGrid = props => {
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, isHeader } = props const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, isHeader } = props
const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows
const xs = parsedXs === undefined || parsedXs === null ? isMobile ? 6 : 4 : parsedXs const xs = parsedXs === undefined || parsedXs === null ? isMobile ? 6 : 4 : parsedXs
//const [apps, setApps] = React.useState([]); //const [apps, setApps] = React.useState([]);
//const [filteredApps, setFilteredApps] = React.useState([]); //const [filteredApps, setFilteredApps] = React.useState([]);
const [formMail, setFormMail] = React.useState("");
const [message, setMessage] = React.useState("");
const [formMessage, setFormMessage] = React.useState("");
const buttonStyle = {borderRadius: 30, height: 50, width: 220, margin: isMobile ? "15px auto 15px auto" : 20, fontSize: 18,}
const isCloud = const isCloud =
window.location.host === "localhost:3002" || window.location.host === "localhost:3002" ||
@@ -59,44 +56,6 @@ const CreatorGrid = props => {
const borderRadius = 3 const borderRadius = 3
window.title = "Shuffle | Workflows | Discover your use-case" window.title = "Shuffle | Workflows | Discover your use-case"
const submitContact = (email, message) => {
const data = {
"firstname": "",
"lastname": "",
"title": "",
"companyname": "",
"email": email,
"phone": "",
"message": message,
}
const errorMessage = "Something went wrong. Please contact frikky@shuffler.io directly."
fetch(globalUrl+"/api/v1/contact", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})
.then(response => response.json())
.then(response => {
if (response.success === true) {
setFormMessage(response.reason)
//toast("Thanks for submitting!")
} else {
setFormMessage(errorMessage)
}
setFormMail("")
setMessage("")
})
.catch(error => {
setFormMessage(errorMessage)
console.log(error)
});
}
// value={currentRefinement} // value={currentRefinement}
const SearchBox = ({currentRefinement, refine, isSearchStalled} ) => { const SearchBox = ({currentRefinement, refine, isSearchStalled} ) => {
var defaultSearch = "" var defaultSearch = ""
@@ -249,62 +208,8 @@ const CreatorGrid = props => {
<CustomHits hitsPerPage={100}/> <CustomHits hitsPerPage={100}/>
</InstantSearch> </InstantSearch>
{showSuggestion === true ? {showSuggestion === true ?
<div style={{maxWidth: isMobile ? "100%" : "60%", margin: "auto", paddingTop: 50, textAlign: "center",}}> <SearchContactForm globalUrl={globalUrl} isMobile={isMobile} tabName="creators" />
<Typography variant="h6" style={{color: "white", marginTop: 50,}}> : null
Can't find what you're looking for?
</Typography>
<div style={{flex: "1", display: "flex", flexDirection: "row"}}>
<TextField
required
style={{flex: "1", marginRight: "15px", backgroundColor: theme.palette.inputColor}}
InputProps={{
style:{
color: "#ffffff",
},
}}
color="primary"
fullWidth={true}
placeholder="Email (optional)"
type="email"
id="email-handler"
autoComplete="email"
margin="normal"
variant="outlined"
onChange={e => setFormMail(e.target.value)}
/>
<TextField
required
style={{flex: "1", backgroundColor: theme.palette.inputColor}}
InputProps={{
style:{
color: "#ffffff",
},
}}
color="primary"
fullWidth={true}
placeholder="What are we missing?"
type=""
id="standard-required"
margin="normal"
variant="outlined"
autoComplete="off"
onChange={e => setMessage(e.target.value)}
/>
</div>
<Button
variant="contained"
color="primary"
style={buttonStyle}
disabled={message.length === 0}
onClick={() => {
submitContact(formMail, message)
}}
>
Submit
</Button>
<Typography style={{color: "white"}} variant="body2">{formMessage}</Typography>
</div>
: null
} }
</div> </div>
) )
@@ -0,0 +1,61 @@
import React, { memo, useContext } from 'react';
import {
Button,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Typography,
} from '@mui/material';
import { getTheme } from '../theme.jsx';
import { Context } from '../context/ContextApi.jsx';
const DeleteConfirmDialog = memo(({ open, onClose, onConfirm, title, description, warningText }) => {
const { themeMode, brandColor } = useContext(Context);
const theme = getTheme(themeMode, brandColor);
return (
<Dialog
open={open}
onClose={onClose}
PaperProps={{
sx: {
borderRadius: theme?.palette?.DialogStyle?.borderRadius,
border: theme?.palette?.DialogStyle?.border,
fontFamily: theme?.typography?.fontFamily,
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
minWidth: 420,
'& .MuiDialogContent-root': { backgroundColor: theme?.palette?.DialogStyle?.backgroundColor },
'& .MuiDialogTitle-root': { backgroundColor: theme?.palette?.DialogStyle?.backgroundColor },
'& .MuiDialogActions-root': { backgroundColor: theme?.palette?.DialogStyle?.backgroundColor },
},
}}
>
<DialogTitle>
<Typography variant="h6">{title}</Typography>
</DialogTitle>
<DialogContent>
<Typography variant="body1">{description}</Typography>
{warningText && (
<Typography variant="body2" color="textSecondary" style={{ marginTop: 8 }}>
{warningText}
</Typography>
)}
</DialogContent>
<DialogActions sx={{ p: 2 }}>
<Button style={{ textTransform: 'none' }} onClick={onClose}>
Cancel
</Button>
<Button
variant="contained"
style={{ textTransform: 'none', backgroundColor: theme?.palette?.deleteColor, color: '#fff' }}
onClick={onConfirm}
>
Delete
</Button>
</DialogActions>
</Dialog>
);
});
export default DeleteConfirmDialog;
+2 -101
View File
@@ -3,11 +3,8 @@ import algoliasearch from 'algoliasearch';
import theme from '../theme.jsx'; import theme from '../theme.jsx';
import { InstantSearch, connectSearchBox, connectHits } from 'react-instantsearch-dom'; import { InstantSearch, connectSearchBox, connectHits } from 'react-instantsearch-dom';
import { import {
Grid,
Paper,
TextField, TextField,
Typography, Typography,
Button,
InputAdornment, InputAdornment,
Avatar, Avatar,
List, List,
@@ -16,6 +13,7 @@ import {
ListItemText, ListItemText,
} from '@mui/material'; } from '@mui/material';
import { Search as SearchIcon } from '@mui/icons-material'; import { Search as SearchIcon } from '@mui/icons-material';
import SearchContactForm from '../components/SearchContactForm.jsx';
import useDebouncedCallback from '../utils/useDebouncedCallback.jsx'; import useDebouncedCallback from '../utils/useDebouncedCallback.jsx';
@@ -23,52 +21,9 @@ const searchClient = algoliasearch("JNSS5CFDZZ", "1e5f29b1550939855de5915eac3bf5
const DiscordChat = props => { const DiscordChat = props => {
const { isMobile, globalUrl } = props const { isMobile, globalUrl } = props
const [value, setValue] = useState("");
const [formMail, setFormMail] = React.useState("");
const [message, setMessage] = React.useState("");
const [formMessage, setFormMessage] = React.useState("");
const buttonStyle = {borderRadius: 30, height: 50, width: 220, margin: isMobile ? "15px auto 15px auto" : 20, fontSize: 18,}
const borderRadius = 3 const borderRadius = 3
const submitContact = (email, message) => {
const data = {
"firstname": "",
"lastname": "",
"title": "",
"companyname": "",
"email": email,
"phone": "",
"message": message,
}
const errorMessage = "Something went wrong. Please contact frikky@shuffler.io directly."
fetch(globalUrl+"/api/v1/contact", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})
.then(response => response.json())
.then(response => {
if (response.success === true) {
setFormMessage(response.reason)
//toast("Thanks for submitting!")
} else {
setFormMessage(errorMessage)
}
setFormMail("")
setMessage("")
})
.catch(error => {
setFormMessage(errorMessage)
console.log(error)
});
}
const SearchBox = ({ currentRefinement, refine }) => { const SearchBox = ({ currentRefinement, refine }) => {
const [inputValue, setInputValue] = useState(""); const [inputValue, setInputValue] = useState("");
const debouncedRefine = useDebouncedCallback((value) => refine(value), 300); const debouncedRefine = useDebouncedCallback((value) => refine(value), 300);
@@ -168,61 +123,7 @@ const DiscordChat = props => {
<CustomHits /> <CustomHits />
</div> </div>
</InstantSearch> </InstantSearch>
<div style={{paddingTop: 0, maxWidth: isMobile ? "100%" : "60%", margin: "auto"}}> <SearchContactForm globalUrl={globalUrl} isMobile={isMobile} tabName="discord chats" />
<Typography variant="h6" style={{color: "white", marginTop: 50,}}>
Can't find what you're looking for?
</Typography>
<div style={{flex: "1", display: "flex", flexDirection: "row", textAlign: "center",}}>
<TextField
required
style={{flex: "1", marginRight: "15px", backgroundColor: theme.palette.inputColor}}
InputProps={{
style:{
color: "#ffffff",
},
}}
color="primary"
fullWidth={true}
placeholder="Email (optional)"
type="email"
id="email-handler"
autoComplete="email"
margin="normal"
variant="outlined"
onChange={e => setFormMail(e.target.value)}
/>
<TextField
required
style={{flex: "1", backgroundColor: theme.palette.inputColor}}
InputProps={{
style:{
color: "#ffffff",
},
}}
color="primary"
fullWidth={true}
placeholder="What are we missing?"
type=""
id="standard-required"
margin="normal"
variant="outlined"
autoComplete="off"
onChange={e => setMessage(e.target.value)}
/>
</div>
<Button
variant="contained"
color="primary"
style={buttonStyle}
disabled={message.length === 0}
onClick={() => {
submitContact(formMail, message)
}}
>
Submit
</Button>
<Typography style={{color: "white"}} variant="body2">{formMessage}</Typography>
</div>
{/* <span style={{position: "absolute", display: "flex", textAlign: "right", float: "right", right: 0, bottom: 120, }}> {/* <span style={{position: "absolute", display: "flex", textAlign: "right", float: "right", right: 0, bottom: 120, }}>
<Typography variant="body2" color="textSecondary" style={{}}> <Typography variant="body2" color="textSecondary" style={{}}>
Search by Search by
+13 -101
View File
@@ -4,6 +4,7 @@ import theme from '../theme.jsx';
import ReactGA from 'react-ga4'; import ReactGA from 'react-ga4';
import {Link} from 'react-router-dom'; import {Link} from 'react-router-dom';
import { removeQuery } from '../components/ScrollToTop.jsx'; import { removeQuery } from '../components/ScrollToTop.jsx';
import SearchContactForm from '../components/SearchContactForm.jsx';
import { Search as SearchIcon, CloudQueue as CloudQueueIcon, Code as CodeIcon, Close as CloseIcon, Folder as FolderIcon, LibraryBooks as LibraryBooksIcon } from '@mui/icons-material'; import { Search as SearchIcon, CloudQueue as CloudQueueIcon, Code as CodeIcon, Close as CloseIcon, Folder as FolderIcon, LibraryBooks as LibraryBooksIcon } from '@mui/icons-material';
import aa from 'search-insights' import aa from 'search-insights'
@@ -30,61 +31,19 @@ import { useDebouncedCallback } from "../utils/useDebouncedCallback.jsx";
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e") const searchClient = algoliasearch("JNSS5CFDZZ", "eb5fd80aa6ed5ab4730d836cff3ea283")
const DocsGrid = props => { const DocsGrid = props => {
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, userdata, } = props const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, userdata, } = props
const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows
const xs = parsedXs === undefined || parsedXs === null ? isMobile ? 6 : 2 : parsedXs const xs = parsedXs === undefined || parsedXs === null ? isMobile ? 6 : 2 : parsedXs
//const [apps, setApps] = React.useState([]); //const [apps, setApps] = React.useState([]);
//const [filteredApps, setFilteredApps] = React.useState([]); //const [filteredApps, setFilteredApps] = React.useState([]);
const [formMail, setFormMail] = React.useState("");
const [message, setMessage] = React.useState("");
const [formMessage, setFormMessage] = React.useState("");
const buttonStyle = {borderRadius: 30, height: 50, width: 220, margin: isMobile ? "15px auto 15px auto" : 20, fontSize: 18,}
const innerColor = "rgba(255,255,255,0.65)" const innerColor = "rgba(255,255,255,0.65)"
const borderRadius = 3 const borderRadius = 3
window.title = "Shuffle | Apps | Find and integrate any app" window.title = "Shuffle | Apps | Find and integrate any app"
const submitContact = (email, message) => {
const data = {
"firstname": "",
"lastname": "",
"title": "",
"companyname": "",
"email": email,
"phone": "",
"message": message,
}
const errorMessage = "Something went wrong. Please contact frikky@shuffler.io directly."
fetch(globalUrl+"/api/v1/contact", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})
.then(response => response.json())
.then(response => {
if (response.success === true) {
setFormMessage(response.reason)
//toast("Thanks for submitting!")
} else {
setFormMessage(errorMessage)
}
setFormMail("")
setMessage("")
})
.catch(error => {
setFormMessage(errorMessage)
console.log(error)
});
}
const SearchBox = ({currentRefinement, refine, isSearchStalled} ) => { const SearchBox = ({currentRefinement, refine, isSearchStalled} ) => {
var defaultSearch = "" var defaultSearch = ""
if (window !== undefined && window.location !== undefined && window.location.search !== undefined && window.location.search !== null) { if (window !== undefined && window.location !== undefined && window.location.search !== undefined && window.location.search !== null) {
@@ -124,8 +83,15 @@ const DocsGrid = props => {
placeholder="Search our Documentation..." placeholder="Search our Documentation..."
id="shuffle_search_field" id="shuffle_search_field"
onChange={(event) => { onChange={(event) => {
removeQuery("q") const value = event.currentTarget.value
debouncedRefine(event.currentTarget.value) debouncedRefine(value)
const urlSearchParams = new URLSearchParams(window.location.search)
if (value) {
urlSearchParams.set("q", value)
} else {
urlSearchParams.delete("q")
}
window.history.replaceState(null, "", value ? `?${urlSearchParams.toString()}` : window.location.pathname)
}} }}
onKeyDown={(event) => { onKeyDown={(event) => {
if(event.key === "Enter") { if(event.key === "Enter") {
@@ -291,62 +257,8 @@ const DocsGrid = props => {
<CustomHits hitsPerPage={5}/> <CustomHits hitsPerPage={5}/>
</InstantSearch> </InstantSearch>
{showSuggestion === true ? {showSuggestion === true ?
<div style={{paddingTop: 0, maxWidth: isMobile ? "100%" : "60%", margin: "auto"}}> <SearchContactForm globalUrl={globalUrl} isMobile={isMobile} tabName="docs" />
<Typography variant="h6" style={{color: "white", marginTop: 50,}}> : null
Can't find what you're looking for?
</Typography>
<div style={{flex: "1", display: "flex", flexDirection: "row", textAlign: "center",}}>
<TextField
required
style={{flex: "1", marginRight: "15px", backgroundColor: theme.palette.inputColor}}
InputProps={{
style:{
color: "#ffffff",
},
}}
color="primary"
fullWidth={true}
placeholder="Email (optional)"
type="email"
id="email-handler"
autoComplete="email"
margin="normal"
variant="outlined"
onChange={e => setFormMail(e.target.value)}
/>
<TextField
required
style={{flex: "1", backgroundColor: theme.palette.inputColor}}
InputProps={{
style:{
color: "#ffffff",
},
}}
color="primary"
fullWidth={true}
placeholder="What are we missing?"
type=""
id="standard-required"
margin="normal"
variant="outlined"
autoComplete="off"
onChange={e => setMessage(e.target.value)}
/>
</div>
<Button
variant="contained"
color="primary"
style={buttonStyle}
disabled={message.length === 0}
onClick={() => {
submitContact(formMail, message)
}}
>
Submit
</Button>
<Typography style={{color: "white"}} variant="body2">{formMessage}</Typography>
</div>
: null
} }
{/* <span style={{position: "absolute", display: "flex", textAlign: "right", float: "right", right: 0, bottom: 120, }}> {/* <span style={{position: "absolute", display: "flex", textAlign: "right", float: "right", right: 0, bottom: 120, }}>
+56 -10
View File
@@ -36,6 +36,7 @@ import {
ExpandLess as ExpandLessIcon, ExpandLess as ExpandLessIcon,
ExpandMore as ExpandMoreIcon, ExpandMore as ExpandMoreIcon,
Delete as DeleteIcon, Delete as DeleteIcon,
Computer as ComputerIcon,
} from "@mui/icons-material"; } from "@mui/icons-material";
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { Context } from '../context/ContextApi.jsx'; import { Context } from '../context/ContextApi.jsx';
@@ -49,6 +50,7 @@ const EnvironmentTab = memo((props) => {
const [modalUser, setModalUser] = React.useState({}); const [modalUser, setModalUser] = React.useState({});
const [loginInfo, setLoginInfo] = React.useState(""); const [loginInfo, setLoginInfo] = React.useState("");
const [modalOpen, setModalOpen] = React.useState(false); const [modalOpen, setModalOpen] = React.useState(false);
const [sensorGroup, setSensorGroup] = React.useState(false);
const [showLoader, setShowLoader] = useState(true) const [showLoader, setShowLoader] = useState(true)
const [commandController, setCommandController] = React.useState({ const [commandController, setCommandController] = React.useState({
pipelines: false, pipelines: false,
@@ -535,13 +537,36 @@ const EnvironmentTab = memo((props) => {
}, },
}, },
}} }}
style={{
}}
> >
<DialogTitle> <DialogTitle style={{display: "flex", }}>
<Typography variant='h5' color="textPrimary" >Add Location</Typography> <Typography variant='h5' color="textPrimary">Add Location</Typography>
<Button
style={{ marginLeft: 485, }}
variant={sensorGroup ? "contained" : "outlined"}
color={sensorGroup ? "primary" : "secondary"}
onClick={() => setSensorGroup(!sensorGroup)}
>
Sensor Group
</Button>
</DialogTitle> </DialogTitle>
<DialogContent> <DialogContent>
<div> <Typography variant='body1' color="textSecondary">
<Typography variant='body2' color="textPrimary">Location Name</Typography> {sensorGroup ?
'With "Sensor Groups" enabled, Runtime Locations allows you to run a lightweight log-collector and response agent onprem.'
:
'Runtime Locations are a way to run automation in Shuffle. By default, it runs in Docker/Kubernetes and allows you to run AI Agents and Workflows in your designated datacenter.'
}
</Typography>
<div style={{marginTop: 15, }}>
<Typography color="textPrimary">
{sensorGroup ?
"Sensor Group name"
:
"Location Name"
}
</Typography>
<TextField <TextField
color="primary" color="primary"
style={{ backgroundColor: theme.palette.textFieldStyle.backgroundColor,}} style={{ backgroundColor: theme.palette.textFieldStyle.backgroundColor,}}
@@ -555,13 +580,14 @@ const EnvironmentTab = memo((props) => {
}} }}
required required
fullWidth={true} fullWidth={true}
placeholder="datacenter froglantern" placeholder="automation location 3"
id="environment_name" id="environment_name"
margin="normal" margin="normal"
variant="outlined" variant="outlined"
onChange={(event) => onChange={(event) => {
changeModalData("environment", event.target.value) changeModalData("environment", event.target.value)
} setUpdate(Math.random())
}}
/> />
</div> </div>
{loginInfo} {loginInfo}
@@ -576,12 +602,13 @@ const EnvironmentTab = memo((props) => {
<Button <Button
variant="contained" variant="contained"
style={{ borderRadius: "2px", fontSize: 16, textTransform: "none", }} style={{ borderRadius: "2px", fontSize: 16, textTransform: "none", }}
disabled={modalUser.environment === undefined || modalUser.environment === ""}
onClick={() => { onClick={() => {
submitEnvironment(modalUser); // Assuming modalUser is available submitEnvironment(modalUser); // Assuming modalUser is available
}} }}
color="primary" color="primary"
> >
Submit Create {sensorGroup ? "Sensor Group" : "Location"}
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
@@ -956,7 +983,7 @@ const EnvironmentTab = memo((props) => {
<ListItem <ListItem
style={{ style={{
display: "grid", display: "grid",
gridTemplateColumns: "80px 80px 80px 150px 100px 80px 400px 100px", gridTemplateColumns: "80px 80px 70px 140px 100px 100px 400px 100px",
width: "100%", width: "100%",
minWidth: showLoader ? 800 : 0, minWidth: showLoader ? 800 : 0,
paddingBottom: 0, paddingBottom: 0,
@@ -1102,6 +1129,11 @@ const EnvironmentTab = memo((props) => {
> >
<ListItemText <ListItemText
primary={ primary={
environment?.sensor_group === true ?
<Tooltip title={`Sensor group for Shuffle Security monitors. Total: ${environment?.sensor_hosts?.length || 0}`}>
<ComputerIcon />
</Tooltip>
:
environment.run_type === "cloud" || environment.run_type === "cloud" ||
environment.name === "Cloud" ? ( environment.name === "Cloud" ? (
<Tooltip title="Cloud" placement="top"> <Tooltip title="Cloud" placement="top">
@@ -1275,6 +1307,9 @@ const EnvironmentTab = memo((props) => {
<Tooltip title={`Make a new environment to set up a Datalake node. Please contact ${supportEmail} if this is something you want to see on Cloud directly.`} placement="top"> <Tooltip title={`Make a new environment to set up a Datalake node. Please contact ${supportEmail} if this is something you want to see on Cloud directly.`} placement="top">
<CancelIcon style={{ color: "rgba(255,255,255,0.3)" }} /> <CancelIcon style={{ color: "rgba(255,255,255,0.3)" }} />
</Tooltip> </Tooltip>
:
environment?.sensor_group === true ?
"N/A"
: :
environment?.data_lake?.enabled && environment?.archived !== true ? ( environment?.data_lake?.enabled && environment?.archived !== true ? (
<a <a
@@ -1333,7 +1368,7 @@ const EnvironmentTab = memo((props) => {
/> />
<ListItemText <ListItemText
primary={environment.Type} primary={environment?.sensor_group === true ? "Sensor Group" : environment.Type}
primaryTypographyProps={{ primaryTypographyProps={{
style:{ style:{
minWidth: 50, minWidth: 50,
@@ -1520,6 +1555,16 @@ const EnvironmentTab = memo((props) => {
<Grid container justifyContent="center" style={{minWidth: 850, maxWidth: 850, }}> <Grid container justifyContent="center" style={{minWidth: 850, maxWidth: 850, }}>
<Grid item xs={12} sm={8} md={6}> <Grid item xs={12} sm={8} md={6}>
<div style={{minWidth: 750, maxWidth: 750, minHeight: 350, display: 'flex', justifyContent: "center", backgroundColor: "transparent", }}> <div style={{minWidth: 750, maxWidth: 750, minHeight: 350, display: 'flex', justifyContent: "center", backgroundColor: "transparent", }}>
{environment?.sensor_group === true ?
<div style={{ paddingTop: 50, paddingBottom: 100, }}>
<Typography variant="h6">
Sensor Group - Host controls available in <a href="https://security.shuffler.io/monitors" target="_blank" rel="noopener noreferrer" style={{textDecoration: "none", color: "#f85a3e",}}>Shuffle Security</a>
</Typography>
<Typography>
Total registered hosts: {environment?.sensor_hosts?.length || 0}.<br/>Host management and response actions is done in Shuffle Security. Click the link above to manage.
</Typography>
</div>
:
<div style={{ paddingTop: 50, paddingBottom: 100, }}> <div style={{ paddingTop: 50, paddingBottom: 100, }}>
<Typography variant="h6"> <Typography variant="h6">
Self-Hosted Orborus instance Self-Hosted Orborus instance
@@ -1703,6 +1748,7 @@ const EnvironmentTab = memo((props) => {
} }
</Typography> </Typography>
</div> </div>
}
</div> </div>
{currentEnvQueue.length === 0 ? null : {currentEnvQueue.length === 0 ? null :
<List style={{ minWidth: 700, maxWidth: 700, maxHeight: 300, overflowY: "auto", scrollbarColor: theme.palette.scrollbarColorTransparent, scrollbarWidth: 'thin', }}> <List style={{ minWidth: 700, maxWidth: 700, maxHeight: 300, overflowY: "auto", scrollbarColor: theme.palette.scrollbarColorTransparent, scrollbarWidth: 'thin', }}>
+80 -150
View File
@@ -29,6 +29,8 @@ import {
Menu, Menu,
Pagination, Pagination,
PaginationItem, PaginationItem,
Box,
InputAdornment,
} from "@mui/material"; } from "@mui/material";
import { DataGrid } from "@mui/x-data-grid"; import { DataGrid } from "@mui/x-data-grid";
@@ -45,9 +47,12 @@ import {
Clear as ClearIcon, Clear as ClearIcon,
Add as AddIcon, Add as AddIcon,
SelectAll, SelectAll,
Search as SearchIcon,
} from "@mui/icons-material"; } from "@mui/icons-material";
import Dropzone from "../components/Dropzone.jsx"; import Dropzone from "../components/Dropzone.jsx";
import SubOrgDistributionDialog from "./SubOrgDistributionDialog.jsx";
import DeleteConfirmDialog from "./DeleteConfirmDialog.jsx";
import ShuffleCodeEditor from "../components/ShuffleCodeEditor1.jsx"; import ShuffleCodeEditor from "../components/ShuffleCodeEditor1.jsx";
import {getTheme} from "../theme.jsx"; import {getTheme} from "../theme.jsx";
import { Context } from "../context/ContextApi.jsx"; import { Context } from "../context/ContextApi.jsx";
@@ -82,6 +87,10 @@ const Files = memo((props) => {
const [showDistributionPopup, setShowDistributionPopup] = useState(false) const [showDistributionPopup, setShowDistributionPopup] = useState(false)
const [selectedSubOrg, setSelectedSubOrg] = useState([]) const [selectedSubOrg, setSelectedSubOrg] = useState([])
const [fileIdSelectedForDistribution, setFileIdSelectedForDistribution] = useState("") const [fileIdSelectedForDistribution, setFileIdSelectedForDistribution] = useState("")
const [fileNameSelectedForDistribution, setFileNameSelectedForDistribution] = useState("")
const [distribOrgOrder, setDistribOrgOrder] = useState([])
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false)
const [deleteConfirmTarget, setDeleteConfirmTarget] = useState(null)
const [totalAmount, setTotalAmount] = useState(0); const [totalAmount, setTotalAmount] = useState(0);
const [page, setPage] = useState(0); const [page, setPage] = useState(0);
const [pageSize, setPageSize] = useState(50) const [pageSize, setPageSize] = useState(50)
@@ -326,7 +335,7 @@ const [filesLoaded, setFilesLoaded] = useState(false);
navigator.clipboard.writeText(file.id); navigator.clipboard.writeText(file.id);
document.execCommand("copy"); document.execCommand("copy");
toast(file.id + " copied to clipboard"); toast.info(file.id + " copied to clipboard");
}} }}
> >
<svg <svg
@@ -369,7 +378,8 @@ const [filesLoaded, setFilesLoaded] = useState(false);
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
deleteFile(file.id, true); setDeleteConfirmTarget({ id: file.id, filename: file.filename });
setDeleteConfirmOpen(true);
}} }}
> >
<svg <svg
@@ -441,6 +451,12 @@ const [filesLoaded, setFilesLoaded] = useState(false);
setSelectedSubOrg([]) setSelectedSubOrg([])
} }
setFileIdSelectedForDistribution(file.id) setFileIdSelectedForDistribution(file.id)
setFileNameSelectedForDistribution(file.filename)
setDistribOrgOrder(
(userdata?.orgs || [])
.filter(o => o.creator_org === userdata.active_org.id)
.map(o => o.id)
)
}} }}
/> />
</Tooltip> </Tooltip>
@@ -534,7 +550,7 @@ const [filesLoaded, setFilesLoaded] = useState(false);
}) })
.then((responseJson) => { .then((responseJson) => {
if (responseJson.success === true) { if (responseJson.success === true) {
toast("Successfully updated file"); toast.success("Successfully updated file");
} }
}) })
.catch((error) => { .catch((error) => {
@@ -848,125 +864,39 @@ const [filesLoaded, setFilesLoaded] = useState(false);
</Dialog> </Dialog>
: null : null
const handleSelectSubOrg = (id, action) => { const deleteConfirmDialog = (
if (action === "all") { <DeleteConfirmDialog
const childOrgs = userdata.orgs.filter( open={deleteConfirmOpen}
(data) => data.creator_org === userdata.active_org.id onClose={() => { setDeleteConfirmOpen(false); setDeleteConfirmTarget(null); }}
); onConfirm={() => {
setSelectedSubOrg((prev) => { if (deleteConfirmTarget?.bulk) {
if (prev.length === childOrgs.length) { const toDelete = [...selectedRows];
// If all child orgs are already selected, clear the selection const count = toDelete.length;
return []; setSelectedRows([]);
} else { toDelete.forEach(fileId => deleteFile(fileId, false));
// Otherwise, select all child org IDs setTimeout(() => {
return childOrgs.map((data) => data.id); getFiles(selectedCategory);
} toast.success('Deleted ' + count + ' file' + (count > 1 ? 's' : ''));
}); }, 3000);
} else if (action === "none") { } else {
setSelectedSubOrg([]); deleteFile(deleteConfirmTarget.id, true);
} else { }
setSelectedSubOrg((prev) => { setDeleteConfirmOpen(false);
if (prev.includes(id)) { setDeleteConfirmTarget(null);
return prev.filter((data) => data !== id); }}
} else { title={
return [...prev, id]; deleteConfirmTarget?.bulk
} ? 'Delete ' + selectedRows?.length + ' File' + (selectedRows?.length > 1 ? 's' : '') + '?'
}); : 'Delete File?'
} }
}; description={
deleteConfirmTarget?.bulk
const fileDistributionModal = showDistributionPopup ? ( ? <>Are you sure you want to delete <b>{selectedRows?.length} file{selectedRows?.length > 1 ? 's' : ''}</b>?</>
<Dialog : <>Are you sure you want to delete <b>{deleteConfirmTarget?.filename}</b>?</>
open={showDistributionPopup} }
onClose={() => setShowDistributionPopup(false)} warningText="This cannot be undone."
PaperProps={{ />
sx: { );
borderRadius: theme?.palette?.DialogStyle?.borderRadius,
border: theme?.palette?.DialogStyle?.border,
fontFamily: theme?.typography?.fontFamily,
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
zIndex: 1000,
minWidth: "600px",
minHeight: "320px",
overflow: "auto",
'& .MuiDialogContent-root': {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
},
'& .MuiDialogTitle-root': {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
},
'& .MuiDialogActions-root': {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
},
},
}}
>
<DialogTitle>
<Typography variant="h5" color="textPrimary" >
Select sub-org to distribute files
</Typography>
</DialogTitle>
<DialogContent style={{ color: "rgba(255,255,255,0.65)" }}>
<MenuItem value="none" onClick={()=> {handleSelectSubOrg(null, "none")}}>None</MenuItem>
<MenuItem value="all" onClick={()=> {handleSelectSubOrg(null, "all")}}>All</MenuItem>
{userdata.orgs.map((data, index) => {
if (data.creator_org !== userdata.active_org.id) {
return null;
}
const imagesize = 22;
const imageStyle = {
width: imagesize,
height: imagesize,
pointerEvents: "none",
marginRight: 10,
marginLeft: data.id === userdata.active_org.id ? 0 : 20,
};
const image = data.image === "" ? (
<img alt={data.name} src={theme.palette.defaultImage} style={imageStyle} />
) : (
<img alt={data.name} src={data.image} style={imageStyle} />
);
return (
<MenuItem
key={index}
value={data.id}
onClick={() => handleSelectSubOrg(data.id)}
style={{ display: "flex", alignItems: "center" }}
>
<Checkbox
checked={selectedSubOrg.includes(data.id)}
/>
{image}
<span style={{ marginLeft: 8 }}>{data.name}</span>
</MenuItem>
);
})}
<div style={{ display: "flex", marginTop: 20 }}>
<Button
style={{ borderRadius: "2px", textTransform: 'none', fontSize:16, color: theme.palette.primary.main }}
onClick={() => setShowDistributionPopup(false)}
>
Cancel
</Button>
<Button
variant="contained"
style={{ borderRadius: "2px", textTransform: 'none', fontSize:16, marginLeft: 10 }}
onClick={() => {
changeDistribution(fileIdSelectedForDistribution, selectedSubOrg);
}}
color="primary"
>
Submit
</Button>
</div>
</DialogContent>
</Dialog>
): null
const deleteFile = (fileId, showSinglDeleteToast) => { const deleteFile = (fileId, showSinglDeleteToast) => {
@@ -1295,7 +1225,17 @@ const [filesLoaded, setFilesLoaded] = useState(false);
}} }}
onDrop={uploadFile} onDrop={uploadFile}
> >
{fileDistributionModal} <SubOrgDistributionDialog
open={showDistributionPopup}
onClose={() => { setShowDistributionPopup(false); }}
title="Distribute File to Sub-Organizations"
extraInfo={fileNameSelectedForDistribution ? `Selected File: ${fileNameSelectedForDistribution}` : null}
orgs={distribOrgOrder.map(id => (userdata?.orgs || []).find(o => o.id === id)).filter(Boolean)}
selectedOrgIds={selectedSubOrg}
onSelectionChange={setSelectedSubOrg}
onSave={(ids) => { changeDistribution(fileIdSelectedForDistribution, ids); setShowDistributionPopup(false); }}
/>
{deleteConfirmDialog}
<div style={{width: "100%", minHeight: 1100, boxSizing: 'border-box', padding: "27px 10px 19px 27px", height:"100%", backgroundColor: theme.palette.platformColor,borderTopRightRadius: '8px', borderBottomRightRadius: 8, borderLeft: theme.palette.defaultBorder, }}> <div style={{width: "100%", minHeight: 1100, boxSizing: 'border-box', padding: "27px 10px 19px 27px", height:"100%", backgroundColor: theme.palette.platformColor,borderTopRightRadius: '8px', borderBottomRightRadius: 8, borderLeft: theme.palette.defaultBorder, }}>
<div style={{height: "100%", }}> <div style={{height: "100%", }}>
@@ -1559,17 +1499,20 @@ const [filesLoaded, setFilesLoaded] = useState(false);
autoFocus autoFocus
/>} />}
<ShuffleCodeEditor {openEditor === true && fileContent !== undefined && fileContent !== null && fileContent.length > 0 ?
isCloud={isCloud} <ShuffleCodeEditor
expansionModalOpen={openEditor} isCloud={isCloud}
setExpansionModalOpen={setOpenEditor} expansionModalOpen={openEditor}
setcodedata = {setFileContent} setExpansionModalOpen={setOpenEditor}
codedata={fileContent} setcodedata = {setFileContent}
isFileEditor = {true} codedata={fileContent}
key = {fileContent} //https://reactjs.org/docs/reconciliation.html#recursing-on-children isFileEditor = {true}
runUpdateText = {runUpdateText} key = {fileContent} //https://reactjs.org/docs/reconciliation.html#recursing-on-children
contentLoading = {contentLoading} runUpdateText = {runUpdateText}
/> contentLoading = {contentLoading}
/>
: null}
{isSelectedFiles?null: {isSelectedFiles?null:
<Divider <Divider
style={{ style={{
@@ -1672,22 +1615,9 @@ const [filesLoaded, setFilesLoaded] = useState(false);
toast("Please select files to delete"); toast("Please select files to delete");
return; return;
} }
setDeleteConfirmTarget({ bulk: true });
for (let i = 0; i < selectedRows.length; i++) { setDeleteConfirmOpen(true);
const fileIdToDelete = selectedRows[i]; }}
deleteFile(fileIdToDelete, false);
if (i === selectedRows.length - 1) {
setTimeout(() => {
setSelectedRows([]);
getFiles(selectedCategory);
toast.success(
`Deleted ${selectedRows.length} file${selectedRows.length === 1 ? "" : "s"}`
);
}, 2500);
}
}
}}
variant={"outlined"} variant={"outlined"}
color="secondary" color="secondary"
startIcon={ startIcon={
File diff suppressed because it is too large Load Diff
+210 -12
View File
@@ -52,6 +52,12 @@ const ShuffleLogo = "/images/Shuffle_logo.png";
const detectionIcon = "/icons/detection.svg"; const detectionIcon = "/icons/detection.svg";
const documentationIcon = "/icons/documentation.svg"; const documentationIcon = "/icons/documentation.svg";
const ExpandMoreAndLessIcon = "/icons/expandMoreIcon.svg"; const ExpandMoreAndLessIcon = "/icons/expandMoreIcon.svg";
const shuffleSecurityLogo = (
<svg width="30" height="30" viewBox="0 0 56 56" fill="none">
<path d="M14 14h28v6H20v16h16v-10h-8v-6h14v22H14V14z" fill="#FF6600" />
</svg>
);
const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, SHUFFLE_VERSION }) => { const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, SHUFFLE_VERSION }) => {
@@ -87,6 +93,17 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, SHUFFLE_V
); );
const [activeOrgData, setActiveOrgData] = useState(null); const [activeOrgData, setActiveOrgData] = useState(null);
const [isProdStatusOn, setIsProdStatusOn] = useState(false); const [isProdStatusOn, setIsProdStatusOn] = useState(false);
const [productAnchorEl, setProductAnchorEl] = useState(null);
const handleProductClick = (event) => {
event.preventDefault();
setProductAnchorEl((prev) => (prev ? null : event.currentTarget));
};
const handleProductClose = () => {
setProductAnchorEl(null);
};
const userOrgs = React.useMemo(() => { const userOrgs = React.useMemo(() => {
return orgOptions.find((option) => option.name === selectedOrg); return orgOptions.find((option) => option.name === selectedOrg);
}, [selectedOrg, orgOptions]); }, [selectedOrg, orgOptions]);
@@ -839,6 +856,8 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, SHUFFLE_V
regiontag = "EU-2"; regiontag = "EU-2";
} else if (regiontag === "ca"){ } else if (regiontag === "ca"){
regiontag = "CA"; regiontag = "CA";
} else if (regiontag === "uk"){
regiontag = "UK";
} }
} }
} }
@@ -1022,7 +1041,7 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, SHUFFLE_V
}} }}
> >
<Tooltip <Tooltip
title="Go to Home" title={showPartnerLogo ? "Go to Home" : "Switch Product"}
placement="top" placement="top"
arrow arrow
componentsProps={{ componentsProps={{
@@ -1042,16 +1061,194 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, SHUFFLE_V
} }
}} }}
> >
<Link to={isCloud && !showPartnerLogo ? "/" : "/workflows"}> <Box
<img onClick={showPartnerLogo ? undefined : handleProductClick}
src={ sx={{
showPartnerLogo ? userdata?.active_org?.image : ShuffleLogo cursor: showPartnerLogo ? "default" : "pointer",
} display: "flex",
alt="Shuffle Logo" alignItems: "center",
style={{ width: showPartnerLogo ? 30 : 24, height: showPartnerLogo ? 30 : 24 }} justifyContent: "center",
/> gap: 0.5
</Link> }}
</Tooltip> >
<Link
to={showPartnerLogo ? (isCloud ? "/" : "/workflows") : "#"}
style={{ display: "flex", alignItems: "center" }}
onClick={(e) => {
!showPartnerLogo && e.preventDefault();
}}
>
<img
src={showPartnerLogo ? userdata?.active_org?.image : ShuffleLogo}
alt="Shuffle Logo"
style={{
width: showPartnerLogo ? 30 : 26,
height: showPartnerLogo ? 30 : 26,
}}
/>
{!showPartnerLogo && expandLeftNav && (
<ExpandMoreIcon
sx={{
fontSize: 16,
color: themeMode === "dark" ? lightText : darkText,
opacity: 1,
ml: 0.5,
transform: productAnchorEl ? "rotate(180deg)" : "rotate(0deg)",
transition: "transform 0.2s ease",
}}
/>
)}
</Link>
</Box>
</Tooltip>
<Menu
anchorEl={productAnchorEl}
open={Boolean(productAnchorEl)}
onClose={handleProductClose}
disableScrollLock={true}
anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
transformOrigin={{ vertical: "top", horizontal: "left" }}
sx={{
zIndex: 1000020,
}}
PaperProps={{
sx: {
mt: 1,
ml: -1,
backgroundColor: themeMode === "dark" ? "#212121 " : "#FFFFFF",
backgroundImage: "none",
color: theme.palette.text.primary,
borderRadius: "12px",
padding: "4px",
minWidth: "240px",
border: `1px solid ${
themeMode === "dark" ? "#333333" : "#E0E0E0"
}`,
boxShadow: "0px 8px 24px rgba(0, 0, 0, 0.4)",
"& .MuiList-root": {
backgroundColor: "transparent",
padding: "4px",
display: "flex",
flexDirection: "column",
gap: "4px",
},
},
}}
>
<MenuItem
onClick={() => {
if (isCloud) {
//ReactGA.event({
// category: "sidebar",
// action: "click_shuffle_security",
// label: "",
//})
window.location.href = "https://security.shuffler.io/incidents?utm_source=shuffler_sidebar";
} else {
const { protocol, hostname } = window.location;
var newPort = 3002;
if (protocol === "https") {
newPort = 3444
}
const newUrl = `${protocol}//${hostname}:${newPort}/incidents`;
window.location.href = newUrl;
}
}}
sx={{
borderRadius: "8px",
padding: "10px 12px",
border: "1px solid transparent",
"&:hover": {
backgroundColor: themeMode === "dark" ? "#2C2C2C" : "#F5F5F5",
},
display: "flex",
gap: "12px",
alignItems: "center",
}}
>
<Box
sx={{
width: 28,
height: 28,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
{shuffleSecurityLogo}
</Box>
<Typography
sx={{
fontSize: "15px",
fontWeight: 500,
color: themeMode === "dark" ? "#E0E0E0" : "#333333",
fontFamily: "Inter, Roboto, sans-serif",
}}
>
<span style={{ color: "#f26402", fontWeight: 600 }}>Shuffle</span>{" "}
Security
</Typography>
</MenuItem>
<MenuItem
onClick={() => {
handleProductClose();
window.location.href =
isCloud && !showPartnerLogo ? "/" : "/workflows";
}}
sx={{
borderRadius: "8px",
padding: "10px 12px",
border:
themeMode === "dark"
? "1px solid rgba(242, 100, 2, 0.3)"
: "1px solid rgba(242, 100, 2, 0.2)",
backgroundColor:
themeMode === "dark"
? "rgba(242, 100, 2, 0.05)"
: "rgba(242, 100, 2, 0.02)",
"&:hover": {
backgroundColor:
themeMode === "dark"
? "rgba(242, 100, 2, 0.1)"
: "rgba(242, 100, 2, 0.06)",
},
display: "flex",
gap: "12px",
alignItems: "center",
}}
>
<Box
sx={{
width: 28,
height: 28,
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<img
src={ShuffleLogo}
alt="Shuffle"
style={{ width: 20, height: 20 }}
/>
</Box>
<Typography
sx={{
fontSize: "15px",
fontWeight: 500,
color: themeMode === "dark" ? "#E0E0E0" : "#333333",
fontFamily: "Inter, Roboto, sans-serif",
}}
>
<span style={{ color: "#f26402", fontWeight: 600 }}>Shuffle</span>{" "}
Core
</Typography>
</MenuItem>
</Menu>
{ {
!isCloud && expandLeftNav && ( !isCloud && expandLeftNav && (
<Typography variant="body2" style={{fontSize: 16, color: themeMode === "dark" ? lightText : darkText, transition: "opacity 0.3s ease", fontWeight: 600, marginTop: -5, marginLeft: 3}}> <Typography variant="body2" style={{fontSize: 16, color: themeMode === "dark" ? lightText : darkText, transition: "opacity 0.3s ease", fontWeight: 600, marginTop: -5, marginLeft: 3}}>
@@ -1747,7 +1944,8 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, SHUFFLE_V
<span style={{ display: "inline-block", width: "100%" }}> <span style={{ display: "inline-block", width: "100%" }}>
<Button <Button
component={Link} component={Link}
to="/partners" to={isCloud ? "/partners" : "https://shuffler.io/partners"}
target={isCloud ? "_self" : "_blank"}
onClick={(event) => { onClick={(event) => {
setCurrentOpenTab("partners"); setCurrentOpenTab("partners");
localStorage.setItem("lastTabOpenByUser", "partners"); localStorage.setItem("lastTabOpenByUser", "partners");
+106 -51
View File
@@ -330,7 +330,7 @@ const LicencePopup = (props) => {
body: JSON.stringify({ body: JSON.stringify({
org_id: selectedOrganization.id, org_id: selectedOrganization.id,
editing: "subscription_update", editing: "subscription_update",
subscription_index: 0, subscription_index: subscription.id,
subscription: subscription, subscription: subscription,
}), }),
mode: "cors", mode: "cors",
@@ -554,7 +554,7 @@ const LicencePopup = (props) => {
const payload = { const payload = {
org_id: selectedOrganization.id, org_id: selectedOrganization.id,
editing: "subscription_update", editing: "subscription_update",
subscription_index: 0, subscription_index: subscription.id,
subscription: { subscription: {
...form, ...form,
// Ensure backend gets array of features // Ensure backend gets array of features
@@ -696,6 +696,15 @@ const LicencePopup = (props) => {
helperText={errors.amount || "0 for Free"} helperText={errors.amount || "0 for Free"}
/> />
<TextField
label="Stripe Sub ID"
value={form.reference || ""}
onChange={(e) => setForm({ ...form, reference: e.target.value })}
fullWidth
placeholder="sub_1234567890abcdef"
helperText="Stripe subscription reference ID"
/>
<TextField <TextField
label="Start date" label="Start date"
type="date" type="date"
@@ -1042,6 +1051,17 @@ const LicencePopup = (props) => {
: localSub?.currency + localSub?.amount : localSub?.currency + localSub?.amount
: "Free"; : "Free";
const calculateAppRunsFromPrice = (amount) => {
const price = parseInt(amount) || 0;
if (price === 0) return 2000; // Free plan
// Calculate Stripe quantity from price ($32 per unit)
const stripeQuantity = Math.max(1, Math.round(price / 32));
// Backend logic: (quantity * 10000) + 2000
return (stripeQuantity * 10000) + 2000;
};
if (typeof window === "undefined" || window.location === undefined) { if (typeof window === "undefined" || window.location === undefined) {
return null; return null;
} }
@@ -1252,7 +1272,7 @@ const LicencePopup = (props) => {
</IconButton> </IconButton>
</Tooltip> </Tooltip>
)} )}
{isPaidPlan ? ( {(isPaidPlan || localSub?.amount === "0") ? (
<div <div
style={{ style={{
display: "flex", display: "flex",
@@ -1336,22 +1356,38 @@ const LicencePopup = (props) => {
) : null} ) : null}
{localSub.cancellationdate !== 0 ? ( {localSub.cancellationdate !== 0 ? (
<Typography <Typography
variant="caption" variant="caption"
color="textSecondary" color="textSecondary"
style={{ marginTop: 2 }} style={{ marginTop: 2 }}
> >
{`Cancelled on ${new Date( {`Cancelled on ${new Date(
(localSub.cancellationdate || localSub.CancellationDate) * (localSub.cancellationdate || localSub.CancellationDate) *
1000 1000
).toLocaleDateString(undefined, { ).toLocaleDateString(undefined, {
day: "2-digit", day: "2-digit",
month: "short", month: "short",
year: "numeric", year: "numeric",
})}`} })}`}
</Typography> </Typography>
) : null} ) : null}
{localSub.amount !== "0" && (
<Typography
variant="caption"
color="textSecondary"
style={{ marginTop: 2 }}
>
{`Purchased on ${new Date(
(localSub.startdate || localSub.Startdate) * 1000
).toLocaleDateString(undefined, {
day: "2-digit",
month: "short",
year: "numeric",
})}`}
</Typography>
)}
<Divider <Divider
style={{ style={{
marginTop: 12, marginTop: 12,
@@ -1364,39 +1400,50 @@ const LicencePopup = (props) => {
(isCloud || (!isCloud && selectedOrganization.cloud_sync)) && ( (isCloud || (!isCloud && selectedOrganization.cloud_sync)) && (
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}> <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
<Typography variant="body2" color="textSecondary" style={{}}> <Typography variant="body2" color="textSecondary" style={{}}>
App Runs {localSub?.active ? "App Runs" : "App Runs included in plan"}
</Typography> </Typography>
<div {localSub?.active ? (
style={{ // Active plan - show current usage and progress bar
display: "flex", <div
alignItems: "flex-start", style={{
flexDirection: "column", display: "flex",
gap: 10, alignItems: "flex-start",
}} flexDirection: "column",
> gap: 10,
}}
>
<Typography
variant="body1"
style={{ minWidth: 140, fontWeight: 600 }}
>
{usedAppRuns?.toLocaleString?.() || usedAppRuns} of{" "}
{appRunsLimit?.toLocaleString?.() || appRunsLimit}
</Typography>
<Box sx={{ width: "100%" }}>
<LinearProgress
variant="determinate"
value={appRunsPct}
sx={{
height: 6,
borderRadius: 6,
backgroundColor: "#3a3a3a",
"& .MuiLinearProgress-bar": {
backgroundColor: "#ff8544",
borderRadius: 6,
},
}}
/>
</Box>
</div>
) : (
// Inactive plan - show only the plan's app runs capacity
<Typography <Typography
variant="body1" variant="body1"
style={{ minWidth: 140, fontWeight: 600 }} style={{ minWidth: 140, fontWeight: 600, fontSize: 18 }}
> >
{usedAppRuns?.toLocaleString?.() || usedAppRuns} of{" "} {calculateAppRunsFromPrice(localSub.amount)?.toLocaleString?.() || calculateAppRunsFromPrice(localSub.amount)}
{appRunsLimit?.toLocaleString?.() || appRunsLimit}
</Typography> </Typography>
<Box sx={{ width: "100%" }}> )}
<LinearProgress
variant="determinate"
value={appRunsPct}
sx={{
height: 6,
borderRadius: 6,
backgroundColor: "#3a3a3a",
"& .MuiLinearProgress-bar": {
backgroundColor: "#ff8544",
borderRadius: 6,
},
}}
/>
</Box>
</div>
</div> </div>
) )
} }
@@ -1480,7 +1527,6 @@ const LicencePopup = (props) => {
}} }}
> >
{isCloud && {isCloud &&
localSub.name.toLowerCase().includes("scale") &&
localSub?.reference && localSub?.reference &&
localSub.reference.length > 0 ? ( localSub.reference.length > 0 ? (
<Button <Button
@@ -1490,8 +1536,11 @@ const LicencePopup = (props) => {
onClick={() => { onClick={() => {
const url = `${globalUrl}/api/v1/orgs/${selectedOrganization.id}/manage_subscription`; const url = `${globalUrl}/api/v1/orgs/${selectedOrganization.id}/manage_subscription`;
fetch(url, { fetch(url, {
method: "GET", method: "POST",
credentials: "include", credentials: "include",
body: JSON.stringify({
subscription_id: localSub.reference
}),
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
}) })
.then((r) => r.json()) .then((r) => r.json())
@@ -1575,8 +1624,8 @@ const LicencePopup = (props) => {
display: "flex", display: "flex",
}} }}
> >
<Grid item maxWidth={licensePopup ? 400 : 450}> <Grid item maxWidth={licensePopup ? 400 : 800} style={{ display: "flex", flexDirection: "row", gap: 16, flexWrap: "wrap" }}>
{isLoading ? ( {isLoading ? (
<SubscriptionSkeleton /> <SubscriptionSkeleton />
) : ( ) : (
<> <>
@@ -1584,8 +1633,14 @@ const LicencePopup = (props) => {
selectedOrganization.subscriptions !== null && selectedOrganization.subscriptions !== null &&
selectedOrganization.subscriptions.length > 0 selectedOrganization.subscriptions.length > 0
? (selectedOrganization.subscriptions || []) ? (selectedOrganization.subscriptions || [])
.slice() .slice()
.map((sub, index) => { .sort((a, b) => {
// Active subscriptions first, then inactive
if (a.active && !b.active) return -1;
if (!a.active && b.active) return 1;
return 0;
})
.map((sub, index) => {
return ( return (
<SubscriptionObject <SubscriptionObject
key={sub.id || index} key={sub.id || index}
+74 -44
View File
@@ -3,6 +3,7 @@ import { Context } from "../context/ContextApi.jsx";
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { useParams, useNavigate, Link } from "react-router-dom"; import { useParams, useNavigate, Link } from "react-router-dom";
import { getTheme } from '../theme.jsx'; import { getTheme } from '../theme.jsx';
import ReactGA from 'react-ga4';
//import { useAlert //import { useAlert
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
@@ -513,57 +514,71 @@ const AuthenticationOauth2 = (props) => {
// Admin consent // Admin consent
//const url = `https://accounts.zoho.com/oauth/v2/auth?response_type=code&client_id=${client_id}&scope=AaaServer.profile.Read&redirect_uri=${redirectUri}&prompt=consent` //const url = `https://accounts.zoho.com/oauth/v2/auth?response_type=code&client_id=${client_id}&scope=AaaServer.profile.Read&redirect_uri=${redirectUri}&prompt=consent`
console.log("URL: ", url)
try { try {
var newwin = window.open(url, "", "width=582,height=700"); var newwin = window.open(url, "", "width=582,height=700");
//console.log(newwin)
var open = true; // This happens if automatic clicks occurr without the user, which is
const timer = setInterval(() => { // disallowed in most cases
if (newwin.closed) { if (newwin === null || newwin === undefined) {
console.log("Closing?") setButtonClicked(false);
if (setFinalized !== undefined) {
setButtonClicked(false); setFinalized(true)
clearInterval(timer);
//alert('"Secure Payment" window closed!');
if (getAppAuthentication !== undefined) {
// This should be orgId, not action Id as to load auth properly
if (workflow !== undefined && workflow !== null && workflow.org_id !== undefined && workflow.org_id !== null && workflow.org_id.length > 0) {
getAppAuthentication(true, true, true, workflow.org_id)
} else {
getAppAuthentication(true, true, true)
} }
}
toast.info("Authentication window closed") } else {
var open = true;
const timer = setInterval(() => {
if (newwin.closed) {
console.log("Closing?")
// This is more a guess than anything setButtonClicked(false);
// Should be handled in getAppAuthentication() clearInterval(timer);
// in the parent component to make it accurate, //alert('"Secure Payment" window closed!');
// seeing as we don't know what the parent component
// wants to happen
if (setFinalized !== undefined) {
setFinalized(true)
}
} else {
//console.log("Not closed")
}
}, 1000);
//do {
// setTimeout(() => {
// console.log(newwin)
// console.log("CLOSED", newwin.closed)
// if (newwin.closed) {
// open = false if (getAppAuthentication !== undefined) {
// } // This should be orgId, not action Id as to load auth properly
// }, 1000) if (workflow !== undefined && workflow !== null && workflow.org_id !== undefined && workflow.org_id !== null && workflow.org_id.length > 0) {
//} getAppAuthentication(true, true, true, workflow.org_id)
//while(open === true) } else {
} catch (e) { getAppAuthentication(true, true, true)
toast("Failed authentication - probably bad credentials. Try again") }
}
setButtonClicked(false); toast.success("Authentication finished. You may close this window.",{
duration: 60,
})
// This is more a guess than anything
// Should be handled in getAppAuthentication()
// in the parent component to make it accurate,
// seeing as we don't know what the parent component
// wants to happen
if (setFinalized !== undefined) {
setFinalized(true)
}
} else {
//console.log("Not closed")
}
}, 2000);
//do {
// setTimeout(() => {
// console.log(newwin)
// console.log("CLOSED", newwin.closed)
// if (newwin.closed) {
// open = false
// }
// }, 1000)
//}
//while(open === true)
}
} catch (e) {
toast("Failed authentication - probably bad credentials. Try again: " + e)
setButtonClicked(false);
} }
return; return;
@@ -714,6 +729,13 @@ const AuthenticationOauth2 = (props) => {
onClick={() => { onClick={() => {
// Hardcode some stuff? // Hardcode some stuff?
// This could prolly be added to the app itself with a "default" client ID // This could prolly be added to the app itself with a "default" client ID
if (isCloud) {
ReactGA.event({
category: "Integration",
action: "Authenticate",
label: `${selectedApp?.name} - One click`,
})
}
startOauth2Request() startOauth2Request()
}} }}
color="primary" color="primary"
@@ -1079,6 +1101,14 @@ const AuthenticationOauth2 = (props) => {
"autoClose": 1500, "autoClose": 1500,
}) })
if (isCloud) {
ReactGA.event({
category: "Integration",
action: "Authenticate",
label: `${selectedApp?.name} - Oauth2 manual`,
})
}
handleOauth2Request(clientId, clientSecret, oauthUrl, selectedScopes, undefined, true); handleOauth2Request(clientId, clientSecret, oauthUrl, selectedScopes, undefined, true);
}} }}
color="primary" color="primary"
@@ -710,7 +710,8 @@ const OrgHeaderexpandedNew = (props) => {
/> />
</span> </span>
</Grid> </Grid>
{!selectedOrganization || selectedOrganization?.creator_org === undefined || selectedOrganization?.creator_org || null || selectedOrganization?.creator_org?.length > 0 ? null :
{!userdata?.support === true && (!selectedOrganization || selectedOrganization?.creator_org === undefined || selectedOrganization?.creator_org === null || selectedOrganization?.creator_org?.length > 0) ? null :
<CloudSyncTab <CloudSyncTab
globalUrl={globalUrl} globalUrl={globalUrl}
userdata={userdata} userdata={userdata}
@@ -1100,6 +1101,9 @@ const RegionChangeModal = memo(({ selectedOrganization, setSelectedRegion, userd
} else if (regiontag === "au") { } else if (regiontag === "au") {
regiontag = "AUS"; regiontag = "AUS";
regionCode = "au" regionCode = "au"
} else if (regiontag === "uk") {
regiontag = "UK";
regionCode = "gb"
} }
} }
File diff suppressed because it is too large Load Diff
@@ -846,6 +846,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
setIsLoading(true); setIsLoading(true);
if(!isCloud || !userdata?.active_org?.is_partner) { if(!isCloud || !userdata?.active_org?.is_partner) {
// If the user is not a partner or if it's not a cloud environment do not make api call :) // If the user is not a partner or if it's not a cloud environment do not make api call :)
setIsLoading(false);
return; return;
} }
// Load usecase data from API // Load usecase data from API
@@ -1087,6 +1087,9 @@ const RuntimeDebugger = (props) => {
options={[{ options={[{
"name": "Agent Runs", "name": "Agent Runs",
"id": "AGENT", "id": "AGENT",
},{
"name": "Sensor Actions",
"id": "SENSOR_ACTION",
}].concat(workflows)} }].concat(workflows)}
fullWidth fullWidth
style={{ style={{
@@ -0,0 +1,121 @@
import React, { useState } from "react";
import theme from "../theme.jsx";
import { TextField, Typography, Button } from "@mui/material";
const SearchContactForm = ({ globalUrl, isMobile, tabName }) => {
const [formMail, setFormMail] = useState("");
const [message, setMessage] = useState("");
const [formMessage, setFormMessage] = useState("");
const submitContact = (email, message) => {
const data = {
firstname: "",
lastname: "",
title: "",
companyname: "",
email: email,
phone: "",
message: message,
};
const errorMessage =
"Something went wrong. Please contact frikky@shuffler.io directly.";
fetch(globalUrl + "/api/v1/contact", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(data),
})
.then((response) => response.json())
.then((response) => {
setFormMessage(
response?.success === true ? response.reason : errorMessage
);
setFormMail("");
setMessage("");
})
.catch(() => {
setFormMessage(errorMessage);
});
};
return (
<div
style={{
paddingTop: 0,
maxWidth: isMobile ? "100%" : "60%",
margin: "auto",
textAlign: "center",
}}
>
<Typography variant="h6" style={{ color: "white", marginTop: 50 }}>
Can't find what you're looking for?
</Typography>
<div
style={{
flex: "1",
display: "flex",
flexDirection: "row",
textAlign: "center",
}}
>
<TextField
required
style={{
flex: "1",
marginRight: 15,
backgroundColor: theme.palette.inputColor,
}}
InputProps={{ style: { color: "#ffffff" } }}
color="primary"
fullWidth={true}
placeholder="Email (optional)"
type="email"
id="email-handler"
autoComplete="email"
margin="normal"
variant="outlined"
value={formMail}
onChange={(e) => setFormMail(e.target.value)}
/>
<TextField
required
style={{ flex: "1", backgroundColor: theme.palette.inputColor }}
InputProps={{ style: { color: "#ffffff" } }}
color="primary"
fullWidth={true}
placeholder={tabName ? `What ${tabName} do you want to see?` : "What are we missing?"}
type=""
id="standard-required"
margin="normal"
variant="outlined"
autoComplete="off"
value={message}
onChange={(e) => setMessage(e.target.value)}
/>
</div>
<Button
variant="contained"
color="primary"
style={{
borderRadius: 30,
height: 50,
width: 220,
margin: isMobile ? "15px auto 15px auto" : 20,
fontSize: 18,
}}
disabled={message.length === 0}
onClick={() => submitContact(formMail, message)}
>
Submit
</Button>
<Typography style={{ color: "white" }} variant="body2">
{formMessage}
</Typography>
</div>
);
};
export default SearchContactForm;
+1 -1
View File
@@ -47,7 +47,7 @@ const chipStyle = {
backgroundColor: "#3d3f43", height: 30, marginRight: 5, paddingLeft: 5, paddingRight: 5, height: 28, cursor: "pointer", borderColor: "#3d3f43", color: "white", backgroundColor: "#3d3f43", height: 30, marginRight: 5, paddingLeft: 5, paddingRight: 5, height: 28, cursor: "pointer", borderColor: "#3d3f43", color: "white",
} }
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e") const searchClient = algoliasearch("JNSS5CFDZZ", "33e4e3564f4f060e96e0531957bed552")
const SearchData = props => { const SearchData = props => {
const { serverside, globalUrl, userdata } = props const { serverside, globalUrl, userdata } = props
let navigate = useNavigate(); let navigate = useNavigate();
-1
View File
@@ -38,7 +38,6 @@ import aa from 'search-insights'
import { InstantSearch, Configure, connectSearchBox, connectHits, Index } from 'react-instantsearch-dom'; import { InstantSearch, Configure, connectSearchBox, connectHits, Index } from 'react-instantsearch-dom';
//import { InstantSearch, SearchBox, Hits, connectSearchBox, connectHits, Index } from 'react-instantsearch-dom'; //import { InstantSearch, SearchBox, Hits, connectSearchBox, connectHits, Index } from 'react-instantsearch-dom';
import { HotKeys } from 'react-hotkeys';
// https://www.algolia.com/doc/api-reference/widgets/search-box/react/ // https://www.algolia.com/doc/api-reference/widgets/search-box/react/
const chipStyle = { const chipStyle = {
backgroundColor: "#3d3f43", height: 30, marginRight: 5, paddingLeft: 5, paddingRight: 5, height: 28, cursor: "pointer", borderColor: "#3d3f43", color: "white", backgroundColor: "#3d3f43", height: 30, marginRight: 5, paddingLeft: 5, paddingRight: 5, height: 28, cursor: "pointer", borderColor: "#3d3f43", color: "white",
+31 -11
View File
@@ -61,8 +61,6 @@ import PaperComponent from "../components/PaperComponent.jsx";
import { padding, textAlign } from '@mui/system'; import { padding, textAlign } from '@mui/system';
import data from '../frameworkStyle.jsx'; import data from '../frameworkStyle.jsx';
import { useNavigate, Link, useParams, useSearchParams } from "react-router-dom"; import { useNavigate, Link, useParams, useSearchParams } from "react-router-dom";
import { tags as t } from '@lezer/highlight';
import AceEditor from "react-ace"; import AceEditor from "react-ace";
import ace from "ace-builds"; import ace from "ace-builds";
@@ -149,7 +147,10 @@ const CodeEditor = (props) => {
// Auto-indent JSON-like content (with safety hehe) // Auto-indent JSON-like content (with safety hehe)
const autoIndentContent = React.useCallback((content) => { const autoIndentContent = React.useCallback((content) => {
return content if (!isFileEditor) {
console.log("Autoindent disabled")
return content
}
// Safety checks :) // Safety checks :)
if (!content || typeof content !== 'string' || content.trim().length === 0) { if (!content || typeof content !== 'string' || content.trim().length === 0) {
@@ -238,6 +239,24 @@ const CodeEditor = (props) => {
expectedOutput(localcodedata) expectedOutput(localcodedata)
}, [localcodedata]) }, [localcodedata])
useEffect(() => {
if (!isFileEditor) {
return
}
if (codedata === undefined || codedata === null || typeof codedata !== 'string') {
return
}
const indentedContent = autoIndentContent(codedata);
if (indentedContent !== undefined && indentedContent !== null) {
console.log("SETTING: ", indentedContent)
setlocalcodedata(indentedContent);
} else {
console.log("INDENT FAILED")
}
}, [])
// Auto-indent when codedata prop changes // Auto-indent when codedata prop changes
useEffect(() => { useEffect(() => {
if (codedata && codedata !== localcodedata && typeof codedata === 'string') { if (codedata && codedata !== localcodedata && typeof codedata === 'string') {
@@ -1597,7 +1616,7 @@ const CodeEditor = (props) => {
if (e.srcElement.className === "ace_content") { if (e.srcElement.className === "ace_content") {
console.log("DRAG STOP IN CONTENT!", e.srcElement.className) console.log("DRAG STOP IN CONTENT!", e.srcElement.className)
let usedposition = e.offsetY const usedposition = e.offsetY
if (usedposition === undefined || usedposition === null) { if (usedposition === undefined || usedposition === null) {
toast.info(`Error: LayerY is undefined or null. Please contact ${supportEmail}`) toast.info(`Error: LayerY is undefined or null. Please contact ${supportEmail}`)
return return
@@ -1784,8 +1803,8 @@ const CodeEditor = (props) => {
// zIndex: 12501, // zIndex: 12501,
pointerEvents: "auto", pointerEvents: "auto",
color: theme.palette.DialogStyle.color, color: theme.palette.DialogStyle.color,
minWidth: isMobile || isWorkflowEditor || fullScreenModeEnabled ? "100%" : isFileEditor ? "650px" : "80%", minWidth: isMobile || isWorkflowEditor || fullScreenModeEnabled ? "100%" : isFileEditor ? 800 : "80%",
maxWidth: isMobile || isWorkflowEditor || fullScreenModeEnabled ? "100%" : isFileEditor ? "650px" : "1100px", maxWidth: isMobile || isWorkflowEditor || fullScreenModeEnabled ? "100%" : isFileEditor ? 800 : "1100px",
minHeight: isMobile || isWorkflowEditor || fullScreenModeEnabled ? "100%" : "auto", minHeight: isMobile || isWorkflowEditor || fullScreenModeEnabled ? "100%" : "auto",
maxHeight: isMobile || isWorkflowEditor || fullScreenModeEnabled ? "100%" : "700px", maxHeight: isMobile || isWorkflowEditor || fullScreenModeEnabled ? "100%" : "700px",
border: "3px solid rgba(255,255,255,0.3)", border: "3px solid rgba(255,255,255,0.3)",
@@ -1981,7 +2000,8 @@ const CodeEditor = (props) => {
paddingLeft: 10, paddingLeft: 10,
}} }}
> >
File Editor ({localcodedata.length}) {/* cba positioning */}
File Editor ({localcodedata.length})&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{validation === true ? <span style={{ color: "lightgreen" }}>Valid JSON</span> : <span style={{ color: "red" }}>Invalid JSON</span>}
</DialogTitle> </DialogTitle>
</div> </div>
@@ -2511,7 +2531,7 @@ const CodeEditor = (props) => {
} }
</Tooltip> </Tooltip>
</IconButton> </IconButton>
{(actionId || triggerId || conditionId) && !isWorkflowEditor && !isFileEditor ? {/*(actionId || triggerId || conditionId) && !isWorkflowEditor && !isFileEditor ?
<> <>
<IconButton <IconButton
style={{ style={{
@@ -2557,7 +2577,7 @@ const CodeEditor = (props) => {
</IconButton> </IconButton>
</> </>
: null : null
} */}
</div> </div>
</div> </div>
} }
@@ -2583,7 +2603,7 @@ const CodeEditor = (props) => {
mode={isWorkflowEditor ? "yaml" : selectedAction === undefined ? "json" : selectedAction.name === "execute_python" ? "python" : selectedAction.name === "execute_bash" ? "bash" : "json"} mode={isWorkflowEditor ? "yaml" : selectedAction === undefined ? "json" : selectedAction.name === "execute_python" ? "python" : selectedAction.name === "execute_bash" ? "bash" : "json"}
theme="gruvbox" theme="gruvbox"
height={fullScreenModeEnabled ? "84vh" : isFileEditor ? 450 : isWorkflowEditor ? "90vh" : 550} height={fullScreenModeEnabled ? "84vh" : isFileEditor ? 450 : isWorkflowEditor ? "90vh" : 550}
width={isFileEditor ? 650 : fullScreenModeEnabled ? "50vw" : isWorkflowEditor ? "90vw" : "100%"} width={isFileEditor ? 800 : fullScreenModeEnabled ? isFileEditor ? "100%" : "50vw" : isWorkflowEditor ? "90vw" : "100%"}
markers={markers} markers={markers}
highlightActiveLine={false} highlightActiveLine={false}
@@ -2717,7 +2737,7 @@ const CodeEditor = (props) => {
</div> </div>
: :
<span style={{ color: theme.palette.text.primary }}> <span style={{ color: theme.palette.text.primary }}>
{selectedAction.name === "execute_python" || selectedAction.name === "execute_bash" ? {selectedAction?.name === "execute_python" || selectedAction?.name === "execute_bash" ?
"Code to run" : "Code to run" :
triggerId ? triggerId ?
`Output: ${triggerName?.replaceAll("_", " ").slice(0, 1).toUpperCase() + triggerName?.replaceAll("_", " ").slice(1)} (${triggerField})` : `Output: ${triggerName?.replaceAll("_", " ").slice(0, 1).toUpperCase() + triggerName?.replaceAll("_", " ").slice(1)} (${triggerField})` :
@@ -0,0 +1,265 @@
import React, { useState, useContext } from "react";
import {
Dialog,
DialogTitle,
DialogContent,
Box,
TextField,
Button,
Typography,
List,
ListItem,
ListItemText,
Checkbox,
InputAdornment,
} from "@mui/material";
import { Search as SearchIcon } from "@mui/icons-material";
import { Context } from "../context/ContextApi.jsx";
import { getTheme } from "../theme.jsx";
/**
* A reusable dialog for selecting/distributing sub-organizations.
*
* Props:
* open {boolean} - controls dialog visibility
* onClose {function} - called on Cancel or backdrop click (no args)
* title {string} - dialog title text
* extraInfo {string} - secondary line below the title (e.g. "Selected Key: xxx")
* orgs {Array} - ordered array of { id, name, image? } objects to display
* selectedOrgIds {string[]} - currently selected org IDs (controlled)
* onSelectionChange {function} - called with a updater fn (prev => next) when selection changes
* onSave {function} - called with the final selectedOrgIds array when Save is clicked
* disabled {boolean} - disables checkboxes and Save button (default: false)
*/
const SubOrgDistributionDialog = ({
open,
onClose,
title,
extraInfo = null,
orgs = [],
selectedOrgIds = [],
onSelectionChange,
onSave,
disabled = false,
}) => {
const [searchQuery, setSearchQuery] = useState("");
const { themeMode, brandColor } = useContext(Context);
const theme = getTheme(themeMode, brandColor);
const safeOrgs = orgs || [];
const filteredOrgs = safeOrgs.filter(
o => o && o.name.toLowerCase().includes(searchQuery.toLowerCase())
);
const handleSelectAll = () => {
if (searchQuery) {
const filteredIds = filteredOrgs.map(o => o.id);
onSelectionChange(prev => [...new Set([...prev, ...filteredIds])]);
} else {
const allIds = safeOrgs.map(o => o.id);
onSelectionChange(prev => [...new Set([...prev, ...allIds])]);
}
};
const handleDeselectAll = () => {
if (searchQuery) {
const filteredIds = filteredOrgs.map(o => o.id);
onSelectionChange(prev => prev.filter(id => !filteredIds.includes(id)));
} else {
onSelectionChange([]);
}
};
const handleToggle = (id) => {
if (disabled) return;
onSelectionChange(prev =>
prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id]
);
};
const handleClose = () => {
setSearchQuery("");
onClose();
};
const filteredSelected = filteredOrgs.filter(o => selectedOrgIds.includes(o.id)).length;
const countText = searchQuery
? `${filteredSelected} of ${filteredOrgs.length} filtered selected`
: `${selectedOrgIds.length} of ${safeOrgs.length} selected`;
const imageSize = 22;
const imageStyle = { width: imageSize, height: imageSize, pointerEvents: "none", marginRight: 10 };
return (
<Dialog
open={open}
onClose={handleClose}
maxWidth="md"
fullWidth
PaperProps={{
sx: {
borderRadius: theme?.palette?.DialogStyle?.borderRadius,
border: theme?.palette?.DialogStyle?.border,
fontFamily: theme?.typography?.fontFamily,
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
zIndex: 1000,
height: "80vh",
display: "flex",
flexDirection: "column",
"& .MuiDialogContent-root": {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
},
"& .MuiDialogTitle-root": {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
},
"& .MuiDialogActions-root": {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
},
},
}}
>
<DialogTitle>
<Typography variant="h6" color="textPrimary" style={{ textTransform: "none" }}>
{title}
</Typography>
{extraInfo && (
<Typography variant="body2" color="textSecondary" style={{ marginTop: 2 }}>
{extraInfo}
</Typography>
)}
</DialogTitle>
<DialogContent sx={{ display: "flex", flexDirection: "column", gap: 2, overflow: "hidden", pt: "8px !important" }}>
<TextField
fullWidth
size="small"
placeholder="Search sub-organizations..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon style={{ opacity: 0.5 }} />
</InputAdornment>
),
style: { color: theme.palette.textFieldStyle?.color },
}}
style={{ backgroundColor: theme.palette.textFieldStyle?.backgroundColor, flexShrink: 0 }}
/>
<div style={{ display: "flex", gap: 8, alignItems: "center", flexShrink: 0 }}>
<Button
variant="outlined"
size="small"
style={{ textTransform: "none" }}
onClick={handleSelectAll}
>
Select All{searchQuery ? " Filtered" : ""}
</Button>
<Button
variant="outlined"
size="small"
style={{ textTransform: "none" }}
onClick={handleDeselectAll}
>
Deselect All{searchQuery ? " Filtered" : ""}
</Button>
<Typography variant="body2" color="textSecondary" style={{ marginLeft: "auto" }}>
{countText}
</Typography>
</div>
<div style={{
flex: 1,
overflowY: "auto",
border: theme.palette.defaultBorder,
borderRadius: 4,
scrollbarColor: theme.palette.scrollbarColorTransparent,
scrollbarWidth: "thin",
}}>
<List dense sx={{ py: 0 }}>
{filteredOrgs.map((org, index) => {
const isSelected = selectedOrgIds.includes(org.id);
const hasImage = org.image !== undefined;
return (
<ListItem
key={org.id}
onClick={() => handleToggle(org.id)}
sx={{
cursor: disabled ? "default" : "pointer",
backgroundColor: index % 2 === 0
? "transparent"
: themeMode === "dark" ? "rgba(255,255,255,0.02)" : "rgba(0,0,0,0.02)",
"&:hover": {
backgroundColor: themeMode === "dark" ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.05)",
},
}}
>
<Checkbox
edge="start"
checked={isSelected}
tabIndex={-1}
disableRipple
color="primary"
disabled={disabled}
/>
{hasImage && (
org.image === "" ? (
<img alt={org.name} src={theme.palette.defaultImage} style={imageStyle} />
) : (
<img alt={org.name} src={org.image} style={imageStyle} />
)
)}
<ListItemText
primary={org.name}
/>
</ListItem>
);
})}
{filteredOrgs.length === 0 && (
<ListItem>
<ListItemText
primary="No sub-organizations found"
style={{ textAlign: "center", opacity: 0.5 }}
/>
</ListItem>
)}
</List>
</div>
</DialogContent>
<Box sx={{
display: "flex",
justifyContent: "flex-end",
alignItems: "center",
p: 2,
borderTop: theme.palette.defaultBorder,
backgroundColor: theme.palette.platformColor,
}}>
<div style={{ display: "flex", gap: 8 }}>
<Button
onClick={handleClose}
style={{ textTransform: "none", color: theme.palette.text.primary }}
>
Cancel
</Button>
<Button
variant="contained"
color="primary"
disabled={disabled}
style={{ textTransform: "none" }}
onClick={() => {
onSave(selectedOrgIds);
setSearchQuery("");
}}
>
Save Changes
</Button>
</div>
</Box>
</Dialog>
);
};
export default SubOrgDistributionDialog;
File diff suppressed because it is too large Load Diff
+67 -96
View File
@@ -1,11 +1,10 @@
import React, { useState, useEffect, useContext, memo } from "react"; import React, { useState, useEffect, useContext, memo } from "react";
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { Context } from "../context/ContextApi.jsx"; import { Context } from "../context/ContextApi.jsx";
import { Link } from "react-router-dom";
import { import {
FormControl, FormControl,
InputLabel, InputLabel,
OutlinedInput,
Checkbox,
Tooltip, Tooltip,
Typography, Typography,
Select, Select,
@@ -31,23 +30,12 @@ import {
import { import {
Cached as CachedIcon, Cached as CachedIcon,
Edit as EditIcon, Edit as EditIcon,
Style,
} from "@mui/icons-material"; } from "@mui/icons-material";
import ModeEditOutlineOutlinedIcon from '@mui/icons-material/ModeEditOutlineOutlined'; import ModeEditOutlineOutlinedIcon from '@mui/icons-material/ModeEditOutlineOutlined';
import ContentCopyOutlinedIcon from '@mui/icons-material/ContentCopyOutlined'; import ContentCopyOutlinedIcon from '@mui/icons-material/ContentCopyOutlined';
import {getTheme} from "../theme.jsx"; import {getTheme} from "../theme.jsx";
const ITEM_HEIGHT = 48; import SubOrgDistributionDialog from "./SubOrgDistributionDialog.jsx";
const ITEM_PADDING_TOP = 8;
const MenuProps = {
PaperProps: {
style: {
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
width: 500,
},
},
getContentAnchorEl: () => null,
};
const logsViewModal = false; const logsViewModal = false;
const userdata = ""; const userdata = "";
@@ -76,10 +64,11 @@ const UserManagmentTab = memo((props) => {
const [logsViewModal, setLogsViewModal] = React.useState(false); const [logsViewModal, setLogsViewModal] = React.useState(false);
const [ipSelected, setIpSelected] = React.useState(""); const [ipSelected, setIpSelected] = React.useState("");
const [userLogViewing, setUserLogViewing] = React.useState({}); const [userLogViewing, setUserLogViewing] = React.useState({});
const [subOrgModalOpen, setSubOrgModalOpen] = React.useState(false);
const [pendingSubOrgs, setPendingSubOrgs] = React.useState([]);
const { themeMode, supportEmail, brandColor } = useContext(Context); const { themeMode, supportEmail, brandColor } = useContext(Context);
const theme = getTheme(themeMode, brandColor); const theme = getTheme(themeMode, brandColor);
useEffect(() => { useEffect(() => {
if (selectedOrganization?.mfa_required !== MFARequired) { if (selectedOrganization?.mfa_required !== MFARequired) {
setMFARequired(selectedOrganization?.mfa_required); setMFARequired(selectedOrganization?.mfa_required);
@@ -247,30 +236,6 @@ const UserManagmentTab = memo((props) => {
}); });
}; };
const handleOrgEditChange = (event) => {
if (userdata.id === selectedUser.id) {
toast("Can't remove orgs from yourself");
return;
}
if (event.target.value.includes("ALL")) {
toast.info("Adding to available all sub-organizations. This may take a minute.")
event.target.value = selectedOrganization.child_orgs.map((org) => org.id)
} else if (event.target.value.includes("None")) {
toast.info("Removing from all sub-organizations. This may take a minute")
event.target.value = []
}
setMatchingOrganizations(event.target.value);
// Workaround for empty orgs
if (event.target.value.length === 0) {
event.target.value.push("REMOVE");
}
setUser(selectedUser.id, "suborgs", event.target.value);
//setUser(selectedUser.id, "suborgs", matchingOrganizations)
};
const userOrgEdit = const userOrgEdit =
selectedUser.id !== undefined && selectedUser.id !== undefined &&
selectedUser?.orgs !== undefined && selectedUser?.orgs !== undefined &&
@@ -278,44 +243,44 @@ const UserManagmentTab = memo((props) => {
selectedOrganization?.child_orgs !== undefined && selectedOrganization?.child_orgs !== undefined &&
selectedOrganization?.child_orgs !== null && selectedOrganization?.child_orgs !== null &&
selectedOrganization?.child_orgs?.length > 0 ? ( selectedOrganization?.child_orgs?.length > 0 ? (
<FormControl fullWidth sx={{ m: 1 }}> <Button
<InputLabel id="demo-multiple-checkbox-label" style={{ padding: 5 }}> variant="outlined"
Accessible Sub-Organizations ( color="primary"
{selectedUser?.orgs ? selectedUser?.orgs?.length - 1 : 0}) fullWidth
</InputLabel> disabled={selectedUser?.id === userdata?.id}
<Select onClick={() => {
fullWidth setPendingSubOrgs([...matchingOrganizations]);
style={{ width: "100%" }} setSubOrgModalOpen(true);
disabled={selectedUser?.id === userdata?.id} }}
labelId="demo-multiple-checkbox-label" sx={{ m: 1, textTransform: 'none', justifyContent: 'space-between', py: 1.5, fontSize: 14 }}
id="demo-multiple-checkbox" >
multiple Manage Sub-Organizations ({matchingOrganizations.length} assigned of {selectedOrganization?.child_orgs?.length || 0})
value={matchingOrganizations} </Button>
onChange={handleOrgEditChange}
input={<OutlinedInput label="Tag" />}
renderValue={(selected) => {
return selected.join(", ");
}}
MenuProps={MenuProps}
>
<MenuItem key={-2} value={"None"}>
<Checkbox checked={false} />
<ListItemText primary={"None"} />
</MenuItem>
<MenuItem key={-1} value={"ALL"}>
<Checkbox checked={false} />
<ListItemText primary={"ALL"} />
</MenuItem>
{selectedOrganization.child_orgs.map((org, index) => (
<MenuItem key={index} value={org.id}>
<Checkbox checked={matchingOrganizations.indexOf(org.id) > -1} />
<ListItemText primary={org.name} />
</MenuItem>
))}
</Select>
</FormControl>
) : null; ) : null;
const subOrgManagementDialog = (
<SubOrgDistributionDialog
open={subOrgModalOpen}
onClose={() => setSubOrgModalOpen(false)}
title={`Manage Sub-Organizations for ${selectedUser?.username || ''}`}
orgs={selectedOrganization?.child_orgs || []}
selectedOrgIds={pendingSubOrgs}
onSelectionChange={setPendingSubOrgs}
onSave={(ids) => {
if (userdata.id === selectedUser.id) {
toast("Can't modify orgs for yourself");
return;
}
const newValue = ids.length === 0 ? ["REMOVE"] : [...ids];
setMatchingOrganizations([...ids]);
setUser(selectedUser.id, "suborgs", newValue);
setSubOrgModalOpen(false);
setSelectedUserModalOpen(false);
}}
disabled={selectedUser?.id === userdata?.id}
/>
);
const getUsers = () => { const getUsers = () => {
fetch(globalUrl + "/api/v1/getusers", { fetch(globalUrl + "/api/v1/getusers", {
method: "GET", method: "GET",
@@ -1117,6 +1082,8 @@ const UserManagmentTab = memo((props) => {
}); });
}; };
var previousreferrer = ""
var nextreferrer = ""
const logview = logsViewModal ? ( const logview = logsViewModal ? (
<Dialog <Dialog
open={logsViewModal} open={logsViewModal}
@@ -1163,26 +1130,23 @@ const UserManagmentTab = memo((props) => {
onChange={(event) => { onChange={(event) => {
setIpSelected(event.target.value); setIpSelected(event.target.value);
getLogs(event.target.value, userLogViewing.id); getLogs(event.target.value, userLogViewing.id);
}} }}
> >
{(() => { {(() => {
const uniqueIPs = new Set(); const uniqueIPs = new Set();
console.log("Login info: ", userLogViewing.login_info)
return userLogViewing.login_info.map((data, index) => { return userLogViewing.login_info.map((data, index) => {
if ( console.log("Data: ", data)
data.ip.includes("127.0.0.1") || if (data.ip.includes("127.0.0.1") || uniqueIPs.has(data.ip)) {
uniqueIPs.has(data.ip) return null
) {
return null;
} }
uniqueIPs.add(data.ip); uniqueIPs.add(data.ip)
return ( return (
<MenuItem key={index} value={data.ip}> <MenuItem key={index} value={data.ip}>
{data.ip} {data?.timestamp ? new Date(data.timestamp * 1000).toLocaleString() : "N/A"} - {data?.ip}
</MenuItem> </MenuItem>
); );
}); });
@@ -1229,12 +1193,13 @@ const UserManagmentTab = memo((props) => {
minWidth: 700, minWidth: 700,
maxWidth: 700, maxWidth: 700,
overflow: "hidden", overflow: "hidden",
marginLeft: 10, marginLeft: 50,
}} }}
/> />
</ListItem> </ListItem>
{logs.map((data, index) => { {logs.map((data, index) => {
//console.log("LOG: ", data) previousreferrer = nextreferrer
nextreferrer = data.referer
return ( return (
// redirect user to logs // redirect user to logs
@@ -1243,6 +1208,8 @@ const UserManagmentTab = memo((props) => {
key={index} key={index}
style={{ style={{
backgroundColor: index % 2 === 0 ? "#1f2023" : "#27292d", backgroundColor: index % 2 === 0 ? "#1f2023" : "#27292d",
paddingTop: data.referer !== previousreferrer ? 50 : 0,
borderTop: data.referer !== previousreferrer ? `1px solid rgba(255,255,255,0.3)` : "none",
}} }}
> >
<ListItemText <ListItemText
@@ -1263,22 +1230,25 @@ const UserManagmentTab = memo((props) => {
}} }}
/> />
<ListItemText <ListItemText
primary={data.referer} primary={data.referer.replace("https://shuffler.io", "")}
style={{ style={{
minWidth: 300, minWidth: 300,
maxWidth: 300, maxWidth: 300,
overflow: "hidden", overflow: "hidden",
}} }}
/> />
<ListItemText <Link to={data.url} target="_blank" style={{ textDecoration: "none", color: theme.palette.linkColor }}>
primary={data.url} <ListItemText
style={{ primary={data.url.replace("https://shuffler.io", "")}
minWidth: 700, style={{
maxWidth: 700, minWidth: 700,
overflow: "hidden", maxWidth: 700,
marginLeft: 10, overflow: "hidden",
}} marginLeft: 50,
/> backgroundColor: data.url.includes("/api/v1/register") ? "#d52b2b" : "inherit",
}}
/>
</Link>
</ListItem> </ListItem>
)})} )})}
</List> </List>
@@ -1290,6 +1260,7 @@ const UserManagmentTab = memo((props) => {
<div style={{ width: "100%", minHeight: 1100, boxSizing: 'border-box', padding: "27px 10px 19px 27px", height:"100%", backgroundColor: theme.palette.platformColor, borderTopRightRadius: '8px', borderBottomRightRadius: 8, borderLeft: "1px solid #494949", }}> <div style={{ width: "100%", minHeight: 1100, boxSizing: 'border-box', padding: "27px 10px 19px 27px", height:"100%", backgroundColor: theme.palette.platformColor, borderTopRightRadius: '8px', borderBottomRightRadius: 8, borderLeft: "1px solid #494949", }}>
{modalView} {modalView}
{editUserModal} {editUserModal}
{subOrgManagementDialog}
{logview} {logview}
<div style={{ height: "100%", maxHeight: 1700, overflowY: "auto", scrollbarColor: theme.palette.scrollbarColorTransparent, scrollbarWidth: 'thin'}}> <div style={{ height: "100%", maxHeight: 1700, overflowY: "auto", scrollbarColor: theme.palette.scrollbarColorTransparent, scrollbarWidth: 'thin'}}>
<div style={{ height: "100%", width: "calc(100% - 20px)", scrollbarColor: theme.palette.scrollbarColorTransparent, scrollbarWidth: 'thin' }}> <div style={{ height: "100%", width: "calc(100% - 20px)", scrollbarColor: theme.palette.scrollbarColorTransparent, scrollbarWidth: 'thin' }}>
+14 -104
View File
@@ -3,6 +3,7 @@ import React, { useEffect, useState } from 'react';
import {Link} from 'react-router-dom'; import {Link} from 'react-router-dom';
import theme from '../theme.jsx'; import theme from '../theme.jsx';
import { removeQuery } from '../components/ScrollToTop.jsx'; import { removeQuery } from '../components/ScrollToTop.jsx';
import SearchContactForm from '../components/SearchContactForm.jsx';
import { Search as SearchIcon, CloudQueue as CloudQueueIcon, Code as CodeIcon } from '@mui/icons-material'; import { Search as SearchIcon, CloudQueue as CloudQueueIcon, Code as CodeIcon } from '@mui/icons-material';
@@ -25,66 +26,23 @@ import { useDebouncedCallback } from "../utils/useDebouncedCallback.jsx";
import WorkflowPaper from "../components/WorkflowPaper.jsx" import WorkflowPaper from "../components/WorkflowPaper.jsx"
import WorkflowPaperNew from "../components/WorkflowPaperNew.jsx" import WorkflowPaperNew from "../components/WorkflowPaperNew.jsx"
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e") const searchClient = algoliasearch("JNSS5CFDZZ", "eb5fd80aa6ed5ab4730d836cff3ea283")
const AppGrid = props => { const AppGrid = props => {
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, alternativeView, onlyResults, inputsearch } = props const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, alternativeView, onlyResults, inputsearch } = props
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows
const xs = parsedXs === undefined || parsedXs === null ? isMobile ? 6 : 4 : parsedXs const xs = parsedXs === undefined || parsedXs === null ? isMobile ? 6 : 4 : parsedXs
//const [apps, setApps] = React.useState([]); //const [apps, setApps] = React.useState([]);
//const [filteredApps, setFilteredApps] = React.useState([]); //const [filteredApps, setFilteredApps] = React.useState([]);
const [formMail, setFormMail] = React.useState("");
const [message, setMessage] = React.useState("");
const [formMessage, setFormMessage] = React.useState("");
const [usecases, setUsecases] = React.useState([]); const [usecases, setUsecases] = React.useState([]);
const [localMessage, setLocalMessage] = React.useState(""); const [localMessage, setLocalMessage] = React.useState("");
const buttonStyle = {borderRadius: 30, height: 50, width: 220, margin: isMobile ? "15px auto 15px auto" : 20, fontSize: 18,}
const innerColor = "rgba(255,255,255,0.65)" const innerColor = "rgba(255,255,255,0.65)"
const borderRadius = 3 const borderRadius = 3
window.title = "Shuffle | Workflows | Discover your use-case" window.title = "Shuffle | Workflows | Discover your use-case"
const submitContact = (email, message) => {
const data = {
"firstname": "",
"lastname": "",
"title": "",
"companyname": "",
"email": email,
"phone": "",
"message": message,
}
const errorMessage = "Something went wrong. Please contact frikky@shuffler.io directly."
fetch(globalUrl+"/api/v1/contact", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})
.then(response => response.json())
.then(response => {
if (response.success === true) {
setFormMessage(response.reason)
//toast("Thanks for submitting!")
} else {
setFormMessage(errorMessage)
}
setFormMail("")
setMessage("")
})
.catch(error => {
setFormMessage(errorMessage)
console.log(error)
});
}
const handleKeysetting = (categorydata, workflows) => { const handleKeysetting = (categorydata, workflows) => {
console.log("Workflows: ", workflows) console.log("Workflows: ", workflows)
//workflows[0].category = ["detect"] //workflows[0].category = ["detect"]
@@ -229,10 +187,16 @@ const AppGrid = props => {
placeholder="Find Workflows..." placeholder="Find Workflows..."
id="shuffle_search_field" id="shuffle_search_field"
onChange={(event) => { onChange={(event) => {
removeQuery("q")
const value = event.currentTarget.value const value = event.currentTarget.value
setInputValue(value) setInputValue(value)
debouncedRefine(value) debouncedRefine(value)
const urlSearchParams = new URLSearchParams(window.location.search)
if (value) {
urlSearchParams.set("q", value)
} else {
urlSearchParams.delete("q")
}
window.history.replaceState(null, "", value ? `?${urlSearchParams.toString()}` : window.location.pathname)
}} }}
onKeyDown={(event) => { onKeyDown={(event) => {
if(event.key === "Enter") { if(event.key === "Enter") {
@@ -333,64 +297,10 @@ const AppGrid = props => {
<CustomHits hitsPerPage={5}/> <CustomHits hitsPerPage={5}/>
</InstantSearch> </InstantSearch>
{showSuggestion === true ? {showSuggestion === true ?
<div style={{maxWidth: isMobile ? "100%" : "60%", margin: "auto", paddingTop: 0, textAlign: "center",}}> <SearchContactForm globalUrl={globalUrl} isMobile={isMobile} tabName="workflows" />
<Typography variant="h6" style={{color: "white", marginTop: 50,}}> : null
Can't find what you're looking for?
</Typography>
<div style={{flex: "1", display: "flex", flexDirection: "row"}}>
<TextField
required
style={{flex: "1", marginRight: "15px", backgroundColor: theme.palette.inputColor}}
InputProps={{
style:{
color: "#ffffff",
},
}}
color="primary"
fullWidth={true}
placeholder="Email (optional)"
type="email"
id="email-handler"
autoComplete="email"
margin="normal"
variant="outlined"
onChange={e => setFormMail(e.target.value)}
/>
<TextField
required
style={{flex: "1", backgroundColor: theme.palette.inputColor}}
InputProps={{
style:{
color: "#ffffff",
},
}}
color="primary"
fullWidth={true}
placeholder="What apps do you want to see?"
type=""
id="standard-required"
margin="normal"
variant="outlined"
autoComplete="off"
onChange={e => setMessage(e.target.value)}
/>
</div>
<Button
variant="contained"
color="primary"
style={buttonStyle}
disabled={message.length === 0}
onClick={() => {
submitContact(formMail, message)
}}
>
Submit
</Button>
<Typography style={{color: "white"}} variant="body2">{formMessage}</Typography>
</div>
: null
} }
{onlyResults === true ? null : {/* {onlyResults === true ? null :
<span style={{position: "absolute", display: "flex", textAlign: "right", float: "right", right: 0, bottom: 120, }}> <span style={{position: "absolute", display: "flex", textAlign: "right", float: "right", right: 0, bottom: 120, }}>
<Typography variant="body2" color="textSecondary" style={{}}> <Typography variant="body2" color="textSecondary" style={{}}>
Search by Search by
@@ -399,7 +309,7 @@ const AppGrid = props => {
<img src={"/images/logo-algolia-nebula-blue-full.svg"} alt="Algolia logo" style={{height: 17, marginLeft: 5, marginTop: 3,}} /> <img src={"/images/logo-algolia-nebula-blue-full.svg"} alt="Algolia logo" style={{height: 17, marginLeft: 5, marginTop: 3,}} />
</a> </a>
</span> </span>
} } */}
</div> </div>
) )
} }
+1 -1
View File
@@ -10,7 +10,7 @@ import algoliasearch from 'algoliasearch';
import { InstantSearch, connectSearchBox, connectHits } from 'react-instantsearch-dom'; import { InstantSearch, connectSearchBox, connectHits } from 'react-instantsearch-dom';
import { Grid, Paper, TextField, ButtonBase, InputAdornment, Typography, Button, Tooltip} from '@mui/material'; import { Grid, Paper, TextField, ButtonBase, InputAdornment, Typography, Button, Tooltip} from '@mui/material';
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e") const searchClient = algoliasearch("JNSS5CFDZZ", "33e4e3564f4f060e96e0531957bed552")
const WorkflowSearch = props => { const WorkflowSearch = props => {
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, newSelectedApp, setNewSelectedApp, defaultSearch, showSearch, ConfiguredHits, selectAble, } = props const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, newSelectedApp, setNewSelectedApp, defaultSearch, showSearch, ConfiguredHits, selectAble, } = props
const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows
+15 -4
View File
@@ -10,7 +10,7 @@ import {
Checkbox Checkbox
} from "@mui/material"; } from "@mui/material";
import { makeStyles } from "@mui/styles"; import { makeStyles } from "@mui/styles";
import { Link } from "react-router-dom"; import { Link, useSearchParams } from "react-router-dom";
import theme from "../theme.jsx"; import theme from "../theme.jsx";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { Context } from "../context/ContextApi.jsx"; import { Context } from "../context/ContextApi.jsx";
@@ -29,6 +29,12 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
// Check if user is admin // Check if user is admin
const isAdmin = userdata?.active_org?.role === "admin" || userdata?.support === true; const isAdmin = userdata?.active_org?.role === "admin" || userdata?.support === true;
// Read region_url override from URL params (only allow shuffler.io domains)
const [searchParams] = useSearchParams();
const rawRegionUrl = searchParams.get("region_url");
const regionUrlOverride = rawRegionUrl && rawRegionUrl.includes("shuffler.io") ? rawRegionUrl : null;
const effectiveGlobalUrl = regionUrlOverride || globalUrl;
// State for tracking user SSO connection status // State for tracking user SSO connection status
const [users, setUsers] = React.useState([]); const [users, setUsers] = React.useState([]);
const [userSSOConnected, setUserSSOConnected] = React.useState(false); const [userSSOConnected, setUserSSOConnected] = React.useState(false);
@@ -109,7 +115,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
// Function to fetch users and check current user's SSO status // Function to fetch users and check current user's SSO status
const checkUserSSOStatus = () => { const checkUserSSOStatus = () => {
setCheckingSSOStatus(true); setCheckingSSOStatus(true);
fetch(globalUrl + "/api/v1/getusers", { fetch(effectiveGlobalUrl + "/api/v1/getusers", {
method: "GET", method: "GET",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -309,7 +315,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
}; };
const HandleTestSSO = () => { const HandleTestSSO = () => {
const url = `${globalUrl}/api/v1/orgs/${selectedOrganization?.id}/change`; const url = `${effectiveGlobalUrl}/api/v1/orgs/${selectedOrganization?.id}/change`;
const data = { const data = {
org_id: selectedOrganization?.id, org_id: selectedOrganization?.id,
sso: true, sso: true,
@@ -366,7 +372,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
}; };
const HandleDisconnectSSO = () => { const HandleDisconnectSSO = () => {
const url = `${globalUrl}/api/v1/disconnect_sso`; const url = `${effectiveGlobalUrl}/api/v1/disconnect_sso`;
const data = { const data = {
org_id: selectedOrganization?.id, org_id: selectedOrganization?.id,
}; };
@@ -444,6 +450,11 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
: "Connect your account with this org's SSO!" : "Connect your account with this org's SSO!"
} }
</Typography> </Typography>
{regionUrlOverride && (
<Typography variant="body2" style={{ margin: "5px 0px 5px 0px", fontSize: 14, color: "#f85a3e" }}>
Using region override: {regionUrlOverride}
</Typography>
)}
<Tooltip <Tooltip
title={ title={
checkingSSOStatus checkingSSOStatus
+85 -95
View File
@@ -77,7 +77,7 @@ const theme = createTheme(adaptV4Theme({
defaultImage: "/images/no_image.png", defaultImage: "/images/no_image.png",
singulOrange: "/images/singul_orange.png", singulOrange: "/images/singul_orange.png",
singulGreen: "/images/singul_green.png", singulGreen: "/images/singul_green.png",
singulBlackWhite: "/images/singul_black_white.png", singulBlackWhite: "/icons/workflow-page/shuffle_agent.png", // For AI Agent stuff
}, },
typography: { typography: {
fontFamily: `"inter", "Roboto", "Helvetica", "Arial", sans-serif`, fontFamily: `"inter", "Roboto", "Helvetica", "Arial", sans-serif`,
@@ -153,20 +153,10 @@ const theme = createTheme(adaptV4Theme({
export default theme; export default theme;
export const getTheme = (themeMode, brandColor) => { export const getTheme = (themeMode, brandColor) =>
// Handle "system" mode by checking user's system preference createTheme({
let resolvedMode = themeMode;
if (themeMode === "system" || !themeMode) {
resolvedMode = window?.matchMedia?.("(prefers-color-scheme: dark)")?.matches ? "dark" : "light";
}
// Ensure mode is only "dark" or "light"
if (resolvedMode !== "dark" && resolvedMode !== "light") {
resolvedMode = "dark";
}
return createTheme({
palette: { palette: {
mode: resolvedMode, mode: themeMode,
main: brandColor || "#FF8544", main: brandColor || "#FF8544",
primary: { primary: {
main: brandColor || "#FF8544", main: brandColor || "#FF8544",
@@ -177,35 +167,36 @@ export const getTheme = (themeMode, brandColor) => {
contrastText:"#000000", contrastText:"#000000",
}, },
text: { text: {
primary: resolvedMode === "dark" ? "#ffffff" : "#1A1A1A", primary: themeMode === "dark" ? "#ffffff" : "#1A1A1A",
secondary: resolvedMode === "dark" ? "#9E9E9E" : "#616161", secondary: themeMode === "dark" ? "#9E9E9E" : "#616161",
}, },
type: resolvedMode, type: themeMode,
inputColor: resolvedMode === "dark" ? "rgba(39,41,45,1)" : "rgba(245, 245, 245, 1)", inputColor: themeMode === "dark" ? "rgba(39,41,45,1)" : "rgba(245, 245, 245, 1)",
textColor: resolvedMode === "dark" ? "#F1F1F1" : "#1A1A1A", textColor: themeMode === "dark" ? "#F1F1F1" : "#1A1A1A",
textPrimary: resolvedMode === "dark" ? "rgba(255, 255, 255, 0.8)" : "rgba(26, 26, 26, 0.8)", textPrimary: themeMode === "dark" ? "rgba(255, 255, 255, 0.8)" : "rgba(26, 26, 26, 0.8)",
surfaceColor: resolvedMode === "dark" ? "#27292d" : "#EFEFEF", surfaceColor: themeMode === "dark" ? "#27292d" : "#EFEFEF",
platformColor: resolvedMode === "dark" ? "#212121" : "#ffffff", platformColor: themeMode === "dark" ? "#212121" : "#ffffff",
backgroundColor: resolvedMode === "dark" ? "#1a1a1a" : "#f1f1f1", backgroundColor: themeMode === "dark" ? "#1a1a1a" : "#f1f1f1",
cytoscapeBackgroundColor: resolvedMode === "dark" ? "#161616" : "#f5f5f5", cytoscapeBackgroundColor: themeMode === "dark" ? "#161616" : "#f5f5f5",
distributionColor: resolvedMode === "dark" ? "#40E0D0" : "#008080", distributionColor: themeMode === "dark" ? "#40E0D0" : "#008080",
cardBackgroundColor: resolvedMode === "dark" ? "#1e1e1e" : "#eaeaea", cardBackgroundColor: themeMode === "dark" ? "#1e1e1e" : "#eaeaea",
cardHoverColor: resolvedMode === "dark" ? "#323232" : "#F0F0F0", cardHoverColor: themeMode === "dark" ? "#323232" : "#F0F0F0",
hoverColor: resolvedMode === "dark" ? "#323232" : "#D6D6D6", hoverColor: themeMode === "dark" ? "#323232" : "#D6D6D6",
usecaseCardColor: resolvedMode === "dark" ? "#2f2f2f" : "rgba(245, 245, 245, 1)", usecaseCardColor: themeMode === "dark" ? "#2f2f2f" : "rgba(245, 245, 245, 1)",
usecaseCardHoverColor: resolvedMode === "dark" ? "#2F2F2F" : "rgba(245, 245, 245, 1)", usecaseCardHoverColor: themeMode === "dark" ? "#2F2F2F" : "rgba(245, 245, 245, 1)",
usecaseDialogFieldColor: resolvedMode === "dark" ? "#2B2B2B" : "#F5F5F5", usecaseDialogFieldColor: themeMode === "dark" ? "#2B2B2B" : "#F5F5F5",
accentColor: resolvedMode === "dark" ? "#ff8544" : "#ff8544", accentColor: themeMode === "dark" ? "#ff8544" : "#ff8544",
green: resolvedMode === "dark" ? "#5cc879" : "#008000", green: themeMode === "dark" ? "#5cc879" : "#008000",
defaultBorder: resolvedMode === "dark" ? '1px solid #494949' : '1px solid #CCCCCC', defaultBorder: themeMode === "dark" ? '1px solid #494949' : '1px solid #CCCCCC',
linkColor: brandColor === "#ff8544" ? "#f86a3e" : brandColor, linkColor: brandColor === "#ff8544" ? "#f86a3e" : brandColor,
slateGrayColor: resolvedMode === "dark" ? "#494949" : "#CCCCCC", slateGrayColor: themeMode === "dark" ? "#494949" : "#CCCCCC",
parsedAppPaperColor: resolvedMode === "dark" ? "#2f2f2f" : "#CCCCCC", parsedAppPaperColor: themeMode === "dark" ? "#2f2f2f" : "#CCCCCC",
welcomeCardSubtextColor: themeMode === "dark" ? "#C8C8C8" : "#2f2f2f",
deleteColor: themeMode === "dark" ? "#FD4C62" : "#d32f2f",
borderRadius: 10, borderRadius: 10,
loaderColor: resolvedMode === "dark" ? "#1a1a1a" : "#E0E0E0", loaderColor: themeMode === "dark" ? "#1a1a1a" : "#E0E0E0",
jsonIconStyle: "round", jsonIconStyle: "round",
jsonTheme: resolvedMode === "dark" ? "summerfruit" : { jsonTheme: themeMode === "dark" ? "summerfruit" : {
base00: "#ffffff", // background base00: "#ffffff", // background
base01: "#f0f0f0", // very light grey base01: "#f0f0f0", // very light grey
base02: "#f5f5f5", // light grey base02: "#f5f5f5", // light grey
@@ -225,11 +216,11 @@ export const getTheme = (themeMode, brandColor) => {
}, },
jsonCollapseStringsAfterLength: 100, jsonCollapseStringsAfterLength: 100,
drawer: { drawer: {
backgroundColor: resolvedMode === "dark" ? "#262626" : "#f9f9f9" backgroundColor: themeMode === "dark" ? "#262626" : "#f9f9f9"
}, },
actionSidebarField: { actionSidebarField: {
backgroundColor: resolvedMode === "dark" ? "#2F2F2F" : "#F1F1F1", backgroundColor: themeMode === "dark" ? "#2F2F2F" : "#F1F1F1",
color: resolvedMode === "dark" ? "#ffffff" : "#000000", color: themeMode === "dark" ? "#ffffff" : "#000000",
borderRadius: 8, borderRadius: 8,
height: 40, height: 40,
border: "none", border: "none",
@@ -238,53 +229,53 @@ export const getTheme = (themeMode, brandColor) => {
padding: 5, padding: 5,
width: "98%", width: "98%",
borderRadius: 5, borderRadius: 5,
border: resolvedMode === "dark" ? "1px solid rgba(255,255,255,0.7)" : "1px solid rgba(0,0,0,0.3)", border: themeMode === "dark" ? "1px solid rgba(255,255,255,0.7)" : "1px solid rgba(0,0,0,0.3)",
backgroundColor: resolvedMode === "dark" backgroundColor: themeMode === "dark"
? "#1A1A1A" ? "#1A1A1A"
: "#f1f1f1", : "#f1f1f1",
color: resolvedMode === "dark" color: themeMode === "dark"
? "#F1F1F1" ? "#F1F1F1"
: "#1A1A1A", : "#1A1A1A",
overflowX: "auto", overflowX: "auto",
}, },
textFieldStyle: { textFieldStyle: {
backgroundColor: resolvedMode === "dark" ? "#212121" : "#FFFFFF", backgroundColor: themeMode === "dark" ? "#212121" : "#FFFFFF",
color: resolvedMode === "dark" ? "#ffffff" : "#000000", color: themeMode === "dark" ? "#ffffff" : "#000000",
borderRadius: "5px", borderRadius: "5px",
height: 40, height: 40,
border: resolvedMode === "dark" ? "1px solid #4D4D4D" : "1px solid #E0E0E0", border: themeMode === "dark" ? "1px solid #4D4D4D" : "1px solid #E0E0E0",
}, },
DialogStyle: { DialogStyle: {
backgroundColor: resolvedMode === "dark" ? "#212121" : "#ffffff", backgroundColor: themeMode === "dark" ? "#212121" : "#ffffff",
borderRadius: 2, borderRadius: 2,
boxShadow: resolvedMode === "dark" ? "0px 0px 10px 0px rgba(0,0,0,0.75)" : "0px 0px 10px 0px rgba(0,0,0,0.2)", boxShadow: themeMode === "dark" ? "0px 0px 10px 0px rgba(0,0,0,0.75)" : "0px 0px 10px 0px rgba(0,0,0,0.2)",
border: resolvedMode === "dark" ? "1px solid #494949" : "1px solid #cccccc", border: themeMode === "dark" ? "1px solid #494949" : "1px solid #cccccc",
}, },
innerTextfieldStyle: { innerTextfieldStyle: {
height: 40, height: 40,
fontSize: 16, fontSize: 16,
backgroundColor: resolvedMode === "dark" ? "#212121" : "#f5f5f5", backgroundColor: themeMode === "dark" ? "#212121" : "#f5f5f5",
}, },
tooltip: { tooltip: {
backgroundColor: resolvedMode === "dark" ? "#212121" : "#ffffff", backgroundColor: themeMode === "dark" ? "#212121" : "#ffffff",
color: resolvedMode === "dark" ? "#ffffff" : "#000000", color: themeMode === "dark" ? "#ffffff" : "#000000",
border: resolvedMode === "dark" ? "1px solid #494949" : "1px solid #cccccc", border: themeMode === "dark" ? "1px solid #494949" : "1px solid #cccccc",
}, },
chipStyle: { chipStyle: {
backgroundColor: resolvedMode === "dark" ? "#333333" : "#F5F5F5", backgroundColor: themeMode === "dark" ? "#333333" : "#F5F5F5",
borderColor: resolvedMode === "dark" ? "#444444" : "#E0E0E0", borderColor: themeMode === "dark" ? "#444444" : "#E0E0E0",
color: resolvedMode === "dark" ? "#FFFFFF" : "#333333", color: themeMode === "dark" ? "#FFFFFF" : "#333333",
}, },
defaultImage: "/images/no_image.png", defaultImage: "/images/no_image.png",
singulOrange: "/images/singul_orange.png", singulOrange: "/images/singul_orange.png",
singulGreen: "/images/singul_green.png", singulGreen: "/images/singul_green.png",
singulBlackWhite: "/icons/workflow-page/shuffle_agent.png", singulBlackWhite: "/icons/workflow-page/shuffle_agent.png",
scrollbarColor: resolvedMode === "dark" ? "#494949 #2f2f2f": "#c1c1c1 #f1f1f1", scrollbarColor: themeMode === "dark" ? "#494949 #2f2f2f": "#c1c1c1 #f1f1f1",
scrollbarColorTransparent: resolvedMode === "dark" ? '#494949 transparent': "#c1c1c1 transparent", scrollbarColorTransparent: themeMode === "dark" ? '#494949 transparent': "#c1c1c1 transparent",
}, },
typography: { typography: {
fontFamily: `"inter", "Roboto", "Helvetica", "Arial", sans-serif`, fontFamily: `"inter", "Roboto", "Helvetica", "Arial", sans-serif`,
color: resolvedMode === "dark" ? "#ffffff" : "#000000", color: themeMode === "dark" ? "#ffffff" : "#000000",
useNextVariants: true, useNextVariants: true,
fontWeightLight: 300, fontWeightLight: 300,
fontWeightRegular: 400, fontWeightRegular: 400,
@@ -292,36 +283,36 @@ export const getTheme = (themeMode, brandColor) => {
fontWeightSemiBold: 600, fontWeightSemiBold: 600,
fontWeightBold: 700, fontWeightBold: 700,
allVariants: { allVariants: {
color: resolvedMode === "dark" ? "#ffffff" : "#1A1A1A", color: themeMode === "dark" ? "#ffffff" : "#1A1A1A",
}, },
h1: { h1: {
fontSize: 40, fontSize: 40,
color: resolvedMode === "dark" ? "#ffffff" : "#1A1A1A" color: themeMode === "dark" ? "#ffffff" : "#1A1A1A"
}, },
h2: { h2: {
fontSize: 36, fontSize: 36,
color: resolvedMode === "dark" ? "#ffffff" : "#1A1A1A" color: themeMode === "dark" ? "#ffffff" : "#1A1A1A"
}, },
h3: { h3: {
fontSize: 32, fontSize: 32,
color: resolvedMode === "dark" ? "#ffffff" : "#1A1A1A" color: themeMode === "dark" ? "#ffffff" : "#1A1A1A"
}, },
h4: { h4: {
fontSize: 30, fontSize: 30,
fontWeight: 500, fontWeight: 500,
color: resolvedMode === "dark" ? "#ffffff" : "#1A1A1A" color: themeMode === "dark" ? "#ffffff" : "#1A1A1A"
}, },
h6: { h6: {
fontSize: 22, fontSize: 22,
color: resolvedMode === "dark" ? "#ffffff" : "#1A1A1A" color: themeMode === "dark" ? "#ffffff" : "#1A1A1A"
}, },
body1: { body1: {
fontSize: 16, fontSize: 16,
color: resolvedMode === "dark" ? "#ffffff" : "#1A1A1A" color: themeMode === "dark" ? "#ffffff" : "#1A1A1A"
}, },
body2: { body2: {
fontSize: 14, fontSize: 14,
color: resolvedMode === "dark" ? "#ffffff" : "#1A1A1A" color: themeMode === "dark" ? "#ffffff" : "#1A1A1A"
}, },
}, },
components: { components: {
@@ -336,7 +327,7 @@ export const getTheme = (themeMode, brandColor) => {
{ {
props: { variant: 'text', color: 'primary' }, props: { variant: 'text', color: 'primary' },
style: { style: {
color: resolvedMode === "dark" ? "#ffffff" : "#1A1A1A", color: themeMode === "dark" ? "#ffffff" : "#1A1A1A",
whiteSpace: "nowrap", whiteSpace: "nowrap",
textWrap: "normal", textWrap: "normal",
}, },
@@ -344,7 +335,7 @@ export const getTheme = (themeMode, brandColor) => {
{ {
props: { variant: 'text', color: 'secondary' }, props: { variant: 'text', color: 'secondary' },
style: { style: {
color: resolvedMode === "dark" ? "#9E9E9E" : "#616161", color: themeMode === "dark" ? "#9E9E9E" : "#616161",
whiteSpace: "nowrap", whiteSpace: "nowrap",
textWrap: "normal", textWrap: "normal",
}, },
@@ -352,24 +343,24 @@ export const getTheme = (themeMode, brandColor) => {
{ {
props: { variant: 'contained', color: 'primary' }, props: { variant: 'contained', color: 'primary' },
style: { style: {
backgroundColor: resolvedMode === "dark" ? brandColor || '#ff8544' : brandColor || '#FF7C35', backgroundColor: themeMode === "dark" ? brandColor || '#ff8544' : brandColor || '#FF7C35',
color: resolvedMode === "dark" ? '#1a1a1a': '#FFFFFF', color: themeMode === "dark" ? '#1a1a1a': '#FFFFFF',
borderRadius: '4px', borderRadius: '4px',
whiteSpace: "nowrap", whiteSpace: "nowrap",
textWrap: "normal", textWrap: "normal",
transition: 'background-color 0.2s ease-in-out', transition: 'background-color 0.2s ease-in-out',
'&:hover': { '&:hover': {
fontWeight: 600, fontWeight: 600,
backgroundColor: resolvedMode === 'dark' ? brandColor || "#ff955c" : brandColor || '#FF8D4F', backgroundColor: themeMode === 'dark' ? brandColor || "#ff955c" : brandColor || '#FF8D4F',
color: resolvedMode === "dark" ? '#1a1a1a': '#FFFFFF', color: themeMode === "dark" ? '#1a1a1a': '#FFFFFF',
}, },
}, },
}, },
{ {
props: { variant: 'contained', color: 'secondary' }, props: { variant: 'contained', color: 'secondary' },
style: { style: {
backgroundColor: resolvedMode === "dark" ? '#494949' : '#C9C9C9', backgroundColor: themeMode === "dark" ? '#494949' : '#C9C9C9',
color: resolvedMode === "dark" ? '#ffffff' : '#4C4C4C', color: themeMode === "dark" ? '#ffffff' : '#4C4C4C',
borderRadius: '4px', borderRadius: '4px',
boxShadow: 'none', boxShadow: 'none',
whiteSpace: "nowrap", whiteSpace: "nowrap",
@@ -377,23 +368,23 @@ export const getTheme = (themeMode, brandColor) => {
textWrap: "normal", textWrap: "normal",
'&:hover': { '&:hover': {
fontWeight: 600, fontWeight: 600,
border: resolvedMode === "dark" ? '1px solid #f1f1f1' : 'none', border: themeMode === "dark" ? '1px solid #f1f1f1' : 'none',
backgroundColor: resolvedMode === "dark" ? '#494949' : '#C9C9C9', backgroundColor: themeMode === "dark" ? '#494949' : '#C9C9C9',
color: resolvedMode === "dark" ? '#ffffff' : '#4C4C4C', color: themeMode === "dark" ? '#ffffff' : '#4C4C4C',
}, },
}, },
}, },
{ {
props: { variant: 'outlined', color: 'primary' }, props: { variant: 'outlined', color: 'primary' },
style: { style: {
borderColor: resolvedMode === "dark" ? brandColor || "#ff8544" : brandColor || "#cc5f1f", borderColor: themeMode === "dark" ? brandColor || "#ff8544" : brandColor || "#cc5f1f",
color: resolvedMode === "dark" ? brandColor || "#ff8544" : brandColor || "#cc5f1f", color: themeMode === "dark" ? brandColor || "#ff8544" : brandColor || "#cc5f1f",
whiteSpace: "nowrap", whiteSpace: "nowrap",
fontWeight: 'normal', fontWeight: 'normal',
textWrap: "normal", textWrap: "normal",
'&:hover': { '&:hover': {
backgroundColor: resolvedMode === "dark" ? brandColor || "#ff8544" : "#ffe8dc", backgroundColor: themeMode === "dark" ? brandColor || "#ff8544" : "#ffe8dc",
color: resolvedMode === "dark" ? "#1a1a1a" : "#8a3d00", color: themeMode === "dark" ? "#1a1a1a" : "#8a3d00",
fontWeight: 600, fontWeight: 600,
}, },
}, },
@@ -402,14 +393,14 @@ export const getTheme = (themeMode, brandColor) => {
props: { variant: 'outlined', color: 'secondary' }, props: { variant: 'outlined', color: 'secondary' },
style: { style: {
border: '1px solid #C5C5C5', border: '1px solid #C5C5C5',
color: resolvedMode === "dark" ? '#C5C5C5' : '#2D2D2D', color: themeMode === "dark" ? '#C5C5C5' : '#2D2D2D',
whiteSpace: "nowrap", whiteSpace: "nowrap",
textWrap: "normal", textWrap: "normal",
'&:hover': { '&:hover': {
backgroundColor: resolvedMode === "dark" ? '#C5C5C5' : '#EFEFEF', backgroundColor: themeMode === "dark" ? '#C5C5C5' : '#EFEFEF',
borderColor: resolvedMode === "dark" ? '#C5C5C5' : '#2D2D2D', borderColor: themeMode === "dark" ? '#C5C5C5' : '#2D2D2D',
fontWeight: 600, fontWeight: 600,
color: resolvedMode === "dark" ? '#1a1a1a' : '#1A1A1A', color: themeMode === "dark" ? '#1a1a1a' : '#1A1A1A',
}, },
}, },
}, },
@@ -432,8 +423,8 @@ export const getTheme = (themeMode, brandColor) => {
background: 'linear-gradient(90deg, #e6743a 0%, #d4456e 50%, #8a4de8 100%)', background: 'linear-gradient(90deg, #e6743a 0%, #d4456e 50%, #8a4de8 100%)',
}, },
'&:disabled': { '&:disabled': {
background: resolvedMode === "dark" ? '#494949' : '#C9C9C9', background: themeMode === "dark" ? '#494949' : '#C9C9C9',
color: resolvedMode === "dark" ? '#9E9E9E' : '#616161', color: themeMode === "dark" ? '#9E9E9E' : '#616161',
}, },
}, },
}, },
@@ -478,9 +469,9 @@ export const getTheme = (themeMode, brandColor) => {
}, },
'&:disabled': { '&:disabled': {
background: 'transparent', background: 'transparent',
color: resolvedMode === "dark" ? '#9E9E9E' : '#616161', color: themeMode === "dark" ? '#9E9E9E' : '#616161',
'&::before': { '&::before': {
background: resolvedMode === "dark" ? '#494949' : '#C9C9C9', background: themeMode === "dark" ? '#494949' : '#C9C9C9',
}, },
}, },
}, },
@@ -490,7 +481,7 @@ export const getTheme = (themeMode, brandColor) => {
MuiTab: { MuiTab: {
styleOverrides: { styleOverrides: {
root: { root: {
color: resolvedMode === "dark" ? "#C5C5C5" : "#1A1A1A", color: themeMode === "dark" ? "#C5C5C5" : "#1A1A1A",
}, },
}, },
}, },
@@ -499,7 +490,7 @@ export const getTheme = (themeMode, brandColor) => {
overrides: { overrides: {
MuiMenu: { MuiMenu: {
list: { list: {
backgroundColor: resolvedMode === "dark" ? "#27292d" : "#ffffff", backgroundColor: themeMode === "dark" ? "#27292d" : "#ffffff",
}, },
}, },
MuiCssBaseline: { MuiCssBaseline: {
@@ -545,5 +536,4 @@ export const getTheme = (themeMode, brandColor) => {
}, },
}, },
}); });
}
+123 -39
View File
@@ -44,6 +44,7 @@ import {
Add as AddIcon, Add as AddIcon,
Warning as WarningIcon, Warning as WarningIcon,
Pause as PauseIcon, Pause as PauseIcon,
Chat as ChatIcon,
} from '@mui/icons-material' } from '@mui/icons-material'
import { import {
@@ -71,6 +72,7 @@ const AgentUI = (props) => {
const [newSelectedApp, setNewSelectedApp] = React.useState({}) const [newSelectedApp, setNewSelectedApp] = React.useState({})
const [appPickerAnchor, setAppPickerAnchor] = React.useState(null) const [appPickerAnchor, setAppPickerAnchor] = React.useState(null)
const [chosenApps, setChosenApps] = useState([]) const [chosenApps, setChosenApps] = useState([])
const [planningEnabled, setPlanningEnabled] = useState([])
const activateApp = (appId) => { const activateApp = (appId) => {
if (appId === undefined || appId === null || appId === "") { if (appId === undefined || appId === null || appId === "") {
@@ -188,11 +190,11 @@ const AgentUI = (props) => {
const agentWrapperStyle = { const agentWrapperStyle = {
width: "100%", width: "100%",
maxHeight: "100vh", minHeight: "100vh",
margin: "auto", margin: "auto",
backgroundColor: theme.palette.backgroundColor, backgroundColor: theme.palette.backgroundColor,
paddingBottom: showAgentStarter ? 0 : 1500, paddingBottom: showAgentStarter ? 0 : 50,
} }
@@ -248,21 +250,25 @@ const AgentUI = (props) => {
return return
} }
// If no node_id provided, look for the AI Agent node
if (node_id === undefined || node_id === null || node_id === "") { if (node_id === undefined || node_id === null || node_id === "") {
// Look for AI agent
/*
for (var key in execution_data.results) { for (var key in execution_data.results) {
const item = execution_data.results[key] const item = execution_data.results[key]
if (item?.action?.app_name !== "AI Agent") { if (item?.action?.app_name === "AI Agent") {
continue node_id = item?.action?.id
break
} }
node_id = item?.action?.id
break
} }
*/
if (node_id === undefined || node_id === null || node_id === "") { if (node_id === undefined || node_id === null || node_id === "") {
// Fallback: if only one result, use it
if (execution_data?.results?.length === 1) {
setAgentActionResult(execution_data.results[0])
const validatedData = validateJson(execution_data.results[0].result)
if (validatedData.valid) {
setData(validatedData.result)
}
}
return return
} }
} }
@@ -270,6 +276,11 @@ const AgentUI = (props) => {
var found = false var found = false
for (var key in execution_data.results) { for (var key in execution_data.results) {
const item = execution_data.results[key] const item = execution_data.results[key]
if (item?.action?.app_name !== "AI Agent") {
continue
}
if (item?.action?.id !== node_id) { if (item?.action?.id !== node_id) {
continue continue
} }
@@ -289,16 +300,6 @@ const AgentUI = (props) => {
if (found === false) { if (found === false) {
toast.warn("Failed to find the relevant AI Agent result") toast.warn("Failed to find the relevant AI Agent result")
if (execution_data?.results?.length === 1) {
setAgentActionResult(execution_data.results[0])
const validatedData = validateJson(execution_data.results[0].result)
if (validatedData.valid) {
setData(validatedData.result)
} else {
toast.warn("Action output result is not valid JSON!")
}
}
} }
} }
@@ -475,7 +476,7 @@ const AgentUI = (props) => {
getAppAuth() getAppAuth()
}, []) }, [])
const maxTimelineWidth = 375 const maxTimelineWidth = 275
const submitQuestions = (decisionId, questionAnswers, isContinuation) => { const submitQuestions = (decisionId, questionAnswers, isContinuation) => {
console.log("Submitting questions: ", decisionId, questionAnswers) console.log("Submitting questions: ", decisionId, questionAnswers)
@@ -521,8 +522,12 @@ const AgentUI = (props) => {
const executionId = params.get("execution_id") const executionId = params.get("execution_id")
const nodeId = params.get("node_id") const nodeId = params.get("node_id")
const authorization = params.get("authorization") const authorization = params.get("authorization")
const workflowIdParam = params.get("workflow_id")
const url = `${globalUrl}/api/v1/workflows/${executionId}/run?reference_execution=${executionId}&authorization=${authorization}&answer=true&note=${encodeURIComponent(JSON.stringify(newArgument))}&agentic=true&decision_id=${decisionId}` // workflow_id param > execution.workflow.id > executionId
const foundWorkflowId = workflowIdParam !== undefined && workflowIdParam !== null && workflowIdParam !== "" ? workflowIdParam : execution?.workflow?.id !== undefined && execution?.workflow?.id !== null && execution?.workflow?.id !== "" ? execution.workflow.id : executionId
const url = `${globalUrl}/api/v1/workflows/${foundWorkflowId}/run?reference_execution=${executionId}&authorization=${authorization}&answer=true&note=${encodeURIComponent(JSON.stringify(newArgument))}&agentic=true&decision_id=${decisionId}&node_id=${nodeId}`
fetch(url, { fetch(url, {
method: "GET", method: "GET",
credentials: "include", credentials: "include",
@@ -893,15 +898,32 @@ const AgentUI = (props) => {
/> />
</div> </div>
<div style={{ <div style={{
minWidth: 300, minWidth: 400,
maxWidth: 300, maxWidth: 400,
maxHeight: 150, maxHeight: 150,
overflowX: "hidden", overflowX: "hidden",
overflowY: "auto", overflowY: "auto",
paddingTop: defaultTopPadding,
paddingBottom: defaultTopPadding,
}}> }}>
{itemLabel} <Markdown
components={markdownComponents}
id="markdown_wrapper"
className={"style.reactMarkdown"}
escapeHtml={false}
skipHtml={false}
remarkPlugins={[remarkGfm]}
style={{
minWidth: 400,
maxWidth: 450,
maxHeight: 150,
overflowX: "hidden",
overflowY: "auto",
paddingTop: defaultTopPadding,
paddingBottom: defaultTopPadding,
}}
>
{itemLabel}
</Markdown>
</div> </div>
<Tooltip title={`Time taken: ${currentDuration} seconds. Started: ${new Date(itemStartTime * 1000).toLocaleString()}\nFinished: ${new Date(itemEndTime * 1000).toLocaleString()}`} placement="right" <Tooltip title={`Time taken: ${currentDuration} seconds. Started: ${new Date(itemStartTime * 1000).toLocaleString()}\nFinished: ${new Date(itemEndTime * 1000).toLocaleString()}`} placement="right"
@@ -1084,7 +1106,7 @@ const AgentUI = (props) => {
</div> </div>
: null} : null}
{questions?.length > 0 && item?.status === "RUNNING" || item?.status === "WAITING" ? {item.category !== "agent" && questions?.length > 0 && (item?.status === "RUNNING" || item?.status === "WAITING") ?
<div> <div>
{questions.map((q, questionIndex) => { {questions.map((q, questionIndex) => {
return ( return (
@@ -1189,7 +1211,25 @@ const AgentUI = (props) => {
const [continuationText, setContinuationText] = useState("") const [continuationText, setContinuationText] = useState("")
var actionResult = execution?.results?.length > 0 ? execution.results[0] : execution // Find the AI Agent result specifically, not just results[0]
var actionResult = null
if (execution?.results?.length > 0) {
for (var key in execution.results) {
const item = execution.results[key]
if (item?.action?.app_name === "AI Agent") {
actionResult = item
break
}
}
// Fallback to first result if no AI Agent found
if (actionResult === null) {
actionResult = execution.results[0]
}
} else {
actionResult = execution
}
const validate = validateJson(actionResult?.result) const validate = validateJson(actionResult?.result)
if (validate.valid === true) { if (validate.valid === true) {
actionResult.result = validate.result actionResult.result = validate.result
@@ -1246,6 +1286,11 @@ const AgentUI = (props) => {
for (var key in agent_data?.decisions) { for (var key in agent_data?.decisions) {
const item = agent_data.decisions[key] const item = agent_data.decisions[key]
if (item.run_details === undefined) {
console.log("Skipping item without run_details:", item)
continue
}
if (item.run_details.started_at === undefined || item.run_details.started_at === null) { if (item.run_details.started_at === undefined || item.run_details.started_at === null) {
item.run_details.started_at = originalStartTime item.run_details.started_at = originalStartTime
} }
@@ -1495,6 +1540,7 @@ const AgentUI = (props) => {
parsedAction = parsedAction.slice(0, -1) // Remove last comma parsedAction = parsedAction.slice(0, -1) // Remove last comma
} }
/*
const data = { const data = {
"id": uuid, "id": uuid,
"name":"agent", "name":"agent",
@@ -1517,9 +1563,23 @@ const AgentUI = (props) => {
"name":"action", "name":"action",
"value": parsedAction, "value": parsedAction,
} }
]} ],
"planning_mode": planningEnabled,
}
const url = `${globalUrl}/api/v1/apps/agent_starter/run` const url = `${globalUrl}/api/v1/apps/agent_starter/run`
*/
const data = {
"jsonrpc": "2.0",
"method": "tools/call",
"params": {
"tool_name": parsedAction,
"input": {
"text": inputText,
},
}
}
const url = `${globalUrl}/api/v1/agent`
fetch(url, { fetch(url, {
method: "POST", method: "POST",
body: JSON.stringify(data), body: JSON.stringify(data),
@@ -1604,8 +1664,8 @@ const AgentUI = (props) => {
</ButtonGroup> </ButtonGroup>
return ( return (
<div> <div style={{paddingBottom: 500, }}>
<div style={{height: showAgentStarter ? "25vh" : "5vh", }}> <div style={{height: showAgentStarter ? "17vh" : "5vh", }}>
</div> </div>
<div style={agentWrapperStyle}> <div style={agentWrapperStyle}>
<div style={{ <div style={{
@@ -1667,10 +1727,14 @@ const AgentUI = (props) => {
agentRequestLoading ? agentRequestLoading ?
<CircularProgress size={24} style={{marginRight: 10, }} /> <CircularProgress size={24} style={{marginRight: 10, }} />
: :
<Tooltip title="This is the input for the AI Agent. It can be any valid JSON."> <Tooltip title="Start the agent. Data can be anything, and it will run based on your selected tools">
<IconButton type="submit"> <IconButton
type="submit"
color={actionInput === "" || actionInput === undefined || actionInput === null ? "secondary" : "primary"}
disabled={actionInput === "" || actionInput === undefined || actionInput === null}
>
<SendIcon <SendIcon
color="primary" color={actionInput === "" || actionInput === undefined || actionInput === null ? "secondary" : "primary"}
/> />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
@@ -1678,12 +1742,29 @@ const AgentUI = (props) => {
}} }}
/> />
<div style={{display: "flex", margin: "auto", marginTop: 30, minWidth: 300, maxWidth: 300, justifyContent: "center", overflowWrap: "wrap", }}> <div style={{display: "flex", margin: "auto", marginTop: 20, minWidth: 500, maxWidth: 500, justifyContent: "center", overflowWrap: "wrap", }}>
<div style={{}}> <div style={{}}>
{/*
<Tooltip title="Enables/Disables planning mode. Agents will not DO anything - just plan what to do. Primarily used for usecase build helping." placement="top">
<span>
<Chip
id="planning_enabled"
icon={<ChatIcon />} label="Planning Mode"
style={chipStyle}
variant={planningEnabled ? "contained" : "outlined"}
onClick={() => {
setPlanningEnabled(!planningEnabled)
}}
disabled={true}
/>
</span>
</Tooltip>
*/}
<Chip <Chip
id="add_app_chip" id="add_app_chip"
icon={<AddIcon />} label="Select Apps / MCPs" icon={<AddIcon />} label="Select Apps / MCPs"
style={chipStyle} style={chipStyle}
variant={"outlined"}
onClick={() => { onClick={() => {
setAppPickerAnchor(document.getElementById("add_app_chip")) setAppPickerAnchor(document.getElementById("add_app_chip"))
}} }}
@@ -1794,18 +1875,21 @@ const AgentUI = (props) => {
<AvatarGroup> <AvatarGroup>
{chosenApps?.map((app, index) => { {chosenApps?.map((app, index) => {
return( return(
<Tooltip title={`Constraint: ${app?.name}`} key={index} placement="top"> <Tooltip title={`MCP / App: ${app?.name}`} key={index} placement="top">
<Avatar <Avatar
key={index} key={index}
alt={app?.name} alt={app?.name}
src={app?.large_image ? app.large_image : app?.image ? app.image : theme.palette.singulBlackWhite} src={app?.large_image ? app.large_image : app?.image ? app.image : theme.palette.singulBlackWhite}
onClick={() => { onClick={() => {
window.open(`/apps/${app.id}`, '_blank', 'noopener,noreferrer'); if (app?.id !== undefined) {
window.open(`/apps/${app.id}`, '_blank', 'noopener,noreferrer');
}
}} }}
style={{ style={{
cursor: "pointer", cursor: "pointer",
width: 30, width: 30,
height: 30, height: 30,
backgroundColor: theme.palette.backgroundColor,
}} }}
/> />
</Tooltip> </Tooltip>
+457 -115
View File
@@ -231,7 +231,7 @@ export const triggers = [
status: "uninitialized", status: "uninitialized",
trigger_type: "SCHEDULE", trigger_type: "SCHEDULE",
errors: null, errors: null,
large_image: "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OCA0OCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4IiByeD0iOCIgZmlsbD0iIzIxQTBCRCIvPgo8Y2lyY2xlIGN4PSIyNCIgY3k9IjI0IiByPSI4Ljc1IiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjEuNSIvPgo8cGF0aCBkPSJNMjguNSAyNEgyNC4yNUMyNC4xMTE5IDI0IDI0IDIzLjg4ODEgMjQgMjMuNzVWMjAuNSIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K", large_image: "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OCA0OCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4IiByeD0iOCIgZmlsbD0iI0UzQTQxQiIvPgo8cmVjdCB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEyIDEyKSIgZmlsbD0iI0UzQTQxQiIvPgo8Y2lyY2xlIGN4PSIyNCIgY3k9IjI0IiByPSI4Ljc1IiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjEuNSIvPgo8cGF0aCBkPSJNMjguNSAyNEgyNC4yNUMyNC4xMTE5IDI0IDI0IDIzLjg4ODEgMjQgMjMuNzVWMjAuNSIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4=",
label: "Schedule", label: "Schedule",
is_valid: true, is_valid: true,
environment: "onprem", environment: "onprem",
@@ -300,6 +300,11 @@ export const triggers = [
"name": "subflow", "name": "subflow",
"example": "", "example": "",
"value": "", "value": "",
},
{
"name": "subflow_failure",
"example": "",
"value": "",
} }
], ],
status: "running", status: "running",
@@ -498,6 +503,31 @@ export function setActionState(actionId, updates, workflowId = null) {
} }
} }
// Will use this function to remove the action data when the node will get removed from the cytoscape.
export function removeActionState(actionId, workflowId = null) {
if (!actionId) return;
try {
const stored = localStorage.getItem(ACTION_STATES_STORAGE_KEY);
if (!stored) return;
const allStates = JSON.parse(stored);
if (workflowId && allStates[workflowId]) {
delete allStates[workflowId][actionId];
// Clean up empty workflow objects
if (Object.keys(allStates[workflowId]).length === 0) {
delete allStates[workflowId];
}
}
localStorage.setItem(ACTION_STATES_STORAGE_KEY, JSON.stringify(allStates));
} catch (e) {
console.error("Failed to remove action state:", e);
}
}
const splitter = "|~|"; const splitter = "|~|";
const svgSize = 24; const svgSize = 24;
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
@@ -505,7 +535,7 @@ const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
//const referenceUrl = "https://shuffler.io/functions/webhooks/" //const referenceUrl = "https://shuffler.io/functions/webhooks/"
//const referenceUrl = window.location.origin+"/api/v1/hooks/" //const referenceUrl = window.location.origin+"/api/v1/hooks/"
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e") const searchClient = algoliasearch("JNSS5CFDZZ", "33e4e3564f4f060e96e0531957bed552")
const AngularWorkflow = (defaultprops) => { const AngularWorkflow = (defaultprops) => {
const { globalUrl, setCookie, isLoggedIn, isLoaded, userdata, data_id, ReactGA, } = defaultprops; const { globalUrl, setCookie, isLoggedIn, isLoaded, userdata, data_id, ReactGA, } = defaultprops;
const {themeMode, supportEmail, brandColor} = useContext(Context) const {themeMode, supportEmail, brandColor} = useContext(Context)
@@ -566,6 +596,8 @@ const AngularWorkflow = (defaultprops) => {
const [originalWorkflow, setOriginalWorkflow] = React.useState({}); const [originalWorkflow, setOriginalWorkflow] = React.useState({});
const [originalSelectedEnvironment, setOriginalSelectedEnvironment] = React.useState({}); const [originalSelectedEnvironment, setOriginalSelectedEnvironment] = React.useState({});
const [subworkflow, setSubworkflow] = React.useState({}); const [subworkflow, setSubworkflow] = React.useState({});
const [subworkflowFailure, setSubworkflowFailure] = React.useState({});
const [subworkflowFailureStartnode, setSubworkflowFailureStartnode] = React.useState("");
const [subworkflowStartnode, setSubworkflowStartnode] = React.useState(""); const [subworkflowStartnode, setSubworkflowStartnode] = React.useState("");
const [leftViewOpen, setLeftViewOpen] = React.useState(isMobile ? false : true); const [leftViewOpen, setLeftViewOpen] = React.useState(isMobile ? false : true);
const [leftBarSize, setLeftBarSize] = React.useState(isMobile ? 0 : 235) const [leftBarSize, setLeftBarSize] = React.useState(isMobile ? 0 : 235)
@@ -1130,7 +1162,14 @@ const AngularWorkflow = (defaultprops) => {
"description": "Translates your JSON data into a standard formats, then stores it in the Shuffle Datastore", "description": "Translates your JSON data into a standard formats, then stores it in the Shuffle Datastore",
"label": "Translate standard", "label": "Translate standard",
"example": "{\"source_data\": \"{\\\"event\\\": \\\"login\\\", \\\"user\\\": \\\"john_doe\\\", \\\"timestamp\\\": \\\"2023-10-01T12:00:00Z\\\"}\", \"standard\": \"OCSF\"}", "example": "{\"source_data\": \"{\\\"event\\\": \\\"login\\\", \\\"user\\\": \\\"john_doe\\\", \\\"timestamp\\\": \\\"2023-10-01T12:00:00Z\\\"}\", \"standard\": \"OCSF\"}",
"parameters": [{ "parameters": [
{
"name": "app_name",
"value": "",
"required": true,
"multiline": false,
},
{
"name": "source_data", "name": "source_data",
"value": "", "value": "",
"required": true, "required": true,
@@ -1150,7 +1189,14 @@ const AngularWorkflow = (defaultprops) => {
"name": "Cases", "name": "Cases",
"description": "Available actions for case management", "description": "Available actions for case management",
"label": "Cases", "label": "Cases",
"parameters": [{ "parameters": [
{
"name": "app_name",
"value": "",
"required": true,
"multiline": false,
},
{
"name": "action", "name": "action",
"value": "list_tickets", "value": "list_tickets",
"options": [ "options": [
@@ -1175,7 +1221,14 @@ const AngularWorkflow = (defaultprops) => {
"name": "Communication", "name": "Communication",
"description": "Available actions for communication", "description": "Available actions for communication",
"label": "Communication", "label": "Communication",
"parameters": [{ "parameters": [
{
"name": "app_name",
"value": "",
"required": true,
"multiline": false,
},
{
"name": "action", "name": "action",
"value": "list_messages", "value": "list_messages",
"options": [ "options": [
@@ -1210,7 +1263,8 @@ const AngularWorkflow = (defaultprops) => {
"disable_user", "disable_user",
"get_identity", "get_identity",
"get_asset", "get_asset",
"search_identity" "search_identity",
"list_users",
], ],
"required": true, "required": true,
}, },
@@ -2199,6 +2253,26 @@ const AngularWorkflow = (defaultprops) => {
} }
} }
if (param.name === "subflow_failure" && param.value !== undefined && param.value !== null && param.value.length > 0) {
if (param.value === workflow?.id) {
setSubworkflowFailure(workflow);
} else {
const sub = responseJson.find((data) => data?.id === param.value);
if (sub !== undefined) {
setSubworkflowFailure(sub);
// Populate startnode if set
const startnodeParam = trigger.parameters.find((p) => p.name === "subflow_failure_startnode");
if (startnodeParam && startnodeParam.value && sub.actions) {
const foundAction = sub.actions.find((a) => a?.id === startnodeParam.value);
if (foundAction) {
setSubworkflowFailureStartnode(foundAction);
}
}
}
}
}
if (param.name === "startnode" && param.value !== undefined && param.value !== null) { if (param.name === "startnode" && param.value !== undefined && param.value !== null) {
if (Object.getOwnPropertyNames(baseSubflow).length > 0) { if (Object.getOwnPropertyNames(baseSubflow).length > 0) {
@@ -2344,7 +2418,7 @@ const AngularWorkflow = (defaultprops) => {
return return
} }
setExecutionsLoading(true); setExecutionsLoading(true);
var url = `${globalUrl}/api/v2/workflows/${id}/executions` var url = `${globalUrl}/api/v2/workflows/${id}/executions`
var method = "GET" var method = "GET"
@@ -2825,6 +2899,7 @@ const AngularWorkflow = (defaultprops) => {
stop() stop()
return return
} }
//console.log(responseJson) //console.log(responseJson)
// Loop nodes and find results // Loop nodes and find results
// Update on every interval? idk // Update on every interval? idk
@@ -3971,6 +4046,8 @@ const AngularWorkflow = (defaultprops) => {
if (actionAppname === appname) { if (actionAppname === appname) {
workflow.actions[actionkey].selectedAuthentication = item; workflow.actions[actionkey].selectedAuthentication = item;
workflow.actions[actionkey].authentication_id = item.id; workflow.actions[actionkey].authentication_id = item.id;
selectedAction.selectedAuthentication = item;
selectedAction.authentication_id = item.id;
appUpdates = true; appUpdates = true;
} }
} }
@@ -5195,7 +5272,8 @@ const AngularWorkflow = (defaultprops) => {
if (responseJson.public) { if (responseJson.public) {
setAppAuthentication([]) // Delay setting appAuthentication to prevent race condition with graph setup
setTimeout(() => setAppAuthentication([]), 100)
setLeftBarSize(300) setLeftBarSize(300)
if (Object.getOwnPropertyNames(creatorProfile).length === 0) { if (Object.getOwnPropertyNames(creatorProfile).length === 0) {
@@ -5586,7 +5664,7 @@ const AngularWorkflow = (defaultprops) => {
} }
ReactDOM.unstable_batchedUpdates(() => { ReactDOM.unstable_batchedUpdates(() => {
setRightSideBarOpen(true); // setRightSideBarOpen(true);
setLastSaved(false); setLastSaved(false);
/* /*
@@ -6772,7 +6850,7 @@ const AngularWorkflow = (defaultprops) => {
} }
//event.target.unselect(); //event.target.unselect();
setRightSideBarOpen(true); // setRightSideBarOpen(true);
return return
} else if (data.buttonType === "copy") { } else if (data.buttonType === "copy") {
@@ -6864,7 +6942,7 @@ const AngularWorkflow = (defaultprops) => {
if (sourcenode !== null && sourcenode !== undefined) { if (sourcenode !== null && sourcenode !== undefined) {
const sourcedata = sourcenode.data() const sourcedata = sourcenode.data()
if (sourcedata.trigger_type !== "SUBFLOW" && sourcedata.trigger_type !== "USERINPUT") { if (sourcedata?.trigger_type !== "SUBFLOW" && sourcedata?.trigger_type !== "USERINPUT") {
continue continue
} }
@@ -7129,12 +7207,12 @@ const AngularWorkflow = (defaultprops) => {
const tmpAuth = JSON.parse(JSON.stringify(newAppAuth)); const tmpAuth = JSON.parse(JSON.stringify(newAppAuth));
const curappName = curapp.name.toLowerCase() const curappName = curapp.name.toLowerCase().replaceAll(" ", "_")
for (let tmpAuthKey in tmpAuth) { for (let tmpAuthKey in tmpAuth) {
var item = tmpAuth[tmpAuthKey]; var item = tmpAuth[tmpAuthKey];
const newfields = {}; const newfields = {};
if (item.app.name.toLowerCase() !== curappName) { if (item.app.name.toLowerCase().replaceAll(" ", "_") !== curappName) {
continue continue
} }
@@ -7516,7 +7594,6 @@ const AngularWorkflow = (defaultprops) => {
setSelectedTriggerIndex(trigger_index) setSelectedTriggerIndex(trigger_index)
setSelectedTrigger(data) setSelectedTrigger(data)
//setSelectedActionEnvironment(data.env)
}, 25) }, 25)
} else if (data.type === "COMMENT") { } else if (data.type === "COMMENT") {
if (selectedNodes?.length > 1) { if (selectedNodes?.length > 1) {
@@ -7829,7 +7906,7 @@ const AngularWorkflow = (defaultprops) => {
continue continue
} }
const paramname = param.name.toLowerCase().trim().replaceAll("_", " "); const paramname = param.name?.toLowerCase()?.trim()?.replaceAll("_", " ");
const foundresult = GetParamMatch(paramname, exampledata, ""); const foundresult = GetParamMatch(paramname, exampledata, "");
if (foundresult.length > 0) { if (foundresult.length > 0) {
@@ -7866,10 +7943,7 @@ const AngularWorkflow = (defaultprops) => {
continue continue
} }
const paramname = param.name const paramname = param.name?.toLowerCase()?.trim()?.replaceAll("_", " ");
.toLowerCase()
.trim()
.replaceAll("_", " ");
const foundresult = GetParamMatch(paramname, exampledata, ""); const foundresult = GetParamMatch(paramname, exampledata, "");
if (foundresult.length > 0) { if (foundresult.length > 0) {
@@ -8169,11 +8243,11 @@ const AngularWorkflow = (defaultprops) => {
if (workflow.branches[branchkey].destination_id === edge.target && workflow.branches[branchkey].source_id === edge.source) { if (workflow.branches[branchkey].destination_id === edge.target && workflow.branches[branchkey].source_id === edge.source) {
console.log("That branch already exists: ", workflow.branches[branchkey]) console.log("That branch already exists: ", workflow.branches[branchkey])
const foundbranch = cy.getElementById(workflow.branches[branchkey].id) //const foundbranch = cy.getElementById(workflow.branches[branchkey].id)
const foundbranch = cy.getElementById(edge.id)
if (foundbranch !== undefined && foundbranch !== null && foundbranch.data() !== undefined && foundbranch.data() !== null) { if (foundbranch !== undefined && foundbranch !== null && foundbranch.data() !== undefined && foundbranch.data() !== null) {
console.log("Removing branch: ", foundbranch.data()) console.log("Removing branch: ", foundbranch.data())
//event.target.remove()
event.target.remove()
found = true found = true
break break
@@ -8545,6 +8619,10 @@ const AngularWorkflow = (defaultprops) => {
workflow.actions = workflow.actions.filter((a) => a.id !== data.id); workflow.actions = workflow.actions.filter((a) => a.id !== data.id);
workflow.triggers = workflow.triggers.filter((a) => a.id !== data.id); workflow.triggers = workflow.triggers.filter((a) => a.id !== data.id);
// Clean up action state from localStorage
removeActionState(data.id, workflow.id);
if (workflow.start === data.id && workflow.actions.length > 0) { if (workflow.start === data.id && workflow.actions.length > 0) {
// FIXME - should check branches connected to startnode, as picking random // FIXME - should check branches connected to startnode, as picking random
// is just confusing // is just confusing
@@ -8654,7 +8732,7 @@ const AngularWorkflow = (defaultprops) => {
if ((event.ctrlKey || event.metaKey) && !event.shiftKey) { if ((event.ctrlKey || event.metaKey) && !event.shiftKey) {
// If any modal/sidebar is open, let browser handle normal copy // If any modal/sidebar is open, let browser handle normal copy
if (isAnyModalOrSidebarOpen) { if (isAnyModalOrSidebarOpen || event.target?.closest('.MuiDialog-root, .MuiModal-root, [role="dialog"]')) {
return return
} }
@@ -10105,8 +10183,8 @@ const AngularWorkflow = (defaultprops) => {
// Calculates how a branch should curve (it's still weird~) // Calculates how a branch should curve (it's still weird~)
// https://codepen.io/guillaumethomas/pen/xxbbBKO // https://codepen.io/guillaumethomas/pen/xxbbBKO
const calculateEdgeCurve = (sourcenodePosition, destinationnodePosition) => { const calculateEdgeCurve = (sourcenodePosition, destinationnodePosition) => {
const xParsed = destinationnodePosition.x - sourcenodePosition.x const xParsed = destinationnodePosition?.x - sourcenodePosition?.x
const yParsed = destinationnodePosition.y - sourcenodePosition.y const yParsed = destinationnodePosition?.y - sourcenodePosition?.y
const z = Math.sqrt(xParsed * xParsed + yParsed * yParsed) const z = Math.sqrt(xParsed * xParsed + yParsed * yParsed)
const costheta = xParsed / z const costheta = xParsed / z
@@ -10273,7 +10351,7 @@ const AngularWorkflow = (defaultprops) => {
action.iconBackground = iconInfo.iconBackgroundColor action.iconBackground = iconInfo.iconBackgroundColor
action.fillstyle = "linear-gradient" action.fillstyle = "linear-gradient"
} }
}else if(!action.isStartNode) { } else if(!action.isStartNode) {
// This is to round the corners of the image // This is to round the corners of the image
// If action has no large_image (e.g. imported/synced workflow where it was stripped), // If action has no large_image (e.g. imported/synced workflow where it was stripped),
// inject it from the available apps in the sidebar // inject it from the available apps in the sidebar
@@ -10283,6 +10361,7 @@ const AngularWorkflow = (defaultprops) => {
apps.find((a) => a.name === action.app_name) apps.find((a) => a.name === action.app_name)
imageSource = (foundApp && foundApp.large_image) ? foundApp.large_image : "" imageSource = (foundApp && foundApp.large_image) ? foundApp.large_image : ""
} }
const originalBase64 = imageSource !== "" ? imageSource : theme.palette.defaultImage const originalBase64 = imageSource !== "" ? imageSource : theme.palette.defaultImage
const roundedImage = await roundBase64Image(originalBase64, 16); const roundedImage = await roundBase64Image(originalBase64, 16);
action = {...action, large_image: roundedImage} action = {...action, large_image: roundedImage}
@@ -11424,10 +11503,10 @@ const AngularWorkflow = (defaultprops) => {
// No matter what, it's being stopped. // No matter what, it's being stopped.
if (!responseJson.success) { if (!responseJson.success) {
if (responseJson.reason !== undefined) { if (responseJson.reason !== undefined) {
toast("Failed to stop schedule: " + responseJson.reason); toast.warn("Failed to stop schedule: " + responseJson.reason);
} }
} else { } else {
toast("Successfully stopped schedule"); toast.success("Successfully stopped schedule");
} }
if (triggerindex !== undefined && triggerindex !== null && triggerindex >= 0) { if (triggerindex !== undefined && triggerindex !== null && triggerindex >= 0) {
@@ -13161,7 +13240,7 @@ const AngularWorkflow = (defaultprops) => {
if (queryID !== undefined && queryID !== null) { if (queryID !== undefined && queryID !== null) {
aa('init', { aa('init', {
appId: "JNSS5CFDZZ", appId: "JNSS5CFDZZ",
apiKey: "c8f882473ff42d41158430be09ec2b4e", apiKey: "33e4e3564f4f060e96e0531957bed552",
}) })
const timestamp = new Date().getTime() const timestamp = new Date().getTime()
@@ -16032,7 +16111,7 @@ const AngularWorkflow = (defaultprops) => {
onClick={() => { onClick={() => {
// Change Direction of the branch target/source // Change Direction of the branch target/source
const foundBranch = cy.getElementById(selectedEdge.id) const foundBranch = cy.getElementById(selectedEdge.id)
if (foundBranch !== undefined && foundBranch !== null) { if (foundBranch !== undefined && foundBranch !== null && foundBranch?.length > 0) {
const source = foundBranch.data("source") const source = foundBranch.data("source")
const target = foundBranch.data("target") const target = foundBranch.data("target")
@@ -19292,6 +19371,236 @@ const AngularWorkflow = (defaultprops) => {
/> />
) : null} ) : null}
{workflow?.triggers &&
workflow?.triggers[selectedTriggerIndex] &&
workflow?.triggers[selectedTriggerIndex].parameters
? (
<div style={{ marginTop: 15, }}>
<b>On Decline</b>
<Typography variant="body2" color="textSecondary">
Optionally trigger a workflow when the user declines
</Typography>
{workflows === undefined ||
workflows === null ||
workflows.length === 0 ? null : (
<Autocomplete
id="subflow_failure_search"
autoHighlight
value={subworkflowFailure}
classes={{ inputRoot: classes.inputRoot }}
ListboxProps={{
style: {
backgroundColor: theme.palette.inputColor,
color: theme.palette.text.primary,
},
}}
sx={{
'& .MuiOutlinedInput-root': {
height: 40,
},
'& .MuiAutocomplete-input': {
padding: '8px',
},
}}
style={{
backgroundColor: theme.palette.inputColor,
borderRadius: theme.palette?.borderRadius,
marginTop: 10,
}}
getOptionSelected={(option, value) => option.id === value.id}
getOptionLabel={(option) => {
if (option === undefined || option === null || option.name === undefined || option.name === null) {
return "No Workflow Selected";
}
const newname = (option.name.charAt(0).toUpperCase() + option.name.substring(1)).replaceAll("_", " ");
return newname;
}}
options={
[{
"id": "",
"name": "No Workflow Selected",
}].concat(workflows)
}
fullWidth
onChange={(event, newValue) => {
if (newValue === null || newValue === undefined || newValue.id === undefined) {
return
}
var failureParamIndex = workflow.triggers[selectedTriggerIndex].parameters.findIndex((param) => param.name === "subflow_failure")
if (failureParamIndex === -1) {
workflow.triggers[selectedTriggerIndex].parameters.push({
"name": "subflow_failure",
"value": "",
})
failureParamIndex = workflow.triggers[selectedTriggerIndex].parameters.length - 1
}
workflow.triggers[selectedTriggerIndex].parameters[failureParamIndex].value = newValue.id
setSubworkflowFailureStartnode("")
// Fetch workflow to get actions for startnode selection
if (newValue.id.length > 0 && (newValue.actions === undefined || newValue.actions === null || newValue.actions.length === 0)) {
fetch(`${globalUrl}/api/v1/workflows/${newValue.id}`, {
method: "GET",
headers: { "Content-Type": "application/json" },
credentials: "include",
})
.then((resp) => resp.json())
.then((responseJson) => {
if (responseJson.id !== undefined) {
setSubworkflowFailure(responseJson)
// Default startnode
const startAction = responseJson.actions?.find((a) => a.id === responseJson.start)
if (startAction) {
setSubworkflowFailureStartnode(startAction)
}
}
})
.catch((error) => {
console.log("Failed fetching decline workflow: ", error)
})
} else {
setSubworkflowFailure(newValue)
const startAction = newValue.actions?.find((a) => a.id === newValue.start)
if (startAction) {
setSubworkflowFailureStartnode(startAction)
}
}
setWorkflow(workflow)
setUpdate(Math.random())
setLastSaved(false)
event.target.blur()
}}
renderOption={(props, data, state) => {
return (
<MenuItem
{...props}
style={{
color: data.id === workflow.id ? "red" : theme.palette.text.primary,
}}
value={data}
>
<PolylineIcon style={{ marginRight: 8 }} />
{data.name}
</MenuItem>
)
}}
renderInput={(params) => {
return (
<div style={{display: "flex", }}>
<TextField
style={theme.palette.textFieldStyle}
{...params}
label="Workflow to trigger on decline"
variant="outlined"
/>
{subworkflowFailure === null || subworkflowFailure === undefined || subworkflowFailure?.id === undefined || subworkflowFailure?.id === null || subworkflowFailure?.id.length === 0 ? null :
<Tooltip title="Show subflow in new window" placement="top">
<a
rel="noopener noreferrer"
href={`/workflows/${subworkflowFailure.id}`}
target="_blank"
style={{
textDecoration: "none",
color: "#FF8544",
marginLeft: 5,
marginTop: 10,
}}
>
<OpenInNewIcon />
</a>
</Tooltip>
}
</div>
);
}}
/>
)}
{subworkflowFailure?.actions !== undefined && subworkflowFailure?.actions !== null && subworkflowFailure?.actions?.length > 0 ? (
<Autocomplete
id="subflow_failure_startnode_search"
autoHighlight
value={subworkflowFailureStartnode}
classes={{ inputRoot: classes.inputRoot }}
ListboxProps={{
style: {
backgroundColor: theme.palette.inputColor,
color: theme.palette.text.primary,
},
}}
sx={{
'& .MuiOutlinedInput-root': {
height: 40,
},
'& .MuiAutocomplete-input': {
padding: '8px',
},
}}
style={{
backgroundColor: theme.palette.inputColor,
borderRadius: theme.palette?.borderRadius,
marginTop: 10,
}}
getOptionSelected={(option, value) => option.id === value.id}
getOptionLabel={(option) => {
if (option === undefined || option === null || option.label === undefined || option.label === null) {
return "Default";
}
const newname = (option.label.charAt(0).toUpperCase() + option.label.substring(1)).replaceAll("_", " ");
return newname;
}}
options={subworkflowFailure.actions}
fullWidth
onChange={(event, newValue) => {
setSubworkflowFailureStartnode(newValue)
var startnodeParamIndex = workflow.triggers[selectedTriggerIndex].parameters.findIndex((param) => param.name === "subflow_failure_startnode")
if (startnodeParamIndex === -1) {
workflow.triggers[selectedTriggerIndex].parameters.push({
"name": "subflow_failure_startnode",
"value": "",
})
startnodeParamIndex = workflow.triggers[selectedTriggerIndex].parameters.length - 1
}
workflow.triggers[selectedTriggerIndex].parameters[startnodeParamIndex].value = newValue?.id || ""
setWorkflow(workflow)
setUpdate(Math.random())
setLastSaved(false)
}}
renderOption={(props, action, state) => {
return (
<MenuItem
{...props}
style={{
backgroundColor: theme.palette.inputColor,
color: theme.palette.text.primary,
}}
value={action}
>
{action.label}
</MenuItem>
)
}}
renderInput={(params) => {
return (
<TextField
style={theme.palette.textFieldStyle}
{...params}
label="Select a start-node (optional)"
variant="outlined"
/>
);
}}
/>
) : null}
</div>
) : null}
<div style={{ marginTop: 50, }} /> <div style={{ marginTop: 50, }} />
<b>Required Input-Questions</b> <b>Required Input-Questions</b>
@@ -19978,6 +20287,7 @@ const AngularWorkflow = (defaultprops) => {
"&.Mui-selected": { "&.Mui-selected": {
backgroundColor: themeMode === "dark" ? "#1e1e1e" : "#CCCCCC", backgroundColor: themeMode === "dark" ? "#1e1e1e" : "#CCCCCC",
color: theme.palette.text.primary, color: theme.palette.text.primary,
borderRadius: "6px !important",
fontWeight: 600, fontWeight: 600,
"&:hover": { "&:hover": {
backgroundColor: themeMode === "dark" ? "rgba(0,0,0,0.3)" : "rgba(0,0,0,0.1)", backgroundColor: themeMode === "dark" ? "rgba(0,0,0,0.3)" : "rgba(0,0,0,0.1)",
@@ -20020,7 +20330,6 @@ const AngularWorkflow = (defaultprops) => {
justifyContent: "space-between", justifyContent: "space-between",
width: "100%", width: "100%",
position: "relative", position: "relative",
minHeight: 80,
}}> }}>
{/* Left: Workflow Name Container */} {/* Left: Workflow Name Container */}
<div style={{ <div style={{
@@ -20055,12 +20364,68 @@ const AngularWorkflow = (defaultprops) => {
}} }}
> >
{workflow?.name !== undefined && workflow?.name !== null && workflow?.name?.length > 0 ? {workflow?.name !== undefined && workflow?.name !== null && workflow?.name?.length > 0 ?
<EditIcon style={{ position: "absolute", top: 7, height: 20, width: 20, }} /> <EditIcon style={{ position: "absolute", top: 9, height: 20, width: 20, }} />
: :
null null
} }
<span style={{ marginLeft: 30, }}>{workflow.name}</span> <span style={{ marginLeft: 30, }}>{workflow.name}</span>
</Typography> </Typography>
{/* Warning Messages */}
{!distributedFromParent || userdata?.support === true ?
isCorrectOrg ? null :
<Typography variant="body2" style={{ marginLeft: 10, marginTop: 0 }}>
<b>Warning</b>: <span
style={{ color: "#FF8544", cursor: "pointer", pointerEvents: "auto", }}
onClick={() => {
toast.info("Changing to correct organisation. Please wait a few seconds.")
changeOrg()
}}
>Change Active Organization</span> to edit this Workflow.
</Typography>
:
suborgWorkflows?.length === 0 ?
<Typography variant="body2" color="textSecondary" style={{ marginLeft: 10, marginTop: 10 }}>
<b>Warning:</b> This workflow is controlled by your parent org and may not be editable.
</Typography>
:
null
}
{parentWorkflows === undefined || parentWorkflows === null || parentWorkflows.length === 0 ? null :
<div style={{ display: "flex", marginLeft: 10, maxWidth: 250, pointerEvents: "auto", marginTop: 5, }}>
<Typography variant="body2" color="textSecondary" style={{ marginRight: 5, marginTop: 5, }}>
<b>Parent Workflows:</b>
</Typography>
{parentWorkflows.slice(0, 5).map((wf, index) => {
return (
<a href={`/workflows/${wf.id}`} target="_blank" rel="noopener noreferrer" key={index}>
<Tooltip arrow placement="bottom" title={
<span style={{}}>
{wf.image !== undefined && wf.image !== null && wf.image.length > 0 ?
<img
src={wf.image}
alt={wf.name}
style={{ backgroundColor: theme.palette.surfaceColor, maxHeight: 200, minHeigth: 200, borderRadius: theme.palette?.borderRadius, }}
/>
: null}
<Typography>
Parent workflow: '{wf.name}'
</Typography>
</span>
}>
<span onClick={() => {
console.log("Click: ", wf)
}}>
<img src={theme.palette.defaultImage} style={{ height: 25, width: 25, cursor: "pointer", border: 15, marginRight: 5, marginTop: 5, filter: "grayscale(90%)", }} />
</span>
</Tooltip>
</a>
)
})}
</div>
}
</div> </div>
{/* Center: Build/Debug Toggle */} {/* Center: Build/Debug Toggle */}
@@ -20237,6 +20602,7 @@ const AngularWorkflow = (defaultprops) => {
saveWorkflow(workflow, undefined, undefined, e.target.value) saveWorkflow(workflow, undefined, undefined, e.target.value)
/* Standard re-loads */ /* Standard re-loads */
setAllTriggers(undefined) setAllTriggers(undefined)
setSelectedTriggerIndex(-1) setSelectedTriggerIndex(-1)
getEnvironments(e.target.value) getEnvironments(e.target.value)
@@ -20561,7 +20927,7 @@ const AngularWorkflow = (defaultprops) => {
id="execution_location" id="execution_location"
style={{ color: theme.palette.text.primary }} style={{ color: theme.palette.text.primary }}
> >
Runtime Location Runtime Location ({selectedActionEnvironment?.Name})
</InputLabel> </InputLabel>
<Select <Select
labelId="execution_location" labelId="execution_location"
@@ -20575,7 +20941,7 @@ const AngularWorkflow = (defaultprops) => {
}} }}
}} }}
value={ value={
selectedActionEnvironment === undefined || selectedActionEnvironment === null || selectedActionEnvironment.Name === undefined || selectedActionEnvironment.Name === null ? isCloud ? "Cloud" : "Shuffle" : selectedActionEnvironment.Name selectedActionEnvironment === undefined || selectedActionEnvironment === null || selectedActionEnvironment?.Name === undefined || selectedActionEnvironment?.Name === null ? isCloud ? "Cloud" : "Shuffle" : selectedActionEnvironment.Name
} }
SelectDisplayProps={{ SelectDisplayProps={{
style: { style: {
@@ -20700,65 +21066,7 @@ const AngularWorkflow = (defaultprops) => {
</div> </div>
</div> </div>
{/* Warning Messages */}
{!distributedFromParent || userdata?.support === true ?
isCorrectOrg ? null :
<Typography variant="body2" style={{ marginLeft: 10, marginTop: 10 }}>
<b>Warning</b>: <span
style={{ color: "#FF8544", cursor: "pointer", pointerEvents: "auto", }}
onClick={() => {
toast("Changing to correct organisation. Please wait a few seconds.")
changeOrg()
}}
>Change Active Organization</span> to edit this Workflow.
</Typography>
:
suborgWorkflows?.length === 0 ?
<Typography variant="body2" color="textSecondary" style={{ marginLeft: 10, marginTop: 10 }}>
<b>Warning:</b> This workflow is controlled by your parent org and may not be editable.
</Typography>
:
null
}
</div> </div>
{parentWorkflows === undefined || parentWorkflows === null || parentWorkflows.length === 0 ? null :
<div style={{ display: "flex", marginLeft: 40, maxWidth: 250, pointerEvents: "auto", marginTop: 5, }}>
<Typography variant="body2" color="textSecondary" style={{ marginRight: 5, marginTop: 5, }}>
<b>Parent Workflows:</b>
</Typography>
{parentWorkflows.slice(0, 5).map((wf, index) => {
return (
<a href={`/workflows/${wf.id}`} target="_blank" rel="noopener noreferrer" key={index}>
<Tooltip arrow placement="left" title={
<span style={{}}>
{wf.image !== undefined && wf.image !== null && wf.image.length > 0 ?
<img
src={wf.image}
alt={wf.name}
style={{ backgroundColor: theme.palette.surfaceColor, maxHeight: 200, minHeigth: 200, borderRadius: theme.palette?.borderRadius, }}
/>
: null}
<Typography>
Parent workflow: '{wf.name}'
</Typography>
</span>
}>
<span onClick={() => {
console.log("Click: ", wf)
}}>
<img src={theme.palette.defaultImage} style={{ height: 25, width: 25, cursor: "pointer", border: 15, marginRight: 5, marginTop: 5, filter: "grayscale(90%)", }} />
</span>
</Tooltip>
</a>
)
})}
</div>
}
</div> </div>
); );
}; };
@@ -21382,6 +21690,9 @@ const AngularWorkflow = (defaultprops) => {
setElements([]) setElements([])
cy.edges().remove() cy.edges().remove()
cy.nodes().remove() cy.nodes().remove()
// Rebuild graph from new workflow data
setTimeout(() => setupGraph(inputworkflow), 100)
} }
} }
@@ -21603,7 +21914,7 @@ const AngularWorkflow = (defaultprops) => {
if (queryID !== undefined && queryID !== null) { if (queryID !== undefined && queryID !== null) {
aa('init', { aa('init', {
appId: "JNSS5CFDZZ", appId: "JNSS5CFDZZ",
apiKey: "c8f882473ff42d41158430be09ec2b4e", apiKey: "33e4e3564f4f060e96e0531957bed552",
}) })
const timestamp = new Date().getTime(); const timestamp = new Date().getTime();
aa('sendEvents', [ aa('sendEvents', [
@@ -22836,11 +23147,16 @@ const AngularWorkflow = (defaultprops) => {
}} }}
/> />
); );
} else if (execution.execution_source === "ShuffleGPT") { } else if (execution.execution_source === "ShuffleGPT" || execution.execution_source == "Singul") {
return ( return (
<AutoAwesomeIcon <img
color="secondary" alt={"Singul"}
style={{ paddingTop: 8, paddingLeft: 4, height: 25, width: 25, }} src={theme.palette.singulGreen}
style={{
width: size,
height: size,
borderRadius: borderRadius,
}}
/> />
); );
} else if (execution.execution_source === "pipeline") { } else if (execution.execution_source === "pipeline") {
@@ -23334,9 +23650,14 @@ const AngularWorkflow = (defaultprops) => {
getWorkflowExecution(workflow.id, "", executionFilter, workflow.org_id) getWorkflowExecution(workflow.id, "", executionFilter, workflow.org_id)
}} }}
color="secondary" color="secondary"
disabled={executionsLoading}
> >
<CachedIcon style={{ marginRight: 10 }} /> {executionsLoading ? (
Refresh Runs <CircularProgress size={20} style={{ marginRight: 10 }} />
) : (
<CachedIcon style={{ marginRight: 10 }} />
)}
{executionsLoading ? "Refreshing runs" : "Refresh Runs"}
</Button> </Button>
<ButtonGroup <ButtonGroup
fullWidth fullWidth
@@ -23667,7 +23988,7 @@ const AngularWorkflow = (defaultprops) => {
)} )}
</div> </div>
) : ( ) : (
<div style={{ backgroundColor: theme.palette.drawer.backgroundColor, padding: isMobile ? "0px 10px 50px 10px" : "25px 15px 150px 15px", maxWidth: isMobile ? "100%" : "100%", overflowX: "hidden", height: "100%" }}> <div style={{ backgroundColor: theme.palette.drawer.backgroundColor, padding: isMobile ? "0px 10px 50px 10px" : "25px 15px 150px 15px", maxWidth: isMobile ? "100%" : "100%", overflowX: "hidden", height: "100%"}}>
<Breadcrumbs <Breadcrumbs
@@ -24513,7 +24834,7 @@ const AngularWorkflow = (defaultprops) => {
{data?.action?.name === "run_schemaless" || data?.action?.name === "run_singul" || data?.action?.name === "singul" && data?.action?.parameters?.length > 4 ? {data?.action?.name === "run_schemaless" || data?.action?.name === "run_singul" || data?.action?.name === "singul" && data?.action?.parameters?.length > 4 ?
<div <div
style={{ position: "relative", flex: 10, float: "right", textAlign: "right", }} style={{position: "relative", flex: 10, float: "right", textAlign: "right", }}
> >
<Tooltip title={`Explore the raw debug-output: ${data?.action?.parameters?.find((param) => param?.name === "x-debug-url")?.value || ""}`}> <Tooltip title={`Explore the raw debug-output: ${data?.action?.parameters?.find((param) => param?.name === "x-debug-url")?.value || ""}`}>
<a <a
@@ -24792,7 +25113,7 @@ const AngularWorkflow = (defaultprops) => {
}} }}
edge="end" edge="end"
> >
<ContentCopyIcon style={{ heigth: 15, }} /> <ContentCopyIcon style={{heigth: 15, }}/>
</IconButton> </IconButton>
</span> </span>
: :
@@ -24800,7 +25121,7 @@ const AngularWorkflow = (defaultprops) => {
variant="body1" variant="body1"
style={{}} style={{}}
> >
<b>{data.name}</b>: <span style={{ color: "rgba(255,255,255,0.5)" }}> <b>{data.name}</b>: <span style={{color: "rgba(255,255,255,0.5)" }}>
{showVariable ? data.value : null} {showVariable ? data.value : null}
</span> </span>
</Typography> </Typography>
@@ -25056,8 +25377,8 @@ const AngularWorkflow = (defaultprops) => {
<div style={{ display: "flex", marginBottom: 15, position: "sticky", top: -31, zIndex: 10000, }}> <div style={{ display: "flex", marginBottom: 15, position: "sticky", top: -31, zIndex: 10000, }}>
{curapp === null ? null : ( {curapp === null ? null : (
<img <img
alt={selectedResult.action.app_name} alt={selectedResult?.action?.app_name}
src={selectedResult === undefined ? theme.palette.defaultImage : selectedResult?.action?.name === "run_userinput" ? triggers[4].large_image : selectedResult.action.app_name === "shuffle-subflow" ? triggers[3].large_image : selectedResult.action !== undefined && selectedResult.action.large_image !== undefined && selectedResult.action.large_image !== null && selectedResult.action.large_image !== "" ? selectedResult.action.large_image : curapp !== undefined ? curapp.large_image : theme.palette.defaultImage} src={selectedResult === undefined ? theme.palette.defaultImage : selectedResult?.action?.name === "run_userinput" ? triggers[4].large_image : selectedResult?.action?.app_name === "shuffle-subflow" ? triggers[3].large_image : selectedResult?.action !== undefined && selectedResult?.action?.large_image !== undefined && selectedResult?.action?.large_image !== null && selectedResult?.action?.large_image !== "" ? selectedResult?.action?.large_image : curapp !== undefined ? curapp.large_image : theme.palette.defaultImage}
style={{ style={{
marginRight: 20, marginRight: 20,
width: imgsize, width: imgsize,
@@ -25078,10 +25399,10 @@ const AngularWorkflow = (defaultprops) => {
marginBottom: "auto", marginBottom: "auto",
}} }}
> >
<b>{selectedResult.action.label.replaceAll("_", " ")}</b> <b>{selectedResult?.action?.label?.replaceAll("_", " ")}</b>
</div> </div>
<Typography variant="body2" color="textSecondary" style={{ }}> <Typography variant="body2" color="textSecondary" style={{ }}>
{selectedResult.action.name} {selectedResult?.action?.name}
</Typography> </Typography>
</div> </div>
</div> </div>
@@ -25189,6 +25510,14 @@ const AngularWorkflow = (defaultprops) => {
return null; return null;
} }
// DO not show body param in GET method for custom_action
if (selectedResult?.action?.name === "custom_action" && data?.name === "body") {
const methodParam = selectedResult.action.parameters.find(p => p.name === "method");
if (methodParam?.value?.toUpperCase() === "GET") {
return null;
}
}
if ( if (
data.example !== undefined && data.example !== undefined &&
data.example !== null && data.example !== null &&
@@ -25354,15 +25683,18 @@ const AngularWorkflow = (defaultprops) => {
Object.getOwnPropertyNames(selectedAction).length > 0 && Object.getOwnPropertyNames(selectedAction).length > 0 &&
rightSideBarOpen && rightSideBarOpen &&
Object.getOwnPropertyNames(selectedTrigger || {}).length === 0 && Object.getOwnPropertyNames(selectedTrigger || {}).length === 0 &&
Object.getOwnPropertyNames(selectedComment || {}).length === 0 Object.getOwnPropertyNames(selectedComment || {}).length === 0 &&
Object.getOwnPropertyNames(selectedEdge || {}).length === 0
} }
hideBackdrop={true} hideBackdrop={true}
transitionDuration={{ enter: 180, exit: 0 }} transitionDuration={{ enter: 180, exit: 0 }}
onClose={() => { onClose={() => {
setRightSideBarOpen(false); setRightSideBarOpen(false);
setSelectedAction({});
}} }}
ModalProps={{ ModalProps={{
keepMounted: true, keepMounted: true,
disableEnforceFocus: true,
}} }}
sx={{ sx={{
pointerEvents: "none", pointerEvents: "none",
@@ -26237,12 +26569,20 @@ const AngularWorkflow = (defaultprops) => {
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button <Button
style={{ width: 150, margin: "auto", }} style={{ width: 150, margin: "auto", paddingBottom: 50, }}
disabled={false} disabled={false}
variant="outlined" variant="outlined"
onClick={() => { onClick={() => {
setAuthenticationOptions(authenticationOption) setAuthenticationOptions(authenticationOption)
handleSubmitCheck() handleSubmitCheck()
if (isCloud) {
ReactGA.event({
category: "Integration",
action: "Authenticate",
label: `${selectedApp?.name} - Manual`,
})
}
}} }}
color="primary" color="primary"
> >
@@ -26954,7 +27294,7 @@ const AngularWorkflow = (defaultprops) => {
setTimeout(() => { setTimeout(() => {
//toast("Running setupgraph with new revision. Actions: " + newrevision.actions.length) //toast("Running setupgraph with new revision. Actions: " + newrevision.actions.length)
setupGraph(newrevision) setupGraph(newrevision)
}, 250) }, 500)
// Re-adding cytoscape triggers // Re-adding cytoscape triggers
@@ -27276,7 +27616,7 @@ const AngularWorkflow = (defaultprops) => {
}; };
// Check if event.target.value is an array. If it is, split with comma // Check if event.target.value is an array. If it is, split with comma
if (parametername !== undefined && parametername !== undefined && parametername?.startsWith("${") && parametername?.endsWith("}")) { if (parametername !== undefined && parametername !== null && typeof parametername === "string" && parametername?.startsWith("${") && parametername?.endsWith("}")) {
var paramcheckIndex = selectedAction.parameters.findIndex(param => param.name === parametername) var paramcheckIndex = selectedAction.parameters.findIndex(param => param.name === parametername)
if (paramcheckIndex !== -1) { if (paramcheckIndex !== -1) {
// Replace the value in the field // Replace the value in the field
@@ -27312,7 +27652,7 @@ const AngularWorkflow = (defaultprops) => {
} }
} }
if (data.startsWith("${") && data.endsWith("}")) { if (typeof data === "string" && data.startsWith("${") && data.endsWith("}")) {
console.log("Changing field with variable: ", data) console.log("Changing field with variable: ", data)
// PARAM FIX - Gonna use the ID field, even though it's a hack // PARAM FIX - Gonna use the ID field, even though it's a hack
@@ -27385,7 +27725,7 @@ const AngularWorkflow = (defaultprops) => {
} }
} }
if (selectedAction.app_name === "Shuffle Tools" && selectedAction.name === "filter_list" && count === 0) { if (selectedAction !== undefined && selectedAction?.app_name === "Shuffle Tools" && selectedAction?.name === "filter_list" && count === 0) {
const parsedvalue = data const parsedvalue = data
if (parsedvalue.includes("#")) { if (parsedvalue.includes("#")) {
const splitparsed = parsedvalue.split(".#.") const splitparsed = parsedvalue.split(".#.")
@@ -27402,13 +27742,15 @@ const AngularWorkflow = (defaultprops) => {
} }
} }
} else { } else {
if (selectedAction.parameters !== undefined && selectedAction.parameters !== null && selectedAction.parameters.length > count) { if (selectedAction !== undefined && selectedAction !== null && selectedAction?.parameters !== undefined && selectedAction?.parameters !== null && selectedAction.parameters?.length > count) {
selectedAction.parameters[count].autocompleted = false selectedAction.parameters[count].autocompleted = false
selectedAction.parameters[count].value = data selectedAction.parameters[count].value = data
} }
} }
setSelectedAction(selectedAction) if (selectedAction !== undefined && selectedAction !== null) {
setSelectedAction(selectedAction)
}
//setUpdate(Math.random()) //setUpdate(Math.random())
} }
+81 -47
View File
@@ -47,10 +47,11 @@ import Markdown from "react-markdown";
import AuthenticationOauth2 from "../components/Oauth2Auth.jsx"; import AuthenticationOauth2 from "../components/Oauth2Auth.jsx";
import algoliasearch from "algoliasearch/lite"; import algoliasearch from "algoliasearch/lite";
import { green } from "../views/AngularWorkflow.jsx" import { green } from "../views/AngularWorkflow.jsx"
import { setAppCache } from "../views/AppExplorer.jsx"
const searchClient = algoliasearch( const searchClient = algoliasearch(
"JNSS5CFDZZ", "JNSS5CFDZZ",
"c8f882473ff42d41158430be09ec2b4e" "363e707135ea95f5523ec1b262d96b03"
) )
// Lazy loading of ApiExplorer component to reduce initial load time // Lazy loading of ApiExplorer component to reduce initial load time
@@ -139,57 +140,90 @@ const ApiExplorerWrapper = (props) => {
} }
const runAlgoliaAppSearch = (appname) => { const runAlgoliaAppSearch = (appname) => {
const index = searchClient.initIndex("appsearch"); if (appname === "HTTP" || appname === "http") {
navigate("/apis")
return
}
if (appname === "HTTP" || appname === "http") { // Check if appname is likely an ID (hash/UUID) or null - skip cache entirely
navigate("/apis") const isLikelyId = !appname || appname === "null" || (appname.length >= 32 && /^[a-f0-9\-]+$/i.test(appname));
return
}
index // Skip cache for IDs/null, proceed directly to Algolia
.search(appname) if (!isLikelyId) {
.then(({ hits }) => { // Check backend cache first to save Algolia costs
fetch(globalUrl + "/api/v1/apps/" + appname + "/cache")
.then((response) => {
if (!response.ok) {
return { success: false };
}
return response.json();
})
.catch((error) => {
return { success: false };
})
.then((cacheData) => {
if (cacheData && cacheData.success && (cacheData.id || cacheData.objectID)) {
const appId = cacheData.id || cacheData.objectID;
setAppCache(globalUrl, appId, appname);
getAppData(appId);
return;
}
if (hits !== undefined && hits !== null && hits.length > 0) { // Cache miss, fall through to Algolia
const appsearchname = appname.replaceAll("_", " ").toLowerCase() runAlgoliaSearch();
var found = false });
for (var key in hits) { return;
const hit = hits[key] }
const newname = hit.name.replaceAll("_", " ").toLowerCase()
if (newname?.includes(appsearchname)) { // For IDs/null, skip cache and go straight to Algolia
found = true runAlgoliaSearch();
getAppData(hit.objectID) function runAlgoliaSearch() {
break // Fallback to Algolia if not in cache
} const index = searchClient.initIndex("appsearch");
} index
.search(appname)
.then(({ hits }) => {
if (hits !== undefined && hits !== null && hits.length > 0) {
const appsearchname = appname.toLowerCase().replace(/[_ \-]/g, "");
var found = false
for (var key in hits) {
const hit = hits[key]
const newname = hit.name?.toLowerCase().replace(/[_ \-]/g, "");
if (!found) { if (newname?.includes(appsearchname)) {
toast.error(`Failed to get API data for '${appname}' (1). Contact ${supportEmail} if this persists.`, { found = true
"autoClose": 10000, if (!isLikelyId) setAppCache(globalUrl, hit.objectID, appname);
}) getAppData(hit.objectID, appname) // pass alias so backend caches 'outlook' real app
break
}
}
setTimeout(()=>{ if (!found) {
navigate("/search?tab=apps"); toast.error(`Failed to get API data for '${appname}' (1). Contact ${supportEmail} if this persists.`, {
},3000) "autoClose": 10000,
} })
} else { setTimeout(()=>{
toast.error(`Failed to get API data for '${appname}' (2). Contact ${supportEmail} if this persists.`, { navigate("/search?tab=apps");
"autoClose": 10000, },3000)
}) }
setTimeout(()=>{ } else {
navigate("/search?tab=apps"); toast.error(`Failed to get API data for '${appname}' (2). Contact ${supportEmail} if this persists.`, {
},3000) "autoClose": 10000,
} })
}) setTimeout(()=>{
.catch((err) => { navigate("/search?tab=apps");
console.log(err); },3000)
}); }
})
.catch((err) => {
console.log(err);
});
}
} }
// Fetch data when appid is available // Fetch data when appid is available
const getAppData = useCallback((appid) => { const getAppData = useCallback((appid, alias) => {
if (appid === undefined || appid === null || appid.length === 0) { if (appid === undefined || appid === null || appid.length === 0) {
toast.warning("No app ID loaded. Showing default API testing window. ") toast.warning("No app ID loaded. Showing default API testing window. ")
setOpenapi({ setOpenapi({
@@ -217,10 +251,10 @@ const ApiExplorerWrapper = (props) => {
return return
} }
if (appid.length !== 32) { if (appid.length !== 32 && appid.length !== 36) {
runAlgoliaAppSearch(appid) runAlgoliaAppSearch(appid)
return return
} }
const url = `${globalUrl}/api/v1/apps/${appid}/config` const url = `${globalUrl}/api/v1/apps/${appid}/config`
@@ -677,7 +711,7 @@ const ApiExplorerWrapper = (props) => {
toast.info(`This API is being rebuilt due to missing functionality. Please wait a minute or two, then try again. If this persists, please report to ${supportEmail}`, { toast.info(`This API is being rebuilt due to missing functionality. Please wait a minute or two, then try again. If this persists, please report to ${supportEmail}`, {
"autoClose": 90000, "autoClose": 90000,
}) })
} else if (data.result.includes("authentication") && data.result.includes("Oauth2")) { } else if (data.result.includes("authentication") && data.result.includes("Oauth2") && validate?.result?.status !== 200) {
toast.error("Oauth2 apps require authentication") toast.error("Oauth2 apps require authentication")
setAuthHighlighted(true) setAuthHighlighted(true)
+24 -14
View File
@@ -4458,7 +4458,7 @@ const AppCreator = (defaultprops) => {
const hasFile = (data["file_field"] !== undefined && data["file_field"] !== null && data["file_field"].length > 0) || data["example_response"] === "shuffle_file_download" const hasFile = (data["file_field"] !== undefined && data["file_field"] !== null && data["file_field"].length > 0) || data["example_response"] === "shuffle_file_download"
if (data?.action_label[0] !== 'No Label') { if (data?.action_label[0] !== 'No Label') {
console.log("Action label: ", data?.name, data?.action_label) //console.log("Action label: ", data?.name, data?.action_label)
} }
var currentActionLabels = JSON.parse(JSON.stringify(actionLabels)) var currentActionLabels = JSON.parse(JSON.stringify(actionLabels))
@@ -4478,7 +4478,7 @@ const AppCreator = (defaultprops) => {
} }
if (!found) { if (!found) {
console.log(`Not found: '${label}' in ${currentActionLabels}`) //console.log(`Not found: '${label}' in ${currentActionLabels}`)
currentActionLabels.push(data?.action_label[labelKey]) currentActionLabels.push(data?.action_label[labelKey])
} }
} }
@@ -4575,22 +4575,32 @@ const AppCreator = (defaultprops) => {
if (e.target.value.length === 0) { if (e.target.value.length === 0) {
e.target.value = ["No Label"] e.target.value = ["No Label"]
} } else if (e.target.value.length >= 1) {
var foundother = false
const foundIndex = actions.findIndex((action) => action.name === data.name) for (var labelkey in e.target.value) {
if (foundIndex !== undefined && foundIndex !== null && foundIndex >= 0) { const label = e.target.value[labelkey]
if (e.target.value.includes("no_label") || e.target.value.includes("No_Label") || e.target.value.includes("No Label")) { if (label !== "no_label" && label !== "No_Label" && label !== "No Label") {
actions[foundIndex].action_label = ["No Label"] foundother = true
setActions(actions) break
setUpdate(Math.random()) }
return
} }
console.log("Should change: ", e.target.value, " Index: ", index) if (foundother) {
actions[foundIndex].action_label = e.target.value e.target.value = e.target.value.filter(label => label !== "no_label" && label !== "No_Label" && label !== "No Label")
} else {
e.target.value = ["No Label"]
}
}
const foundActionIndex = actions.findIndex((action) => action.name === data.name)
if (foundActionIndex !== undefined && foundActionIndex !== null && foundActionIndex >= 0) {
actions[foundActionIndex].action_label = e.target.value
setActions(actions) setActions(actions)
setUpdate(Math.random()) setUpdate(Math.random())
} else {
console.log("Could not find action: ", data.name)
} }
}} }}
value={data?.action_label} value={data?.action_label}
+90 -28
View File
@@ -65,7 +65,6 @@ import { Context } from "../context/ContextApi.jsx";
import { import {
SearchBox, SearchBox,
StaticRefinementList,
RefinementList, RefinementList,
InstantSearch, InstantSearch,
connectSearchBox, connectSearchBox,
@@ -93,9 +92,18 @@ import aa from "search-insights";
// 2 = OpenAPI (Invalid) // 2 = OpenAPI (Invalid)
const searchClient = algoliasearch( const searchClient = algoliasearch(
"JNSS5CFDZZ", "JNSS5CFDZZ",
"c8f882473ff42d41158430be09ec2b4e" "363e707135ea95f5523ec1b262d96b03"
) )
export const setAppCache = (globalUrl, appId, alias) => {
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
if (!isCloud) return;
fetch(globalUrl + "/api/v1/apps/" + appId + "/cache?alias=" + encodeURIComponent(alias), {
method: "POST",
credentials: "include",
}).catch(() => {});
};
const AppExplorer = (props) => { const AppExplorer = (props) => {
const { const {
globalUrl, globalUrl,
@@ -214,6 +222,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
); );
const [relatedWorkflows, setRelatedWorkflows] = useState(0); const [relatedWorkflows, setRelatedWorkflows] = useState(0);
const [relatedApps, setRelatedApps] = useState(0); const [relatedApps, setRelatedApps] = useState(0);
const [hasCachedCounts, setHasCachedCounts] = useState(false);
const [appAuthentication, setAppAuthentication] = React.useState([]); const [appAuthentication, setAppAuthentication] = React.useState([]);
const [authLoaded, setAuthLoaded] = useState(false); const [authLoaded, setAuthLoaded] = useState(false);
const baseResult = "The execution result will show up here"; const baseResult = "The execution result will show up here";
@@ -301,7 +310,9 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
if (serverside) { if (serverside) {
console.log("Not getting app because serverside."); console.log("Not getting app because serverside.");
} else { } else {
if (params.appid.length === 32 || params.appid.length === 36) { if (params.appid === "new" || params.appid === "edit") {
return;
} else if (params.appid.length === 32 || params.appid.length === 36) {
handleEditApp(params.appid); handleEditApp(params.appid);
runAlgoliaAppSearch(params.appid, false, true); runAlgoliaAppSearch(params.appid, false, true);
} else { } else {
@@ -455,7 +466,6 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
rel="noopener noreferrer" rel="noopener noreferrer"
target="_blank" target="_blank"
href={data.url} href={data.url}
target="_blank"
style={{ textDecoration: "none", color: "#f85a3e" }} style={{ textDecoration: "none", color: "#f85a3e" }}
> >
<Tooltip title={data.url} placement="bottom"> <Tooltip title={data.url} placement="bottom">
@@ -762,7 +772,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
"Accept": "application/json", "Accept": "application/json",
"Org-Id": org_id !== undefined && org_id !== null && org_id?.length > 0 ? org_id : userdata.active_org.id "Org-Id": org_id !== undefined && org_id !== null && org_id?.length > 0 ? org_id : userdata?.active_org?.id
}, },
credentials: "include", credentials: "include",
}) })
@@ -818,7 +828,9 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
setAppId(appid) setAppId(appid)
fetch(globalUrl + "/api/v1/apps/" + appid + "/config", { const url = globalUrl + "/api/v1/apps/" + appid + "/config"
fetch(url, {
method: "GET", method: "GET",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -1450,25 +1462,71 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
}; };
const runAlgoliaAppSearch = (appname, isOriginal, triggerOnly) => { const runAlgoliaAppSearch = (appname, isOriginal, triggerOnly) => {
const index = searchClient.initIndex("appsearch");
console.log("Running appsearch for: ", appname); if (appname === "integration" || appname == "singul") {
if (appname === "integration" || appname == "singul") { // Redirect to https://singul.io
// Redirect to https://singul.io window.location.href = "https://singul.io"
window.location.href = "https://singul.io" } else if (appname === "shuffle_agent" || appname === "agent" || appname === "agents" || appname == "shuffle-agent" || appname == "ai_agents" || appname == "AI Agent") {
} else if (appname === "shuffle_agent" || appname === "agent" || appname === "agents" || appname == "shuffle-agent") { navigate("/agents")
navigate("/agents") return
return }
}
// Check if appname is likely an ID (hash/UUID) or null - skip call
const isLikelyId = !appname || appname === "null" || (appname.length >= 32 && /^[a-f0-9\-]+$/i.test(appname));
const setCache = (appId) => setAppCache(globalUrl, appId, appname);
// Skip cache for IDs/null, proceed directly to Algolia
if (!isLikelyId) {
// For app names, try cache first
fetch(globalUrl + "/api/v1/apps/" + appname + "/cache")
.then((response) => {
if (!response.ok) {
return { success: false };
}
return response.json();
})
.catch((error) => {
return { success: false };
})
.then((cacheData) => {
if (cacheData && cacheData.success && (cacheData.id || cacheData.objectID)) {
const appId = cacheData.id || cacheData.objectID;
setCache(appId);
if (isOriginal !== false) {
handleEditApp(appId);
if (cacheData.related_workflows !== undefined) {
setRelatedWorkflows(cacheData.related_workflows);
setRelatedApps(cacheData.related_apps || 0);
setHasCachedCounts(true);
}
} else {
setSecondaryApp({ objectID: appId, name: appname });
}
return;
}
runAlgoliaSearch();
});
return;
}
// For IDs/null, skip cache and go straight to Algolia
runAlgoliaSearch();
function runAlgoliaSearch() {
const index = searchClient.initIndex("appsearch");
index index
.search(appname) .search(appname)
.then(({ hits }) => { .then(({ hits }) => {
const appsearchname = appname.replaceAll("_", " ").toLowerCase(); const appsearchname = appname.toLowerCase().replace(/[_ \-]/g, "");
var found = false var found = false
if (hits !== undefined && hits !== null && hits.length === 1) { if (hits !== undefined && hits !== null && hits.length === 1) {
found = true found = true
if (!isLikelyId) setCache(hits[0].objectID);
if (isOriginal !== false) { if (isOriginal !== false) {
handleEditApp(hits[0].objectID) handleEditApp(hits[0].objectID)
} else { } else {
@@ -1483,7 +1541,8 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
continue; continue;
} }
if (hit["name"].replaceAll("_", " ").toLowerCase().includes(appsearchname) || hit["objectID"] === appname) { const hitNameClean = hit["name"].toLowerCase().replace(/[_ \-]/g, "");
if (hitNameClean.includes(appsearchname) || hit["objectID"] === appname) {
/* /*
if (hit.triggers !== undefined && hit.triggers !== null && hit.triggers.length > 0) { if (hit.triggers !== undefined && hit.triggers !== null && hit.triggers.length > 0) {
var parsedtriggers = [] var parsedtriggers = []
@@ -1505,9 +1564,11 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
} else { } else {
if (isOriginal !== false) { if (isOriginal !== false) {
found = true found = true
if (!isLikelyId) setCache(hit.objectID);
handleEditApp(hit.objectID); handleEditApp(hit.objectID);
} else { } else {
console.log("Found second app: ", hit); console.log("Found second app: ", hit);
if (!isLikelyId) setCache(hit.objectID);
hit.name = hit.name.charAt(0).toUpperCase() + hit.name.slice(1); hit.name = hit.name.charAt(0).toUpperCase() + hit.name.slice(1);
setSecondaryApp(hit); setSecondaryApp(hit);
} }
@@ -1519,6 +1580,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
if (!found) { if (!found) {
if (hits.length > 0) { if (hits.length > 0) {
setCache(hits[0].objectID);
if (isOriginal !== false) { if (isOriginal !== false) {
handleEditApp(hits[0].objectID) handleEditApp(hits[0].objectID)
} else { } else {
@@ -1536,6 +1598,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
.catch((err) => { .catch((err) => {
console.log(err); console.log(err);
}); });
}
}; };
@@ -3153,7 +3216,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
<Tab style={{color: theme.palette.text.primary, textTransform: "none", }} icon={appType === 0 || appType === 2 ? <OpenInNewIcon /> : <AppsIcon />} label={appType === 0 || appType === 2 ? "Try the API" : "Try it out"} /> <Tab style={{color: theme.palette.text.primary, textTransform: "none", }} icon={appType === 0 || appType === 2 ? <OpenInNewIcon /> : <AppsIcon />} label={appType === 0 || appType === 2 ? "Try the API" : "Try it out"} />
<Tab icon={<ShowChartIcon />} style={{color: theme.palette.text.primary, textTransform: "none", }} label="Stats & Downloads" /> <Tab icon={<ShowChartIcon />} style={{color: theme.palette.text.primary, textTransform: "none", }} label="Stats & Downloads" />
<Tab icon={<PolylineIcon />} style={{color: theme.palette.text.primary, textTransform: "none", }} disabled style={{color: theme.palette.text.secondary}} label="Integrations" /> <Tab icon={<PolylineIcon />} style={{color: theme.palette.text.secondary, textTransform: "none", }} disabled label="Integrations" />
<Tab icon={<PersonIcon />} disabled={userdata.support !== true} style={{color: userdata.support !== true ? theme.palette.text.secondary: theme.palette.text.primary, textTransform: "none", }} label="Creator" value={4} /> <Tab icon={<PersonIcon />} disabled={userdata.support !== true} style={{color: userdata.support !== true ? theme.palette.text.secondary: theme.palette.text.primary, textTransform: "none", }} label="Creator" value={4} />
</Tabs> </Tabs>
<div style={{ marginTop: 25 }}> <div style={{ marginTop: 25 }}>
@@ -3420,7 +3483,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
</div> </div>
<div style={{ textAlign: "center", marginTop: 25 }}> <div style={{ textAlign: "center", marginTop: 25 }}>
<Link <Link
rel="noopener noreferrer" rel="noopener noreferrer nofollow"
to={`/register?app_one=${app.name}&app_two=${secondaryApp.name}&message=You need to login first to connect ${app.name} and ${secondaryApp.name}&view=/apps/${params.appid}/integrations/${secondaryApp.name}`} to={`/register?app_one=${app.name}&app_two=${secondaryApp.name}&message=You need to login first to connect ${app.name} and ${secondaryApp.name}&view=/apps/${params.appid}/integrations/${secondaryApp.name}`}
style={{ textDecoration: "none" }} style={{ textDecoration: "none" }}
> >
@@ -3710,7 +3773,6 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
color="primary" color="primary"
id="checkbox-search" id="checkbox-search"
variant="body1"
style={{ style={{
backgroundColor: theme.palette.inputColor, backgroundColor: theme.palette.inputColor,
borderRadius: theme.palette?.borderRadius, borderRadius: theme.palette?.borderRadius,
@@ -4136,7 +4198,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
Activate for all suborgs Activate for all suborgs
</MenuItem> </MenuItem>
{userdata.orgs.map((data, index) => { {userdata?.orgs?.map((data, index) => {
if (data.creator_org !== userdata.active_org.id) return null; if (data.creator_org !== userdata.active_org.id) return null;
const imageSize = 28; const imageSize = 28;
@@ -4296,7 +4358,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
if (queryID !== undefined && queryID !== null) { if (queryID !== undefined && queryID !== null) {
aa("init", { aa("init", {
appId: "JNSS5CFDZZ", appId: "JNSS5CFDZZ",
apiKey: "c8f882473ff42d41158430be09ec2b4e", apiKey: "26986891ddd374051039420cd77b0fff",
}); });
const timestamp = new Date().getTime(); const timestamp = new Date().getTime();
@@ -4385,7 +4447,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
if (queryID !== undefined && queryID !== null) { if (queryID !== undefined && queryID !== null) {
aa("init", { aa("init", {
appId: "JNSS5CFDZZ", appId: "JNSS5CFDZZ",
apiKey: "c8f882473ff42d41158430be09ec2b4e", apiKey: "363e707135ea95f5523ec1b262d96b03",
}); });
const timestamp = new Date().getTime(); const timestamp = new Date().getTime();
@@ -4573,9 +4635,9 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
maxWidth: height, maxWidth: height,
maxHeight: height, maxHeight: height,
}} }}
onClick={() => { // onClick={() => {
upload.click(); // upload.click();
}} // }}
> >
{imageInfo} {imageInfo}
</div> </div>
@@ -4787,7 +4849,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
> >
<div style={{ padding: "50px 0" }}> <div style={{ padding: "50px 0" }}>
<Typography variant="h4"> <Typography variant="h4">
{relatedWorkflows !== 0 ? ( {hasCachedCounts || relatedWorkflows !== 0 ? (
relatedWorkflows relatedWorkflows
) : ( ) : (
<InstantSearch <InstantSearch
@@ -4881,7 +4943,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
> >
<div style={{ padding: "50px 0" }}> <div style={{ padding: "50px 0" }}>
<Typography variant="h4"> <Typography variant="h4">
{relatedApps !== 0 ? ( {hasCachedCounts || relatedApps !== 0 ? (
relatedApps relatedApps
) : ( ) : (
<InstantSearch <InstantSearch
+11 -7
View File
@@ -70,10 +70,11 @@ const inputColor = "#383B40";
const chipStyle = { const chipStyle = {
backgroundColor: "#3d3f43", backgroundColor: "#3d3f43",
height: 28, height: 30,
marginRight: 5, marginRight: 5,
paddingLeft: 5, paddingLeft: 5,
paddingRight: 5, paddingRight: 5,
height: 28,
cursor: "pointer", cursor: "pointer",
borderColor: "#3d3f43", borderColor: "#3d3f43",
color: "white", color: "white",
@@ -279,7 +280,7 @@ export const GetParsedPaths = (inputdata, basekey) => {
return parsedValues; return parsedValues;
}; };
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e") const searchClient = algoliasearch("JNSS5CFDZZ", "33e4e3564f4f060e96e0531957bed552")
const Apps = (props) => { const Apps = (props) => {
const { globalUrl, isLoggedIn, isLoaded, userdata, serverside, } = props; const { globalUrl, isLoggedIn, isLoaded, userdata, serverside, } = props;
@@ -633,7 +634,7 @@ const Apps = (props) => {
// dropdown with copy etc I guess // dropdown with copy etc I guess
const AppPaper = (props) => { const AppPaper = (props) => {
const { app } = props const { app } = props
let data = app const data = app
if (data.name === "" && data.id === "") { if (data.name === "" && data.id === "") {
return null; return null;
@@ -801,6 +802,7 @@ const Apps = (props) => {
justifyContent: "center", justifyContent: "center",
overflow: "hidden", overflow: "hidden",
maxHeight: 43, maxHeight: 43,
overflow: "hidden",
}} }}
> >
<Typography variant="body2" color="textSecondary"> <Typography variant="body2" color="textSecondary">
@@ -1163,6 +1165,7 @@ const Apps = (props) => {
position: "absolute", position: "absolute",
top: -10, top: -10,
right: isCloud ? 50 : 0, right: isCloud ? 50 : 0,
backgroundColor: theme.palette.surfaceColor,
backgroundColor: inputColor, backgroundColor: inputColor,
color: "white", color: "white",
height: 35, height: 35,
@@ -1302,7 +1305,7 @@ const Apps = (props) => {
if (queryID !== undefined && queryID !== null) { if (queryID !== undefined && queryID !== null) {
aa("init", { aa("init", {
appId: "JNSS5CFDZZ", appId: "JNSS5CFDZZ",
apiKey: "c8f882473ff42d41158430be09ec2b4e", apiKey: "33e4e3564f4f060e96e0531957bed552",
}); });
const timestamp = new Date().getTime(); const timestamp = new Date().getTime();
@@ -1508,6 +1511,7 @@ const Apps = (props) => {
borderRadius: 17 / 2, borderRadius: 17 / 2,
backgroundColor: itemColor, backgroundColor: itemColor,
marginRight: 10, marginRight: 10,
marginTop: 2,
marginTop: "auto", marginTop: "auto",
marginBottom: "auto", marginBottom: "auto",
}} }}
@@ -1941,7 +1945,7 @@ const Apps = (props) => {
const baseImage = <LibraryBooksIcon /> const baseImage = <LibraryBooksIcon />
return ( return (
<div style={{ position: "absolute", marginTop: 15, marginLeft: 0, marginRight: 10, color: "white", zIndex: 1001, backgroundColor: theme.palette.inputColor, minWidth: leftBarSize - 10, maxWidth: leftBarSize - 10, boxShadows: "none", overflowX: "hidden", }}> <div style={{ position: "relative", marginTop: 15, marginLeft: 0, marginRight: 10, position: "absolute", color: "white", zIndex: 1001, backgroundColor: theme.palette.inputColor, minWidth: leftBarSize - 10, maxWidth: leftBarSize - 10, boxShadows: "none", overflowX: "hidden", }}>
<List style={{ backgroundColor: theme.palette.inputColor, }}> <List style={{ backgroundColor: theme.palette.inputColor, }}>
{hits.length === 0 ? {hits.length === 0 ?
<ListItem style={outerlistitemStyle}> <ListItem style={outerlistitemStyle}>
@@ -2032,7 +2036,7 @@ const Apps = (props) => {
if (queryID !== undefined && queryID !== null) { if (queryID !== undefined && queryID !== null) {
aa('init', { aa('init', {
appId: "JNSS5CFDZZ", appId: "JNSS5CFDZZ",
apiKey: "c8f882473ff42d41158430be09ec2b4e", apiKey: "33e4e3564f4f060e96e0531957bed552",
}) })
const timestamp = new Date().getTime() const timestamp = new Date().getTime()
@@ -3011,7 +3015,7 @@ const Apps = (props) => {
<FormControl> <FormControl>
<DialogTitle> <DialogTitle>
<div style={{ color: "rgba(255,255,255,0.9)" }}> <div style={{ color: "rgba(255,255,255,0.9)" }}>
Generate an app based on documentation (beta) Generate an app based on documentation
</div> </div>
</DialogTitle> </DialogTitle>
<DialogContent style={{ color: "rgba(255,255,255,0.65)" }}> <DialogContent style={{ color: "rgba(255,255,255,0.65)" }}>
+99 -31
View File
@@ -34,7 +34,7 @@ import {
import InputAdornment from '@mui/material/InputAdornment'; import InputAdornment from '@mui/material/InputAdornment';
import { ClearRefinements, connectHits, connectSearchBox, connectStateResults, InstantSearch, RefinementList, connectRefinementList, Configure } from "react-instantsearch-dom"; import { ClearRefinements, connectInfiniteHits, connectSearchBox, connectStateResults, InstantSearch, RefinementList, connectRefinementList, Configure } from "react-instantsearch-dom";
import { removeQuery } from "../components/ScrollToTop.jsx"; import { removeQuery } from "../components/ScrollToTop.jsx";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import algoliasearch from "algoliasearch/lite"; import algoliasearch from "algoliasearch/lite";
@@ -47,7 +47,7 @@ import Dropzone from "../components/Dropzone.jsx";
const searchClient = algoliasearch( const searchClient = algoliasearch(
"JNSS5CFDZZ", "JNSS5CFDZZ",
"c8f882473ff42d41158430be09ec2b4e" "eb5fd80aa6ed5ab4730d836cff3ea283"
); );
// AppCard Component // AppCard Component
@@ -297,6 +297,8 @@ const AppCard = ({ data, index, mouseHoverIndex, setMouseHoverIndex, globalUrl,
const Hits = ({ const Hits = ({
userdata, userdata,
hits, hits,
hasMore,
refineNext,
handleAppClick, handleAppClick,
setIsAnyAppActivated, setIsAnyAppActivated,
searchQuery, searchQuery,
@@ -310,13 +312,37 @@ const Hits = ({
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
const [deactivatedIndexes, setDeactivatedIndexes] = React.useState([]); const [deactivatedIndexes, setDeactivatedIndexes] = React.useState([]);
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [isLoadingMore, setIsLoadingMore] = useState(false);
const loadMoreRef = useRef(null);
const isFetchingMore = useRef(false);
const { themeMode } = useContext(Context); const { themeMode } = useContext(Context);
const theme = getTheme(themeMode); const theme = getTheme(themeMode);
useEffect(() => { useEffect(() => {
var baseurl = globalUrl; isFetchingMore.current = false;
setIsLoadingMore(false);
}, [hits.length]);
useEffect(() => {
return; // infinite scroll disabled
if (!loadMoreRef.current) return;
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && hasMore && !isFetchingMore.current) {
isFetchingMore.current = true;
setIsLoadingMore(true);
refineNext();
}
},
{ rootMargin: "200px" }
);
observer.observe(loadMoreRef.current);
return () => observer.disconnect();
}, [hasMore, refineNext]);
useEffect(() => {
setIsLoading(true) setIsLoading(true)
fetch(baseurl + "/api/v1/me", { fetch(globalUrl + "/api/v1/me", {
credentials: "include", credentials: "include",
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -332,7 +358,7 @@ const Hits = ({
.catch(error => { .catch(error => {
console.log("Failed login check: ", error); console.log("Failed login check: ", error);
}); });
}, [currTab]); }, []);
const normalizedString = (name) => { const normalizedString = (name) => {
if (typeof name === 'string') { if (typeof name === 'string') {
@@ -348,6 +374,16 @@ const Hits = ({
} }
}, [currTab, window.location]); }, [currTab, window.location]);
const sortedHits = (() => {
const list = [...(hits || [])];
if (!allActivatedAppIds?.length) return list;
return list.sort((a, b) => {
const aActive = allActivatedAppIds.includes(a.objectID) ? 1 : 0;
const bActive = allActivatedAppIds.includes(b.objectID) ? 1 : 0;
return bActive - aActive;
});
})();
//Function for activation and deactivation of app //Function for activation and deactivation of app
const handleActivateButton = (event, data, type) => { const handleActivateButton = (event, data, type) => {
@@ -384,7 +420,7 @@ const Hits = ({
} else { } else {
//toast.success(`App ${type}d Successfully!`); //toast.success(`App ${type}d Successfully!`);
if (type === 'activate') { if (type === 'activate') {
setAllActivatedAppIds(prev => [...prev, data.objectID]); setAllActivatedAppIds(prev => [...(prev ?? []), data.objectID]);
setIsAnyAppActivated(true); setIsAnyAppActivated(true);
} }
if (type === 'deactivate') { if (type === 'deactivate') {
@@ -405,8 +441,7 @@ const Hits = ({
return ( return (
<div> <div>
{!isLoading ? {(
(
<div> <div>
{hits?.length === 0 && searchQuery.length >= 0 ? ( {hits?.length === 0 && searchQuery.length >= 0 ? (
<div style={{ <div style={{
@@ -452,7 +487,7 @@ const Hits = ({
paddingBottom: 40 paddingBottom: 40
}} }}
> >
{hits?.map((data, index) => { {sortedHits.map((data, index) => {
const appUrl = const appUrl =
isCloud isCloud
? `/apps/${data.objectID}?queryID=${data.__queryID}` ? `/apps/${data.objectID}?queryID=${data.__queryID}`
@@ -704,13 +739,18 @@ const Hits = ({
</Zoom> </Zoom>
); );
})} })}
<div ref={loadMoreRef} style={{ gridColumn: "1 / -1", height: 10 }} />
{isLoadingMore && (
<>
{[...Array(3)].map((_, i) => (
<AppSkeleton key={`skeleton-more-${i}`} />
))}
</>
)}
</div> </div>
)} )}
</div> </div>
) : ( )}
<LoadingGrid />
)
}
</div > </div >
); );
} }
@@ -811,7 +851,7 @@ const SearchBox = ({ refine, searchQuery, setSearchQuery }) => {
}; };
const CustomSearchBox = connectSearchBox(SearchBox); const CustomSearchBox = connectSearchBox(SearchBox);
const CustomHits = connectHits(Hits); const CustomHits = connectInfiniteHits(Hits);
// Custom Category Dropdown Component // Custom Category Dropdown Component
const CategoryDropdown = ({ items, currentRefinement, refine }) => { const CategoryDropdown = ({ items, currentRefinement, refine }) => {
@@ -1108,7 +1148,7 @@ const Apps2 = (props) => {
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [selectedCategory, setSelectedCategory] = useState([]); const [selectedCategory, setSelectedCategory] = useState([]);
const [selectedLabel, setSelectedLabel] = useState([]); const [selectedLabel, setSelectedLabel] = useState([]);
const [currTab, setCurrTab] = useState(0); const [currTab, setCurrTab] = useState(isLoggedIn ? 0 : 2);
const [categories, setCategories] = useState([]); const [categories, setCategories] = useState([]);
const [labels, setLabels] = useState([]); const [labels, setLabels] = useState([]);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
@@ -1142,6 +1182,21 @@ const Apps2 = (props) => {
const {themeMode, brandColor} = useContext(Context); const {themeMode, brandColor} = useContext(Context);
const theme = getTheme(themeMode, brandColor); const theme = getTheme(themeMode, brandColor);
const conditionalSearchClient = useMemo(() => ({
...searchClient,
search(requests) {
if (currTab !== 2) {
return Promise.resolve({
results: requests.map(() => ({
hits: [], nbHits: 0, page: 0, nbPages: 0, hitsPerPage: 0,
processingTimeMS: 0, exhaustiveNbHits: true, query: "", params: "",
})),
});
}
return searchClient.search(requests);
},
}), [currTab]);
const baseRepository = "https://github.com/frikky/shuffle-apps"; const baseRepository = "https://github.com/frikky/shuffle-apps";
const isCloud = const isCloud =
@@ -1165,6 +1220,17 @@ const Apps2 = (props) => {
} }
}, [location.search]); }, [location.search]);
// When auth loads and user is logged in, default to tab 0 if no tab param in URL
useEffect(() => {
if (isLoggedIn) {
const queryParams = new URLSearchParams(location.search);
const tabParam = queryParams.get('tab');
if (!tabParam) {
setCurrTab(0);
}
}
}, [isLoggedIn]);
useEffect(() => { useEffect(() => {
const getFramework = () => { const getFramework = () => {
@@ -1900,7 +1966,7 @@ const Apps2 = (props) => {
onDrop={uploadFile} onDrop={uploadFile}
> >
<div style={{ paddingTop: 70, paddingLeft: leftSideBarOpenByClick ? 200 : 0, transition: "padding-left 0.3s ease", backgroundColor: theme.palette.backgroundColor, fontFamily: theme?.typography?.fontFamily, zoom: 0.7, }}> <div style={{ paddingTop: 70, paddingLeft: leftSideBarOpenByClick ? 200 : 0, transition: "padding-left 0.3s ease", backgroundColor: theme.palette.backgroundColor, fontFamily: theme?.typography?.fontFamily, zoom: 0.7, }}>
<InstantSearch searchClient={searchClient} indexName="appsearch"> <InstantSearch key={currTab === 2 ? "active" : "inactive"} searchClient={conditionalSearchClient} indexName="appsearch">
<AppModal <AppModal
open={openModal} open={openModal}
onClose={handleAppModalClose} onClose={handleAppModalClose}
@@ -2412,24 +2478,26 @@ const Apps2 = (props) => {
} }
{ {
currTab === 2 && currTab === 2 && (
<CustomHits <>
isLoggedIn={isLoggedIn} <Configure clickAnalytics hitsPerPage={20} />
userdata={userdata} <CustomHits
handleAppClick={handleAppClick} isLoggedIn={isLoggedIn}
setIsAnyAppActivated={setIsAnyAppActivated} userdata={userdata}
hitsPerPage={5} handleAppClick={handleAppClick}
globalUrl={globalUrl} setIsAnyAppActivated={setIsAnyAppActivated}
searchQuery={searchQuery} globalUrl={globalUrl}
mouseHoverIndex={mouseHoverIndex} searchQuery={searchQuery}
setMouseHoverIndex={setMouseHoverIndex} mouseHoverIndex={mouseHoverIndex}
currTab={currTab} setMouseHoverIndex={setMouseHoverIndex}
leftSideBarOpenByClick={leftSideBarOpenByClick} currTab={currTab}
/> leftSideBarOpenByClick={leftSideBarOpenByClick}
/>
</>
)
} }
</div> </div>
</div > </div >
<Configure clickAnalytics />
</InstantSearch> </InstantSearch>
</div> </div>
</Dropzone> </Dropzone>
+18 -1
View File
@@ -473,6 +473,7 @@ const Docs = (defaultprops) => {
const [hasRedirected, setHasRedirected] = useState(false) const [hasRedirected, setHasRedirected] = useState(false)
var isArticlePage = window.location.pathname.includes("/articles/") || window.location.pathname === "/articles" ? true : false; var isArticlePage = window.location.pathname.includes("/articles/") || window.location.pathname === "/articles" ? true : false;
const searchFieldRef = useRef(null); const searchFieldRef = useRef(null);
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io" || window.location.host === "migration.shuffler.io";
const handleDocRedirectForPartners = () => { const handleDocRedirectForPartners = () => {
if (hasRedirected) return; if (hasRedirected) return;
@@ -1279,6 +1280,7 @@ const Docs = (defaultprops) => {
overflow: "hidden", overflow: "hidden",
paddingBottom: 100, paddingBottom: 100,
marginLeft: mobile ? 0 : 50, marginLeft: mobile ? 0 : 50,
marginTop: 50,
textAlign: "center", textAlign: "center",
margin: "auto", margin: "auto",
marginTop: 50, marginTop: 50,
@@ -1289,6 +1291,9 @@ const Docs = (defaultprops) => {
{showPartnerLogo === true ? null : {showPartnerLogo === true ? null :
<div style={{ display: "flex", marginTop: 25, }}> <div style={{ display: "flex", marginTop: 25, }}>
<CustomButton title="Talk to Support" icon=<img src="/images/Shuffle_logo_new.png" style={{ height: 35, width: 35, border: "", borderRadius: theme.palette?.borderRadius, }} /> link="https://shuffler.io/contact?category=support" /> <CustomButton title="Talk to Support" icon=<img src="/images/Shuffle_logo_new.png" style={{ height: 35, width: 35, border: "", borderRadius: theme.palette?.borderRadius, }} /> link="https://shuffler.io/contact?category=support" />
{isCloud ?
<CustomButton title="Support LLM" icon=<img src={theme.palette.singulBlackWhite} style={{ height: 35, width: 35, border: "", borderRadius: theme.palette?.borderRadius, }} /> link="https://shuffler.io/chat" />
: null}
<CustomButton title="Ask the community" icon=<img src="/images/social/discord.png" style={{ height: 35, width: 35, border: "", borderRadius: theme.palette?.borderRadius, }} /> link="https://discord.gg/B2CBzUm" /> <CustomButton title="Ask the community" icon=<img src="/images/social/discord.png" style={{ height: 35, width: 35, border: "", borderRadius: theme.palette?.borderRadius, }} /> link="https://discord.gg/B2CBzUm" />
</div> </div>
} }
@@ -1566,9 +1571,21 @@ const Docs = (defaultprops) => {
</Button> </Button>
</Tooltip> </Tooltip>
)} )}
<div />
{isCloud && window.location.pathname !== "/docs" ?
<Link to="/chat">
<Button
variant="outlined"
color="secondary"
style={{marginBottom: 20, }}
>
Support LLM
</Button>
</Link>
: null}
{tocLines.length > 0 ? {tocLines.length > 0 ?
( (
<h4 style={{ fontWeight: 600, margin: 0, fontSize: "16px", marginBottom: "8px", color: theme.palette.text.primary, }}>Table Of Content</h4> <h4 style={{ marginTop: 20, fontWeight: 600, margin: 0, fontSize: "16px", marginBottom: "8px", color: theme.palette.text.primary, }}>Table Of Content</h4>
) : null} ) : null}
<div <div
+21 -14
View File
@@ -160,6 +160,21 @@ const FreePlanCard = ({ classes }) => {
const MarketplaceCard = ({ classes, isCloud }) => { const MarketplaceCard = ({ classes, isCloud }) => {
const marketplaceOptions = [ const marketplaceOptions = [
{
name: "Google Cloud Platform",
logo: "https://cdn.cdnlogo.com/logos/g/75/google-cloud.svg",
tooltipText: "Click to deploy on Google Cloud Platform!",
valid: true,
link: "https://console.cloud.google.com/marketplace/product/shuffle-public/shuffle"
},
{
name: "Amazon Web Services",
logo: isCloud ? "/icons/aws_logo.svg" : "/images/icons/aws_logo.svg",
logoStyle: { width: 48, height: 48, marginRight: 12 },
tooltipText: "Click to deploy on AWS Marketplace!",
valid: true,
link: "https://aws.amazon.com/marketplace/pp/prodview-typ7upg6kwntk"
},
{ {
name: "Open Source Install", name: "Open Source Install",
logo: "https://static.cdnlogo.com/logos/g/69/github-icon.svg", logo: "https://static.cdnlogo.com/logos/g/69/github-icon.svg",
@@ -168,23 +183,11 @@ const MarketplaceCard = ({ classes, isCloud }) => {
link: "https://github.com/shuffle/shuffle/blob/main/.github/install-guide.md" link: "https://github.com/shuffle/shuffle/blob/main/.github/install-guide.md"
}, },
{
name: "Amazon Web Services",
logo: "https://cdn.cdnlogo.com/logos/a/19/aws.svg",
tooltipText: "Coming soon to AWS Marketplace!",
valid: false,
},
{ {
name: "Microsoft Azure", name: "Microsoft Azure",
logo: "https://cdn.cdnlogo.com/logos/a/12/azure.svg", logo: "https://cdn.cdnlogo.com/logos/a/12/azure.svg",
tooltipText: "Coming soon to Azure Marketplace!", tooltipText: "Coming soon to Azure Marketplace!",
valid: false, valid: false,
},
{
name: "Google Cloud Platform",
logo: "https://cdn.cdnlogo.com/logos/g/75/google-cloud.svg",
tooltipText: "Coming soon to Google Cloud Marketplace!",
valid: false,
} }
]; ];
@@ -251,6 +254,7 @@ const MarketplaceCard = ({ classes, isCloud }) => {
className={classes.marketplaceIcon} className={classes.marketplaceIcon}
style={{ style={{
filter: option.valid === true ? null : "grayscale(1)", filter: option.valid === true ? null : "grayscale(1)",
...(option.logoStyle || {}),
}} }}
/> />
<Typography className={classes.marketplaceText}> <Typography className={classes.marketplaceText}>
@@ -584,6 +588,7 @@ const LoginPage = props => {
}); });
} }
setLoginInfo(responseJson["reason"]) setLoginInfo(responseJson["reason"])
setLoginLoading(false)
} 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 !== "") {
toast.info("Set region to " + responseJson.region_url) toast.info("Set region to " + responseJson.region_url)
@@ -607,6 +612,7 @@ const LoginPage = props => {
} }
else if (responseJson["reason"] !== undefined && responseJson["reason"] !== null && responseJson["reason"].includes("error")) { else if (responseJson["reason"] !== undefined && responseJson["reason"] !== null && responseJson["reason"].includes("error")) {
setLoginInfo(responseJson["reason"]) setLoginInfo(responseJson["reason"])
setLoginLoading(false)
return return
} }
@@ -687,6 +693,7 @@ const LoginPage = props => {
}); });
} }
setLoginInfo(responseJson["reason"]) setLoginInfo(responseJson["reason"])
setLoginLoading(false)
} else { } else {
if (responseJson["reason"] === "shuffle_account") { if (responseJson["reason"] === "shuffle_account") {
window.location.href = "/login?message=Please+login+with+your+Shuffle+account" window.location.href = "/login?message=Please+login+with+your+Shuffle+account"
@@ -812,7 +819,7 @@ const LoginPage = props => {
width: "max-content", width: "max-content",
}} }}
> >
<form onSubmit={onSubmit} style={{ margin: 15, width: "max-content", overflow: "hidden", textAlign: "center", }}> <form onSubmit={onSubmit} style={{ margin: 15, width: isMobile ? "100%" : "360px", width: "max-content", overflow: "hidden", textAlign: "center", }}>
<img <img
style={{ style={{
height: isMobile ? 44 : 60, height: isMobile ? 44 : 60,
@@ -1047,7 +1054,7 @@ const LoginPage = props => {
</div> </div>
</div> </div>
<div style={{ marginTop: "10px", color: "white" }}> <div style={{ marginTop: 30, color: "white" }}>
{loginInfo} {loginInfo}
</div> </div>
+2 -2
View File
@@ -375,7 +375,7 @@ const NewDashboard = (props) => {
return ( return (
<div style={{ maxWidth: 1366, margin: '0 auto', padding: 16, paddingTop: 50, paddingBottom: 30, paddingLeft: leftSideBarOpenByClick ? 270 : 80, transition: 'padding-left 0.3s ease', position: 'relative' }}> <div style={{ maxWidth: 1366, margin: '0 auto', padding: 16, paddingTop: 50, paddingBottom: 30, paddingLeft: leftSideBarOpenByClick ? 270 : 80, transition: 'padding-left 0.3s ease', position: 'relative' }}>
{(onboardingOpen && !loadingSelectedOrgStats) && ( {/*(onboardingOpen && !loadingSelectedOrgStats) && (
<DashboardOnboarding <DashboardOnboarding
open={onboardingOpen} open={onboardingOpen}
globalUrl={globalUrl} globalUrl={globalUrl}
@@ -396,7 +396,7 @@ const NewDashboard = (props) => {
headerTitle="Unlock your Dashboard" headerTitle="Unlock your Dashboard"
headerSubtitle="Complete these steps to start seeing insights." headerSubtitle="Complete these steps to start seeing insights."
/> />
)} )*/}
{showOverlay && ( {showOverlay && (
<div style={{ position: 'absolute', inset: 0, background: 'rgba(17,17,17,0.6)', backdropFilter: 'blur(2px)', zIndex: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 12 }}> <div style={{ position: 'absolute', inset: 0, background: 'rgba(17,17,17,0.6)', backdropFilter: 'blur(2px)', zIndex: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 12 }}>
+111 -63
View File
@@ -95,7 +95,7 @@ const RunWorkflow = (defaultprops) => {
const answer = searchParams.get("answer") const answer = searchParams.get("answer")
const execution_id = searchParams.get("reference_execution") const execution_id = searchParams.get("reference_execution")
const authorization = searchParams.get("authorization") const authorization = searchParams.get("authorization")
const sourceNode = searchParams.get("source_node") const sourceNode = searchParams.get("source_node") || searchParams.get("start")
const decisionId = searchParams.get("decision_id") // ONLY for agentic workflows const decisionId = searchParams.get("decision_id") // ONLY for agentic workflows
const backendUrl = searchParams.get("backend_url") || globalUrl const backendUrl = searchParams.get("backend_url") || globalUrl
@@ -847,7 +847,9 @@ const RunWorkflow = (defaultprops) => {
}) })
.then((responseJson) => { .then((responseJson) => {
if (responseJson.success === false) { if (responseJson.success === false) {
toast.warn("Failed getting the workflow. Please contact support@shuffler.io if this persists.") if (workflow_id !== execution_id) {
toast.warn("Failed getting the workflow. Please contact support@shuffler.io if this persists.")
}
return return
} }
@@ -1182,9 +1184,9 @@ const RunWorkflow = (defaultprops) => {
if (props.match.params.key === undefined) { if (props.match.params.key === undefined) {
setExplorerUi(true) setExplorerUi(true)
if (isLoggedIn) { if (isLoggedIn && userdata?.active_org?.id) {
loadForms(userdata.active_org.id) loadForms(userdata?.active_org?.id)
handleGetOrg(userdata.active_org.id) handleGetOrg(userdata?.active_org?.id)
} }
return return
@@ -1269,24 +1271,28 @@ const RunWorkflow = (defaultprops) => {
//console.log("IMG: ", image, "ORG: ", selectedOrganization) //console.log("IMG: ", image, "ORG: ", selectedOrganization)
if (!disabledButtons && answer !== undefined && answer !== null && organization !== "Unknown" && buttonClicked.length === 0) { useEffect(() => {
console.log("Finding button!") if (disabledButtons || answer === undefined || answer === null || organization === "Unknown" || buttonClicked.length > 0) {
// Find the button return
var buttonid = "" }
// Show rejection form for answer=false instead of auto-clicking
if (answer === "false") { if (answer === "false") {
buttonid = "abort_execution" return
} else if (answer === "true") { }
var buttonid = ""
if (answer === "true") {
buttonid = "continue_execution" buttonid = "continue_execution"
} }
if (buttonid !== "") { if (buttonid !== "") {
const foundButton = document.getElementById(buttonid) const foundButton = document.getElementById(buttonid)
console.log("Button: ", foundButton)
if (foundButton !== undefined && foundButton !== null) { if (foundButton !== undefined && foundButton !== null) {
foundButton.click() foundButton.click()
} }
} }
} }, [disabledButtons, answer, organization, buttonClicked])
const FormList = () => { const FormList = () => {
return ( return (
@@ -1697,61 +1703,103 @@ const RunWorkflow = (defaultprops) => {
} }
{disabledButtons ? null : {disabledButtons ? null :
<Typography variant="body2" color="textSecondary" style={{textAlign: "center", marginTop: 10, }}> answer === "false" ?
What do you want to do? <Typography variant="body2" color="textSecondary" style={{textAlign: "center", marginTop: 10, }}>
</Typography> Why are you declining?
</Typography>
:
<Typography variant="body2" color="textSecondary" style={{textAlign: "center", marginTop: 10, }}>
What do you want to do?
</Typography>
} }
<div fullWidth style={{width: "100%", marginTop: 10, marginBottom: 10, display: "flex", }}> {answer === "false" ?
<Button <div style={{width: "100%", marginTop: 10, marginBottom: 10, }}>
fullWidth <TextField
id="continue_execution" fullWidth
variant="contained" multiline
disabled={!handleValidateForm(executionArgument) || disabledButtons} minRows={3}
color="primary" maxRows={6}
style={{ variant="outlined"
flex: 1, placeholder="Provide a reason for declining (optional)"
textTransform: "none", value={executionArgument || ""}
}} onChange={(e) => {
onClick={() => { setExecutionArgument(e.target.value)
// Timeout 2500 just in case }}
setTimeout(() => { style={{marginBottom: 10, }}
setButtonClicked("FINISHED") />
setExecutionData({ <Button
status: "FINISHED", fullWidth
}) id="abort_execution"
}, 2500) variant="contained"
disabled={disabledButtons}
color="primary"
style={{textTransform: "none", }}
onClick={() => {
setTimeout(() => {
setButtonClicked("ABORTED")
setExecutionData({
status: "ABORTED",
})
}, 2500)
onSubmit(null, execution_id, authorization, true) onSubmit(null, execution_id, authorization, false)
}}>
Confirm Decline
</Button>
</div>
:
<div fullWidth style={{width: "100%", marginTop: 10, marginBottom: 10, display: "flex", }}>
<Button
fullWidth
id="continue_execution"
variant="contained"
disabled={!handleValidateForm(executionArgument) || disabledButtons}
color="primary"
style={{
flex: 1,
textTransform: "none",
}}
onClick={() => {
// Timeout 2500 just in case
setTimeout(() => {
setButtonClicked("FINISHED")
setExecutionData({
status: "FINISHED",
})
}, 2500)
onSubmit(null, execution_id, authorization, true)
}}>
Continue</Button>
<Typography variant="body1" style={{marginLeft: 3, marginRight: 3, marginTop: 3, }}>
&nbsp;or&nbsp;
</Typography>
<Button
fullWidth
id="abort_execution"
variant="outlined"
disabled={!handleValidateForm(executionArgument) || disabledButtons}
color="primary"
style={{
flex: 1,
textTransform: "none",
}} onClick={() => {
setTimeout(() => {
setButtonClicked("ABORTED")
setExecutionData({
status: "ABORTED",
})
}, 2500)
onSubmit(null, execution_id, authorization, false)
}}> }}>
Continue</Button> Stop
<Typography variant="body1" style={{marginLeft: 3, marginRight: 3, marginTop: 3, }}> </Button>
&nbsp;or&nbsp; </div>
</Typography> }
<Button
fullWidth
id="abort_execution"
variant="outlined"
disabled={!handleValidateForm(executionArgument) || disabledButtons}
color="primary"
style={{
flex: 1,
textTransform: "none",
}} onClick={() => {
setTimeout(() => {
setButtonClicked("ABORTED")
setExecutionData({
status: "ABORTED",
})
}, 2500)
onSubmit(null, execution_id, authorization, false) {answer !== "false" && handleValidateForm(executionArgument) === false && disabledButtons === false ?
}}>
Stop
</Button>
</div>
{handleValidateForm(executionArgument) === false && disabledButtons === false ?
<Typography variant="body2" color="textSecondary" style={{textAlign: "center", marginTop: 10, underline: "1px solid grey", }}> <Typography variant="body2" color="textSecondary" style={{textAlign: "center", marginTop: 10, underline: "1px solid grey", }}>
All required questions have not been answered yet. All required questions have not been answered yet.
</Typography> </Typography>
+36 -10
View File
@@ -25,6 +25,8 @@ const SetAuthentication = (props) => {
const [loadFail, setLoadFail] = useState(""); const [loadFail, setLoadFail] = useState("");
const [appAuthentication, setAppAuthentication] = React.useState([]); const [appAuthentication, setAppAuthentication] = React.useState([]);
const [showShuffle, setShowShuffle] = React.useState(false);
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
//const alert = useAlert(); //const alert = useAlert();
@@ -107,10 +109,16 @@ const SetAuthentication = (props) => {
// Set the orgsession to be a cookie for __session cookie // Set the orgsession to be a cookie for __session cookie
setTimeout(() => { setTimeout(() => {
// Not sure this does anything lol
document.cookie = "__session=" + orgsession + "; path=/; max-age=1800; SameSite=Lax"; // Cookie expires in 30 min document.cookie = "__session=" + orgsession + "; path=/; max-age=1800; SameSite=Lax"; // Cookie expires in 30 min
}, 1000) }, 1000)
} }
const fromSource = urlParams.get("source")
if (fromSource === "shuffle") {
setShowShuffle(true)
}
if (appid === null) { if (appid === null) {
setLoadFail( setLoadFail(
<span> <span>
@@ -146,23 +154,29 @@ const SetAuthentication = (props) => {
const appname = app.name !== undefined ? app.name : "" const appname = app.name !== undefined ? app.name : ""
const appLink = "/apps/" + app.id || "" const appLink = "/apps/" + app.id || ""
console.log("App: ", app)
return ( return (
<div style={{width: 1000, margin: "auto", paddingTop: 50, }}> <div style={{width: 1000, margin: "auto", paddingTop: 50, backgroundColor: theme.palette.backgroundColor, height: "200vh",}}>
{loadFail !== "" ? {loadFail !== "" ?
loadFail loadFail
: :
<><div> <><div>
<Typography variant="h4" style={{ marginBottom: 20, }}> <Typography variant="h4" style={{ marginBottom: 20, }}>
You are invited to: Configure <a href={appLink} target="_blank" style={{ color: '#FF8444', textDecoration: 'none' }}>{appname}</a> Authentication {showShuffle ?
<span><a href={"https://shuffler.io"} target="_blank" style={{color: "#ff8444", textDecoration: "none", }}>Shuffle</a> Core handles this type of login to keep it securely stored.</span>
:
<span>You are invited to: Configure <a href={appLink} target="_blank" style={{ color: '#FF8444', textDecoration: 'none' }}>{appname}</a> Authentication</span>
}
</Typography> </Typography>
<Typography variant="h6" style={{ marginBottom: 20, }}> <Typography variant="h6" style={{ marginBottom: 20, }}>
What does this mean? What does this mean?
</Typography> </Typography>
<Typography variant="body1" style={{ marginBottom: 20, color: "rgba(255,255,255,0.4)", }}> <Typography variant="body1" style={{ marginBottom: 20, color: "rgba(255,255,255,0.4)", }}>
A Shuffle Organization has invited you to configure authentication for this app so that they can use this authentication in one of their workflows. {showShuffle ?
`By authenticating, you make it possible for Shuffle Security to use ${appname} in your security automation. Click the button below to get started. After authentication is done, you may close this window.`
:
"A Shuffle Organization has invited you to configure authentication for this app so that they can use this authentication in one of their workflows."
}
</Typography> </Typography>
<Typography variant="body1" style={{ marginBottom: 20, }}> <Typography variant="body1" style={{ marginBottom: 20, }}>
@@ -171,26 +185,38 @@ const SetAuthentication = (props) => {
: :
app.authentication.type === "oauth2" || app.authentication.type === "oauth2-app" ? app.authentication.type === "oauth2" || app.authentication.type === "oauth2-app" ?
<AuthenticationOauth2 <AuthenticationOauth2
selectedApp={app} selectedApp={{
...app,
"name": app.name?.replaceAll(" ", "_")
}}
selectedAction={{ selectedAction={{
"app_name": app.name, "app_name": app.name?.replaceAll(" ", "_"),
"app_id": app.id, "app_id": app.id,
"app_version": app.version, "app_version": app.app_version,
"large_image": app.large_image, "large_image": app.large_image,
}} }}
authenticationType={app.authentication} authenticationType={app.authentication}
isCloud={true} isCloud={true}
authButtonOnly={true} authButtonOnly={true}
getAppAuthentication={undefined} /> getAppAuthentication={undefined}
autoAuth={true}
/>
: :
<AuthenticationWindow <AuthenticationWindow
globalUrl={globalUrl} globalUrl={globalUrl}
selectedApp={app} selectedApp={{
...app,
"name": app.name?.toLowerCase()?.replaceAll(" ", "_"),
}}
authFieldsOnly={true} authFieldsOnly={true}
getAppAuthentication={undefined} getAppAuthentication={undefined}
appAuthentication={appAuthentication} />} appAuthentication={appAuthentication} />}
</Typography> </Typography>
<Typography variant="body1" style={{ marginBottom: 20, color: "rgba(255,255,255,0.4)", }}>
If you need any help, please contact support@shuffler.io
</Typography>
{/* {/*
<Typography variant="h6" style={{ marginTop: 50, marginBottom: 20, }}> <Typography variant="h6" style={{ marginTop: 50, marginBottom: 20, }}>
What can they do with this? What can they do with this?
+4 -1
View File
@@ -751,12 +751,15 @@ const UsecaseListComponent = (props) => {
return ( return (
<div key={index} style={{marginTop: 75, }}> <div key={index} style={{marginTop: 75, }}>
<Typography variant="body1" style={{color: themeMode === "dark" ? usecase.color : usecaseLightThemeColor[usecase.name.slice(3, 100).toLowerCase()], textAlign: "left", marginBottom: 10, }}> <Typography variant="body1" style={{color: themeMode === "dark" ? usecase.color : usecaseLightThemeColor[usecase.name.slice(3, 100).toLowerCase()], textAlign: "left", marginBottom: 10, }}>
<b>{index+1}. {usecase.name.slice(3, 100)}</b> <b>{index+1}. {usecase[1]?.name === "." ? usecase?.name.slice(3, 100) : usecase.name}</b>
</Typography> </Typography>
<Grid container spacing={1}> <Grid container spacing={1}>
{usecase.list.map((subcase, subindex) => { {usecase.list.map((subcase, subindex) => {
const selectedItem = subindex === expandedItem && index === expandedIndex const selectedItem = subindex === expandedItem && index === expandedIndex
if (subcase?.disabled === true) {
return null
}
if (subcase.matches === undefined || subcase.matches === null) { if (subcase.matches === undefined || subcase.matches === null) {
subcase.matches = [] subcase.matches = []
+184 -82
View File
@@ -4,7 +4,7 @@ import WelcomeForm2 from "../components/WelcomeForm2.jsx";
import AppFramework from "../components/AppFramework.jsx"; import AppFramework from "../components/AppFramework.jsx";
import {isMobile} from "react-device-detect"; import {isMobile} from "react-device-detect";
import { import {
ArrowForwardIos as ArrowForwardIosIcon, HelpOutline as HelpOutlineIcon,
} from '@mui/icons-material'; } from '@mui/icons-material';
import { import {
@@ -20,13 +20,17 @@ import {
Stepper, Stepper,
Step, Step,
StepLabel, StepLabel,
Tooltip,
} from '@mui/material'; } from '@mui/material';
import theme from '../theme.jsx'; import { getTheme } from '../theme.jsx';
import { useNavigate, Link } from "react-router-dom"; import { useNavigate, Link, useLocation } from "react-router-dom";
import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos'; import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos';
import Drift from "react-driftjs"; import Drift from "react-driftjs";
import { Context } from "../context/ContextApi.jsx";
const Welcome = (props) => { const Welcome = (props) => {
const { themeMode } = React.useContext(Context);
const theme = getTheme(themeMode);
const { globalUrl, surfaceColor, newColor, mini, inputColor, userdata, isLoggedIn, isLoaded, serverside, checkLogin } = props; const { globalUrl, surfaceColor, newColor, mini, inputColor, userdata, isLoggedIn, isLoaded, serverside, checkLogin } = props;
const [skipped, setSkipped] = React.useState(new Set()); const [skipped, setSkipped] = React.useState(new Set());
const [inputUsecase, setInputUsecase] = useState({}); const [inputUsecase, setInputUsecase] = useState({});
@@ -41,6 +45,8 @@ const Welcome = (props) => {
const [workflows, setWorkflows] = React.useState([]); const [workflows, setWorkflows] = React.useState([]);
let navigate = useNavigate(); let navigate = useNavigate();
const location = useLocation();
useEffect(() => { useEffect(() => {
if (checkLogin !== undefined) { if (checkLogin !== undefined) {
checkLogin() checkLogin()
@@ -268,9 +274,11 @@ const Welcome = (props) => {
getFramework() getFramework()
getApps() getApps()
getAvailableWorkflows() getAvailableWorkflows()
}, [])
if (window.location.search !== undefined && window.location.search !== null) { useEffect(() => {
const urlSearchParams = new URLSearchParams(window.location.search); if (location.search !== undefined && location.search !== null) {
const urlSearchParams = new URLSearchParams(location.search);
const params = Object.fromEntries(urlSearchParams.entries()); const params = Object.fromEntries(urlSearchParams.entries());
const foundTab = params["tab"]; const foundTab = params["tab"];
@@ -288,9 +296,13 @@ const Welcome = (props) => {
//setRenderDone( //setRenderDone(
} else {
setShowWelcome(false)
} }
} else {
setShowWelcome(false)
} }
}, []) }, [location.search])
const isStepSkipped = step => { const isStepSkipped = step => {
return skipped.has(step) return skipped.has(step)
@@ -339,6 +351,59 @@ const Welcome = (props) => {
const defaultImage = "/images/experienced.png" const defaultImage = "/images/experienced.png"
const experienced_image = userdata !== undefined && userdata !== null && userdata.active_org !== undefined && userdata.active_org.image !== undefined && userdata.active_org.image !== null && userdata.active_org.image !== "" ? userdata.active_org.image : defaultImage const experienced_image = userdata !== undefined && userdata !== null && userdata.active_org !== undefined && userdata.active_org.image !== undefined && userdata.active_org.image !== null && userdata.active_org.image !== "" ? userdata.active_org.image : defaultImage
const welcomeCardsOptions = [
{
badge: "New easy setup",
title: "Shuffle Security",
subtitle: "Set security specific usecases automatically",
icon: (
<svg width="50" height="50" viewBox="0 0 56 56" fill="none">
<path d="M14 14h28v6H20v16h16v-10h-8v-6h14v22H14V14z" fill="#FF6600" />
</svg>
),
onClick: () => {
if (isCloud) {
ReactGA.event({
category: "welcome",
action: "click_shuffle_security",
label: "",
})
navigate("https://security.shuffler.io/onboarding?utm_source=shuffler_onboarding")
} else {
// Default new port is 3002/3444
const { protocol, hostname } = window.location;
var newPort = 3002;
if (protocol === "https") {
newPort = 3444
}
const newUrl = `${protocol}//${hostname}:${newPort}/onboarding`;
window.location.href = newUrl;
}
},
tooltip: "Automate your security operations with built-in use cases.",
},
{
title: "Shuffle Core",
subtitle: "Build my workflows manually or with templates.",
icon: <img src="/images/Shuffle_logo.png" alt="Shuffle" style={{width: 30, height: 30, objectFit: "contain"}} />,
onClick: () => {
if (isCloud) {
ReactGA.event({
category: "welcome",
action: "click_shuffle_infrastructure",
label: "",
})
}
navigate("/welcome?tab=2")
},
tooltip: "Create custom workflows tailored to your specific infrastructure.",
}
];
return ( return (
<div style={{ margin: "auto", paddingBottom: 150, minHeight: 1500, paddingTop: 50, }}> <div style={{ margin: "auto", paddingBottom: 150, minHeight: 1500, paddingTop: 50, }}>
{/* {/*
@@ -382,6 +447,33 @@ const Welcome = (props) => {
})} })}
</Stepper> </Stepper>
</div> */} </div> */}
<div style={{ maxWidth: isMobile ? null : 500, margin: "auto", display: "flex", justifyContent: "flex-start", marginTop: isCloud ? "auto" : 20, marginBottom: -30 }}>
<Button
startIcon={<ArrowBackIosIcon />}
onClick={() => {
setShowWelcome(false);
navigate("/welcome");
}}
style={{ color: theme.palette.text.secondary, textTransform: "none", fontSize: 16 }}
disableRipple={true}
sx={{
'&:hover': {
backgroundColor: 'transparent',
color: theme.palette.text.primary,
fontWeight: 'inherit',
border: 'none',
'& .MuiButton-startIcon': {
transform: 'translateX(-4px)',
}
},
'& .MuiButton-startIcon': {
transition: 'transform 0.2s ease-in-out',
}
}}
>
Back
</Button>
</div>
<Grid container spacing={1} style={{ padding: isMobile ? 10 : 0, maxWidth: isMobile ? null : 500, minWidth: isMobile ? null : 500, marginRight : "auto" , marginLeft: "auto" }}> <Grid container spacing={1} style={{ padding: isMobile ? 10 : 0, maxWidth: isMobile ? null : 500, minWidth: isMobile ? null : 500, marginRight : "auto" , marginLeft: "auto" }}>
<Grid item xs={window.location.href.includes("tab=2") ? 6 : 12}> <Grid item xs={window.location.href.includes("tab=2") ? 6 : 12}>
<div> <div>
@@ -423,87 +515,97 @@ const Welcome = (props) => {
</div> </div>
: :
<Fade in={true}> <Fade in={true}>
<div style={{maxWidth: isMobile ? null : 590, margin: "auto", marginTop: 50,textAlign : isMobile ? "center" : null }}> <div style={{maxWidth: isMobile ? null : 620, margin: "auto", marginTop: 70, padding: isMobile ? "0 16px" : 0}}>
<Typography variant="h4" style={{color: "#F1F1F1", marginTop: 50, fontSize: 32}}> <Typography variant="h4" style={{color: theme.palette.textColor, marginTop: 50, fontSize: isMobile ? 26 : 34, fontWeight: 700, lineHeight: 1.25}}>
Help us get to know you What do you want to do with Shuffle?
</Typography> </Typography>
<Typography variant="body1" style={{color: "#9E9E9E", marginTop: 12 ,marginBottom: 50, fontSize: 16}}> <Typography variant="body1" style={{color: theme.palette.welcomeCardSubtextColor, marginTop: 12, marginBottom: 36, fontSize: 16, fontFamily: theme?.typography?.fontFamily}}>
Let us help you create a smoother journey. Let us help you create a smoother journey.
</Typography> </Typography>
{/* <Typography variant="body1" style={{color: "#9E9E9E", marginTop: 12 ,marginBottom: 50, fontSize: 16}}>
We will use this information to personalize your automation
</Typography> */}
<div style={{display: isMobile ? null : "flex", marginTop: 70, width: isMobile ? 280 : 700, margin: "auto",}}>
<div style={{border: "2px solid #806BFF", borderRadius: theme.palette?.borderRadius, }}>
<Card style={paperObject} onClick={() => {
if (isCloud) {
ReactGA.event({
category: "welcome",
action: "click_welcome_continue",
label: "",
})
} else {
//setActiveStep(1)
}
navigate("/welcome?tab=2") <div style={{display: "flex", flexDirection: "column", gap: 20, marginTop : 50}}>
setActiveStep(1)
setShowWelcome(true)
}}>
<CardActionArea style={actionObject}>
<img src="/images/welcome-to-shuffle.png" style={imageStyle} />
<Typography variant="h4" style={{color: "#F1F1F1"}}>
New to Shuffle
</Typography>
<Typography variant="body1" style={{marginTop: 10, color: "rgba(255,255,255,0.8)"}}>
Let us guide you for an easier experience
</Typography>
</CardActionArea>
</Card>
</div>
<div style={{marginLeft: 25, marginRight: 25,
marginTop: isMobile ? 30 : null, }}>
{/* <Typography style={{marginTop: 200, }}>
OR
</Typography> */}
</div>
<Card disabled style={paperObject} onClick={() => {
if (isCloud) {
ReactGA.event({
category: "welcome",
action: "click_getting_started",
label: "",
})
}
navigate("/workflows?message=Skipped intro") {welcomeCardsOptions.map((card) => (
}}> <div key={card?.title?.replace(" ", "-")} style={{position: "relative"}}>
<CardActionArea style={actionObject}> {card.badge && (
<img src={experienced_image} style={{padding: experienced_image === defaultImage ? 2 : 10, objectFit: "scale-down", minHeight: experienced_image === defaultImage ? 40 : 70, maxHeight: experienced_image === defaultImage ? 40 : 70, bordeRadius: theme.palette?.borderRadius*2, marginBottom: experienced_image === defaultImage ? 24 : 2 }} /> <div style={{
<Typography variant="h4" style={{color: "#F1F1F1"}}> position: "absolute",
Experienced top: -14,
</Typography> left: 18,
<Typography variant="body1" style={{marginTop: 10, color: "rgba(255,255,255,0.8)"}}> backgroundColor: "#F2653B",
Head to Shuffle right away color: "#fff",
</Typography> borderRadius: 20,
</CardActionArea> padding: "4px 14px",
</Card> fontSize: 13,
</div> fontWeight: 600,
zIndex: 1,
letterSpacing: "0.01em",
}}>
{card.badge}
</div>
)}
<Card
style={{
backgroundColor: theme.palette.platformColor,
backgroundImage: "none",
borderRadius: 8,
boxShadow: "none",
}}
onClick={card.onClick}
>
<CardActionArea
style={{
padding: "20px 20px",
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
borderRadius: 12,
position: "relative",
}}
>
<div style={{display: "flex", alignItems: "center", gap: 18}}>
<div style={{
width: 52,
height: 52,
minWidth: 52,
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: 10,
}}>
{card.icon}
</div>
<div style={{textAlign: "left"}}>
<Typography style={{color: theme.palette?.actionSidebarField?.color, fontWeight: 700, fontSize: 18, lineHeight: 1.3}}>
{card.title}
</Typography>
<Typography style={{color: theme.palette.welcomeCardSubtextColor, fontSize: 13, marginTop: 6}}>
{card.subtitle}
</Typography>
</div>
</div>
<Tooltip title={card.tooltip} placement="right"
componentsProps={{
tooltip: {
sx: {
backgroundColor: "rgba(33, 33, 33, 1)",
color: "rgba(241, 241, 241, 1)",
fontSize: 14,
border: "1px solid rgba(73, 73, 73, 1)",
fontFamily: theme?.typography?.fontFamily,
}
},
}}
arrow>
<div style={{ position: "absolute", top: 16, right: 16, display: "flex", alignItems: "center", justifyContent: "center" }}>
<HelpOutlineIcon style={{color: theme.palette.welcomeCardSubtextColor, fontSize: 22}} />
</div>
</Tooltip>
</CardActionArea>
</Card>
</div>
))}
<div style={{ flexDirection: "row", textAlign: isMobile ? "center" : null, margin: isMobile ? "auto" : null }}>
<Button variant="contained" type="submit" fullWidth style={buttonStyle} onClick={() => {
navigate(`/welcome?tab=2`)
setActiveStep(1)
setShowWelcome(true)
ReactGA.event({
category: "welcome",
action: "click_welcome_continue",
label: "",
})
}}>
Continue
</Button>
</div> </div>
</div> </div>
</Fade> </Fade>
+45 -5
View File
@@ -134,7 +134,7 @@ import {green, yellow, red, grey, triggers as wfTriggers, } from "../views/Angul
import Licensed from "../components/Licensed.jsx"; import Licensed from "../components/Licensed.jsx";
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e"); const searchClient = algoliasearch("JNSS5CFDZZ", "33e4e3564f4f060e96e0531957bed552");
const svgSize = 24; const svgSize = 24;
const imagesize = 23; const imagesize = 23;
@@ -633,10 +633,13 @@ export const GetIconInfo = (action) => {
svgViewBox: "0 0 48 48", svgViewBox: "0 0 48 48",
}, },
search: { search: {
icon: "M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z", icon: "M21 29C25.4183 29 29 25.4183 29 21C29 16.5817 25.4183 13 21 13C16.5817 13 13 16.5817 13 21C13 25.4183 16.5817 29 21 29Z M33 33L26.657 26.657",
iconColor: "white", iconColor: "white",
iconBackgroundColor: "green",
originalIcon: <SearchIcon />, originalIcon: <SearchIcon />,
useStroke: true,
svgViewBox: "0 0 48 48",
iconBackgroundColor: "rgba(242, 101, 59, 1)",
fillGradient: ["rgba(242, 101, 59, 1)", "rgba(242, 101, 59, 1)"],
}, },
eradication: { eradication: {
icon: "", icon: "",
@@ -1211,6 +1214,16 @@ const Workflows2 = (props) => {
}; };
const handleCreateWorkflow = () => { const handleCreateWorkflow = () => {
if (isCloud) {
ReactGA.event({
category: "Workflow",
action: "Create_Workflow",
label: "Create New Workflow Button"
});
}
setModalOpen(true) setModalOpen(true)
setIsEditing(false) setIsEditing(false)
setNewWorkflowName("") setNewWorkflowName("")
@@ -2932,6 +2945,16 @@ const Workflows2 = (props) => {
} }
const deleteWorkflow = (id, bulk) => { const deleteWorkflow = (id, bulk) => {
// remove workflow from local state immediately
setWorkflows(prev => {
const updated = (prev || []).filter(wf => wf.id !== id);
try {
localStorage.setItem("workflows", JSON.stringify(updated));
} catch (e) {}
return updated;
});
setFilteredWorkflows(prev => (prev || []).filter(wf => wf.id !== id));
fetch(globalUrl + "/api/v1/workflows/" + id, { fetch(globalUrl + "/api/v1/workflows/" + id, {
method: "DELETE", method: "DELETE",
headers: { headers: {
@@ -3158,6 +3181,14 @@ const Workflows2 = (props) => {
sx={{ backgroundColor: theme.palette.textFieldStyle.backgroundColor, color: theme.palette.text.primary, "&:hover": {backgroundColor: theme.palette.hoverColor} }} sx={{ backgroundColor: theme.palette.textFieldStyle.backgroundColor, color: theme.palette.text.primary, "&:hover": {backgroundColor: theme.palette.hoverColor} }}
disabled={isDistributed} disabled={isDistributed}
onClick={(event) => { onClick={(event) => {
if (isCloud) {
ReactGA.event({
category: "Workflow",
action: "Edit_Workflow_Details",
label: "Editing a workflow from the workflow list",
});
}
event.stopPropagation() event.stopPropagation()
if (data.actions !== undefined && data.actions !== null && data.actions.length > 0 && data.image !== "") { if (data.actions !== undefined && data.actions !== null && data.actions.length > 0 && data.image !== "") {
setEditing(data) setEditing(data)
@@ -3451,6 +3482,15 @@ const Workflows2 = (props) => {
<Tooltip arrow <Tooltip arrow
onClick={() => {
if (isCloud) {
ReactGA.event({
category: "Workflow",
action: "View_Page",
label: "Workflow Editor",
});
}
}}
onMouseEnter={() => { onMouseEnter={() => {
/* /*
if (data.image === undefined || data.image === null || data.image === "" && !loadingWorkflows.includes(data.id)) { if (data.image === undefined || data.image === null || data.image === "" && !loadingWorkflows.includes(data.id)) {
@@ -4048,7 +4088,7 @@ const Workflows2 = (props) => {
setLoadWorkflowsModalOpen(false); setLoadWorkflowsModalOpen(false);
if (workflowids.length > 0) { if (workflowids?.length > 0) {
setHighlightIds(workflowids) setHighlightIds(workflowids)
} }
}; };
@@ -5751,7 +5791,7 @@ const Workflows2 = (props) => {
view === "grid" && currTab !== 2 ? ( view === "grid" && currTab !== 2 ? (
<> <>
{currTab === 4 && backgroundWorkflows.map((data, index) => { {currTab === 4 && backgroundWorkflows.map((data, index) => {
if (data.triggers.length === 0) { if (data?.triggers === null || data?.triggers === undefined || data?.triggers?.length === 0) {
return null return null
} }
+1 -1
View File
@@ -10,7 +10,7 @@ require (
github.com/gorilla/mux v1.8.1 github.com/gorilla/mux v1.8.1
github.com/satori/go.uuid v1.2.0 github.com/satori/go.uuid v1.2.0
github.com/shuffle/shuffle-shared v1.2.45 github.com/shuffle/shuffle-shared v1.2.45
github.com/shuffle/singul v0.0.30 github.com/shuffle/singul v0.0.32
k8s.io/api v0.34.2 k8s.io/api v0.34.2
k8s.io/apimachinery v0.34.2 k8s.io/apimachinery v0.34.2
k8s.io/client-go v0.34.2 k8s.io/client-go v0.34.2
+2 -2
View File
@@ -320,8 +320,8 @@ github.com/shuffle/opensearch-go/v4 v4.0.0 h1:Mh85CD1MwOgXiFFYlzS1llnvdqL3CztRdR
github.com/shuffle/opensearch-go/v4 v4.0.0/go.mod h1:gVLZKQE5khQWMb68XBtgKrhu78oLGL2zHwAGnFMDwC0= github.com/shuffle/opensearch-go/v4 v4.0.0/go.mod h1:gVLZKQE5khQWMb68XBtgKrhu78oLGL2zHwAGnFMDwC0=
github.com/shuffle/shuffle-shared v1.2.45 h1:d4gnF6qkrmmZVz4FEraEBzU7Kq8VbfAbiGlBoYeFD7o= github.com/shuffle/shuffle-shared v1.2.45 h1:d4gnF6qkrmmZVz4FEraEBzU7Kq8VbfAbiGlBoYeFD7o=
github.com/shuffle/shuffle-shared v1.2.45/go.mod h1:1DvYhS/4659FdHxzEpHTPuFUW0IfwQSuyluiGTwz9jo= github.com/shuffle/shuffle-shared v1.2.45/go.mod h1:1DvYhS/4659FdHxzEpHTPuFUW0IfwQSuyluiGTwz9jo=
github.com/shuffle/singul v0.0.30 h1:xYTpGHWzZ9lX1P6/kJIz6kng5q6gUdua3IMhsK399e8= github.com/shuffle/singul v0.0.32 h1:EoJjKCWfbtHqjo/UgofSvTp7rcYEH7k3eGXrZgXcMt8=
github.com/shuffle/singul v0.0.30/go.mod h1:2dGXQMk8q4QOivomuJ9QkrpevpmTM1QYemJixbY5jW0= github.com/shuffle/singul v0.0.32/go.mod h1:An0DHl+E+bAO1ziIbqUJdLXhjixN6DOyv0GwlXwsKHc=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+190 -87
View File
@@ -193,6 +193,40 @@ func restoreActionConfig(ctx context.Context, executionID string, action *shuffl
} }
} }
func getAppProxyValue(primaryKey, fallbackKey string) string {
value := strings.TrimSpace(os.Getenv(primaryKey))
if len(value) > 0 {
return value
}
return strings.TrimSpace(os.Getenv(fallbackKey))
}
func appendAppProxyEnv(env []string) []string {
httpProxy := getAppProxyValue("HTTP_PROXY", "SHUFFLE_APP_HTTP_PROXY")
httpsProxy := getAppProxyValue("HTTPS_PROXY", "SHUFFLE_APP_HTTPS_PROXY")
noProxy := getAppProxyValue("NO_PROXY", "SHUFFLE_APP_NO_PROXY")
noProxyLower := getAppProxyValue("no_proxy", "SHUFFLE_APP_no_proxy")
if len(httpProxy) > 0 {
env = append(env, fmt.Sprintf("HTTP_PROXY=%s", httpProxy))
}
if len(httpsProxy) > 0 {
env = append(env, fmt.Sprintf("HTTPS_PROXY=%s", httpsProxy))
}
if len(noProxy) > 0 {
env = append(env, fmt.Sprintf("NO_PROXY=%s", noProxy))
}
if len(noProxyLower) > 0 {
env = append(env, fmt.Sprintf("no_proxy=%s", noProxyLower))
}
return env
}
// Images to be autodeployed in the latest version of Shuffle. // Images to be autodeployed in the latest version of Shuffle.
var autoDeploy = map[string]string{ var autoDeploy = map[string]string{
"http:1.4.0": "frikky/shuffle:http_1.4.0", "http:1.4.0": "frikky/shuffle:http_1.4.0",
@@ -1784,10 +1818,7 @@ func handleExecutionResult(workflowExecution shuffle.WorkflowExecution) {
if strings.ToLower(os.Getenv("SHUFFLE_PASS_APP_PROXY")) == "true" { if strings.ToLower(os.Getenv("SHUFFLE_PASS_APP_PROXY")) == "true" {
//log.Printf("APPENDING PROXY TO THE APP!") //log.Printf("APPENDING PROXY TO THE APP!")
env = append(env, fmt.Sprintf("HTTP_PROXY=%s", os.Getenv("HTTP_PROXY"))) env = appendAppProxyEnv(env)
env = append(env, fmt.Sprintf("HTTPS_PROXY=%s", os.Getenv("HTTPS_PROXY")))
env = append(env, fmt.Sprintf("NO_PROXY=%s", os.Getenv("NO_PROXY")))
env = append(env, fmt.Sprintf("no_proxy=%s", os.Getenv("no_proxy")))
} }
overrideHttpProxy := os.Getenv("SHUFFLE_INTERNAL_HTTP_PROXY") overrideHttpProxy := os.Getenv("SHUFFLE_INTERNAL_HTTP_PROXY")
@@ -2280,11 +2311,6 @@ func handleSubflowPoller(ctx context.Context, workflowExecution shuffle.Workflow
} }
} }
if len(data) == 0 {
log.Printf("[WARNING] Stream result missing execution ID and authorization; injecting them from workflow execution")
data = fmt.Sprintf(`{"execution_id": "%s", "authorization": "%s"}`, workflowExecution.ExecutionId, workflowExecution.Authorization)
}
key := fmt.Sprintf("%s:%s", workflowExecution.ExecutionId, subflowId) key := fmt.Sprintf("%s:%s", workflowExecution.ExecutionId, subflowId)
cacheKey := fmt.Sprintf("workflowexecution_%s", workflowExecution.ExecutionId) cacheKey := fmt.Sprintf("workflowexecution_%s", workflowExecution.ExecutionId)
usedCache := false usedCache := false
@@ -2337,6 +2363,11 @@ func handleSubflowPoller(ctx context.Context, workflowExecution shuffle.Workflow
return errors.New("Subflow status not found yet (cache)") return errors.New("Subflow status not found yet (cache)")
} }
if len(data) == 0 {
log.Printf("[WARNING] Stream result missing execution ID and authorization; injecting them from workflow execution")
data = fmt.Sprintf(`{"execution_id": "%s", "authorization": "%s"}`, workflowExecution.ExecutionId, workflowExecution.Authorization)
}
req, err := http.NewRequest( req, err := http.NewRequest(
"POST", "POST",
streamResultUrl, streamResultUrl,
@@ -2601,6 +2632,112 @@ func arrayContains(visited []string, id string) bool {
return found return found
} }
func isTerminalResultStatus(status string) bool {
return status == "SUCCESS" || status == "FINISHED" || status == "FAILURE" || status == "ABORTED"
}
func hasWaitForResultParameter(params []shuffle.WorkflowAppActionParameter) bool {
for _, param := range params {
if param.Name == "check_result" && strings.ToLower(param.Value) == "true" {
return true
}
}
return false
}
func getSubflowBarrierActionIDs(workflowExecution shuffle.WorkflowExecution) map[string]struct{} {
actionIDs := map[string]struct{}{}
for _, action := range workflowExecution.Workflow.Actions {
if action.AppName != "shuffle-subflow" && action.AppName != "shuffle-subflow-v2" && action.AppName != "Shuffle Workflow" {
continue
}
if !hasWaitForResultParameter(action.Parameters) {
continue
}
actionIDs[action.ID] = struct{}{}
}
for _, trigger := range workflowExecution.Workflow.Triggers {
if trigger.AppName != "shuffle-subflow" && trigger.AppName != "shuffle-subflow-v2" && trigger.AppName != "Shuffle Workflow" {
continue
}
if !hasWaitForResultParameter(trigger.Parameters) {
continue
}
actionIDs[trigger.ID] = struct{}{}
}
return actionIDs
}
func getSubflowBarrierProgress(workflowExecution shuffle.WorkflowExecution) (int, int, int) {
barrierActions := getSubflowBarrierActionIDs(workflowExecution)
expected := len(barrierActions)
if expected == 0 {
return 0, 0, 0
}
terminalByAction := map[string]string{}
for _, result := range workflowExecution.Results {
if _, ok := barrierActions[result.Action.ID]; !ok {
continue
}
if !isTerminalResultStatus(result.Status) {
continue
}
terminalByAction[result.Action.ID] = result.Status
}
completed := len(terminalByAction)
failed := 0
for _, status := range terminalByAction {
if status == "FAILURE" || status == "ABORTED" {
failed += 1
}
}
return expected, completed, failed
}
func enforceSubflowBarrier(workflowExecution *shuffle.WorkflowExecution) (int, int, int) {
if workflowExecution == nil {
return 0, 0, 0
}
if workflowExecution.Status == "FINISHED" || workflowExecution.Status == "FAILURE" || workflowExecution.Status == "ABORTED" {
return 0, 0, 0
}
expected, completed, failed := getSubflowBarrierProgress(*workflowExecution)
if expected == 0 {
return 0, 0, 0
}
if completed < expected {
workflowExecution.Status = "WAITING"
return expected, completed, failed
}
if workflowExecution.Status == "WAITING" {
if failed > 0 {
workflowExecution.Status = "FAILURE"
workflowExecution.Result = fmt.Sprintf("Subflow barrier failed: %d of %d subflows failed or aborted", failed, expected)
} else {
workflowExecution.Status = "EXECUTING"
}
}
return expected, completed, failed
}
func getResult(workflowExecution shuffle.WorkflowExecution, id string) shuffle.ActionResult { func getResult(workflowExecution shuffle.WorkflowExecution, id string) shuffle.ActionResult {
for _, actionResult := range workflowExecution.Results { for _, actionResult := range workflowExecution.Results {
if actionResult.Action.ID == id { if actionResult.Action.ID == id {
@@ -3712,10 +3849,7 @@ func deploySwarmService(dockercli *dockerclient.Client, name, image string, depl
} }
if strings.ToLower(os.Getenv("SHUFFLE_PASS_APP_PROXY")) == "true" { if strings.ToLower(os.Getenv("SHUFFLE_PASS_APP_PROXY")) == "true" {
serviceSpec.TaskTemplate.ContainerSpec.Env = append(serviceSpec.TaskTemplate.ContainerSpec.Env, fmt.Sprintf("HTTP_PROXY=%s", os.Getenv("HTTP_PROXY"))) serviceSpec.TaskTemplate.ContainerSpec.Env = appendAppProxyEnv(serviceSpec.TaskTemplate.ContainerSpec.Env)
serviceSpec.TaskTemplate.ContainerSpec.Env = append(serviceSpec.TaskTemplate.ContainerSpec.Env, fmt.Sprintf("HTTPS_PROXY=%s", os.Getenv("HTTPS_PROXY")))
serviceSpec.TaskTemplate.ContainerSpec.Env = append(serviceSpec.TaskTemplate.ContainerSpec.Env, fmt.Sprintf("NO_PROXY=%s", os.Getenv("NO_PROXY")))
serviceSpec.TaskTemplate.ContainerSpec.Env = append(serviceSpec.TaskTemplate.ContainerSpec.Env, fmt.Sprintf("no_proxy=%s", os.Getenv("no_proxy")))
} }
overrideHttpProxy := os.Getenv("SHUFFLE_INTERNAL_HTTP_PROXY") overrideHttpProxy := os.Getenv("SHUFFLE_INTERNAL_HTTP_PROXY")
@@ -4236,6 +4370,10 @@ func sendAppRequest(ctx context.Context, incomingUrl, appName string, port int,
callbackUrl := os.Getenv("SHUFFLE_WORKER_SERVER_URL") callbackUrl := os.Getenv("SHUFFLE_WORKER_SERVER_URL")
if len(callbackUrl) > 0 { if len(callbackUrl) > 0 {
parsedRequest.BaseUrl = callbackUrl parsedRequest.BaseUrl = callbackUrl
if parsedRequest.Action.AppName == "shuffle-subflow" || parsedRequest.Action.AppName == "shuffle-subflow-v2" || parsedRequest.Action.AppName == "User Input" {
parsedRequest.BaseUrl = fmt.Sprintf("http://%s:%d", hostname, baseport)
//parsedRequest.Url = parsedRequest.BaseUrl
}
} else if len(hostname) > 0 { } else if len(hostname) > 0 {
// Run with proper hostname, but set to shuffle-worker to avoid specific host target. // Run with proper hostname, but set to shuffle-worker to avoid specific host target.
// This means running with VIP instead. // This means running with VIP instead.
@@ -4462,10 +4600,7 @@ func baseDeploy() {
if strings.ToLower(os.Getenv("SHUFFLE_PASS_APP_PROXY")) == "true" { if strings.ToLower(os.Getenv("SHUFFLE_PASS_APP_PROXY")) == "true" {
//log.Printf("APPENDING PROXY TO THE APP!") //log.Printf("APPENDING PROXY TO THE APP!")
env = append(env, fmt.Sprintf("HTTP_PROXY=%s", os.Getenv("HTTP_PROXY"))) env = appendAppProxyEnv(env)
env = append(env, fmt.Sprintf("HTTPS_PROXY=%s", os.Getenv("HTTPS_PROXY")))
env = append(env, fmt.Sprintf("NO_PROXY=%s", os.Getenv("NO_PROXY")))
env = append(env, fmt.Sprintf("no_proxy=%s", os.Getenv("no_proxy")))
} }
if len(os.Getenv("SHUFFLE_APP_SDK_TIMEOUT")) > 0 { if len(os.Getenv("SHUFFLE_APP_SDK_TIMEOUT")) > 0 {
@@ -4932,7 +5067,7 @@ func main() {
} }
} }
func checkUnfinished(resp http.ResponseWriter, request *http.Request, execRequest shuffle.OrborusExecutionRequest) { func checkUnfinished(execRequest shuffle.OrborusExecutionRequest) {
// Meant as a function that periodically checks whether previous executions have finished or not. // Meant as a function that periodically checks whether previous executions have finished or not.
// Should probably be based on executedIds and finishedIds // Should probably be based on executedIds and finishedIds
// Schedule a check in the future instead? // Schedule a check in the future instead?
@@ -4961,38 +5096,16 @@ func checkUnfinished(resp http.ResponseWriter, request *http.Request, execReques
sendResult(*exec, data) sendResult(*exec, data)
} }
func handleRunExecution(resp http.ResponseWriter, request *http.Request) { func processRunExecution(execRequest shuffle.OrborusExecutionRequest) {
defer request.Body.Close()
body, err := ioutil.ReadAll(request.Body)
if err != nil {
log.Printf("[WARNING] Failed reading body for stream result queue")
resp.WriteHeader(400)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err)))
return
}
//log.Printf("[DEBUG] In run execution with body length %d", len(body))
var execRequest shuffle.OrborusExecutionRequest
err = json.Unmarshal(body, &execRequest)
if err != nil {
log.Printf("[WARNING] Failed shuffle.WorkflowExecution unmarshaling: %s", err)
resp.WriteHeader(400)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err)))
return
}
// Checks if a workflow is done 30 seconds later, and sends info to backend no matter what // Checks if a workflow is done 30 seconds later, and sends info to backend no matter what
go func() { go func() {
time.Sleep(time.Duration(30) * time.Second) time.Sleep(time.Duration(30) * time.Second)
checkUnfinished(resp, request, execRequest) checkUnfinished(execRequest)
}() }()
window.AddEvent(time.Now()) window.AddEvent(time.Now())
ctx := context.Background() ctx := context.Background()
// FIXME: This should be PER EXECUTION
//if strings.ToLower(os.Getenv("SHUFFLE_PASS_APP_PROXY")) == "true" {
// Is it ok if these are standard? Should they be update-able after launch? Hmm
if len(execRequest.HTTPProxy) > 0 { if len(execRequest.HTTPProxy) > 0 {
log.Printf("[DEBUG] Sending proxy info to child process") log.Printf("[DEBUG] Sending proxy info to child process")
os.Setenv("SHUFFLE_PASS_APP_PROXY", execRequest.ShufflePassProxyToApp) os.Setenv("SHUFFLE_PASS_APP_PROXY", execRequest.ShufflePassProxyToApp)
@@ -5022,9 +5135,7 @@ func handleRunExecution(resp http.ResponseWriter, request *http.Request) {
baseUrl = execRequest.BaseUrl baseUrl = execRequest.BaseUrl
} }
// Setting to just have an auth available.
if len(execRequest.Authorization) > 0 && len(os.Getenv("AUTHORIZATION")) == 0 { if len(execRequest.Authorization) > 0 && len(os.Getenv("AUTHORIZATION")) == 0 {
//log.Printf("[DEBUG] Sending proxy info to child process")
os.Setenv("AUTHORIZATION", execRequest.Authorization) os.Setenv("AUTHORIZATION", execRequest.Authorization)
} }
@@ -5037,71 +5148,47 @@ func handleRunExecution(resp http.ResponseWriter, request *http.Request) {
) )
if err != nil { if err != nil {
log.Printf("[ERROR][%s] Failed to create a new request", execRequest.ExecutionId) log.Printf("[ERROR][%s] Failed to create stream results request: %s", execRequest.ExecutionId, err)
resp.WriteHeader(500)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err)))
return return
} }
client := shuffle.GetExternalClient(streamResultUrl) client := shuffle.GetExternalClient(streamResultUrl)
newresp, err := client.Do(req) newresp, err := client.Do(req)
if err != nil { if err != nil {
log.Printf("[ERROR] Failed making request (2): %s", err) log.Printf("[ERROR][%s] Failed making stream results request: %s", execRequest.ExecutionId, err)
resp.WriteHeader(500)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err)))
return return
} }
defer newresp.Body.Close() defer newresp.Body.Close()
body, err = ioutil.ReadAll(newresp.Body) body, err := ioutil.ReadAll(newresp.Body)
if err != nil { if err != nil {
log.Printf("[ERROR][%s] Failed reading body (2): %s", execRequest.ExecutionId, err) log.Printf("[ERROR][%s] Failed reading stream results response body: %s", execRequest.ExecutionId, err)
resp.WriteHeader(500)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err)))
return return
} }
if newresp.StatusCode != 200 { if newresp.StatusCode != 200 {
log.Printf("[ERROR][%s] Bad statuscode: %d, %s", execRequest.ExecutionId, newresp.StatusCode, string(body)) log.Printf("[ERROR][%s] Bad statuscode from stream results: %d, %s", execRequest.ExecutionId, newresp.StatusCode, string(body))
if strings.Contains(string(body), "Workflowexecution is already finished") {
log.Printf("[DEBUG] Shutting down (19)")
//shutdown(workflowExecution, "", "", true)
}
resp.WriteHeader(500)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Bad statuscode: %d"}`, newresp.StatusCode)))
return return
} }
err = json.Unmarshal(body, &workflowExecution) err = json.Unmarshal(body, &workflowExecution)
if err != nil { if err != nil {
log.Printf("[ERROR] Failed workflowExecution unmarshal: %s", err) log.Printf("[ERROR][%s] Failed workflowExecution unmarshal: %s", execRequest.ExecutionId, err)
resp.WriteHeader(500)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err)))
return return
} }
//err = shuffle.SetWorkflowExecution(ctx, workflowExecution, true)
err = setWorkflowExecution(ctx, workflowExecution, true) err = setWorkflowExecution(ctx, workflowExecution, true)
if err != nil { if err != nil {
log.Printf("[ERROR] Failed initializing execution saving for %s: %s", workflowExecution.ExecutionId, err) log.Printf("[ERROR][%s] Failed initializing execution saving: %s", workflowExecution.ExecutionId, err)
} }
if workflowExecution.Status == "FINISHED" || workflowExecution.Status == "SUCCESS" { if workflowExecution.Status == "FINISHED" || workflowExecution.Status == "SUCCESS" {
log.Printf("[DEBUG] Workflow %s is finished. Exiting worker.", workflowExecution.ExecutionId) log.Printf("[DEBUG] Workflow %s is finished before dispatch. Exiting async run setup.", workflowExecution.ExecutionId)
log.Printf("[DEBUG] Shutting down (20)")
resp.WriteHeader(200)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Bad status for execution - already %s. Returning with 200 OK"}`, workflowExecution.Status)))
return return
} }
//startAction, extra, children, parents, visited, executed, nextActions, environments := shuffle.GetExecutionVariables(ctx, workflowExecution.ExecutionId)
extra := 0 extra := 0
for _, trigger := range workflowExecution.Workflow.Triggers { for _, trigger := range workflowExecution.Workflow.Triggers {
//log.Printf("Appname trigger (0): %s", trigger.AppName)
if trigger.AppName == "User Input" || trigger.AppName == "Shuffle Workflow" { if trigger.AppName == "User Input" || trigger.AppName == "Shuffle Workflow" {
extra += 1 extra += 1
} }
@@ -5110,15 +5197,10 @@ func handleRunExecution(resp http.ResponseWriter, request *http.Request) {
log.Printf("[INFO][%s] (1) Status: %s, Results: %d, actions: %d", workflowExecution.ExecutionId, workflowExecution.Status, len(workflowExecution.Results), len(workflowExecution.Workflow.Actions)+extra) log.Printf("[INFO][%s] (1) Status: %s, Results: %d, actions: %d", workflowExecution.ExecutionId, workflowExecution.Status, len(workflowExecution.Results), len(workflowExecution.Workflow.Actions)+extra)
if workflowExecution.Status != "EXECUTING" { if workflowExecution.Status != "EXECUTING" {
log.Printf("[WARNING] Exiting as worker execution has status %s!", workflowExecution.Status) log.Printf("[WARNING][%s] Exiting async run as execution status is %s", workflowExecution.ExecutionId, workflowExecution.Status)
log.Printf("[DEBUG] Shutting down (38)")
resp.WriteHeader(400)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Bad status %s for the workflow execution %s"}`, workflowExecution.Status, workflowExecution.ExecutionId)))
return return
} }
//log.Printf("[DEBUG] Starting execution :O")
cacheKey := fmt.Sprintf("workflowexecution_%s", workflowExecution.ExecutionId) cacheKey := fmt.Sprintf("workflowexecution_%s", workflowExecution.ExecutionId)
execData, err := json.Marshal(workflowExecution) execData, err := json.Marshal(workflowExecution)
if err != nil { if err != nil {
@@ -5133,15 +5215,36 @@ func handleRunExecution(resp http.ResponseWriter, request *http.Request) {
err = executionInit(workflowExecution) err = executionInit(workflowExecution)
if err != nil { if err != nil {
log.Printf("[DEBUG][%s] Shutting down (30) - Workflow setup failed: %s", workflowExecution.ExecutionId, err) log.Printf("[DEBUG][%s] Shutting down (30) - Workflow setup failed: %s", workflowExecution.ExecutionId, err)
resp.WriteHeader(500)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Error in execution init: %s"}`, err)))
return return
//shutdown(workflowExecution, "", "", true)
} }
handleExecutionResult(workflowExecution) handleExecutionResult(workflowExecution)
resp.WriteHeader(200) }
resp.Write([]byte(fmt.Sprintf(`{"success": true}`)))
func handleRunExecution(resp http.ResponseWriter, request *http.Request) {
defer request.Body.Close()
body, err := ioutil.ReadAll(request.Body)
if err != nil {
log.Printf("[WARNING] Failed reading body for stream result queue")
resp.WriteHeader(400)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err)))
return
}
//log.Printf("[DEBUG] In run execution with body length %d", len(body))
var execRequest shuffle.OrborusExecutionRequest
err = json.Unmarshal(body, &execRequest)
if err != nil {
log.Printf("[WARNING] Failed shuffle.WorkflowExecution unmarshaling: %s", err)
resp.WriteHeader(400)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err)))
return
}
resp.WriteHeader(http.StatusAccepted)
resp.Write([]byte(`{"success": true, "accepted": true}`))
go processRunExecution(execRequest)
} }
func handleDownloadImage(resp http.ResponseWriter, request *http.Request) { func handleDownloadImage(resp http.ResponseWriter, request *http.Request) {