Merge branch 'nightly' of https://github.com/shuffle/shuffle into nightly

This commit is contained in:
Frikky
2026-05-07 17:35:32 +02:00
53 changed files with 3622 additions and 5475 deletions
@@ -1,3 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 4.7 KiB

-4
View File
@@ -142,10 +142,6 @@ 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);
} }
+139 -56
View File
@@ -20,7 +20,6 @@ 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,
@@ -64,11 +63,10 @@ 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",
"33e4e3564f4f060e96e0531957bed552" "c8f882473ff42d41158430be09ec2b4e"
) )
const AppAuthTab = memo((props) => { const AppAuthTab = memo((props) => {
@@ -91,13 +89,9 @@ 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)
@@ -189,7 +183,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;
@@ -219,6 +213,33 @@ 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,
@@ -263,22 +284,100 @@ const AppAuthTab = memo((props) => {
editAuthenticationConfig(id, "suborg_distribute", [...new Set(selectedSubOrg)]) editAuthenticationConfig(id, "suborg_distribute", [...new Set(selectedSubOrg)])
} }
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} />
);
const deleteConfirmDialog = ( return (
<DeleteConfirmDialog <MenuItem
open={deleteConfirmOpen} key={index}
onClose={() => { setDeleteConfirmOpen(false); setDeleteConfirmTarget(null); }} value={data.id}
onConfirm={() => { onClick={() => handleSelectSubOrg(data.id)}
deleteAuthentication(deleteConfirmTarget); style={{ display: "flex", alignItems: "center" }}
setDeleteConfirmOpen(false); >
setDeleteConfirmTarget(null); <Checkbox
}} checked={selectedSubOrg.includes(data.id)}
title="Delete Authentication?" />
description={<>Are you sure you want to delete <b>{deleteConfirmTarget?.app?.name}</b> authentication?</>} {image}
warningText="This cannot be undone. Any workflows using this authentication will lose access." <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
@@ -886,17 +985,7 @@ 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}
<SubOrgDistributionDialog {cacheDistributionModal}
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'}}>
@@ -1273,10 +1362,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={() => {
setDeleteConfirmTarget(data); deleteAuthentication(data);
setDeleteConfirmOpen(true);
}} }}
> >
<img src='/icons/deleteIcon.svg' alt='delete icon' color="secondary" /> <img src='/icons/deleteIcon.svg' alt='delete icon' color="secondary" />
@@ -1311,27 +1400,21 @@ const AppAuthTab = memo((props) => {
color="secondary" color="secondary"
onClick={() => { onClick={() => {
setShowDistributionPopup(true) setShowDistributionPopup(true)
let initialSelected = []; if(data?.suborg_distribution?.length > 0){
if (data?.suborg_distributed) { setSelectedSubOrg(data.suborg_distribution)
const allSuborg = userdata?.orgs?.map((d) => { }else{
if (d.creator_org !== userdata.active_org.id) return null; setSelectedSubOrg([])
return d.id; }
}); setSelectedAuthId(data.id)
initialSelected = allSuborg.filter((d) => d !== null); if (data?.suborg_distributed) {
} else if (data?.suborg_distribution?.length > 0) { const allSuborg = userdata?.orgs?.map((data, index) => {
initialSelected = data.suborg_distribution; if (data.creator_org !== userdata.active_org.id) {
return null;
}
return data.id;
})
setSelectedSubOrg(allSuborg.filter((data) => data !== null))
} }
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 Generate an app based on documentation (beta)
</Typography> </Typography>
<IconButton <IconButton
onClick={() => { onClick={() => {
+140 -93
View File
@@ -1,9 +1,10 @@
import React, { useEffect, useState, useRef, useMemo } from "react"; import React, { useEffect, useState, useRef } 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";
@@ -26,7 +27,7 @@ import {
InstantSearch, InstantSearch,
Configure, Configure,
connectSearchBox, connectSearchBox,
connectInfiniteHits, connectHits,
connectHitInsights, connectHitInsights,
RefinementList, RefinementList,
ClearRefinements, ClearRefinements,
@@ -38,7 +39,6 @@ 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",
"eb5fd80aa6ed5ab4730d836cff3ea283" "c8f882473ff42d41158430be09ec2b4e"
); );
//const searchClient = algoliasearch("L55H18ZINA", "a19be455e7e75ee8f20a93d26b9fc6d6") //const searchClient = algoliasearch("L55H18ZINA", "a19be455e7e75ee8f20a93d26b9fc6d6")
@@ -77,13 +77,62 @@ 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 = "";
@@ -99,11 +148,10 @@ 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;
if (searchQuery !== foundQuery) { searchQuery = foundQuery
setSearchQuery(foundQuery);
}
} }
} }
//}, []) //}, [])
@@ -186,13 +234,7 @@ const AppGrid = (props) => {
onChange={(event) => { onChange={(event) => {
const value = event.currentTarget.value; const value = event.currentTarget.value;
setSearchQuery(value); setSearchQuery(value);
const urlSearchParams = new URLSearchParams(window.location.search); removeQuery("q");
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) => {
@@ -210,21 +252,6 @@ 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');
@@ -242,6 +269,7 @@ 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()}`);
}; };
@@ -253,8 +281,6 @@ 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
@@ -262,32 +288,6 @@ 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,16 +373,6 @@ 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 = {
@@ -414,7 +404,6 @@ 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,
@@ -429,7 +418,7 @@ const AppGrid = (props) => {
scrollbarColor: "#494949 #2f2f2f", scrollbarColor: "#494949 #2f2f2f",
}} }}
> >
{sortedHits.map((data, index) => { {hits?.map((data, index) => {
const appUrl = const appUrl =
isCloud === true ? isCloud === true ?
`/apps/${data.objectID}` `/apps/${data.objectID}`
@@ -640,12 +629,6 @@ 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 >
)} )}
@@ -943,7 +926,7 @@ const AppGrid = (props) => {
//Component to display all apps. //Component to display all apps.
const AllApps = ({ setIsAnyAppActivated }) => { const AllApps = ({ setIsAnyAppActivated }) => {
var [searchQuery, setSearchQuery] = useState(() => new URLSearchParams(window.location.search).get('q') || ""); var [searchQuery, setSearchQuery] = useState("");
return ( return (
<div <div
@@ -1004,7 +987,6 @@ const AppGrid = (props) => {
}} }}
onClick={() => { onClick={() => {
setSearchQuery(''); setSearchQuery('');
removeQuery("q");
}} }}
/> />
)} )}
@@ -1033,15 +1015,7 @@ 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) => {
const value = event.currentTarget.value; setSearchQuery(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") {
@@ -1618,7 +1592,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(() => new URLSearchParams(window.location.search).get('q') || ""); const [searchQuery, setSearchQuery] = useState("");
const [appsToShow, setAppsToShow] = useState([]); const [appsToShow, setAppsToShow] = useState([]);
useEffect(() => { useEffect(() => {
if (currTab === 1) { if (currTab === 1) {
@@ -2029,7 +2003,7 @@ const AppGrid = (props) => {
}; };
const CustomSearchBox = connectSearchBox(SearchBox); const CustomSearchBox = connectSearchBox(SearchBox);
const CustomHits = connectInfiniteHits(Hits); const CustomHits = connectHits(Hits);
const DisplayAllAppsTab = () => { const DisplayAllAppsTab = () => {
const [selectedCategoryForUsersAndOgsApps, setselectedCategoryForUsersAndOgsApps] = useState([]); const [selectedCategoryForUsersAndOgsApps, setselectedCategoryForUsersAndOgsApps] = useState([]);
@@ -2038,7 +2012,7 @@ const AppGrid = (props) => {
return ( return (
<div> <div>
<InstantSearch key={currTab === 0 ? "active" : "inactive"} searchClient={conditionalSearchClient} indexName="appsearch"> <InstantSearch searchClient={searchClient} 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 />
@@ -2063,7 +2037,7 @@ const AppGrid = (props) => {
/> />
</div> </div>
{currTab === 0 && <Configure clickAnalytics hitsPerPage={20} />} <Configure clickAnalytics />
</InstantSearch> </InstantSearch>
</div> </div>
); );
@@ -2086,7 +2060,80 @@ const AppGrid = (props) => {
> >
<DisplayAllAppsTab /> <DisplayAllAppsTab />
{showSuggestion === true ? ( {showSuggestion === true ? (
<SearchContactForm globalUrl={globalUrl} isMobile={isMobile} tabName="apps" /> <div
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",
"33e4e3564f4f060e96e0531957bed552" "c8f882473ff42d41158430be09ec2b4e"
);; );;
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", "33e4e3564f4f060e96e0531957bed552") const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
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", "33e4e3564f4f060e96e0531957bed552") const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
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)
-16
View File
@@ -92,11 +92,6 @@ 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
} }
@@ -383,17 +378,6 @@ 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]) {
+155 -100
View File
@@ -1,8 +1,6 @@
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";
@@ -19,6 +17,8 @@ import {
Button, Button,
Tabs, Tabs,
Tab, Tab,
List,
ListItem,
ListItemText, ListItemText,
IconButton, IconButton,
Dialog, Dialog,
@@ -82,7 +82,6 @@ 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";
@@ -128,9 +127,6 @@ 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)
@@ -237,7 +233,7 @@ const CacheView = memo((props) => {
{ {
"name": "Enrich", "name": "Enrich",
"description": "Enriches the data. Uses regex keys and runs a workflow in the background. Added to the 'enrichments' key.", "description": "Enriches the data. Only runs on valid JSON data AND if the 'enrichment' field does not exist.",
"type": "singul", "type": "singul",
"options": [{ "options": [{
"key": "", "key": "",
@@ -335,14 +331,6 @@ 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", "_")
@@ -582,26 +570,17 @@ 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());
}); });
}; };
@@ -852,11 +831,6 @@ 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, }}>
@@ -926,6 +900,33 @@ 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)
@@ -939,6 +940,8 @@ const CacheView = memo((props) => {
selected_suborgs: selectedSubOrg, selected_suborgs: selectedSubOrg,
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`;
@@ -972,41 +975,98 @@ 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 deleteConfirmDialog = ( const image = data.image === "" ? (
<DeleteConfirmDialog <img alt={data.name} src={theme.palette.defaultImage} style={imageStyle} />
open={deleteConfirmOpen} ) : (
onClose={() => { setDeleteConfirmOpen(false); setDeleteConfirmTarget(null); }} <img alt={data.name} src={data.image} style={imageStyle} />
onConfirm={() => { );
if (deleteConfirmTarget?.bulk) {
const itemsToDelete = selectedRows.map(rowId =>
listCache.find(item => `${item.key}_${item.category || ""}` === rowId)
).filter(Boolean);
const count = itemsToDelete.length; return (
setSelectedRows([]); <MenuItem
itemsToDelete.forEach(item => deleteEntry(orgId, item.key, item.category, false)); 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>
);
})}
setTimeout(() => { <div style={{ display: "flex", marginTop: 20 }}>
listOrgCache(orgId, selectedCategory, 0, pageSize, page); <Button
toast.success("Deleted " + count + " keys from datastore"); style={{ borderRadius: "2px", textTransform: 'none', fontSize:16, color: theme.palette.primary.main }}
}, 3000); onClick={() => setShowDistributionPopup(false)}
} else { color="primary"
deleteEntry(orgId, deleteConfirmTarget.key, deleteConfirmTarget.category); >
} Cancel
setDeleteConfirmOpen(false); </Button>
setDeleteConfirmTarget(null); <Button
}} variant="contained"
title={deleteConfirmTarget?.bulk ? `Delete ${selectedRows?.length} Key${selectedRows?.length > 1 ? "s" : ""}?` : "Delete Key?"} style={{ borderRadius: "2px", textTransform: 'none', fontSize:16, marginLeft: 10 }}
description={ onClick={() => {
deleteConfirmTarget?.bulk changeDistribution(selectedCacheKey, selectedSubOrg);
? <>Are you sure you want to delete <b>{selectedRows?.length} key{selectedRows?.length > 1 ? "s" : ""}</b>?</> }}
: <>Are you sure you want to delete <b>{deleteConfirmTarget?.key}</b>?</> color="primary"
} >
warningText="This cannot be undone. Any workflows using these keys will lose access." Submit
/> </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.
@@ -1699,10 +1759,6 @@ 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}
@@ -1879,7 +1935,6 @@ 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)
@@ -1964,8 +2019,7 @@ const CacheView = memo((props) => {
onClick={(e) => { onClick={(e) => {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
setDeleteConfirmTarget({ key: data.key, category: data.category }) deleteEntry(orgId, data.key, data.category)
setDeleteConfirmOpen(true)
}} }}
> >
<svg <svg
@@ -2040,21 +2094,13 @@ 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){
initialSelected = data.suborg_distribution; setSelectedSubOrg(data.suborg_distribution)
}else{
setSelectedSubOrg([])
} }
setSelectedSubOrg(initialSelected); setSelectedCacheKey(data.key)
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>
@@ -2070,7 +2116,6 @@ 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,
@@ -2138,17 +2183,7 @@ const CacheView = memo((props) => {
apps={apps} apps={apps}
/> />
<SubOrgDistributionDialog {cacheDistributionModal}
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' }}>
@@ -2169,6 +2204,7 @@ 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"
@@ -2236,12 +2272,12 @@ const CacheView = memo((props) => {
datastoreCategories !== null && datastoreCategories !== null &&
datastoreCategories.length > 1 ? ( datastoreCategories.length > 1 ? (
<FormControl style={{ minWidth: 275, maxWidth: 275, marginTop: 8, }}> <FormControl style={{ minWidth: 250, maxWidth: 250, marginTop: 8, }}>
<Autocomplete <Autocomplete
labelId="category-choice" labelId="category-choice"
style={{ style={{
minWidth: 275, minWidth: 250,
maxWidth: 275, maxWidth: 250,
}} }}
ListboxProps={{ ListboxProps={{
style: { style: {
@@ -2521,13 +2557,13 @@ const CacheView = memo((props) => {
marginLeft: 3, marginLeft: 3,
}} }}
variant="outlined" variant="outlined"
color={isAutomatingAccess ? "primary" : "secondary"} color="secondary"
disabled={selectedCategory === undefined || selectedCategory === "" || selectedCategory === "default"} disabled={selectedCategory === undefined || selectedCategory === "" || selectedCategory === "default"}
onClick={() => { onClick={() => {
setShowSettingsMenu(true) setShowSettingsMenu(true)
}} }}
> >
<SettingsIcon style={{}} /> <SettingsIcon style={{color: theme.palette.secondary.main, }} />
</Button> </Button>
</Tooltip> </Tooltip>
</ButtonGroup> </ButtonGroup>
@@ -2841,8 +2877,27 @@ const CacheView = memo((props) => {
<Button <Button
style={{ marginLeft: 50, }} style={{ marginLeft: 50, }}
onClick={() => { onClick={() => {
setDeleteConfirmTarget({ bulk: true }); setCachedLoaded(false)
setDeleteConfirmOpen(true); for (var key in selectedRows) {
// 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"
@@ -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", "33e4e3564f4f060e96e0531957bed552") const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
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} webhook={true} /> <IngestItem type="Enable Threat feeds" index={2} />
<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: "33e4e3564f4f060e96e0531957bed552", apiKey: "c8f882473ff42d41158430be09ec2b4e",
}) })
const timestamp = new Date().getTime() const timestamp = new Date().getTime()
+99 -4
View File
@@ -4,7 +4,6 @@ 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,
@@ -39,14 +38,18 @@ import {
} from "@mui/material" } from "@mui/material"
import { useDebouncedCallback } from "../utils/useDebouncedCallback.jsx"; import { useDebouncedCallback } from "../utils/useDebouncedCallback.jsx";
const searchClient = algoliasearch("JNSS5CFDZZ", "33e4e3564f4f060e96e0531957bed552") const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
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" ||
@@ -56,6 +59,44 @@ 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 = ""
@@ -208,8 +249,62 @@ const CreatorGrid = props => {
<CustomHits hitsPerPage={100}/> <CustomHits hitsPerPage={100}/>
</InstantSearch> </InstantSearch>
{showSuggestion === true ? {showSuggestion === true ?
<SearchContactForm globalUrl={globalUrl} isMobile={isMobile} tabName="creators" /> <div style={{maxWidth: isMobile ? "100%" : "60%", margin: "auto", paddingTop: 50, textAlign: "center",}}>
: null <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"}}>
<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>
) )
@@ -1,61 +0,0 @@
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;
+101 -2
View File
@@ -3,8 +3,11 @@ 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,
@@ -13,7 +16,6 @@ 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';
@@ -21,9 +23,52 @@ 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);
@@ -123,7 +168,61 @@ const DiscordChat = props => {
<CustomHits /> <CustomHits />
</div> </div>
</InstantSearch> </InstantSearch>
<SearchContactForm globalUrl={globalUrl} isMobile={isMobile} tabName="discord chats" /> <div 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: "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
+101 -13
View File
@@ -4,7 +4,6 @@ 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'
@@ -31,19 +30,61 @@ import { useDebouncedCallback } from "../utils/useDebouncedCallback.jsx";
const searchClient = algoliasearch("JNSS5CFDZZ", "eb5fd80aa6ed5ab4730d836cff3ea283") const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
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) {
@@ -83,15 +124,8 @@ const DocsGrid = props => {
placeholder="Search our Documentation..." placeholder="Search our Documentation..."
id="shuffle_search_field" id="shuffle_search_field"
onChange={(event) => { onChange={(event) => {
const value = event.currentTarget.value removeQuery("q")
debouncedRefine(value) debouncedRefine(event.currentTarget.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") {
@@ -257,8 +291,62 @@ const DocsGrid = props => {
<CustomHits hitsPerPage={5}/> <CustomHits hitsPerPage={5}/>
</InstantSearch> </InstantSearch>
{showSuggestion === true ? {showSuggestion === true ?
<SearchContactForm globalUrl={globalUrl} isMobile={isMobile} tabName="docs" /> <div style={{paddingTop: 0, maxWidth: isMobile ? "100%" : "60%", margin: "auto"}}>
: null <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>
: 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, }}>
+10 -56
View File
@@ -36,7 +36,6 @@ 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';
@@ -50,7 +49,6 @@ 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,
@@ -537,36 +535,13 @@ const EnvironmentTab = memo((props) => {
}, },
}, },
}} }}
style={{
}}
> >
<DialogTitle style={{display: "flex", }}> <DialogTitle>
<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>
<Typography variant='body1' color="textSecondary"> <div>
{sensorGroup ? <Typography variant='body2' color="textPrimary">Location Name</Typography>
'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,}}
@@ -580,14 +555,13 @@ const EnvironmentTab = memo((props) => {
}} }}
required required
fullWidth={true} fullWidth={true}
placeholder="automation location 3" placeholder="datacenter froglantern"
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}
@@ -602,13 +576,12 @@ 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"
> >
Create {sensorGroup ? "Sensor Group" : "Location"} Submit
</Button> </Button>
</DialogActions> </DialogActions>
</Dialog> </Dialog>
@@ -983,7 +956,7 @@ const EnvironmentTab = memo((props) => {
<ListItem <ListItem
style={{ style={{
display: "grid", display: "grid",
gridTemplateColumns: "80px 80px 70px 140px 100px 100px 400px 100px", gridTemplateColumns: "80px 80px 80px 150px 100px 80px 400px 100px",
width: "100%", width: "100%",
minWidth: showLoader ? 800 : 0, minWidth: showLoader ? 800 : 0,
paddingBottom: 0, paddingBottom: 0,
@@ -1129,11 +1102,6 @@ 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">
@@ -1307,9 +1275,6 @@ 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
@@ -1368,7 +1333,7 @@ const EnvironmentTab = memo((props) => {
/> />
<ListItemText <ListItemText
primary={environment?.sensor_group === true ? "Sensor Group" : environment.Type} primary={environment.Type}
primaryTypographyProps={{ primaryTypographyProps={{
style:{ style:{
minWidth: 50, minWidth: 50,
@@ -1555,16 +1520,6 @@ 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
@@ -1748,7 +1703,6 @@ 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', }}>
+150 -80
View File
@@ -29,8 +29,6 @@ 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";
@@ -47,12 +45,9 @@ 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";
@@ -87,10 +82,6 @@ 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)
@@ -335,7 +326,7 @@ const [filesLoaded, setFilesLoaded] = useState(false);
navigator.clipboard.writeText(file.id); navigator.clipboard.writeText(file.id);
document.execCommand("copy"); document.execCommand("copy");
toast.info(file.id + " copied to clipboard"); toast(file.id + " copied to clipboard");
}} }}
> >
<svg <svg
@@ -378,8 +369,7 @@ const [filesLoaded, setFilesLoaded] = useState(false);
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
setDeleteConfirmTarget({ id: file.id, filename: file.filename }); deleteFile(file.id, true);
setDeleteConfirmOpen(true);
}} }}
> >
<svg <svg
@@ -451,12 +441,6 @@ 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>
@@ -550,7 +534,7 @@ const [filesLoaded, setFilesLoaded] = useState(false);
}) })
.then((responseJson) => { .then((responseJson) => {
if (responseJson.success === true) { if (responseJson.success === true) {
toast.success("Successfully updated file"); toast("Successfully updated file");
} }
}) })
.catch((error) => { .catch((error) => {
@@ -864,39 +848,125 @@ const [filesLoaded, setFilesLoaded] = useState(false);
</Dialog> </Dialog>
: null : null
const deleteConfirmDialog = ( const handleSelectSubOrg = (id, action) => {
<DeleteConfirmDialog if (action === "all") {
open={deleteConfirmOpen} const childOrgs = userdata.orgs.filter(
onClose={() => { setDeleteConfirmOpen(false); setDeleteConfirmTarget(null); }} (data) => data.creator_org === userdata.active_org.id
onConfirm={() => { );
if (deleteConfirmTarget?.bulk) { setSelectedSubOrg((prev) => {
const toDelete = [...selectedRows]; if (prev.length === childOrgs.length) {
const count = toDelete.length; // If all child orgs are already selected, clear the selection
setSelectedRows([]); return [];
toDelete.forEach(fileId => deleteFile(fileId, false)); } else {
setTimeout(() => { // Otherwise, select all child org IDs
getFiles(selectedCategory); return childOrgs.map((data) => data.id);
toast.success('Deleted ' + count + ' file' + (count > 1 ? 's' : '')); }
}, 3000); });
} else { } else if (action === "none") {
deleteFile(deleteConfirmTarget.id, true); setSelectedSubOrg([]);
} } else {
setDeleteConfirmOpen(false); setSelectedSubOrg((prev) => {
setDeleteConfirmTarget(null); if (prev.includes(id)) {
}} return prev.filter((data) => data !== id);
title={ } else {
deleteConfirmTarget?.bulk return [...prev, id];
? 'Delete ' + selectedRows?.length + ' File' + (selectedRows?.length > 1 ? 's' : '') + '?' }
: 'Delete File?' });
} }
description={ };
deleteConfirmTarget?.bulk
? <>Are you sure you want to delete <b>{selectedRows?.length} file{selectedRows?.length > 1 ? 's' : ''}</b>?</> const fileDistributionModal = showDistributionPopup ? (
: <>Are you sure you want to delete <b>{deleteConfirmTarget?.filename}</b>?</> <Dialog
} open={showDistributionPopup}
warningText="This cannot be undone." 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 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) => {
@@ -1225,17 +1295,7 @@ const [filesLoaded, setFilesLoaded] = useState(false);
}} }}
onDrop={uploadFile} onDrop={uploadFile}
> >
<SubOrgDistributionDialog {fileDistributionModal}
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%", }}>
@@ -1499,20 +1559,17 @@ const [filesLoaded, setFilesLoaded] = useState(false);
autoFocus autoFocus
/>} />}
{openEditor === true && fileContent !== undefined && fileContent !== null && fileContent.length > 0 ? <ShuffleCodeEditor
<ShuffleCodeEditor isCloud={isCloud}
isCloud={isCloud} expansionModalOpen={openEditor}
expansionModalOpen={openEditor} setExpansionModalOpen={setOpenEditor}
setExpansionModalOpen={setOpenEditor} setcodedata = {setFileContent}
setcodedata = {setFileContent} codedata={fileContent}
codedata={fileContent} isFileEditor = {true}
isFileEditor = {true} key = {fileContent} //https://reactjs.org/docs/reconciliation.html#recursing-on-children
key = {fileContent} //https://reactjs.org/docs/reconciliation.html#recursing-on-children runUpdateText = {runUpdateText}
runUpdateText = {runUpdateText} contentLoading = {contentLoading}
contentLoading = {contentLoading} />
/>
: null}
{isSelectedFiles?null: {isSelectedFiles?null:
<Divider <Divider
style={{ style={{
@@ -1615,9 +1672,22 @@ const [filesLoaded, setFilesLoaded] = useState(false);
toast("Please select files to delete"); toast("Please select files to delete");
return; return;
} }
setDeleteConfirmTarget({ bulk: true });
setDeleteConfirmOpen(true); for (let i = 0; i < selectedRows.length; i++) {
}} 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
+14 -212
View File
@@ -52,12 +52,6 @@ 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 }) => {
@@ -93,17 +87,6 @@ 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]);
@@ -137,7 +120,7 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, SHUFFLE_V
setCurrentSelectedTheme(userdata?.theme); setCurrentSelectedTheme(userdata?.theme);
} }
}, [userdata]); }, [userdata]);
const CustomPopper = (props) => { const CustomPopper = (props) => {
@@ -856,8 +839,6 @@ 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";
} }
} }
} }
@@ -1041,7 +1022,7 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, SHUFFLE_V
}} }}
> >
<Tooltip <Tooltip
title={showPartnerLogo ? "Go to Home" : "Switch Product"} title="Go to Home"
placement="top" placement="top"
arrow arrow
componentsProps={{ componentsProps={{
@@ -1061,194 +1042,16 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, SHUFFLE_V
} }
}} }}
> >
<Box <Link to={isCloud && !showPartnerLogo ? "/" : "/workflows"}>
onClick={showPartnerLogo ? undefined : handleProductClick} <img
sx={{ src={
cursor: showPartnerLogo ? "default" : "pointer", showPartnerLogo ? userdata?.active_org?.image : ShuffleLogo
display: "flex", }
alignItems: "center", alt="Shuffle Logo"
justifyContent: "center", style={{ width: showPartnerLogo ? 30 : 24, height: showPartnerLogo ? 30 : 24 }}
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}}>
@@ -1944,8 +1747,7 @@ 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={isCloud ? "/partners" : "https://shuffler.io/partners"} to="/partners"
target={isCloud ? "_self" : "_blank"}
onClick={(event) => { onClick={(event) => {
setCurrentOpenTab("partners"); setCurrentOpenTab("partners");
localStorage.setItem("lastTabOpenByUser", "partners"); localStorage.setItem("lastTabOpenByUser", "partners");
@@ -2488,4 +2290,4 @@ const ModalView = memo(({searchBarModalOpen, setSearchBarModalOpen, globalUrl, s
</Dialog> </Dialog>
) )
) )
}); });
+51 -106
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: subscription.id, subscription_index: 0,
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: subscription.id, subscription_index: 0,
subscription: { subscription: {
...form, ...form,
// Ensure backend gets array of features // Ensure backend gets array of features
@@ -696,15 +696,6 @@ 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"
@@ -1051,17 +1042,6 @@ 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;
} }
@@ -1272,7 +1252,7 @@ const LicencePopup = (props) => {
</IconButton> </IconButton>
</Tooltip> </Tooltip>
)} )}
{(isPaidPlan || localSub?.amount === "0") ? ( {isPaidPlan ? (
<div <div
style={{ style={{
display: "flex", display: "flex",
@@ -1356,38 +1336,22 @@ 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,
@@ -1400,50 +1364,39 @@ 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={{}}>
{localSub?.active ? "App Runs" : "App Runs included in plan"} App Runs
</Typography> </Typography>
{localSub?.active ? ( <div
// Active plan - show current usage and progress bar style={{
<div display: "flex",
style={{ alignItems: "flex-start",
display: "flex", flexDirection: "column",
alignItems: "flex-start", gap: 10,
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, fontSize: 18 }} style={{ minWidth: 140, fontWeight: 600 }}
> >
{calculateAppRunsFromPrice(localSub.amount)?.toLocaleString?.() || calculateAppRunsFromPrice(localSub.amount)} {usedAppRuns?.toLocaleString?.() || usedAppRuns} of{" "}
{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>
) )
} }
@@ -1527,6 +1480,7 @@ const LicencePopup = (props) => {
}} }}
> >
{isCloud && {isCloud &&
localSub.name.toLowerCase().includes("scale") &&
localSub?.reference && localSub?.reference &&
localSub.reference.length > 0 ? ( localSub.reference.length > 0 ? (
<Button <Button
@@ -1536,11 +1490,8 @@ 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: "POST", method: "GET",
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())
@@ -1624,8 +1575,8 @@ const LicencePopup = (props) => {
display: "flex", display: "flex",
}} }}
> >
<Grid item maxWidth={licensePopup ? 400 : 800} style={{ display: "flex", flexDirection: "row", gap: 16, flexWrap: "wrap" }}> <Grid item maxWidth={licensePopup ? 400 : 450}>
{isLoading ? ( {isLoading ? (
<SubscriptionSkeleton /> <SubscriptionSkeleton />
) : ( ) : (
<> <>
@@ -1633,14 +1584,8 @@ const LicencePopup = (props) => {
selectedOrganization.subscriptions !== null && selectedOrganization.subscriptions !== null &&
selectedOrganization.subscriptions.length > 0 selectedOrganization.subscriptions.length > 0
? (selectedOrganization.subscriptions || []) ? (selectedOrganization.subscriptions || [])
.slice() .slice()
.sort((a, b) => { .map((sub, index) => {
// 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}
+46 -76
View File
@@ -3,7 +3,6 @@ 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";
@@ -479,7 +478,7 @@ const AuthenticationOauth2 = (props) => {
if (prompt !== undefined && prompt !== null && prompt.length > 0) { if (prompt !== undefined && prompt !== null && prompt.length > 0) {
defaultPrompt = prompt defaultPrompt = prompt
} }
var url = `${authenticationType.redirect_uri}?client_id=${client_id}&redirect_uri=${redirectUri}&response_type=code&prompt=${defaultPrompt}&scope=${resources}&state=${state}&access_type=offline`; var url = `${authenticationType.redirect_uri}?client_id=${client_id}&redirect_uri=${redirectUri}&response_type=code&prompt=${defaultPrompt}&scope=${resources}&state=${state}&access_type=offline`;
if (admin_consent === true) { if (admin_consent === true) {
console.log("Running Oauth2 WITH admin consent") console.log("Running Oauth2 WITH admin consent")
@@ -514,71 +513,57 @@ 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)
// This happens if automatic clicks occurr without the user, which is var open = true;
// disallowed in most cases const timer = setInterval(() => {
if (newwin === null || newwin === undefined) { if (newwin.closed) {
setButtonClicked(false); console.log("Closing?")
if (setFinalized !== undefined) {
setFinalized(true) setButtonClicked(false);
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)
} }
}
} else { toast.info("Authentication window closed")
var open = true;
const timer = setInterval(() => {
if (newwin.closed) {
console.log("Closing?")
setButtonClicked(false); // This is more a guess than anything
clearInterval(timer); // Should be handled in getAppAuthentication()
//alert('"Secure Payment" window closed!'); // 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")
}
}, 1000);
//do {
// setTimeout(() => {
// console.log(newwin)
// console.log("CLOSED", newwin.closed)
// if (newwin.closed) {
if (getAppAuthentication !== undefined) { // open = false
// 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) { // }, 1000)
getAppAuthentication(true, true, true, workflow.org_id) //}
} else { //while(open === true)
getAppAuthentication(true, true, true) } catch (e) {
} 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;
@@ -729,13 +714,6 @@ 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"
@@ -1101,14 +1079,6 @@ 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,8 +710,7 @@ 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}
@@ -1101,9 +1100,6 @@ 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,7 +846,6 @@ 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,9 +1087,6 @@ 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={{
@@ -1,121 +0,0 @@
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", "33e4e3564f4f060e96e0531957bed552") const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
const SearchData = props => { const SearchData = props => {
const { serverside, globalUrl, userdata } = props const { serverside, globalUrl, userdata } = props
let navigate = useNavigate(); let navigate = useNavigate();
+2 -1
View File
@@ -38,6 +38,7 @@ 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",
@@ -167,4 +168,4 @@ const SearchField = props => {
) )
} }
export default SearchField; export default SearchField;
+11 -31
View File
@@ -61,6 +61,8 @@ 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";
@@ -147,10 +149,7 @@ 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) => {
if (!isFileEditor) { return content
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) {
@@ -239,24 +238,6 @@ 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') {
@@ -1616,7 +1597,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)
const usedposition = e.offsetY let 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
@@ -1803,8 +1784,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 ? 800 : "80%", minWidth: isMobile || isWorkflowEditor || fullScreenModeEnabled ? "100%" : isFileEditor ? "650px" : "80%",
maxWidth: isMobile || isWorkflowEditor || fullScreenModeEnabled ? "100%" : isFileEditor ? 800 : "1100px", maxWidth: isMobile || isWorkflowEditor || fullScreenModeEnabled ? "100%" : isFileEditor ? "650px" : "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)",
@@ -2000,8 +1981,7 @@ const CodeEditor = (props) => {
paddingLeft: 10, paddingLeft: 10,
}} }}
> >
{/* cba positioning */} File Editor ({localcodedata.length})
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>
@@ -2531,7 +2511,7 @@ const CodeEditor = (props) => {
} }
</Tooltip> </Tooltip>
</IconButton> </IconButton>
{/*(actionId || triggerId || conditionId) && !isWorkflowEditor && !isFileEditor ? {(actionId || triggerId || conditionId) && !isWorkflowEditor && !isFileEditor ?
<> <>
<IconButton <IconButton
style={{ style={{
@@ -2577,7 +2557,7 @@ const CodeEditor = (props) => {
</IconButton> </IconButton>
</> </>
: null : null
*/} }
</div> </div>
</div> </div>
} }
@@ -2603,7 +2583,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 ? 800 : fullScreenModeEnabled ? isFileEditor ? "100%" : "50vw" : isWorkflowEditor ? "90vw" : "100%"} width={isFileEditor ? 650 : fullScreenModeEnabled ? "50vw" : isWorkflowEditor ? "90vw" : "100%"}
markers={markers} markers={markers}
highlightActiveLine={false} highlightActiveLine={false}
@@ -2737,7 +2717,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})` :
@@ -1,265 +0,0 @@
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
+96 -67
View File
@@ -1,10 +1,11 @@
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,
@@ -30,12 +31,23 @@ 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";
import SubOrgDistributionDialog from "./SubOrgDistributionDialog.jsx"; const ITEM_HEIGHT = 48;
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 = "";
@@ -64,11 +76,10 @@ 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);
@@ -236,6 +247,30 @@ 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 &&
@@ -243,44 +278,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 ? (
<Button <FormControl fullWidth sx={{ m: 1 }}>
variant="outlined" <InputLabel id="demo-multiple-checkbox-label" style={{ padding: 5 }}>
color="primary" Accessible Sub-Organizations (
fullWidth {selectedUser?.orgs ? selectedUser?.orgs?.length - 1 : 0})
disabled={selectedUser?.id === userdata?.id} </InputLabel>
onClick={() => { <Select
setPendingSubOrgs([...matchingOrganizations]); fullWidth
setSubOrgModalOpen(true); style={{ width: "100%" }}
}} disabled={selectedUser?.id === userdata?.id}
sx={{ m: 1, textTransform: 'none', justifyContent: 'space-between', py: 1.5, fontSize: 14 }} labelId="demo-multiple-checkbox-label"
> id="demo-multiple-checkbox"
Manage Sub-Organizations ({matchingOrganizations.length} assigned of {selectedOrganization?.child_orgs?.length || 0}) multiple
</Button> value={matchingOrganizations}
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",
@@ -1082,8 +1117,6 @@ const UserManagmentTab = memo((props) => {
}); });
}; };
var previousreferrer = ""
var nextreferrer = ""
const logview = logsViewModal ? ( const logview = logsViewModal ? (
<Dialog <Dialog
open={logsViewModal} open={logsViewModal}
@@ -1130,23 +1163,26 @@ 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) => {
console.log("Data: ", data) if (
if (data.ip.includes("127.0.0.1") || uniqueIPs.has(data.ip)) { data.ip.includes("127.0.0.1") ||
return null uniqueIPs.has(data.ip)
) {
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?.timestamp ? new Date(data.timestamp * 1000).toLocaleString() : "N/A"} - {data?.ip} {data.ip}
</MenuItem> </MenuItem>
); );
}); });
@@ -1193,13 +1229,12 @@ const UserManagmentTab = memo((props) => {
minWidth: 700, minWidth: 700,
maxWidth: 700, maxWidth: 700,
overflow: "hidden", overflow: "hidden",
marginLeft: 50, marginLeft: 10,
}} }}
/> />
</ListItem> </ListItem>
{logs.map((data, index) => { {logs.map((data, index) => {
previousreferrer = nextreferrer //console.log("LOG: ", data)
nextreferrer = data.referer
return ( return (
// redirect user to logs // redirect user to logs
@@ -1208,8 +1243,6 @@ 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
@@ -1230,25 +1263,22 @@ const UserManagmentTab = memo((props) => {
}} }}
/> />
<ListItemText <ListItemText
primary={data.referer.replace("https://shuffler.io", "")} primary={data.referer}
style={{ style={{
minWidth: 300, minWidth: 300,
maxWidth: 300, maxWidth: 300,
overflow: "hidden", overflow: "hidden",
}} }}
/> />
<Link to={data.url} target="_blank" style={{ textDecoration: "none", color: theme.palette.linkColor }}> <ListItemText
<ListItemText primary={data.url}
primary={data.url.replace("https://shuffler.io", "")} style={{
style={{ minWidth: 700,
minWidth: 700, maxWidth: 700,
maxWidth: 700, overflow: "hidden",
overflow: "hidden", marginLeft: 10,
marginLeft: 50, }}
backgroundColor: data.url.includes("/api/v1/register") ? "#d52b2b" : "inherit", />
}}
/>
</Link>
</ListItem> </ListItem>
)})} )})}
</List> </List>
@@ -1260,7 +1290,6 @@ 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' }}>
+104 -14
View File
@@ -3,7 +3,6 @@ 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';
@@ -26,23 +25,66 @@ 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", "eb5fd80aa6ed5ab4730d836cff3ea283") const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
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"]
@@ -187,16 +229,10 @@ 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") {
@@ -297,10 +333,64 @@ const AppGrid = props => {
<CustomHits hitsPerPage={5}/> <CustomHits hitsPerPage={5}/>
</InstantSearch> </InstantSearch>
{showSuggestion === true ? {showSuggestion === true ?
<SearchContactForm globalUrl={globalUrl} isMobile={isMobile} tabName="workflows" /> <div style={{maxWidth: isMobile ? "100%" : "60%", margin: "auto", paddingTop: 0, textAlign: "center",}}>
: null <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"}}>
<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
@@ -309,7 +399,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", "33e4e3564f4f060e96e0531957bed552") const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
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
+12 -23
View File
@@ -1,16 +1,16 @@
import { useEffect, useContext } from "react"; import { useEffect, useContext } from "react";
import React from "react"; import React from "react";
import { import {
Typography, Typography,
Switch, Switch,
Button, Button,
Tooltip, Tooltip,
TextField, TextField,
Grid, Grid,
Checkbox Checkbox
} from "@mui/material"; } from "@mui/material";
import { makeStyles } from "@mui/styles"; import { makeStyles } from "@mui/styles";
import { Link, useSearchParams } from "react-router-dom"; import { Link } 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";
@@ -28,13 +28,7 @@ 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);
@@ -115,7 +109,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(effectiveGlobalUrl + "/api/v1/getusers", { fetch(globalUrl + "/api/v1/getusers", {
method: "GET", method: "GET",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -315,7 +309,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
}; };
const HandleTestSSO = () => { const HandleTestSSO = () => {
const url = `${effectiveGlobalUrl}/api/v1/orgs/${selectedOrganization?.id}/change`; const url = `${globalUrl}/api/v1/orgs/${selectedOrganization?.id}/change`;
const data = { const data = {
org_id: selectedOrganization?.id, org_id: selectedOrganization?.id,
sso: true, sso: true,
@@ -372,7 +366,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
}; };
const HandleDisconnectSSO = () => { const HandleDisconnectSSO = () => {
const url = `${effectiveGlobalUrl}/api/v1/disconnect_sso`; const url = `${globalUrl}/api/v1/disconnect_sso`;
const data = { const data = {
org_id: selectedOrganization?.id, org_id: selectedOrganization?.id,
}; };
@@ -450,11 +444,6 @@ 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
+95 -85
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: "/icons/workflow-page/shuffle_agent.png", // For AI Agent stuff singulBlackWhite: "/images/singul_black_white.png",
}, },
typography: { typography: {
fontFamily: `"inter", "Roboto", "Helvetica", "Arial", sans-serif`, fontFamily: `"inter", "Roboto", "Helvetica", "Arial", sans-serif`,
@@ -153,10 +153,20 @@ const theme = createTheme(adaptV4Theme({
export default theme; export default theme;
export const getTheme = (themeMode, brandColor) => export const getTheme = (themeMode, brandColor) => {
createTheme({ // Handle "system" mode by checking user's system preference
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: themeMode, mode: resolvedMode,
main: brandColor || "#FF8544", main: brandColor || "#FF8544",
primary: { primary: {
main: brandColor || "#FF8544", main: brandColor || "#FF8544",
@@ -167,36 +177,35 @@ export const getTheme = (themeMode, brandColor) =>
contrastText:"#000000", contrastText:"#000000",
}, },
text: { text: {
primary: themeMode === "dark" ? "#ffffff" : "#1A1A1A", primary: resolvedMode === "dark" ? "#ffffff" : "#1A1A1A",
secondary: themeMode === "dark" ? "#9E9E9E" : "#616161", secondary: resolvedMode === "dark" ? "#9E9E9E" : "#616161",
}, },
type: themeMode, type: resolvedMode,
inputColor: themeMode === "dark" ? "rgba(39,41,45,1)" : "rgba(245, 245, 245, 1)", inputColor: resolvedMode === "dark" ? "rgba(39,41,45,1)" : "rgba(245, 245, 245, 1)",
textColor: themeMode === "dark" ? "#F1F1F1" : "#1A1A1A", textColor: resolvedMode === "dark" ? "#F1F1F1" : "#1A1A1A",
textPrimary: themeMode === "dark" ? "rgba(255, 255, 255, 0.8)" : "rgba(26, 26, 26, 0.8)", textPrimary: resolvedMode === "dark" ? "rgba(255, 255, 255, 0.8)" : "rgba(26, 26, 26, 0.8)",
surfaceColor: themeMode === "dark" ? "#27292d" : "#EFEFEF", surfaceColor: resolvedMode === "dark" ? "#27292d" : "#EFEFEF",
platformColor: themeMode === "dark" ? "#212121" : "#ffffff", platformColor: resolvedMode === "dark" ? "#212121" : "#ffffff",
backgroundColor: themeMode === "dark" ? "#1a1a1a" : "#f1f1f1", backgroundColor: resolvedMode === "dark" ? "#1a1a1a" : "#f1f1f1",
cytoscapeBackgroundColor: themeMode === "dark" ? "#161616" : "#f5f5f5", cytoscapeBackgroundColor: resolvedMode === "dark" ? "#161616" : "#f5f5f5",
distributionColor: themeMode === "dark" ? "#40E0D0" : "#008080", distributionColor: resolvedMode === "dark" ? "#40E0D0" : "#008080",
cardBackgroundColor: themeMode === "dark" ? "#1e1e1e" : "#eaeaea", cardBackgroundColor: resolvedMode === "dark" ? "#1e1e1e" : "#eaeaea",
cardHoverColor: themeMode === "dark" ? "#323232" : "#F0F0F0", cardHoverColor: resolvedMode === "dark" ? "#323232" : "#F0F0F0",
hoverColor: themeMode === "dark" ? "#323232" : "#D6D6D6", hoverColor: resolvedMode === "dark" ? "#323232" : "#D6D6D6",
usecaseCardColor: themeMode === "dark" ? "#2f2f2f" : "rgba(245, 245, 245, 1)", usecaseCardColor: resolvedMode === "dark" ? "#2f2f2f" : "rgba(245, 245, 245, 1)",
usecaseCardHoverColor: themeMode === "dark" ? "#2F2F2F" : "rgba(245, 245, 245, 1)", usecaseCardHoverColor: resolvedMode === "dark" ? "#2F2F2F" : "rgba(245, 245, 245, 1)",
usecaseDialogFieldColor: themeMode === "dark" ? "#2B2B2B" : "#F5F5F5", usecaseDialogFieldColor: resolvedMode === "dark" ? "#2B2B2B" : "#F5F5F5",
accentColor: themeMode === "dark" ? "#ff8544" : "#ff8544", accentColor: resolvedMode === "dark" ? "#ff8544" : "#ff8544",
green: themeMode === "dark" ? "#5cc879" : "#008000", green: resolvedMode === "dark" ? "#5cc879" : "#008000",
defaultBorder: themeMode === "dark" ? '1px solid #494949' : '1px solid #CCCCCC', defaultBorder: resolvedMode === "dark" ? '1px solid #494949' : '1px solid #CCCCCC',
linkColor: brandColor === "#ff8544" ? "#f86a3e" : brandColor, linkColor: brandColor === "#ff8544" ? "#f86a3e" : brandColor,
slateGrayColor: themeMode === "dark" ? "#494949" : "#CCCCCC", slateGrayColor: resolvedMode === "dark" ? "#494949" : "#CCCCCC",
parsedAppPaperColor: themeMode === "dark" ? "#2f2f2f" : "#CCCCCC", parsedAppPaperColor: resolvedMode === "dark" ? "#2f2f2f" : "#CCCCCC",
welcomeCardSubtextColor: themeMode === "dark" ? "#C8C8C8" : "#2f2f2f",
deleteColor: themeMode === "dark" ? "#FD4C62" : "#d32f2f",
borderRadius: 10, borderRadius: 10,
loaderColor: themeMode === "dark" ? "#1a1a1a" : "#E0E0E0", loaderColor: resolvedMode === "dark" ? "#1a1a1a" : "#E0E0E0",
jsonIconStyle: "round", jsonIconStyle: "round",
jsonTheme: themeMode === "dark" ? "summerfruit" : { jsonTheme: resolvedMode === "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
@@ -216,11 +225,11 @@ export const getTheme = (themeMode, brandColor) =>
}, },
jsonCollapseStringsAfterLength: 100, jsonCollapseStringsAfterLength: 100,
drawer: { drawer: {
backgroundColor: themeMode === "dark" ? "#262626" : "#f9f9f9" backgroundColor: resolvedMode === "dark" ? "#262626" : "#f9f9f9"
}, },
actionSidebarField: { actionSidebarField: {
backgroundColor: themeMode === "dark" ? "#2F2F2F" : "#F1F1F1", backgroundColor: resolvedMode === "dark" ? "#2F2F2F" : "#F1F1F1",
color: themeMode === "dark" ? "#ffffff" : "#000000", color: resolvedMode === "dark" ? "#ffffff" : "#000000",
borderRadius: 8, borderRadius: 8,
height: 40, height: 40,
border: "none", border: "none",
@@ -229,53 +238,53 @@ export const getTheme = (themeMode, brandColor) =>
padding: 5, padding: 5,
width: "98%", width: "98%",
borderRadius: 5, borderRadius: 5,
border: themeMode === "dark" ? "1px solid rgba(255,255,255,0.7)" : "1px solid rgba(0,0,0,0.3)", border: resolvedMode === "dark" ? "1px solid rgba(255,255,255,0.7)" : "1px solid rgba(0,0,0,0.3)",
backgroundColor: themeMode === "dark" backgroundColor: resolvedMode === "dark"
? "#1A1A1A" ? "#1A1A1A"
: "#f1f1f1", : "#f1f1f1",
color: themeMode === "dark" color: resolvedMode === "dark"
? "#F1F1F1" ? "#F1F1F1"
: "#1A1A1A", : "#1A1A1A",
overflowX: "auto", overflowX: "auto",
}, },
textFieldStyle: { textFieldStyle: {
backgroundColor: themeMode === "dark" ? "#212121" : "#FFFFFF", backgroundColor: resolvedMode === "dark" ? "#212121" : "#FFFFFF",
color: themeMode === "dark" ? "#ffffff" : "#000000", color: resolvedMode === "dark" ? "#ffffff" : "#000000",
borderRadius: "5px", borderRadius: "5px",
height: 40, height: 40,
border: themeMode === "dark" ? "1px solid #4D4D4D" : "1px solid #E0E0E0", border: resolvedMode === "dark" ? "1px solid #4D4D4D" : "1px solid #E0E0E0",
}, },
DialogStyle: { DialogStyle: {
backgroundColor: themeMode === "dark" ? "#212121" : "#ffffff", backgroundColor: resolvedMode === "dark" ? "#212121" : "#ffffff",
borderRadius: 2, borderRadius: 2,
boxShadow: themeMode === "dark" ? "0px 0px 10px 0px rgba(0,0,0,0.75)" : "0px 0px 10px 0px rgba(0,0,0,0.2)", boxShadow: resolvedMode === "dark" ? "0px 0px 10px 0px rgba(0,0,0,0.75)" : "0px 0px 10px 0px rgba(0,0,0,0.2)",
border: themeMode === "dark" ? "1px solid #494949" : "1px solid #cccccc", border: resolvedMode === "dark" ? "1px solid #494949" : "1px solid #cccccc",
}, },
innerTextfieldStyle: { innerTextfieldStyle: {
height: 40, height: 40,
fontSize: 16, fontSize: 16,
backgroundColor: themeMode === "dark" ? "#212121" : "#f5f5f5", backgroundColor: resolvedMode === "dark" ? "#212121" : "#f5f5f5",
}, },
tooltip: { tooltip: {
backgroundColor: themeMode === "dark" ? "#212121" : "#ffffff", backgroundColor: resolvedMode === "dark" ? "#212121" : "#ffffff",
color: themeMode === "dark" ? "#ffffff" : "#000000", color: resolvedMode === "dark" ? "#ffffff" : "#000000",
border: themeMode === "dark" ? "1px solid #494949" : "1px solid #cccccc", border: resolvedMode === "dark" ? "1px solid #494949" : "1px solid #cccccc",
}, },
chipStyle: { chipStyle: {
backgroundColor: themeMode === "dark" ? "#333333" : "#F5F5F5", backgroundColor: resolvedMode === "dark" ? "#333333" : "#F5F5F5",
borderColor: themeMode === "dark" ? "#444444" : "#E0E0E0", borderColor: resolvedMode === "dark" ? "#444444" : "#E0E0E0",
color: themeMode === "dark" ? "#FFFFFF" : "#333333", color: resolvedMode === "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: themeMode === "dark" ? "#494949 #2f2f2f": "#c1c1c1 #f1f1f1", scrollbarColor: resolvedMode === "dark" ? "#494949 #2f2f2f": "#c1c1c1 #f1f1f1",
scrollbarColorTransparent: themeMode === "dark" ? '#494949 transparent': "#c1c1c1 transparent", scrollbarColorTransparent: resolvedMode === "dark" ? '#494949 transparent': "#c1c1c1 transparent",
}, },
typography: { typography: {
fontFamily: `"inter", "Roboto", "Helvetica", "Arial", sans-serif`, fontFamily: `"inter", "Roboto", "Helvetica", "Arial", sans-serif`,
color: themeMode === "dark" ? "#ffffff" : "#000000", color: resolvedMode === "dark" ? "#ffffff" : "#000000",
useNextVariants: true, useNextVariants: true,
fontWeightLight: 300, fontWeightLight: 300,
fontWeightRegular: 400, fontWeightRegular: 400,
@@ -283,36 +292,36 @@ export const getTheme = (themeMode, brandColor) =>
fontWeightSemiBold: 600, fontWeightSemiBold: 600,
fontWeightBold: 700, fontWeightBold: 700,
allVariants: { allVariants: {
color: themeMode === "dark" ? "#ffffff" : "#1A1A1A", color: resolvedMode === "dark" ? "#ffffff" : "#1A1A1A",
}, },
h1: { h1: {
fontSize: 40, fontSize: 40,
color: themeMode === "dark" ? "#ffffff" : "#1A1A1A" color: resolvedMode === "dark" ? "#ffffff" : "#1A1A1A"
}, },
h2: { h2: {
fontSize: 36, fontSize: 36,
color: themeMode === "dark" ? "#ffffff" : "#1A1A1A" color: resolvedMode === "dark" ? "#ffffff" : "#1A1A1A"
}, },
h3: { h3: {
fontSize: 32, fontSize: 32,
color: themeMode === "dark" ? "#ffffff" : "#1A1A1A" color: resolvedMode === "dark" ? "#ffffff" : "#1A1A1A"
}, },
h4: { h4: {
fontSize: 30, fontSize: 30,
fontWeight: 500, fontWeight: 500,
color: themeMode === "dark" ? "#ffffff" : "#1A1A1A" color: resolvedMode === "dark" ? "#ffffff" : "#1A1A1A"
}, },
h6: { h6: {
fontSize: 22, fontSize: 22,
color: themeMode === "dark" ? "#ffffff" : "#1A1A1A" color: resolvedMode === "dark" ? "#ffffff" : "#1A1A1A"
}, },
body1: { body1: {
fontSize: 16, fontSize: 16,
color: themeMode === "dark" ? "#ffffff" : "#1A1A1A" color: resolvedMode === "dark" ? "#ffffff" : "#1A1A1A"
}, },
body2: { body2: {
fontSize: 14, fontSize: 14,
color: themeMode === "dark" ? "#ffffff" : "#1A1A1A" color: resolvedMode === "dark" ? "#ffffff" : "#1A1A1A"
}, },
}, },
components: { components: {
@@ -327,7 +336,7 @@ export const getTheme = (themeMode, brandColor) =>
{ {
props: { variant: 'text', color: 'primary' }, props: { variant: 'text', color: 'primary' },
style: { style: {
color: themeMode === "dark" ? "#ffffff" : "#1A1A1A", color: resolvedMode === "dark" ? "#ffffff" : "#1A1A1A",
whiteSpace: "nowrap", whiteSpace: "nowrap",
textWrap: "normal", textWrap: "normal",
}, },
@@ -335,7 +344,7 @@ export const getTheme = (themeMode, brandColor) =>
{ {
props: { variant: 'text', color: 'secondary' }, props: { variant: 'text', color: 'secondary' },
style: { style: {
color: themeMode === "dark" ? "#9E9E9E" : "#616161", color: resolvedMode === "dark" ? "#9E9E9E" : "#616161",
whiteSpace: "nowrap", whiteSpace: "nowrap",
textWrap: "normal", textWrap: "normal",
}, },
@@ -343,24 +352,24 @@ export const getTheme = (themeMode, brandColor) =>
{ {
props: { variant: 'contained', color: 'primary' }, props: { variant: 'contained', color: 'primary' },
style: { style: {
backgroundColor: themeMode === "dark" ? brandColor || '#ff8544' : brandColor || '#FF7C35', backgroundColor: resolvedMode === "dark" ? brandColor || '#ff8544' : brandColor || '#FF7C35',
color: themeMode === "dark" ? '#1a1a1a': '#FFFFFF', color: resolvedMode === "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: themeMode === 'dark' ? brandColor || "#ff955c" : brandColor || '#FF8D4F', backgroundColor: resolvedMode === 'dark' ? brandColor || "#ff955c" : brandColor || '#FF8D4F',
color: themeMode === "dark" ? '#1a1a1a': '#FFFFFF', color: resolvedMode === "dark" ? '#1a1a1a': '#FFFFFF',
}, },
}, },
}, },
{ {
props: { variant: 'contained', color: 'secondary' }, props: { variant: 'contained', color: 'secondary' },
style: { style: {
backgroundColor: themeMode === "dark" ? '#494949' : '#C9C9C9', backgroundColor: resolvedMode === "dark" ? '#494949' : '#C9C9C9',
color: themeMode === "dark" ? '#ffffff' : '#4C4C4C', color: resolvedMode === "dark" ? '#ffffff' : '#4C4C4C',
borderRadius: '4px', borderRadius: '4px',
boxShadow: 'none', boxShadow: 'none',
whiteSpace: "nowrap", whiteSpace: "nowrap",
@@ -368,23 +377,23 @@ export const getTheme = (themeMode, brandColor) =>
textWrap: "normal", textWrap: "normal",
'&:hover': { '&:hover': {
fontWeight: 600, fontWeight: 600,
border: themeMode === "dark" ? '1px solid #f1f1f1' : 'none', border: resolvedMode === "dark" ? '1px solid #f1f1f1' : 'none',
backgroundColor: themeMode === "dark" ? '#494949' : '#C9C9C9', backgroundColor: resolvedMode === "dark" ? '#494949' : '#C9C9C9',
color: themeMode === "dark" ? '#ffffff' : '#4C4C4C', color: resolvedMode === "dark" ? '#ffffff' : '#4C4C4C',
}, },
}, },
}, },
{ {
props: { variant: 'outlined', color: 'primary' }, props: { variant: 'outlined', color: 'primary' },
style: { style: {
borderColor: themeMode === "dark" ? brandColor || "#ff8544" : brandColor || "#cc5f1f", borderColor: resolvedMode === "dark" ? brandColor || "#ff8544" : brandColor || "#cc5f1f",
color: themeMode === "dark" ? brandColor || "#ff8544" : brandColor || "#cc5f1f", color: resolvedMode === "dark" ? brandColor || "#ff8544" : brandColor || "#cc5f1f",
whiteSpace: "nowrap", whiteSpace: "nowrap",
fontWeight: 'normal', fontWeight: 'normal',
textWrap: "normal", textWrap: "normal",
'&:hover': { '&:hover': {
backgroundColor: themeMode === "dark" ? brandColor || "#ff8544" : "#ffe8dc", backgroundColor: resolvedMode === "dark" ? brandColor || "#ff8544" : "#ffe8dc",
color: themeMode === "dark" ? "#1a1a1a" : "#8a3d00", color: resolvedMode === "dark" ? "#1a1a1a" : "#8a3d00",
fontWeight: 600, fontWeight: 600,
}, },
}, },
@@ -393,14 +402,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: themeMode === "dark" ? '#C5C5C5' : '#2D2D2D', color: resolvedMode === "dark" ? '#C5C5C5' : '#2D2D2D',
whiteSpace: "nowrap", whiteSpace: "nowrap",
textWrap: "normal", textWrap: "normal",
'&:hover': { '&:hover': {
backgroundColor: themeMode === "dark" ? '#C5C5C5' : '#EFEFEF', backgroundColor: resolvedMode === "dark" ? '#C5C5C5' : '#EFEFEF',
borderColor: themeMode === "dark" ? '#C5C5C5' : '#2D2D2D', borderColor: resolvedMode === "dark" ? '#C5C5C5' : '#2D2D2D',
fontWeight: 600, fontWeight: 600,
color: themeMode === "dark" ? '#1a1a1a' : '#1A1A1A', color: resolvedMode === "dark" ? '#1a1a1a' : '#1A1A1A',
}, },
}, },
}, },
@@ -423,8 +432,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: themeMode === "dark" ? '#494949' : '#C9C9C9', background: resolvedMode === "dark" ? '#494949' : '#C9C9C9',
color: themeMode === "dark" ? '#9E9E9E' : '#616161', color: resolvedMode === "dark" ? '#9E9E9E' : '#616161',
}, },
}, },
}, },
@@ -469,9 +478,9 @@ export const getTheme = (themeMode, brandColor) =>
}, },
'&:disabled': { '&:disabled': {
background: 'transparent', background: 'transparent',
color: themeMode === "dark" ? '#9E9E9E' : '#616161', color: resolvedMode === "dark" ? '#9E9E9E' : '#616161',
'&::before': { '&::before': {
background: themeMode === "dark" ? '#494949' : '#C9C9C9', background: resolvedMode === "dark" ? '#494949' : '#C9C9C9',
}, },
}, },
}, },
@@ -481,7 +490,7 @@ export const getTheme = (themeMode, brandColor) =>
MuiTab: { MuiTab: {
styleOverrides: { styleOverrides: {
root: { root: {
color: themeMode === "dark" ? "#C5C5C5" : "#1A1A1A", color: resolvedMode === "dark" ? "#C5C5C5" : "#1A1A1A",
}, },
}, },
}, },
@@ -490,7 +499,7 @@ export const getTheme = (themeMode, brandColor) =>
overrides: { overrides: {
MuiMenu: { MuiMenu: {
list: { list: {
backgroundColor: themeMode === "dark" ? "#27292d" : "#ffffff", backgroundColor: resolvedMode === "dark" ? "#27292d" : "#ffffff",
}, },
}, },
MuiCssBaseline: { MuiCssBaseline: {
@@ -536,4 +545,5 @@ export const getTheme = (themeMode, brandColor) =>
}, },
}, },
}); });
}
+42 -126
View File
@@ -44,7 +44,6 @@ 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 {
@@ -72,7 +71,6 @@ 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 === "") {
@@ -190,11 +188,11 @@ const AgentUI = (props) => {
const agentWrapperStyle = { const agentWrapperStyle = {
width: "100%", width: "100%",
minHeight: "100vh", maxHeight: "100vh",
margin: "auto", margin: "auto",
backgroundColor: theme.palette.backgroundColor, backgroundColor: theme.palette.backgroundColor,
paddingBottom: showAgentStarter ? 0 : 50, paddingBottom: showAgentStarter ? 0 : 1500,
} }
@@ -250,25 +248,21 @@ 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") {
node_id = item?.action?.id continue
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
} }
} }
@@ -276,11 +270,6 @@ 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
} }
@@ -300,6 +289,16 @@ 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!")
}
}
} }
} }
@@ -476,7 +475,7 @@ const AgentUI = (props) => {
getAppAuth() getAppAuth()
}, []) }, [])
const maxTimelineWidth = 275 const maxTimelineWidth = 375
const submitQuestions = (decisionId, questionAnswers, isContinuation) => { const submitQuestions = (decisionId, questionAnswers, isContinuation) => {
console.log("Submitting questions: ", decisionId, questionAnswers) console.log("Submitting questions: ", decisionId, questionAnswers)
@@ -522,12 +521,8 @@ 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")
// workflow_id param > execution.workflow.id > executionId 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}`
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",
@@ -898,32 +893,15 @@ const AgentUI = (props) => {
/> />
</div> </div>
<div style={{ <div style={{
minWidth: 400, minWidth: 300,
maxWidth: 400, maxWidth: 300,
maxHeight: 150, maxHeight: 150,
overflowX: "hidden", overflowX: "hidden",
overflowY: "auto", overflowY: "auto",
paddingTop: defaultTopPadding,
paddingBottom: defaultTopPadding,
}}> }}>
<Markdown {itemLabel}
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"
@@ -1106,7 +1084,7 @@ const AgentUI = (props) => {
</div> </div>
: null} : null}
{item.category !== "agent" && questions?.length > 0 && (item?.status === "RUNNING" || item?.status === "WAITING") ? {questions?.length > 0 && item?.status === "RUNNING" || item?.status === "WAITING" ?
<div> <div>
{questions.map((q, questionIndex) => { {questions.map((q, questionIndex) => {
return ( return (
@@ -1211,25 +1189,7 @@ const AgentUI = (props) => {
const [continuationText, setContinuationText] = useState("") const [continuationText, setContinuationText] = useState("")
// Find the AI Agent result specifically, not just results[0] var actionResult = execution?.results?.length > 0 ? execution.results[0] : execution
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
@@ -1286,11 +1246,6 @@ 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
} }
@@ -1540,7 +1495,6 @@ 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",
@@ -1563,23 +1517,9 @@ 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),
@@ -1664,8 +1604,8 @@ const AgentUI = (props) => {
</ButtonGroup> </ButtonGroup>
return ( return (
<div style={{paddingBottom: 500, }}> <div>
<div style={{height: showAgentStarter ? "17vh" : "5vh", }}> <div style={{height: showAgentStarter ? "25vh" : "5vh", }}>
</div> </div>
<div style={agentWrapperStyle}> <div style={agentWrapperStyle}>
<div style={{ <div style={{
@@ -1727,14 +1667,10 @@ const AgentUI = (props) => {
agentRequestLoading ? agentRequestLoading ?
<CircularProgress size={24} style={{marginRight: 10, }} /> <CircularProgress size={24} style={{marginRight: 10, }} />
: :
<Tooltip title="Start the agent. Data can be anything, and it will run based on your selected tools"> <Tooltip title="This is the input for the AI Agent. It can be any valid JSON.">
<IconButton <IconButton type="submit">
type="submit"
color={actionInput === "" || actionInput === undefined || actionInput === null ? "secondary" : "primary"}
disabled={actionInput === "" || actionInput === undefined || actionInput === null}
>
<SendIcon <SendIcon
color={actionInput === "" || actionInput === undefined || actionInput === null ? "secondary" : "primary"} color="primary"
/> />
</IconButton> </IconButton>
</Tooltip> </Tooltip>
@@ -1742,29 +1678,12 @@ const AgentUI = (props) => {
}} }}
/> />
<div style={{display: "flex", margin: "auto", marginTop: 20, minWidth: 500, maxWidth: 500, justifyContent: "center", overflowWrap: "wrap", }}> <div style={{display: "flex", margin: "auto", marginTop: 30, minWidth: 300, maxWidth: 300, 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"))
}} }}
@@ -1875,21 +1794,18 @@ const AgentUI = (props) => {
<AvatarGroup> <AvatarGroup>
{chosenApps?.map((app, index) => { {chosenApps?.map((app, index) => {
return( return(
<Tooltip title={`MCP / App: ${app?.name}`} key={index} placement="top"> <Tooltip title={`Constraint: ${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={() => {
if (app?.id !== undefined) { window.open(`/apps/${app.id}`, '_blank', 'noopener,noreferrer');
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>
+115 -457
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,PHN2ZyB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OCA0OCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4IiByeD0iOCIgZmlsbD0iI0UzQTQxQiIvPgo8cmVjdCB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEyIDEyKSIgZmlsbD0iI0UzQTQxQiIvPgo8Y2lyY2xlIGN4PSIyNCIgY3k9IjI0IiByPSI4Ljc1IiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjEuNSIvPgo8cGF0aCBkPSJNMjguNSAyNEgyNC4yNUMyNC4xMTE5IDI0IDI0IDIzLjg4ODEgMjQgMjMuNzVWMjAuNSIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4=", large_image: "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OCA0OCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4IiByeD0iOCIgZmlsbD0iIzIxQTBCRCIvPgo8Y2lyY2xlIGN4PSIyNCIgY3k9IjI0IiByPSI4Ljc1IiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjEuNSIvPgo8cGF0aCBkPSJNMjguNSAyNEgyNC4yNUMyNC4xMTE5IDI0IDI0IDIzLjg4ODEgMjQgMjMuNzVWMjAuNSIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K",
label: "Schedule", label: "Schedule",
is_valid: true, is_valid: true,
environment: "onprem", environment: "onprem",
@@ -300,11 +300,6 @@ export const triggers = [
"name": "subflow", "name": "subflow",
"example": "", "example": "",
"value": "", "value": "",
},
{
"name": "subflow_failure",
"example": "",
"value": "",
} }
], ],
status: "running", status: "running",
@@ -503,31 +498,6 @@ 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);
@@ -535,7 +505,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", "33e4e3564f4f060e96e0531957bed552") const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
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)
@@ -596,8 +566,6 @@ 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)
@@ -1162,14 +1130,7 @@ 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,
@@ -1189,14 +1150,7 @@ 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": [
@@ -1221,14 +1175,7 @@ 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": [
@@ -1263,8 +1210,7 @@ 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,
}, },
@@ -2253,26 +2199,6 @@ 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) {
@@ -2418,7 +2344,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"
@@ -2899,7 +2825,6 @@ 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
@@ -4046,8 +3971,6 @@ 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;
} }
} }
@@ -5272,8 +5195,7 @@ const AngularWorkflow = (defaultprops) => {
if (responseJson.public) { if (responseJson.public) {
// Delay setting appAuthentication to prevent race condition with graph setup setAppAuthentication([])
setTimeout(() => setAppAuthentication([]), 100)
setLeftBarSize(300) setLeftBarSize(300)
if (Object.getOwnPropertyNames(creatorProfile).length === 0) { if (Object.getOwnPropertyNames(creatorProfile).length === 0) {
@@ -5664,7 +5586,7 @@ const AngularWorkflow = (defaultprops) => {
} }
ReactDOM.unstable_batchedUpdates(() => { ReactDOM.unstable_batchedUpdates(() => {
// setRightSideBarOpen(true); setRightSideBarOpen(true);
setLastSaved(false); setLastSaved(false);
/* /*
@@ -6850,7 +6772,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") {
@@ -6942,7 +6864,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
} }
@@ -7207,12 +7129,12 @@ const AngularWorkflow = (defaultprops) => {
const tmpAuth = JSON.parse(JSON.stringify(newAppAuth)); const tmpAuth = JSON.parse(JSON.stringify(newAppAuth));
const curappName = curapp.name.toLowerCase().replaceAll(" ", "_") const curappName = curapp.name.toLowerCase()
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().replaceAll(" ", "_") !== curappName) { if (item.app.name.toLowerCase() !== curappName) {
continue continue
} }
@@ -7594,6 +7516,7 @@ 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) {
@@ -7906,7 +7829,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) {
@@ -7943,7 +7866,10 @@ 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) {
@@ -8243,11 +8169,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
@@ -8619,10 +8545,6 @@ 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
@@ -8732,7 +8654,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 || event.target?.closest('.MuiDialog-root, .MuiModal-root, [role="dialog"]')) { if (isAnyModalOrSidebarOpen) {
return return
} }
@@ -10183,8 +10105,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
@@ -10351,7 +10273,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
@@ -10361,7 +10283,6 @@ 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}
@@ -11503,10 +11424,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.warn("Failed to stop schedule: " + responseJson.reason); toast("Failed to stop schedule: " + responseJson.reason);
} }
} else { } else {
toast.success("Successfully stopped schedule"); toast("Successfully stopped schedule");
} }
if (triggerindex !== undefined && triggerindex !== null && triggerindex >= 0) { if (triggerindex !== undefined && triggerindex !== null && triggerindex >= 0) {
@@ -13240,7 +13161,7 @@ const AngularWorkflow = (defaultprops) => {
if (queryID !== undefined && queryID !== null) { if (queryID !== undefined && queryID !== null) {
aa('init', { aa('init', {
appId: "JNSS5CFDZZ", appId: "JNSS5CFDZZ",
apiKey: "33e4e3564f4f060e96e0531957bed552", apiKey: "c8f882473ff42d41158430be09ec2b4e",
}) })
const timestamp = new Date().getTime() const timestamp = new Date().getTime()
@@ -16111,7 +16032,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 && foundBranch?.length > 0) { if (foundBranch !== undefined && foundBranch !== null) {
const source = foundBranch.data("source") const source = foundBranch.data("source")
const target = foundBranch.data("target") const target = foundBranch.data("target")
@@ -19371,236 +19292,6 @@ 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>
@@ -20287,7 +19978,6 @@ 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)",
@@ -20330,6 +20020,7 @@ 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={{
@@ -20364,68 +20055,12 @@ 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: 9, height: 20, width: 20, }} /> <EditIcon style={{ position: "absolute", top: 7, 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 */}
@@ -20602,7 +20237,6 @@ 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)
@@ -20927,7 +20561,7 @@ const AngularWorkflow = (defaultprops) => {
id="execution_location" id="execution_location"
style={{ color: theme.palette.text.primary }} style={{ color: theme.palette.text.primary }}
> >
Runtime Location ({selectedActionEnvironment?.Name}) Runtime Location
</InputLabel> </InputLabel>
<Select <Select
labelId="execution_location" labelId="execution_location"
@@ -20941,7 +20575,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: {
@@ -21066,7 +20700,65 @@ 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>
); );
}; };
@@ -21690,9 +21382,6 @@ 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)
} }
} }
@@ -21914,7 +21603,7 @@ const AngularWorkflow = (defaultprops) => {
if (queryID !== undefined && queryID !== null) { if (queryID !== undefined && queryID !== null) {
aa('init', { aa('init', {
appId: "JNSS5CFDZZ", appId: "JNSS5CFDZZ",
apiKey: "33e4e3564f4f060e96e0531957bed552", apiKey: "c8f882473ff42d41158430be09ec2b4e",
}) })
const timestamp = new Date().getTime(); const timestamp = new Date().getTime();
aa('sendEvents', [ aa('sendEvents', [
@@ -23147,16 +22836,11 @@ const AngularWorkflow = (defaultprops) => {
}} }}
/> />
); );
} else if (execution.execution_source === "ShuffleGPT" || execution.execution_source == "Singul") { } else if (execution.execution_source === "ShuffleGPT") {
return ( return (
<img <AutoAwesomeIcon
alt={"Singul"} color="secondary"
src={theme.palette.singulGreen} style={{ paddingTop: 8, paddingLeft: 4, height: 25, width: 25, }}
style={{
width: size,
height: size,
borderRadius: borderRadius,
}}
/> />
); );
} else if (execution.execution_source === "pipeline") { } else if (execution.execution_source === "pipeline") {
@@ -23650,14 +23334,9 @@ const AngularWorkflow = (defaultprops) => {
getWorkflowExecution(workflow.id, "", executionFilter, workflow.org_id) getWorkflowExecution(workflow.id, "", executionFilter, workflow.org_id)
}} }}
color="secondary" color="secondary"
disabled={executionsLoading}
> >
{executionsLoading ? ( <CachedIcon style={{ marginRight: 10 }} />
<CircularProgress size={20} style={{ marginRight: 10 }} /> Refresh Runs
) : (
<CachedIcon style={{ marginRight: 10 }} />
)}
{executionsLoading ? "Refreshing runs" : "Refresh Runs"}
</Button> </Button>
<ButtonGroup <ButtonGroup
fullWidth fullWidth
@@ -23988,7 +23667,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
@@ -24834,7 +24513,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
@@ -25113,7 +24792,7 @@ const AngularWorkflow = (defaultprops) => {
}} }}
edge="end" edge="end"
> >
<ContentCopyIcon style={{heigth: 15, }}/> <ContentCopyIcon style={{ heigth: 15, }} />
</IconButton> </IconButton>
</span> </span>
: :
@@ -25121,7 +24800,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>
@@ -25377,8 +25056,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,
@@ -25399,10 +25078,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>
@@ -25510,14 +25189,6 @@ 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 &&
@@ -25683,18 +25354,15 @@ 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",
@@ -26569,20 +26237,12 @@ const AngularWorkflow = (defaultprops) => {
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>
<Button <Button
style={{ width: 150, margin: "auto", paddingBottom: 50, }} style={{ width: 150, margin: "auto", }}
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"
> >
@@ -27294,7 +26954,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)
}, 500) }, 250)
// Re-adding cytoscape triggers // Re-adding cytoscape triggers
@@ -27616,7 +27276,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 !== null && typeof parametername === "string" && parametername?.startsWith("${") && parametername?.endsWith("}")) { if (parametername !== undefined && parametername !== undefined && 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
@@ -27652,7 +27312,7 @@ const AngularWorkflow = (defaultprops) => {
} }
} }
if (typeof data === "string" && data.startsWith("${") && data.endsWith("}")) { if (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
@@ -27725,7 +27385,7 @@ const AngularWorkflow = (defaultprops) => {
} }
} }
if (selectedAction !== undefined && selectedAction?.app_name === "Shuffle Tools" && selectedAction?.name === "filter_list" && count === 0) { if (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(".#.")
@@ -27742,15 +27402,13 @@ const AngularWorkflow = (defaultprops) => {
} }
} }
} else { } else {
if (selectedAction !== undefined && selectedAction !== null && selectedAction?.parameters !== undefined && selectedAction?.parameters !== null && selectedAction.parameters?.length > count) { if (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
} }
} }
if (selectedAction !== undefined && selectedAction !== null) { setSelectedAction(selectedAction)
setSelectedAction(selectedAction)
}
//setUpdate(Math.random()) //setUpdate(Math.random())
} }
+47 -81
View File
@@ -47,11 +47,10 @@ 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",
"363e707135ea95f5523ec1b262d96b03" "c8f882473ff42d41158430be09ec2b4e"
) )
// Lazy loading of ApiExplorer component to reduce initial load time // Lazy loading of ApiExplorer component to reduce initial load time
@@ -140,90 +139,57 @@ const ApiExplorerWrapper = (props) => {
} }
const runAlgoliaAppSearch = (appname) => { const runAlgoliaAppSearch = (appname) => {
if (appname === "HTTP" || appname === "http") { const index = searchClient.initIndex("appsearch");
navigate("/apis")
return
}
// Check if appname is likely an ID (hash/UUID) or null - skip cache entirely if (appname === "HTTP" || appname === "http") {
const isLikelyId = !appname || appname === "null" || (appname.length >= 32 && /^[a-f0-9\-]+$/i.test(appname)); navigate("/apis")
return
}
// Skip cache for IDs/null, proceed directly to Algolia index
if (!isLikelyId) { .search(appname)
// Check backend cache first to save Algolia costs .then(({ hits }) => {
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;
}
// Cache miss, fall through to Algolia if (hits !== undefined && hits !== null && hits.length > 0) {
runAlgoliaSearch(); const appsearchname = appname.replaceAll("_", " ").toLowerCase()
}); var found = false
return; for (var key in hits) {
} const hit = hits[key]
const newname = hit.name.replaceAll("_", " ").toLowerCase()
// For IDs/null, skip cache and go straight to Algolia if (newname?.includes(appsearchname)) {
runAlgoliaSearch(); found = true
function runAlgoliaSearch() { getAppData(hit.objectID)
// Fallback to Algolia if not in cache break
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 (newname?.includes(appsearchname)) { if (!found) {
found = true toast.error(`Failed to get API data for '${appname}' (1). Contact ${supportEmail} if this persists.`, {
if (!isLikelyId) setAppCache(globalUrl, hit.objectID, appname); "autoClose": 10000,
getAppData(hit.objectID, appname) // pass alias so backend caches 'outlook' real app })
break
}
}
if (!found) { setTimeout(()=>{
toast.error(`Failed to get API data for '${appname}' (1). 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,
} })
} 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(()=>{ })
navigate("/search?tab=apps"); .catch((err) => {
},3000) console.log(err);
} });
})
.catch((err) => {
console.log(err);
});
}
} }
// Fetch data when appid is available // Fetch data when appid is available
const getAppData = useCallback((appid, alias) => { const getAppData = useCallback((appid) => {
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({
@@ -251,10 +217,10 @@ const ApiExplorerWrapper = (props) => {
return return
} }
if (appid.length !== 32 && appid.length !== 36) { if (appid.length !== 32) {
runAlgoliaAppSearch(appid) runAlgoliaAppSearch(appid)
return return
} }
const url = `${globalUrl}/api/v1/apps/${appid}/config` const url = `${globalUrl}/api/v1/apps/${appid}/config`
@@ -711,7 +677,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") && validate?.result?.status !== 200) { } else if (data.result.includes("authentication") && data.result.includes("Oauth2")) {
toast.error("Oauth2 apps require authentication") toast.error("Oauth2 apps require authentication")
setAuthHighlighted(true) setAuthHighlighted(true)
+14 -24
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,32 +4575,22 @@ 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
for (var labelkey in e.target.value) {
const label = e.target.value[labelkey]
if (label !== "no_label" && label !== "No_Label" && label !== "No Label") {
foundother = true
break
}
}
if (foundother) {
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) const foundIndex = actions.findIndex((action) => action.name === data.name)
if (foundActionIndex !== undefined && foundActionIndex !== null && foundActionIndex >= 0) { if (foundIndex !== undefined && foundIndex !== null && foundIndex >= 0) {
actions[foundActionIndex].action_label = e.target.value if (e.target.value.includes("no_label") || e.target.value.includes("No_Label") || e.target.value.includes("No Label")) {
actions[foundIndex].action_label = ["No Label"]
setActions(actions)
setUpdate(Math.random())
return
}
console.log("Should change: ", e.target.value, " Index: ", index)
actions[foundIndex].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}
+27 -90
View File
@@ -92,18 +92,9 @@ import aa from "search-insights";
// 2 = OpenAPI (Invalid) // 2 = OpenAPI (Invalid)
const searchClient = algoliasearch( const searchClient = algoliasearch(
"JNSS5CFDZZ", "JNSS5CFDZZ",
"363e707135ea95f5523ec1b262d96b03" "c8f882473ff42d41158430be09ec2b4e"
) )
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,
@@ -222,7 +213,6 @@ 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";
@@ -310,9 +300,7 @@ 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 === "new" || params.appid === "edit") { if (params.appid.length === 32 || params.appid.length === 36) {
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 {
@@ -466,6 +454,7 @@ 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">
@@ -772,7 +761,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",
}) })
@@ -828,9 +817,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
setAppId(appid) setAppId(appid)
const url = globalUrl + "/api/v1/apps/" + appid + "/config" fetch(globalUrl + "/api/v1/apps/" + appid + "/config", {
fetch(url, {
method: "GET", method: "GET",
headers: { headers: {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -1462,71 +1449,25 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
}; };
const runAlgoliaAppSearch = (appname, isOriginal, triggerOnly) => { const runAlgoliaAppSearch = (appname, isOriginal, triggerOnly) => {
const index = searchClient.initIndex("appsearch");
if (appname === "integration" || appname == "singul") { console.log("Running appsearch for: ", appname);
// Redirect to https://singul.io if (appname === "integration" || appname == "singul") {
window.location.href = "https://singul.io" // Redirect to https://singul.io
} else if (appname === "shuffle_agent" || appname === "agent" || appname === "agents" || appname == "shuffle-agent" || appname == "ai_agents" || appname == "AI Agent") { window.location.href = "https://singul.io"
navigate("/agents") } else if (appname === "shuffle_agent" || appname === "agent" || appname === "agents" || appname == "shuffle-agent") {
return navigate("/agents")
} 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.toLowerCase().replace(/[_ \-]/g, ""); const appsearchname = appname.replaceAll("_", " ").toLowerCase();
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 {
@@ -1541,8 +1482,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
continue; continue;
} }
const hitNameClean = hit["name"].toLowerCase().replace(/[_ \-]/g, ""); if (hit["name"].replaceAll("_", " ").toLowerCase().includes(appsearchname) || hit["objectID"] === appname) {
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 = []
@@ -1564,11 +1504,9 @@ 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);
} }
@@ -1580,7 +1518,6 @@ 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 {
@@ -1598,7 +1535,6 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
.catch((err) => { .catch((err) => {
console.log(err); console.log(err);
}); });
}
}; };
@@ -3216,7 +3152,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.secondary, textTransform: "none", }} disabled label="Integrations" /> <Tab icon={<PolylineIcon />} style={{color: theme.palette.text.primary, textTransform: "none", }} disabled style={{color: theme.palette.text.secondary}} 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 }}>
@@ -3483,7 +3419,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 nofollow" rel="noopener noreferrer"
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" }}
> >
@@ -3773,6 +3709,7 @@ 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,
@@ -4198,7 +4135,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;
@@ -4358,7 +4295,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: "26986891ddd374051039420cd77b0fff", apiKey: "c8f882473ff42d41158430be09ec2b4e",
}); });
const timestamp = new Date().getTime(); const timestamp = new Date().getTime();
@@ -4447,7 +4384,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: "363e707135ea95f5523ec1b262d96b03", apiKey: "c8f882473ff42d41158430be09ec2b4e",
}); });
const timestamp = new Date().getTime(); const timestamp = new Date().getTime();
@@ -4635,9 +4572,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>
@@ -4849,7 +4786,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">
{hasCachedCounts || relatedWorkflows !== 0 ? ( {relatedWorkflows !== 0 ? (
relatedWorkflows relatedWorkflows
) : ( ) : (
<InstantSearch <InstantSearch
@@ -4943,7 +4880,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">
{hasCachedCounts || relatedApps !== 0 ? ( {relatedApps !== 0 ? (
relatedApps relatedApps
) : ( ) : (
<InstantSearch <InstantSearch
+7 -11
View File
@@ -70,11 +70,10 @@ const inputColor = "#383B40";
const chipStyle = { const chipStyle = {
backgroundColor: "#3d3f43", backgroundColor: "#3d3f43",
height: 30, height: 28,
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",
@@ -280,7 +279,7 @@ export const GetParsedPaths = (inputdata, basekey) => {
return parsedValues; return parsedValues;
}; };
const searchClient = algoliasearch("JNSS5CFDZZ", "33e4e3564f4f060e96e0531957bed552") const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
const Apps = (props) => { const Apps = (props) => {
const { globalUrl, isLoggedIn, isLoaded, userdata, serverside, } = props; const { globalUrl, isLoggedIn, isLoaded, userdata, serverside, } = props;
@@ -634,7 +633,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
const data = app let data = app
if (data.name === "" && data.id === "") { if (data.name === "" && data.id === "") {
return null; return null;
@@ -802,7 +801,6 @@ 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">
@@ -1165,7 +1163,6 @@ 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,
@@ -1305,7 +1302,7 @@ const Apps = (props) => {
if (queryID !== undefined && queryID !== null) { if (queryID !== undefined && queryID !== null) {
aa("init", { aa("init", {
appId: "JNSS5CFDZZ", appId: "JNSS5CFDZZ",
apiKey: "33e4e3564f4f060e96e0531957bed552", apiKey: "c8f882473ff42d41158430be09ec2b4e",
}); });
const timestamp = new Date().getTime(); const timestamp = new Date().getTime();
@@ -1511,7 +1508,6 @@ 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",
}} }}
@@ -1945,7 +1941,7 @@ const Apps = (props) => {
const baseImage = <LibraryBooksIcon /> const baseImage = <LibraryBooksIcon />
return ( return (
<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", }}> <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", }}>
<List style={{ backgroundColor: theme.palette.inputColor, }}> <List style={{ backgroundColor: theme.palette.inputColor, }}>
{hits.length === 0 ? {hits.length === 0 ?
<ListItem style={outerlistitemStyle}> <ListItem style={outerlistitemStyle}>
@@ -2036,7 +2032,7 @@ const Apps = (props) => {
if (queryID !== undefined && queryID !== null) { if (queryID !== undefined && queryID !== null) {
aa('init', { aa('init', {
appId: "JNSS5CFDZZ", appId: "JNSS5CFDZZ",
apiKey: "33e4e3564f4f060e96e0531957bed552", apiKey: "c8f882473ff42d41158430be09ec2b4e",
}) })
const timestamp = new Date().getTime() const timestamp = new Date().getTime()
@@ -3015,7 +3011,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 Generate an app based on documentation (beta)
</div> </div>
</DialogTitle> </DialogTitle>
<DialogContent style={{ color: "rgba(255,255,255,0.65)" }}> <DialogContent style={{ color: "rgba(255,255,255,0.65)" }}>
+31 -99
View File
@@ -34,7 +34,7 @@ import {
import InputAdornment from '@mui/material/InputAdornment'; import InputAdornment from '@mui/material/InputAdornment';
import { ClearRefinements, connectInfiniteHits, connectSearchBox, connectStateResults, InstantSearch, RefinementList, connectRefinementList, Configure } from "react-instantsearch-dom"; import { ClearRefinements, connectHits, 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",
"eb5fd80aa6ed5ab4730d836cff3ea283" "c8f882473ff42d41158430be09ec2b4e"
); );
// AppCard Component // AppCard Component
@@ -297,8 +297,6 @@ const AppCard = ({ data, index, mouseHoverIndex, setMouseHoverIndex, globalUrl,
const Hits = ({ const Hits = ({
userdata, userdata,
hits, hits,
hasMore,
refineNext,
handleAppClick, handleAppClick,
setIsAnyAppActivated, setIsAnyAppActivated,
searchQuery, searchQuery,
@@ -312,37 +310,13 @@ 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(() => {
isFetchingMore.current = false; var baseurl = globalUrl;
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(globalUrl + "/api/v1/me", { fetch(baseurl + "/api/v1/me", {
credentials: "include", credentials: "include",
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@@ -358,7 +332,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') {
@@ -374,16 +348,6 @@ 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) => {
@@ -420,7 +384,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') {
@@ -441,7 +405,8 @@ 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={{
@@ -487,7 +452,7 @@ const Hits = ({
paddingBottom: 40 paddingBottom: 40
}} }}
> >
{sortedHits.map((data, index) => { {hits?.map((data, index) => {
const appUrl = const appUrl =
isCloud isCloud
? `/apps/${data.objectID}?queryID=${data.__queryID}` ? `/apps/${data.objectID}?queryID=${data.__queryID}`
@@ -739,18 +704,13 @@ 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 >
); );
} }
@@ -851,7 +811,7 @@ const SearchBox = ({ refine, searchQuery, setSearchQuery }) => {
}; };
const CustomSearchBox = connectSearchBox(SearchBox); const CustomSearchBox = connectSearchBox(SearchBox);
const CustomHits = connectInfiniteHits(Hits); const CustomHits = connectHits(Hits);
// Custom Category Dropdown Component // Custom Category Dropdown Component
const CategoryDropdown = ({ items, currentRefinement, refine }) => { const CategoryDropdown = ({ items, currentRefinement, refine }) => {
@@ -1148,7 +1108,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(isLoggedIn ? 0 : 2); const [currTab, setCurrTab] = useState(0);
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);
@@ -1182,21 +1142,6 @@ 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 =
@@ -1220,17 +1165,6 @@ 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 = () => {
@@ -1966,7 +1900,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 key={currTab === 2 ? "active" : "inactive"} searchClient={conditionalSearchClient} indexName="appsearch"> <InstantSearch searchClient={searchClient} indexName="appsearch">
<AppModal <AppModal
open={openModal} open={openModal}
onClose={handleAppModalClose} onClose={handleAppModalClose}
@@ -2478,26 +2412,24 @@ const Apps2 = (props) => {
} }
{ {
currTab === 2 && ( currTab === 2 &&
<> <CustomHits
<Configure clickAnalytics hitsPerPage={20} /> isLoggedIn={isLoggedIn}
<CustomHits userdata={userdata}
isLoggedIn={isLoggedIn} handleAppClick={handleAppClick}
userdata={userdata} setIsAnyAppActivated={setIsAnyAppActivated}
handleAppClick={handleAppClick} hitsPerPage={5}
setIsAnyAppActivated={setIsAnyAppActivated} globalUrl={globalUrl}
globalUrl={globalUrl} searchQuery={searchQuery}
searchQuery={searchQuery} mouseHoverIndex={mouseHoverIndex}
mouseHoverIndex={mouseHoverIndex} setMouseHoverIndex={setMouseHoverIndex}
setMouseHoverIndex={setMouseHoverIndex} currTab={currTab}
currTab={currTab} leftSideBarOpenByClick={leftSideBarOpenByClick}
leftSideBarOpenByClick={leftSideBarOpenByClick} />
/>
</>
)
} }
</div> </div>
</div > </div >
<Configure clickAnalytics />
</InstantSearch> </InstantSearch>
</div> </div>
</Dropzone> </Dropzone>
+1 -18
View File
@@ -473,7 +473,6 @@ 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;
@@ -1280,7 +1279,6 @@ 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,
@@ -1291,9 +1289,6 @@ 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>
} }
@@ -1571,21 +1566,9 @@ 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={{ marginTop: 20, fontWeight: 600, margin: 0, fontSize: "16px", marginBottom: "8px", color: theme.palette.text.primary, }}>Table Of Content</h4> <h4 style={{ fontWeight: 600, margin: 0, fontSize: "16px", marginBottom: "8px", color: theme.palette.text.primary, }}>Table Of Content</h4>
) : null} ) : null}
<div <div
+14 -21
View File
@@ -160,21 +160,6 @@ 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",
@@ -183,11 +168,23 @@ 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,
} }
]; ];
@@ -254,7 +251,6 @@ 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}>
@@ -588,7 +584,6 @@ 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)
@@ -612,7 +607,6 @@ 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
} }
@@ -693,7 +687,6 @@ 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"
@@ -819,7 +812,7 @@ const LoginPage = props => {
width: "max-content", width: "max-content",
}} }}
> >
<form onSubmit={onSubmit} style={{ margin: 15, width: isMobile ? "100%" : "360px", width: "max-content", overflow: "hidden", textAlign: "center", }}> <form onSubmit={onSubmit} style={{ margin: 15, width: "max-content", overflow: "hidden", textAlign: "center", }}>
<img <img
style={{ style={{
height: isMobile ? 44 : 60, height: isMobile ? 44 : 60,
@@ -1054,7 +1047,7 @@ const LoginPage = props => {
</div> </div>
</div> </div>
<div style={{ marginTop: 30, color: "white" }}> <div style={{ marginTop: "10px", 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 }}>
+63 -111
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") || searchParams.get("start") const sourceNode = searchParams.get("source_node")
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,9 +847,7 @@ const RunWorkflow = (defaultprops) => {
}) })
.then((responseJson) => { .then((responseJson) => {
if (responseJson.success === false) { if (responseJson.success === false) {
if (workflow_id !== execution_id) { toast.warn("Failed getting the workflow. Please contact support@shuffler.io if this persists.")
toast.warn("Failed getting the workflow. Please contact support@shuffler.io if this persists.")
}
return return
} }
@@ -1184,9 +1182,9 @@ const RunWorkflow = (defaultprops) => {
if (props.match.params.key === undefined) { if (props.match.params.key === undefined) {
setExplorerUi(true) setExplorerUi(true)
if (isLoggedIn && userdata?.active_org?.id) { if (isLoggedIn) {
loadForms(userdata?.active_org?.id) loadForms(userdata.active_org.id)
handleGetOrg(userdata?.active_org?.id) handleGetOrg(userdata.active_org.id)
} }
return return
@@ -1271,28 +1269,24 @@ const RunWorkflow = (defaultprops) => {
//console.log("IMG: ", image, "ORG: ", selectedOrganization) //console.log("IMG: ", image, "ORG: ", selectedOrganization)
useEffect(() => { if (!disabledButtons && answer !== undefined && answer !== null && organization !== "Unknown" && buttonClicked.length === 0) {
if (disabledButtons || answer === undefined || answer === null || organization === "Unknown" || buttonClicked.length > 0) { console.log("Finding button!")
return // Find the button
}
// Show rejection form for answer=false instead of auto-clicking
if (answer === "false") {
return
}
var buttonid = "" var buttonid = ""
if (answer === "true") { if (answer === "false") {
buttonid = "abort_execution"
} else 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 (
@@ -1703,103 +1697,61 @@ const RunWorkflow = (defaultprops) => {
} }
{disabledButtons ? null : {disabledButtons ? null :
answer === "false" ? <Typography variant="body2" color="textSecondary" style={{textAlign: "center", marginTop: 10, }}>
<Typography variant="body2" color="textSecondary" style={{textAlign: "center", marginTop: 10, }}> What do you want to do?
Why are you declining? </Typography>
</Typography>
:
<Typography variant="body2" color="textSecondary" style={{textAlign: "center", marginTop: 10, }}>
What do you want to do?
</Typography>
} }
{answer === "false" ? <div fullWidth style={{width: "100%", marginTop: 10, marginBottom: 10, display: "flex", }}>
<div style={{width: "100%", marginTop: 10, marginBottom: 10, }}> <Button
<TextField fullWidth
fullWidth id="continue_execution"
multiline variant="contained"
minRows={3} disabled={!handleValidateForm(executionArgument) || disabledButtons}
maxRows={6} color="primary"
variant="outlined" style={{
placeholder="Provide a reason for declining (optional)" flex: 1,
value={executionArgument || ""} textTransform: "none",
onChange={(e) => { }}
setExecutionArgument(e.target.value) onClick={() => {
}} // Timeout 2500 just in case
style={{marginBottom: 10, }} setTimeout(() => {
/> setButtonClicked("FINISHED")
<Button setExecutionData({
fullWidth status: "FINISHED",
id="abort_execution" })
variant="contained" }, 2500)
disabled={disabledButtons}
color="primary"
style={{textTransform: "none", }}
onClick={() => {
setTimeout(() => {
setButtonClicked("ABORTED")
setExecutionData({
status: "ABORTED",
})
}, 2500)
onSubmit(null, execution_id, authorization, false) onSubmit(null, execution_id, authorization, true)
}}>
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)
}}> }}>
Stop Continue</Button>
</Button> <Typography variant="body1" style={{marginLeft: 3, marginRight: 3, marginTop: 3, }}>
</div> &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)
{answer !== "false" && handleValidateForm(executionArgument) === false && disabledButtons === false ? onSubmit(null, execution_id, authorization, 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>
+10 -36
View File
@@ -25,8 +25,6 @@ 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();
@@ -109,16 +107,10 @@ 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>
@@ -153,30 +145,24 @@ 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, backgroundColor: theme.palette.backgroundColor, height: "200vh",}}> <div style={{width: 1000, margin: "auto", paddingTop: 50, }}>
{loadFail !== "" ? {loadFail !== "" ?
loadFail loadFail
: :
<><div> <><div>
<Typography variant="h4" style={{ marginBottom: 20, }}> <Typography variant="h4" style={{ marginBottom: 20, }}>
{showShuffle ? You are invited to: Configure <a href={appLink} target="_blank" style={{ color: '#FF8444', textDecoration: 'none' }}>{appname}</a> Authentication
<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)", }}>
{showShuffle ? A Shuffle Organization has invited you to configure authentication for this app so that they can use this authentication in one of their workflows.
`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, }}>
@@ -185,38 +171,26 @@ const SetAuthentication = (props) => {
: :
app.authentication.type === "oauth2" || app.authentication.type === "oauth2-app" ? app.authentication.type === "oauth2" || app.authentication.type === "oauth2-app" ?
<AuthenticationOauth2 <AuthenticationOauth2
selectedApp={{ selectedApp={app}
...app,
"name": app.name?.replaceAll(" ", "_")
}}
selectedAction={{ selectedAction={{
"app_name": app.name?.replaceAll(" ", "_"), "app_name": app.name,
"app_id": app.id, "app_id": app.id,
"app_version": app.app_version, "app_version": 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={{ selectedApp={app}
...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?
+1 -4
View File
@@ -751,15 +751,12 @@ 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[1]?.name === "." ? usecase?.name.slice(3, 100) : usecase.name}</b> <b>{index+1}. {usecase.name.slice(3, 100)}</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 = []
+82 -184
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 {
HelpOutline as HelpOutlineIcon, ArrowForwardIos as ArrowForwardIosIcon,
} from '@mui/icons-material'; } from '@mui/icons-material';
import { import {
@@ -20,17 +20,13 @@ import {
Stepper, Stepper,
Step, Step,
StepLabel, StepLabel,
Tooltip,
} from '@mui/material'; } from '@mui/material';
import { getTheme } from '../theme.jsx'; import theme from '../theme.jsx';
import { useNavigate, Link, useLocation } from "react-router-dom"; import { useNavigate, Link } 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({});
@@ -45,8 +41,6 @@ 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()
@@ -274,11 +268,9 @@ const Welcome = (props) => {
getFramework() getFramework()
getApps() getApps()
getAvailableWorkflows() getAvailableWorkflows()
}, [])
useEffect(() => { if (window.location.search !== undefined && window.location.search !== null) {
if (location.search !== undefined && location.search !== null) { const urlSearchParams = new URLSearchParams(window.location.search);
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"];
@@ -296,13 +288,9 @@ 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)
@@ -351,59 +339,6 @@ 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, }}>
{/* {/*
@@ -447,33 +382,6 @@ 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>
@@ -515,97 +423,87 @@ const Welcome = (props) => {
</div> </div>
: :
<Fade in={true}> <Fade in={true}>
<div style={{maxWidth: isMobile ? null : 620, margin: "auto", marginTop: 70, padding: isMobile ? "0 16px" : 0}}> <div style={{maxWidth: isMobile ? null : 590, margin: "auto", marginTop: 50,textAlign : isMobile ? "center" : null }}>
<Typography variant="h4" style={{color: theme.palette.textColor, marginTop: 50, fontSize: isMobile ? 26 : 34, fontWeight: 700, lineHeight: 1.25}}> <Typography variant="h4" style={{color: "#F1F1F1", marginTop: 50, fontSize: 32}}>
What do you want to do with Shuffle? Help us get to know you
</Typography> </Typography>
<Typography variant="body1" style={{color: theme.palette.welcomeCardSubtextColor, marginTop: 12, marginBottom: 36, fontSize: 16, fontFamily: theme?.typography?.fontFamily}}> <Typography variant="body1" style={{color: "#9E9E9E", marginTop: 12 ,marginBottom: 50, fontSize: 16}}>
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)
}
<div style={{display: "flex", flexDirection: "column", gap: 20, marginTop : 50}}> navigate("/welcome?tab=2")
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: "",
})
}
{welcomeCardsOptions.map((card) => ( navigate("/workflows?message=Skipped intro")
<div key={card?.title?.replace(" ", "-")} style={{position: "relative"}}> }}>
{card.badge && ( <CardActionArea style={actionObject}>
<div style={{ <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 }} />
position: "absolute", <Typography variant="h4" style={{color: "#F1F1F1"}}>
top: -14, Experienced
left: 18, </Typography>
backgroundColor: "#F2653B", <Typography variant="body1" style={{marginTop: 10, color: "rgba(255,255,255,0.8)"}}>
color: "#fff", Head to Shuffle right away
borderRadius: 20, </Typography>
padding: "4px 14px", </CardActionArea>
fontSize: 13, </Card>
fontWeight: 600, </div>
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>
+5 -45
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", "33e4e3564f4f060e96e0531957bed552"); const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e");
const svgSize = 24; const svgSize = 24;
const imagesize = 23; const imagesize = 23;
@@ -633,13 +633,10 @@ export const GetIconInfo = (action) => {
svgViewBox: "0 0 48 48", svgViewBox: "0 0 48 48",
}, },
search: { search: {
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", 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",
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: "",
@@ -1214,16 +1211,6 @@ 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("")
@@ -2945,16 +2932,6 @@ 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: {
@@ -3181,14 +3158,6 @@ 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)
@@ -3482,15 +3451,6 @@ 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)) {
@@ -4088,7 +4048,7 @@ const Workflows2 = (props) => {
setLoadWorkflowsModalOpen(false); setLoadWorkflowsModalOpen(false);
if (workflowids?.length > 0) { if (workflowids.length > 0) {
setHighlightIds(workflowids) setHighlightIds(workflowids)
} }
}; };
@@ -5791,7 +5751,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 === null || data?.triggers === undefined || data?.triggers?.length === 0) { if (data.triggers.length === 0) {
return null return null
} }