Merge pull request #1974 from LalitDeore/shaffuru-sync-fix
shaffuru files sync
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="100" height="100" viewBox="0,0,256,256">
|
||||
<g fill="none" fill-rule="nonzero" stroke="none" stroke-width="1" stroke-linecap="butt" stroke-linejoin="miter" stroke-miterlimit="10" stroke-dasharray="" stroke-dashoffset="0" font-family="none" font-weight="none" font-size="none" text-anchor="none" style="mix-blend-mode: normal"><g transform="scale(5.33333,5.33333)"><path d="M13.527,21.529c0,0.597 0.064,1.08 0.176,1.435c0.128,0.355 0.287,0.742 0.511,1.161c0.08,0.129 0.112,0.258 0.112,0.371c0,0.161 -0.096,0.322 -0.303,0.484l-1.006,0.677c-0.144,0.097 -0.287,0.145 -0.415,0.145c-0.16,0 -0.319,-0.081 -0.479,-0.226c-0.224,-0.242 -0.415,-0.5 -0.575,-0.758c-0.16,-0.274 -0.319,-0.58 -0.495,-0.951c-1.245,1.483 -2.81,2.225 -4.694,2.225c-1.341,0 -2.411,-0.387 -3.193,-1.161c-0.782,-0.774 -1.181,-1.806 -1.181,-3.096c0,-1.37 0.479,-2.483 1.453,-3.321c0.974,-0.838 2.267,-1.258 3.911,-1.258c0.543,0 1.102,0.048 1.692,0.129c0.59,0.081 1.197,0.21 1.836,0.355v-1.177c0,-1.225 -0.255,-2.08 -0.75,-2.58c-0.511,-0.5 -1.373,-0.742 -2.602,-0.742c-0.559,0 -1.133,0.064 -1.724,0.21c-0.591,0.145 -1.165,0.322 -1.724,0.548c-0.255,0.113 -0.447,0.177 -0.559,0.21c-0.112,0.032 -0.192,0.048 -0.255,0.048c-0.224,0 -0.335,-0.161 -0.335,-0.5v-0.79c0,-0.258 0.032,-0.451 0.112,-0.564c0.08,-0.113 0.224,-0.226 0.447,-0.339c0.559,-0.29 1.229,-0.532 2.012,-0.726c0.782,-0.21 1.612,-0.306 2.49,-0.306c1.9,0 3.289,0.435 4.183,1.306c0.878,0.871 1.325,2.193 1.325,3.966v5.224h0.03zM7.045,23.979c0.527,0 1.07,-0.097 1.644,-0.29c0.575,-0.193 1.086,-0.548 1.517,-1.032c0.255,-0.306 0.447,-0.645 0.543,-1.032c0.096,-0.387 0.16,-0.855 0.16,-1.403v-0.677c-0.463,-0.113 -0.958,-0.21 -1.469,-0.274c-0.511,-0.064 -1.006,-0.097 -1.501,-0.097c-1.07,0 -1.852,0.21 -2.379,0.645c-0.527,0.435 -0.782,1.048 -0.782,1.854c0,0.758 0.192,1.322 0.591,1.709c0.383,0.404 0.942,0.597 1.676,0.597zM19.865,25.721c-0.287,0 -0.479,-0.048 -0.607,-0.161c-0.128,-0.097 -0.239,-0.322 -0.335,-0.629l-3.752,-12.463c-0.096,-0.322 -0.144,-0.532 -0.144,-0.645c0,-0.258 0.128,-0.403 0.383,-0.403h1.565c0.303,0 0.511,0.048 0.623,0.161c0.128,0.097 0.223,0.322 0.319,0.629l2.682,10.674l2.49,-10.674c0.08,-0.322 0.176,-0.532 0.303,-0.629c0.128,-0.097 0.351,-0.161 0.639,-0.161h1.277c0.303,0 0.511,0.048 0.639,0.161c0.128,0.097 0.239,0.322 0.303,0.629l2.522,10.803l2.762,-10.803c0.096,-0.322 0.208,-0.532 0.319,-0.629c0.128,-0.097 0.335,-0.161 0.623,-0.161h1.485c0.255,0 0.399,0.129 0.399,0.403c0,0.081 -0.016,0.161 -0.032,0.258c-0.016,0.097 -0.048,0.226 -0.112,0.403l-3.847,12.463c-0.096,0.322 -0.208,0.532 -0.335,0.629c-0.127,0.097 -0.335,0.161 -0.607,0.161h-1.373c-0.303,0 -0.511,-0.048 -0.639,-0.161c-0.128,-0.113 -0.239,-0.322 -0.303,-0.645l-2.474,-10.4l-2.458,10.384c-0.08,0.322 -0.176,0.532 -0.303,0.645c-0.128,0.113 -0.351,0.161 -0.639,0.161zM40.379,26.156c-0.83,0 -1.66,-0.097 -2.458,-0.29c-0.798,-0.193 -1.421,-0.403 -1.836,-0.645c-0.255,-0.145 -0.431,-0.306 -0.495,-0.451c-0.064,-0.145 -0.096,-0.306 -0.096,-0.451v-0.822c0,-0.339 0.128,-0.5 0.367,-0.5c0.096,0 0.192,0.016 0.287,0.048c0.096,0.032 0.239,0.097 0.399,0.161c0.543,0.242 1.133,0.435 1.756,0.564c0.639,0.129 1.261,0.193 1.9,0.193c1.006,0 1.788,-0.177 2.331,-0.532c0.543,-0.355 0.83,-0.871 0.83,-1.532c0,-0.451 -0.144,-0.822 -0.431,-1.129c-0.287,-0.306 -0.83,-0.58 -1.612,-0.838l-2.315,-0.726c-1.165,-0.371 -2.027,-0.919 -2.554,-1.645c-0.527,-0.709 -0.798,-1.499 -0.798,-2.338c0,-0.677 0.144,-1.274 0.431,-1.79c0.287,-0.516 0.671,-0.967 1.149,-1.322c0.479,-0.371 1.022,-0.645 1.66,-0.838c0.639,-0.192 1.309,-0.273 2.012,-0.273c0.351,0 0.718,0.016 1.07,0.064c0.367,0.048 0.702,0.113 1.038,0.177c0.319,0.081 0.623,0.161 0.91,0.258c0.287,0.097 0.511,0.193 0.671,0.29c0.224,0.129 0.383,0.258 0.479,0.403c0.096,0.129 0.144,0.306 0.144,0.532v0.758c0,0.339 -0.128,0.516 -0.367,0.516c-0.128,0 -0.335,-0.064 -0.607,-0.193c-0.91,-0.419 -1.932,-0.629 -3.065,-0.629c-0.91,0 -1.628,0.145 -2.123,0.451c-0.495,0.306 -0.75,0.774 -0.75,1.435c0,0.451 0.16,0.838 0.479,1.145c0.319,0.306 0.91,0.613 1.756,0.887l2.267,0.726c1.149,0.371 1.98,0.887 2.474,1.548c0.494,0.661 0.734,1.419 0.734,2.257c0,0.693 -0.144,1.322 -0.415,1.87c-0.287,0.548 -0.671,1.032 -1.165,1.419c-0.495,0.403 -1.086,0.693 -1.772,0.903c-0.721,0.226 -1.471,0.339 -2.285,0.339z" fill="#ffffff"></path><path d="M43.396,33.992c-5.252,3.918 -12.883,5.998 -19.445,5.998c-9.195,0 -17.481,-3.434 -23.739,-9.142c-0.495,-0.451 -0.048,-1.064 0.543,-0.709c6.769,3.966 15.118,6.369 23.755,6.369c5.827,0 12.229,-1.225 18.119,-3.741c0.879,-0.403 1.629,0.58 0.767,1.225zM45.583,31.477c-0.671,-0.871 -4.438,-0.419 -6.146,-0.21c-0.511,0.064 -0.591,-0.387 -0.128,-0.726c3.001,-2.128 7.934,-1.516 8.509,-0.806c0.575,0.726 -0.16,5.708 -2.969,8.094c-0.431,0.371 -0.846,0.177 -0.655,-0.306c0.639,-1.596 2.06,-5.192 1.389,-6.046z" fill="#ff9900"></path></g></g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.7 KiB |
@@ -142,6 +142,10 @@ const AdminNavBar = (props) => {
|
||||
// navigate(`?tab=organization`, { replace: true });
|
||||
// }
|
||||
// }
|
||||
const adminTab = queryParams.get('admin_tab');
|
||||
if (adminTab) {
|
||||
setSelectedItem("Organization");
|
||||
}
|
||||
if (partnerTab) {
|
||||
setSelectedItem("Partner");
|
||||
}
|
||||
|
||||
@@ -1977,7 +1977,7 @@ const Action = memo((
|
||||
const actionId = action.name.replace(/ /g, "-").replace(/_/g, "-");
|
||||
window.history.pushState(null, "", `#${actionId}`);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import { isMobile } from "react-device-detect"
|
||||
import PaperComponent from "../components/PaperComponent.jsx";
|
||||
import { CodeHandler, Img, OuterLink, } from '../views/Docs.jsx'
|
||||
import { v4 as uuidv4} from "uuid";
|
||||
import DeleteConfirmDialog from "./DeleteConfirmDialog.jsx";
|
||||
|
||||
import {
|
||||
Divider,
|
||||
@@ -63,10 +64,11 @@ import {
|
||||
} from "react-instantsearch-dom";
|
||||
import aa from "search-insights";
|
||||
import { Context } from '../context/ContextApi.jsx';
|
||||
import SubOrgDistributionDialog from './SubOrgDistributionDialog.jsx';
|
||||
|
||||
const searchClient = algoliasearch(
|
||||
"JNSS5CFDZZ",
|
||||
"c8f882473ff42d41158430be09ec2b4e"
|
||||
"33e4e3564f4f060e96e0531957bed552"
|
||||
)
|
||||
|
||||
const AppAuthTab = memo((props) => {
|
||||
@@ -89,9 +91,13 @@ const AppAuthTab = memo((props) => {
|
||||
const [searchQuery, setSearchQuery] = React.useState("");
|
||||
const [showAppModal, setShowAppModal] = useState(false)
|
||||
const [selectedAuthId, setSelectedAuthId] = useState("");
|
||||
const [selectedAuthName, setSelectedAuthName] = useState("");
|
||||
const [showDistributionPopup, setShowDistributionPopup] = useState(false);
|
||||
const [showAuthenticationLoader, setShowAuthenticationLoader] = 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 theme = getTheme(themeMode, brandColor)
|
||||
|
||||
@@ -183,7 +189,7 @@ const AppAuthTab = memo((props) => {
|
||||
};
|
||||
|
||||
const deleteAuthentication = (data) => {
|
||||
toast("Deleting auth " + data.label);
|
||||
toast("Deleting auth " + data?.label);
|
||||
|
||||
// Just use this one?
|
||||
const url = globalUrl + "/api/v1/apps/authentication/" + data.id;
|
||||
@@ -213,33 +219,6 @@ const AppAuthTab = memo((props) => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleSelectSubOrg = (id, action) => {
|
||||
if (action === "all") {
|
||||
const childOrgs = userdata.orgs.filter(
|
||||
(data) => data.creator_org === userdata.active_org.id
|
||||
);
|
||||
setSelectedSubOrg((prev) => {
|
||||
if (prev.length === childOrgs.length) {
|
||||
// If all child orgs are already selected, clear the selection
|
||||
return [];
|
||||
} else {
|
||||
// Otherwise, select all child org IDs
|
||||
return childOrgs.map((data) => data.id);
|
||||
}
|
||||
});
|
||||
} else if (action === "none") {
|
||||
setSelectedSubOrg([]);
|
||||
} else {
|
||||
setSelectedSubOrg((prev) => {
|
||||
if (prev.includes(id)) {
|
||||
return prev.filter((data) => data !== id);
|
||||
} else {
|
||||
return [...prev, id];
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const editAuthenticationConfig = (id, parentAction, selectedSuborgs) => {
|
||||
const data = {
|
||||
id: id,
|
||||
@@ -285,99 +264,21 @@ const AppAuthTab = memo((props) => {
|
||||
editAuthenticationConfig(id, "suborg_distribute", [...new Set(selectedSubOrg)])
|
||||
}
|
||||
|
||||
const deleteConfirmDialog = (
|
||||
<DeleteConfirmDialog
|
||||
open={deleteConfirmOpen}
|
||||
onClose={() => { setDeleteConfirmOpen(false); setDeleteConfirmTarget(null); }}
|
||||
onConfirm={() => {
|
||||
deleteAuthentication(deleteConfirmTarget);
|
||||
setDeleteConfirmOpen(false);
|
||||
setDeleteConfirmTarget(null);
|
||||
}}
|
||||
title="Delete Authentication?"
|
||||
description={<>Are you sure you want to delete <b>{deleteConfirmTarget?.app?.name}</b> authentication?</>}
|
||||
warningText="This cannot be undone. Any workflows using this authentication will lose access."
|
||||
/>
|
||||
);
|
||||
|
||||
const cacheDistributionModal = showDistributionPopup ? (
|
||||
<Dialog
|
||||
open={showDistributionPopup}
|
||||
onClose={() => {setShowDistributionPopup(false);setSelectedAuthId("")}}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
borderRadius: theme?.palette?.DialogStyle?.borderRadius,
|
||||
border: theme?.palette?.DialogStyle?.border,
|
||||
fontFamily: theme?.typography?.fontFamily,
|
||||
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
||||
zIndex: 1000,
|
||||
minWidth: "600px",
|
||||
minHeight: "320px",
|
||||
overflow: "auto",
|
||||
'& .MuiDialogContent-root': {
|
||||
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
||||
},
|
||||
'& .MuiDialogTitle-root': {
|
||||
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
||||
},
|
||||
'& .MuiDialogActions-root': {
|
||||
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DialogTitle>
|
||||
<Typography variant="h5" color="textPrimary">
|
||||
Select sub-org to distribute Datastore key
|
||||
</Typography>
|
||||
</DialogTitle>
|
||||
<DialogContent style={{ color: "rgba(255,255,255,0.65)" }}>
|
||||
<MenuItem value="none" onClick={()=> {handleSelectSubOrg(null, "none")}}>None</MenuItem>
|
||||
<MenuItem value="all" onClick={()=> {handleSelectSubOrg(null, "all")}}>All</MenuItem>
|
||||
{userdata.orgs.map((data, index) => {
|
||||
if (data.creator_org !== userdata.active_org.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const imagesize = 22;
|
||||
const imageStyle = {
|
||||
width: imagesize,
|
||||
height: imagesize,
|
||||
pointerEvents: "none",
|
||||
marginRight: 10,
|
||||
marginLeft: data.id === userdata.active_org.id ? 0 : 20,
|
||||
};
|
||||
|
||||
const image = data.image === "" ? (
|
||||
<img alt={data.name} src={theme.palette.defaultImage} style={imageStyle} />
|
||||
) : (
|
||||
<img alt={data.name} src={data.image} style={imageStyle} />
|
||||
);
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
key={index}
|
||||
value={data.id}
|
||||
onClick={() => handleSelectSubOrg(data.id)}
|
||||
style={{ display: "flex", alignItems: "center" }}
|
||||
>
|
||||
<Checkbox
|
||||
checked={selectedSubOrg.includes(data.id)}
|
||||
/>
|
||||
{image}
|
||||
<span style={{ marginLeft: 8 }}>{data.name}</span>
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
|
||||
<div style={{ display: "flex", marginTop: 20 }}>
|
||||
<Button
|
||||
style={{ borderRadius: "2px", textTransform: 'none', fontSize:16, color: theme.palette.primary.main }}
|
||||
onClick={() => {setShowDistributionPopup(false); setSelectedAuthId("")}}
|
||||
color="primary"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
style={{ borderRadius: "2px", textTransform: 'none', fontSize:16, marginLeft: 10 }}
|
||||
onClick={() => {
|
||||
changeDistribution(selectedAuthId, selectedSubOrg);
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
) : null;
|
||||
|
||||
const editAuthenticationModal = selectedAuthenticationModalOpen ? (
|
||||
<Dialog
|
||||
@@ -985,7 +886,17 @@ const AppAuthTab = memo((props) => {
|
||||
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, }}>
|
||||
{appModal}
|
||||
{cacheDistributionModal}
|
||||
<SubOrgDistributionDialog
|
||||
open={showDistributionPopup}
|
||||
onClose={() => { setShowDistributionPopup(false); setSelectedAuthId(""); setSelectedAuthName(""); }}
|
||||
title="Distribute App Auth to Sub-Organizations"
|
||||
extraInfo={selectedAuthName ? `Selected Auth: ${selectedAuthName}` : null}
|
||||
orgs={distribOrgOrder.map(id => (userdata?.orgs || []).find(o => o.id === id)).filter(Boolean)}
|
||||
selectedOrgIds={selectedSubOrg}
|
||||
onSelectionChange={setSelectedSubOrg}
|
||||
onSave={(ids) => { changeDistribution(selectedAuthId, ids); }}
|
||||
/>
|
||||
{deleteConfirmDialog}
|
||||
<div style={{ height: "100%", width: "calc(100% - 20px)", scrollbarColor: theme.palette.scrollbarColorTransparent, scrollbarWidth: 'thin'}}>
|
||||
<div style={{ width: 'auto', display:'flex',}}>
|
||||
<div style={{display: 'flex', flexDirection: 'column'}}>
|
||||
@@ -1362,10 +1273,10 @@ const AppAuthTab = memo((props) => {
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<IconButton
|
||||
style={{ }}
|
||||
disabled={data.org_id !== selectedOrganization.id}
|
||||
onClick={() => {
|
||||
deleteAuthentication(data);
|
||||
setDeleteConfirmTarget(data);
|
||||
setDeleteConfirmOpen(true);
|
||||
}}
|
||||
>
|
||||
<img src='/icons/deleteIcon.svg' alt='delete icon' color="secondary" />
|
||||
@@ -1400,21 +1311,27 @@ const AppAuthTab = memo((props) => {
|
||||
color="secondary"
|
||||
onClick={() => {
|
||||
setShowDistributionPopup(true)
|
||||
if(data?.suborg_distribution?.length > 0){
|
||||
setSelectedSubOrg(data.suborg_distribution)
|
||||
}else{
|
||||
setSelectedSubOrg([])
|
||||
}
|
||||
setSelectedAuthId(data.id)
|
||||
let initialSelected = [];
|
||||
if (data?.suborg_distributed) {
|
||||
const allSuborg = userdata?.orgs?.map((data, index) => {
|
||||
if (data.creator_org !== userdata.active_org.id) {
|
||||
return null;
|
||||
}
|
||||
return data.id;
|
||||
})
|
||||
setSelectedSubOrg(allSuborg.filter((data) => data !== null))
|
||||
const allSuborg = userdata?.orgs?.map((d) => {
|
||||
if (d.creator_org !== userdata.active_org.id) return null;
|
||||
return d.id;
|
||||
});
|
||||
initialSelected = allSuborg.filter((d) => d !== null);
|
||||
} else if (data?.suborg_distribution?.length > 0) {
|
||||
initialSelected = data.suborg_distribution;
|
||||
}
|
||||
setSelectedSubOrg(initialSelected);
|
||||
setSelectedAuthId(data.id);
|
||||
setSelectedAuthName(data?.app?.name);
|
||||
const suborgs = (userdata?.orgs || []).filter(o => o.creator_org === userdata?.active_org?.id);
|
||||
const sorted = [...suborgs].sort((a, b) => {
|
||||
const aS = initialSelected.includes(a.id);
|
||||
const bS = initialSelected.includes(b.id);
|
||||
if (aS !== bS) return bS - aS;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
setDistribOrgOrder(sorted.map(o => o.id));
|
||||
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -708,7 +708,7 @@ const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud, startOpenA
|
||||
fontWeight: 500,
|
||||
fontFamily: theme?.typography?.fontFamily,
|
||||
}}>
|
||||
Generate an app based on documentation (beta)
|
||||
Generate an app based on documentation
|
||||
</Typography>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import React, { useEffect, useState, useRef } from "react";
|
||||
import React, { useEffect, useState, useRef, useMemo } from "react";
|
||||
|
||||
import theme from "../theme.jsx";
|
||||
import ReactGA from "react-ga4";
|
||||
import { Link } from "react-router-dom";
|
||||
import { removeQuery } from "../components/ScrollToTop.jsx";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { Tabs, Tab, Collapse } from "@mui/material";
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
@@ -27,7 +26,7 @@ import {
|
||||
InstantSearch,
|
||||
Configure,
|
||||
connectSearchBox,
|
||||
connectHits,
|
||||
connectInfiniteHits,
|
||||
connectHitInsights,
|
||||
RefinementList,
|
||||
ClearRefinements,
|
||||
@@ -39,6 +38,7 @@ import aa from "search-insights";
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import "./FilterCSS.css";
|
||||
import SearchContactForm from "../components/SearchContactForm.jsx";
|
||||
|
||||
import {
|
||||
Zoom,
|
||||
@@ -54,7 +54,7 @@ import {
|
||||
|
||||
const searchClient = algoliasearch(
|
||||
"JNSS5CFDZZ",
|
||||
"c8f882473ff42d41158430be09ec2b4e"
|
||||
"eb5fd80aa6ed5ab4730d836cff3ea283"
|
||||
);
|
||||
//const searchClient = algoliasearch("L55H18ZINA", "a19be455e7e75ee8f20a93d26b9fc6d6")
|
||||
|
||||
@@ -77,62 +77,13 @@ const AppGrid = (props) => {
|
||||
const xs =
|
||||
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 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 borderRadius = 3;
|
||||
window.title = "Shuffle | Apps | Find and integrate any app";
|
||||
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 }) => {
|
||||
var defaultSearch = "";
|
||||
|
||||
@@ -148,10 +99,11 @@ const AppGrid = (props) => {
|
||||
const params = Object.fromEntries(urlSearchParams.entries());
|
||||
const foundQuery = params["q"];
|
||||
if (foundQuery !== null && foundQuery !== undefined) {
|
||||
console.log("Got query: ", foundQuery);
|
||||
refine(foundQuery);
|
||||
defaultSearch = foundQuery;
|
||||
searchQuery = foundQuery
|
||||
if (searchQuery !== foundQuery) {
|
||||
setSearchQuery(foundQuery);
|
||||
}
|
||||
}
|
||||
}
|
||||
//}, [])
|
||||
@@ -234,7 +186,13 @@ const AppGrid = (props) => {
|
||||
onChange={(event) => {
|
||||
const value = event.currentTarget.value;
|
||||
setSearchQuery(value);
|
||||
removeQuery("q");
|
||||
const urlSearchParams = new URLSearchParams(window.location.search);
|
||||
if (value) {
|
||||
urlSearchParams.set("q", value);
|
||||
} else {
|
||||
urlSearchParams.delete("q");
|
||||
}
|
||||
window.history.replaceState({}, '', `${window.location.pathname}?${urlSearchParams.toString()}`);
|
||||
debouncedRefine(value);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
@@ -252,6 +210,21 @@ const AppGrid = (props) => {
|
||||
const [currTab, setCurrTab] = useState(0);
|
||||
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(() => {
|
||||
const queryParams = new URLSearchParams(location.search);
|
||||
const tabParam = queryParams.get('tab');
|
||||
@@ -269,7 +242,6 @@ const AppGrid = (props) => {
|
||||
const newQueryParam = newTab === 0 ? 'all_apps' : newTab === 1 ? 'org_apps' : 'my_apps';
|
||||
const queryParams = new URLSearchParams(location.search);
|
||||
queryParams.set('tab', newQueryParam);
|
||||
queryParams.delete('q');
|
||||
window.history.replaceState({}, '', `${location.pathname}?${queryParams.toString()}`);
|
||||
};
|
||||
|
||||
@@ -281,6 +253,8 @@ const AppGrid = (props) => {
|
||||
// Component to fetch all public app from the algolia.
|
||||
const Hits = ({
|
||||
hits,
|
||||
hasMore,
|
||||
refineNext,
|
||||
insights,
|
||||
setIsAnyAppActivated,
|
||||
searchQuery
|
||||
@@ -288,6 +262,32 @@ const AppGrid = (props) => {
|
||||
const [mouseHoverIndex, setMouseHoverIndex] = useState(-1);
|
||||
var counted = 0;
|
||||
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) => {
|
||||
if (typeof name === 'string') {
|
||||
@@ -359,11 +359,11 @@ const AppGrid = (props) => {
|
||||
} else {
|
||||
//toast.success(`App ${type}d Successfully!`);
|
||||
if (type === 'activate') {
|
||||
setAllActivatedAppIds(prev => [...prev, data.objectID]);
|
||||
setAllActivatedAppIds(prev => [...(prev || []), data.objectID]);
|
||||
setIsAnyAppActivated(true);
|
||||
}
|
||||
if (type === 'deactivate') {
|
||||
const updatedIds = allActivatedAppIds.filter(id => id !== data.objectID);
|
||||
const updatedIds = (allActivatedAppIds || []).filter(id => id !== data.objectID);
|
||||
setAllActivatedAppIds(updatedIds);
|
||||
}
|
||||
}
|
||||
@@ -373,6 +373,16 @@ const AppGrid = (props) => {
|
||||
});
|
||||
}
|
||||
|
||||
const sortedHits = useMemo(() => {
|
||||
const list = [...(hits || [])];
|
||||
if (!allActivatedAppIds?.length) return list;
|
||||
return list.sort((a, b) => {
|
||||
const aActive = allActivatedAppIds.includes(a.objectID) ? 1 : 0;
|
||||
const bActive = allActivatedAppIds.includes(b.objectID) ? 1 : 0;
|
||||
return bActive - aActive;
|
||||
});
|
||||
}, [hits, hits?.length, allActivatedAppIds]);
|
||||
|
||||
let workflowDelay = 0;
|
||||
const isHeader = true;
|
||||
const paperStyle = {
|
||||
@@ -404,6 +414,7 @@ const AppGrid = (props) => {
|
||||
) : (
|
||||
<Grid item spacing={2} justifyContent="flex-start">
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
style={{
|
||||
gap: 16,
|
||||
marginTop: 16,
|
||||
@@ -418,7 +429,7 @@ const AppGrid = (props) => {
|
||||
scrollbarColor: "#494949 #2f2f2f",
|
||||
}}
|
||||
>
|
||||
{hits?.map((data, index) => {
|
||||
{sortedHits.map((data, index) => {
|
||||
const appUrl =
|
||||
isCloud === true ?
|
||||
`/apps/${data.objectID}`
|
||||
@@ -629,6 +640,12 @@ const AppGrid = (props) => {
|
||||
);
|
||||
})
|
||||
}
|
||||
<div ref={loadMoreRef} style={{ width: "100%", height: 10 }} />
|
||||
{isLoadingMore && (
|
||||
<div style={{ width: "100%", display: "flex", justifyContent: "center", padding: 10 }}>
|
||||
<CircularProgress size={24} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Grid >
|
||||
)}
|
||||
@@ -926,7 +943,7 @@ const AppGrid = (props) => {
|
||||
|
||||
//Component to display all apps.
|
||||
const AllApps = ({ setIsAnyAppActivated }) => {
|
||||
var [searchQuery, setSearchQuery] = useState("");
|
||||
var [searchQuery, setSearchQuery] = useState(() => new URLSearchParams(window.location.search).get('q') || "");
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -987,6 +1004,7 @@ const AppGrid = (props) => {
|
||||
}}
|
||||
onClick={() => {
|
||||
setSearchQuery('');
|
||||
removeQuery("q");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -1015,7 +1033,15 @@ const AppGrid = (props) => {
|
||||
placeholder="Search your Activated or self-built apps"
|
||||
id="shuffle_search_field"
|
||||
onChange={(event) => {
|
||||
setSearchQuery(event.currentTarget.value);
|
||||
const value = event.currentTarget.value;
|
||||
setSearchQuery(value);
|
||||
const urlSearchParams = new URLSearchParams(window.location.search);
|
||||
if (value) {
|
||||
urlSearchParams.set("q", value);
|
||||
} else {
|
||||
urlSearchParams.delete("q");
|
||||
}
|
||||
window.history.replaceState({}, '', `${window.location.pathname}?${urlSearchParams.toString()}`);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if(event.key === "Enter") {
|
||||
@@ -1592,7 +1618,7 @@ const AppGrid = (props) => {
|
||||
//Component to fetch all apps created by user and Org
|
||||
const UserAndOrgApps = ({ selectedCategoryForUsersAndOgsApps, selectedTagsForUserAndOrgApps, selectedOptionOfCreatedWith, setselectedCategoryForUsersAndOgsApps, setSelectedTagsForUserAndOrgApps, setSelectedOptionOfCreatedWith }) => {
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [searchQuery, setSearchQuery] = useState(() => new URLSearchParams(window.location.search).get('q') || "");
|
||||
const [appsToShow, setAppsToShow] = useState([]);
|
||||
useEffect(() => {
|
||||
if (currTab === 1) {
|
||||
@@ -2003,7 +2029,7 @@ const AppGrid = (props) => {
|
||||
};
|
||||
|
||||
const CustomSearchBox = connectSearchBox(SearchBox);
|
||||
const CustomHits = connectHits(Hits);
|
||||
const CustomHits = connectInfiniteHits(Hits);
|
||||
|
||||
const DisplayAllAppsTab = () => {
|
||||
const [selectedCategoryForUsersAndOgsApps, setselectedCategoryForUsersAndOgsApps] = useState([]);
|
||||
@@ -2012,7 +2038,7 @@ const AppGrid = (props) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<InstantSearch searchClient={searchClient} indexName="appsearch">
|
||||
<InstantSearch key={currTab === 0 ? "active" : "inactive"} searchClient={conditionalSearchClient} indexName="appsearch">
|
||||
<div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'center', paddingRight: 215 }}>
|
||||
{currTab === 0 ? (
|
||||
<FilterForAllApps />
|
||||
@@ -2037,7 +2063,7 @@ const AppGrid = (props) => {
|
||||
/>
|
||||
|
||||
</div>
|
||||
<Configure clickAnalytics />
|
||||
{currTab === 0 && <Configure clickAnalytics hitsPerPage={20} />}
|
||||
</InstantSearch>
|
||||
</div>
|
||||
);
|
||||
@@ -2060,80 +2086,7 @@ const AppGrid = (props) => {
|
||||
>
|
||||
<DisplayAllAppsTab />
|
||||
{showSuggestion === true ? (
|
||||
<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>
|
||||
<SearchContactForm globalUrl={globalUrl} isMobile={isMobile} tabName="apps" />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -35,7 +35,7 @@ import { Context } from '../context/ContextApi.jsx';
|
||||
|
||||
const searchClient = algoliasearch(
|
||||
"JNSS5CFDZZ",
|
||||
"c8f882473ff42d41158430be09ec2b4e"
|
||||
"33e4e3564f4f060e96e0531957bed552"
|
||||
);;
|
||||
|
||||
const AppModal = ({ open, onClose, app, globalUrl, getApps}) => {
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
InputAdornment,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "33e4e3564f4f060e96e0531957bed552")
|
||||
const Appsearch = props => {
|
||||
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, newSelectedApp, setNewSelectedApp, defaultSearch, showSearch, ConfiguredHits, userdata, cy, isCreatorPage, actionImageList, setActionImageList, setUserSpecialzedApp, placeholder,
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
} from '@mui/material';
|
||||
|
||||
import aa from 'search-insights'
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "33e4e3564f4f060e96e0531957bed552")
|
||||
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 { themeMode } = useContext(Context)
|
||||
|
||||
@@ -52,6 +52,7 @@ import {
|
||||
Cancel as CancelIcon,
|
||||
Shield as ShieldIcon,
|
||||
Cancel as XCircleIcon,
|
||||
LockOutlined as LockIcon,
|
||||
FlashOn as ZapIcon,
|
||||
People as UsersIcon,
|
||||
FmdGoodOutlined as FmdGoodOutlinedIcon,
|
||||
@@ -203,7 +204,7 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) =>
|
||||
>
|
||||
{isProdStatusOn
|
||||
? 'Your organization has full access to all enterprise features and capabilities.'
|
||||
: 'View your current limits and available features. Upgrade to unlock enterprise capabilities.'}
|
||||
: 'Your organization is running on the open-source plan. Upgrade to Enterprise to remove limits and unlock advanced capabilities.'}
|
||||
</Typography>
|
||||
|
||||
{/* Features Grid */}
|
||||
@@ -223,8 +224,8 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) =>
|
||||
})
|
||||
.map((feature, index) => {
|
||||
const Icon = feature.icon;
|
||||
const isAvailable = isProdStatusOn && feature.isActive;
|
||||
const statusColor = isAvailable ? colors.success : colors.disabled;
|
||||
const isAvailable = isProdStatusOn;
|
||||
const statusColor = isAvailable ? colors.success : colors.warning;
|
||||
const bgColor = isAvailable ? themeMode === "dark" ? "#212121" : "#ffffff" : colors.disabledBg;
|
||||
|
||||
return (
|
||||
@@ -235,19 +236,34 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) =>
|
||||
alignItems: 'center',
|
||||
gap: 14,
|
||||
padding: '14px 16px',
|
||||
paddingLeft: !isAvailable ? 20 : 16,
|
||||
borderRadius: 10,
|
||||
background: bgColor,
|
||||
border: `1px solid ${isAvailable ? colors.success + '40' : colors.border}`,
|
||||
transition: 'all 0.2s',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{!isAvailable && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: 3,
|
||||
background: colors.warning,
|
||||
borderRadius: '10px 0 0 10px',
|
||||
}} />
|
||||
)}
|
||||
|
||||
{/* Icon */}
|
||||
<div
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 8,
|
||||
background: isAvailable ? colors.success + '20' : colors.disabledBg,
|
||||
background: isAvailable ? colors.success + '20' : `${colors.warning}20`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
@@ -264,7 +280,7 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) =>
|
||||
fontSize: 15,
|
||||
fontWeight: 600,
|
||||
color: colors.textPrimary,
|
||||
marginBottom: 4,
|
||||
marginBottom: 3,
|
||||
}}
|
||||
>
|
||||
{feature.label}
|
||||
@@ -272,7 +288,7 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) =>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 13,
|
||||
color: colors.textSecondary,
|
||||
color: !isAvailable ? colors.warning : colors.textSecondary,
|
||||
lineHeight: 1.4,
|
||||
}}
|
||||
>
|
||||
@@ -285,8 +301,8 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) =>
|
||||
style={{ color: colors.success, fontSize: 20, flexShrink: 0 }}
|
||||
/>
|
||||
) : (
|
||||
<XCircleIcon
|
||||
style={{ color: colors.disabled, fontSize: 20, flexShrink: 0 }}
|
||||
<LockIcon
|
||||
style={{ color: colors.warning, fontSize: 20, flexShrink: 0 }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -304,80 +320,323 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) =>
|
||||
/>
|
||||
|
||||
{!isProdStatusOn && (
|
||||
<div
|
||||
style={{
|
||||
padding: 20,
|
||||
<div style={{
|
||||
borderRadius: 12,
|
||||
background: themeMode == "dark" ? "#212121" : "#ffffff",
|
||||
border: `1px solid ${colors.border}`,
|
||||
marginBottom: 20,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: 12, marginBottom: 12 }}>
|
||||
<InfoIcon style={{ color: colors.accent, flexShrink: 0, marginTop: 2, fontSize: 20 }} />
|
||||
<div>
|
||||
<Typography variant="h6" style={{ fontWeight: 600, margin: '0 0 8px 0' }}>
|
||||
About Shuffle Enterprise
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="textSecondary"
|
||||
style={{
|
||||
margin: 0,
|
||||
lineHeight: 1.6,
|
||||
}}
|
||||
>
|
||||
Shuffle Enterprise is designed for organizations that require scalability, high
|
||||
availability, dedicated support, and robust infrastructure to run mission-critical
|
||||
workflows in production environments. <a href="https://shuffler.io/articles/Shuffle_Open_Source" target="_blank" rel="noreferrer" style={{color: "#FF8544" }}>learn more</a>
|
||||
</Typography>
|
||||
overflow: 'hidden',
|
||||
border: `1px solid ${colors.accent}40`,
|
||||
marginBottom: 24,
|
||||
}}>
|
||||
{/* Header */}
|
||||
<div style={{
|
||||
background: `linear-gradient(135deg, ${colors.accent}14 0%, ${colors.accent}06 100%)`,
|
||||
padding: '16px 20px',
|
||||
borderBottom: `1px solid ${colors.accent}20`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
}}>
|
||||
<div style={{
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 8,
|
||||
background: `${colors.accent}20`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<ShieldIcon style={{ color: colors.accent, fontSize: 20 }} />
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="h6" style={{ fontWeight: 700, margin: 0, fontSize: 16 }}>
|
||||
Unlock Shuffle Enterprise
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" style={{ margin: 0, fontSize: 12, lineHeight: 1.4 }}>
|
||||
Scale your security operations without limits
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>)}
|
||||
|
||||
{/* CTA Section */}
|
||||
{!isProdStatusOn && (
|
||||
<div
|
||||
style={{
|
||||
padding: 20,
|
||||
borderRadius: 12,
|
||||
background: `linear-gradient(135deg, #FF854415 0%, #FF854408 100%)`,
|
||||
border: `1px solid #FF854440`,
|
||||
marginBottom: 24,
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Typography variant="h6" style={{ fontWeight: 600, margin: '0 0 8px 0' }}>
|
||||
Ready to upgrade?
|
||||
{/* Body */}
|
||||
<div style={{
|
||||
padding: '16px 20px 20px',
|
||||
background: themeMode === "dark" ? "#212121" : "#ffffff",
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
gap: 8,
|
||||
marginBottom: 16,
|
||||
}}>
|
||||
{[
|
||||
{ icon: ZapIcon, text: 'Higher App Run Limits' },
|
||||
{ icon: UsersIcon, text: 'Multi-Tenant Support' },
|
||||
{ icon: ShieldIcon, text: 'Enterprise SLA' },
|
||||
{ icon: FmdGoodOutlinedIcon, text: 'Multi-Location Deploy' },
|
||||
].map((item, i) => {
|
||||
const ItemIcon = item.icon;
|
||||
return (
|
||||
<div key={i} style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
padding: '8px 12px',
|
||||
borderRadius: 8,
|
||||
background: themeMode === "dark" ? `${colors.accent}08` : `${colors.accent}06`,
|
||||
}}>
|
||||
<ItemIcon style={{ color: colors.accent, fontSize: 16 }} />
|
||||
<span style={{ fontSize: 13, color: colors.textPrimary, fontWeight: 500 }}>
|
||||
{item.text}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Typography variant="body2" color="textSecondary" style={{
|
||||
marginBottom: 16,
|
||||
lineHeight: 1.6,
|
||||
fontSize: 13,
|
||||
}}>
|
||||
Purpose-built for security teams that need scalability, high availability, and dedicated
|
||||
expert support to run mission-critical workflows in production.{' '}
|
||||
<a href="https://shuffler.io/articles/Shuffle_Open_Source" target="_blank" rel="noreferrer" style={{ color: colors.accent }}>
|
||||
Learn more
|
||||
</a>
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="textSecondary"
|
||||
style={{
|
||||
margin: 0,
|
||||
lineHeight: 1.6,
|
||||
}}
|
||||
|
||||
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
|
||||
<Button
|
||||
href="https://shuffler.io/pricing?env=Self-Hosted"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
endIcon={<ArrowRightIcon sx={{ fontSize: 16 }} />}
|
||||
>
|
||||
Upgrade Now
|
||||
</Button>
|
||||
<Button
|
||||
href="https://shuffler.io/contact?category=talk_to_sales"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
variant="outlined"
|
||||
startIcon={<MailIcon sx={{ fontSize: 16 }} />}
|
||||
>
|
||||
Contact our team to learn more about enterprise features and pricing.
|
||||
</Typography>
|
||||
Talk to Sales
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
href="https://shuffler.io/contact?category=talk_to_sales"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<MailIcon sx={{ fontSize: 16}} />}
|
||||
endIcon={<ArrowRightIcon sx={{ fontSize: 16 }} />}
|
||||
>
|
||||
Contact Sales
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const AppRunsQueueCard = memo(({ environment, isAirGapped, isCloudSynching, totalRuns, limit, theme, navigate }) => {
|
||||
|
||||
const usagePct = limit > 0 ? (totalRuns / limit) * 100 : 0;
|
||||
const hardPauseLimit = limit * 2;
|
||||
const hardPausePct = hardPauseLimit > 0 ? Math.min((totalRuns / hardPauseLimit) * 100, 100) : 0;
|
||||
const mainBarPct = Math.min(usagePct, 100);
|
||||
|
||||
const queueSize = environment?.queue !== undefined && environment?.queue !== null
|
||||
? Math.max(0, environment.queue)
|
||||
: 0;
|
||||
|
||||
const isThrottled = isCloudSynching ? false : (isAirGapped ? hardPausePct >= 100 : usagePct >= 100);
|
||||
|
||||
let status, statusColor, statusBg;
|
||||
if (isThrottled) {
|
||||
status = 'Throttled';
|
||||
statusColor = '#ef4444';
|
||||
statusBg = 'rgba(239, 68, 68, 0.12)';
|
||||
} else if (!isCloudSynching && usagePct >= 80) {
|
||||
status = 'Warning';
|
||||
statusColor = '#f59e0b';
|
||||
statusBg = 'rgba(245, 158, 11, 0.12)';
|
||||
} else {
|
||||
status = 'Healthy';
|
||||
statusColor = theme.palette.green;
|
||||
statusBg = `${theme.palette.green}1f`;
|
||||
}
|
||||
|
||||
const throttleRate = isThrottled ? '1/min' : '\u2014';
|
||||
const estClearTime = isThrottled && queueSize > 0 ? `${queueSize} min` : '\u2014';
|
||||
const mainBarColor = isThrottled ? '#ef4444' : usagePct >= 80 && !isCloudSynching ? '#f59e0b' : theme.palette.green;
|
||||
|
||||
const envTypeLabel = environment?.run_type === 'cloud' ? 'Cloud' : 'On-prem';
|
||||
const envName = environment?.Name || environment?.name || 'Default';
|
||||
|
||||
const borderColor = theme.palette.slateGrayColor;
|
||||
const trackBg = theme.palette.slateGrayColor;
|
||||
const mutedText = theme.palette.text.secondary;
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
border: `1px solid ${borderColor}`,
|
||||
borderRadius: 12,
|
||||
padding: '20px 24px',
|
||||
backgroundColor: theme.palette.platformColor,
|
||||
marginBottom: 16,
|
||||
maxWidth: 800,
|
||||
}}>
|
||||
{/* Title row */}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
|
||||
<Typography variant="body2" style={{ color: mutedText, fontSize: 13, fontWeight: 500 }}>
|
||||
{envTypeLabel} - {envName} · App runs / month
|
||||
</Typography>
|
||||
<div style={{
|
||||
padding: '4px 14px',
|
||||
borderRadius: 20,
|
||||
backgroundColor: statusBg,
|
||||
border: `1px solid ${statusColor}60`,
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
}}>
|
||||
<span style={{ width: 7, height: 7, borderRadius: '50%', backgroundColor: statusColor, display: 'inline-block', boxShadow: `0 0 6px ${statusColor}` }} />
|
||||
<span style={{ color: statusColor, fontWeight: 700, fontSize: 13 }}>{status}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main number */}
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 6, marginBottom: 2 }}>
|
||||
<span style={{ fontSize: 28, fontWeight: 700, color: theme.palette.text.primary }}>
|
||||
{totalRuns.toLocaleString()}
|
||||
</span>
|
||||
<span style={{ fontSize: 18, color: mutedText }}>
|
||||
/ {limit.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{isAirGapped && !isCloudSynching && (
|
||||
<Typography variant="caption" style={{ color: mutedText, fontSize: 12, display: 'block', marginBottom: 14 }}>
|
||||
No throttle until {hardPauseLimit.toLocaleString()} runs · 2× your plan limit
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{/* Main usage bar */}
|
||||
<div style={{ position: 'relative', height: 8, borderRadius: 4, backgroundColor: trackBg, marginBottom: 4 }}>
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
left: 0, top: 0, bottom: 0,
|
||||
width: `${mainBarPct}%`,
|
||||
borderRadius: 4,
|
||||
backgroundColor: mainBarColor,
|
||||
transition: 'width 0.4s ease',
|
||||
}} />
|
||||
{/* 80% threshold marker */}
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
left: '80%',
|
||||
top: -3,
|
||||
bottom: -3,
|
||||
width: 2,
|
||||
backgroundColor: borderColor,
|
||||
borderRadius: 1,
|
||||
}} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 14 }}>
|
||||
<Typography variant="caption" style={{ color: mutedText, fontSize: 11 }}>0</Typography>
|
||||
<Typography variant="caption" style={{ color: mutedText, fontSize: 11 }}>80% threshold</Typography>
|
||||
<Typography variant="caption" style={{ color: mutedText, fontSize: 11 }}>{limit.toLocaleString()}</Typography>
|
||||
</div>
|
||||
|
||||
{/* Throttle limit row */}
|
||||
{isAirGapped && (
|
||||
<>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
|
||||
<Typography variant="caption" style={{ color: mutedText, fontSize: 12 }}>
|
||||
Burst throttle threshold (2× limit) · workflows throttle to 1/min above this
|
||||
</Typography>
|
||||
<Typography variant="caption" style={{ color: mutedText, fontSize: 12 }}>
|
||||
{totalRuns.toLocaleString()} / {hardPauseLimit.toLocaleString()}
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
<div style={{ position: 'relative', height: 6, borderRadius: 3, backgroundColor: trackBg, marginBottom: 16 }}>
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
left: 0, top: 0, bottom: 0,
|
||||
width: `${hardPausePct}%`,
|
||||
borderRadius: 3,
|
||||
backgroundColor: '#ef4444',
|
||||
transition: 'width 0.4s ease',
|
||||
}} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Alert box for Warning / Throttled */}
|
||||
{status !== 'Healthy' && (
|
||||
<div style={{
|
||||
borderRadius: 8,
|
||||
padding: '14px 16px',
|
||||
backgroundColor: 'rgba(245, 158, 11, 0.08)',
|
||||
border: '1px solid rgba(245, 158, 11, 0.3)',
|
||||
marginBottom: 16,
|
||||
}}>
|
||||
<Typography variant="body2" style={{ color: '#f59e0b', fontWeight: 700, marginBottom: 6, fontSize: 14 }}>
|
||||
{isThrottled ? 'Running slow \u2014 workflows are still running' : 'Approaching your monthly limit'}
|
||||
</Typography>
|
||||
<Typography variant="body2" style={{ color: theme.palette.text.primary, fontSize: 13, lineHeight: 1.6, marginBottom: 12 }}>
|
||||
{isThrottled
|
||||
? isAirGapped
|
||||
? `You've exceeded the burst threshold of ${hardPauseLimit.toLocaleString()} runs (2\u00d7 your plan limit). Your workflows are still running \u2014 there is no hard stop. Executions slow to 1 per minute until next month.`
|
||||
: `You've exceeded your ${limit.toLocaleString()} monthly limit. Your instance keeps running \u2014 executions slow to 1 per minute until next month. Nothing is lost. You can view or clear the queue from the Locations tab.`
|
||||
: isAirGapped
|
||||
? `You've used ${totalRuns.toLocaleString()} of ${limit.toLocaleString()} app runs. Workflows run normally \u2014 slowdown only begins at ${hardPauseLimit.toLocaleString()} runs (2\u00d7 your plan limit). No action needed.`
|
||||
: `You've used ${totalRuns.toLocaleString()} of ${limit.toLocaleString()} app runs (${Math.max(0, limit - totalRuns).toLocaleString()} remaining). If you reach 100%, executions continue at a reduced rate of 1 per minute, nothing stops or is lost.`
|
||||
}
|
||||
</Typography>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
sx={{
|
||||
borderColor: '#f59e0b',
|
||||
color: '#f59e0b',
|
||||
textTransform: 'none',
|
||||
fontSize: 13,
|
||||
borderRadius: '6px',
|
||||
padding: '5px 14px',
|
||||
}}
|
||||
onClick={() => {
|
||||
if (isThrottled) navigate('/admin?tab=locations');
|
||||
else window.open('https://shuffler.io/contact', '_blank', 'noopener,noreferrer');
|
||||
}}
|
||||
>
|
||||
{isThrottled ? 'Manage queue' : 'Contact Us'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stats row */}
|
||||
<div style={{ display: 'flex', borderTop: `1px solid ${borderColor}`, paddingTop: 16 }}>
|
||||
{[
|
||||
{ label: 'Queued jobs', value: queueSize },
|
||||
{ label: 'Throttle rate', value: throttleRate },
|
||||
{ label: 'Est. clear time', value: estClearTime },
|
||||
].map((stat, i) => (
|
||||
<div key={i} style={{
|
||||
flex: 1,
|
||||
paddingRight: i < 2 ? 16 : 0,
|
||||
borderRight: i < 2 ? `1px solid ${borderColor}` : 'none',
|
||||
marginRight: i < 2 ? 16 : 0,
|
||||
}}>
|
||||
<Typography variant="caption" style={{ color: mutedText, fontSize: 12, display: 'block', marginBottom: 4 }}>
|
||||
{stat.label}
|
||||
</Typography>
|
||||
<Typography style={{ fontWeight: 700, fontSize: 18, color: theme.palette.text.primary }}>
|
||||
{stat.value}
|
||||
</Typography>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const Billing = memo((props) => {
|
||||
const { globalUrl, userdata, serverside, billingInfo, stripeKey,isLoaded, selectedOrganization, handleGetOrg, clickedFromOrgTab, removeCookie} = props;
|
||||
//const alert = useAlert();
|
||||
@@ -413,7 +672,7 @@ const Billing = memo((props) => {
|
||||
const [statistics, setStatistics] = useState([])
|
||||
const [monthlyAppRunsParent, setMonthlyAppRunsParent] = useState(0)
|
||||
const [monthlyAllSuborgExecutions, setMonthlyAllSuborgExecutions] = useState(0)
|
||||
|
||||
const [billingEnvironments, setBillingEnvironments] = useState([])
|
||||
useEffect(() => {
|
||||
if (monthlyAppRunsParent > 0 || monthlyAllSuborgExecutions > 0) {
|
||||
const percentage = ((monthlyAppRunsParent + monthlyAllSuborgExecutions) / userdata.app_execution_limit) * 100;
|
||||
@@ -525,6 +784,29 @@ const Billing = memo((props) => {
|
||||
}, [])
|
||||
|
||||
|
||||
const getBillingEnvironments = () => {
|
||||
fetch(globalUrl + "/api/v1/getenvironments", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status !== 200) return;
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson && Array.isArray(responseJson)) {
|
||||
setBillingEnvironments(responseJson);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("Error fetching environments for billing:", error);
|
||||
});
|
||||
};
|
||||
|
||||
const getStats = (orgid) => {
|
||||
|
||||
if (orgid === undefined || orgid === null) {
|
||||
@@ -563,6 +845,9 @@ const Billing = memo((props) => {
|
||||
useEffect(() => {
|
||||
if (selectedOrganization && selectedOrganization?.id?.length > 0) {
|
||||
getStats(selectedOrganization.id);
|
||||
if (!isCloud) {
|
||||
getBillingEnvironments();
|
||||
}
|
||||
}
|
||||
}, [selectedOrganization]);
|
||||
|
||||
@@ -2387,6 +2672,17 @@ const Billing = memo((props) => {
|
||||
|
||||
const isChildOrg = userdata?.active_org?.creator_org !== "" && userdata?.active_org?.creator_org !== undefined && userdata?.active_org?.creator_org !== null
|
||||
|
||||
const activeQueueEnvs = Array.isArray(billingEnvironments) ? billingEnvironments.filter(env => env != null && !env.archived && env.Type !== 'cloud') : [];
|
||||
const totalQueueSize = activeQueueEnvs.reduce((sum, env) => sum + Math.max(0, env?.queue || 0), 0);
|
||||
const aggregatedQueueEnv = {
|
||||
Name: `${activeQueueEnvs.length} Runtime Location${activeQueueEnvs.length !== 1 ? 's' : ''}`,
|
||||
run_type: 'on-prem',
|
||||
queue: totalQueueSize,
|
||||
};
|
||||
const appExecLimit = selectedOrganization?.sync_features?.app_executions?.limit ?? 0;
|
||||
const isAirGapped = selectedOrganization != null && (selectedOrganization.cloud_sync_active === true || selectedOrganization.cloud_sync === true) ? false : appExecLimit < 300000 ? false : true;
|
||||
const isCloudSynching = selectedOrganization != null && selectedOrganization.cloud_sync === true && appExecLimit >= 300000;
|
||||
|
||||
useEffect(() => {
|
||||
if (isChildOrg && currentTab === 0) {
|
||||
setCurrentTab(1);
|
||||
@@ -2800,6 +3096,28 @@ const Billing = memo((props) => {
|
||||
|
||||
</div>
|
||||
) : null*/}
|
||||
|
||||
{/* Queue Management */}
|
||||
{!isCloud && activeQueueEnvs.length > 0 && !isChildOrg && (
|
||||
<div style={{ maxWidth: 800, marginTop: 32, marginBottom: 8 }}>
|
||||
<Typography variant="h6" style={{ marginBottom: 6, fontSize: 20, fontWeight: 600 }}>
|
||||
Queue Management
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" style={{ marginBottom: 16, fontSize: 14 }}>
|
||||
Real-time status of your app run usage and workflow queue across all runtime locations.
|
||||
</Typography>
|
||||
<AppRunsQueueCard
|
||||
environment={aggregatedQueueEnv}
|
||||
totalRuns={Number(monthlyAppRunsParent ?? 0) + Number(monthlyAllSuborgExecutions ?? 0)}
|
||||
limit={selectedOrganization?.sync_features?.app_executions?.limit || 25000}
|
||||
theme={theme}
|
||||
navigate={navigate}
|
||||
isAirGapped={isAirGapped}
|
||||
isCloudSynching={isCloudSynching}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isChildOrg && isCloud && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', marginTop: 50, maxWidth: 860 }} id="professional-services">
|
||||
<Typography variant="h6" style={{ marginBottom: 5, fontSize: 24, fontWeight: 500 }}>
|
||||
|
||||
@@ -92,6 +92,11 @@ const AppStats = (defaultprops) => {
|
||||
const statKey = syncStats === true ? "onprem_stats" : "daily_statistics"
|
||||
const dailyStats = inputdata[statKey]
|
||||
if (dailyStats === undefined || dailyStats === null) {
|
||||
setAppruns(undefined)
|
||||
setWorkflowRuns(undefined)
|
||||
setSubflowRuns(undefined)
|
||||
setChildOrgsAppRuns(undefined)
|
||||
setApprunCosts(undefined)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -378,6 +383,17 @@ const AppStats = (defaultprops) => {
|
||||
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
|
||||
var mtd_cost = 0
|
||||
for (let key in statistics[statKey]) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, { useState, useEffect, useContext, memo } from "react";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
import { getTheme } from "../theme.jsx";
|
||||
import SubOrgDistributionDialog from "./SubOrgDistributionDialog.jsx";
|
||||
import DeleteConfirmDialog from "./DeleteConfirmDialog.jsx";
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import ReactJson from "react-json-view-ssr";
|
||||
@@ -17,8 +19,6 @@ import {
|
||||
Button,
|
||||
Tabs,
|
||||
Tab,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
IconButton,
|
||||
Dialog,
|
||||
@@ -82,6 +82,7 @@ import {
|
||||
Hub as HubIcon,
|
||||
Key as KeyIcon,
|
||||
FlashOn as FlashOnIcon,
|
||||
Search as SearchIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { Context } from "../context/ContextApi.jsx";
|
||||
|
||||
@@ -127,6 +128,9 @@ const CacheView = memo((props) => {
|
||||
const [showDistributionPopup, setShowDistributionPopup] = useState(false);
|
||||
const [selectedSubOrg, setSelectedSubOrg] = 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 [page, setPage] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(50)
|
||||
@@ -233,7 +237,7 @@ const CacheView = memo((props) => {
|
||||
|
||||
{
|
||||
"name": "Enrich",
|
||||
"description": "Enriches the data. Only runs on valid JSON data AND if the 'enrichment' field does not exist.",
|
||||
"description": "Enriches the data. Uses regex keys and runs a workflow in the background. Added to the 'enrichments' key.",
|
||||
"type": "singul",
|
||||
"options": [{
|
||||
"key": "",
|
||||
@@ -331,6 +335,14 @@ const CacheView = memo((props) => {
|
||||
|
||||
// In order to make linking weird urls from workflow page work.
|
||||
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")) {
|
||||
const newParam = categoryParam.substring(5).replaceAll("%20", "_")
|
||||
|
||||
@@ -570,17 +582,26 @@ const CacheView = memo((props) => {
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
if (refreshList === undefined || refreshList === null || refreshList === true) {
|
||||
|
||||
toast.success("Deleted datastore entry");
|
||||
setTimeout(() => {
|
||||
listOrgCache(orgId, selectedCategory, 0, pageSize, page)
|
||||
}, 1000);
|
||||
}
|
||||
} 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.`)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
if (refreshList === undefined || refreshList === null || refreshList === true) {
|
||||
setTimeout(() => {
|
||||
listOrgCache(orgId, selectedCategory, 0, pageSize, page)
|
||||
}, 1000);
|
||||
}
|
||||
toast(error.toString());
|
||||
});
|
||||
};
|
||||
@@ -831,6 +852,11 @@ const CacheView = memo((props) => {
|
||||
Category: {dataValue.category}
|
||||
</Typography>
|
||||
: 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 ?
|
||||
<div style={{display: "flex", marginTop: 12, }}>
|
||||
<Typography variant="body2" color="textSecondary" style={{ marginRight: 10, marginTop: 4, }}>
|
||||
@@ -900,33 +926,6 @@ const CacheView = memo((props) => {
|
||||
</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) => {
|
||||
|
||||
editFileConfig(id, [...new Set(selectedSubOrg)], selectedCategory)
|
||||
@@ -941,8 +940,6 @@ const CacheView = memo((props) => {
|
||||
category: category === undefined || category === "" || category === "default" ? "" : category,
|
||||
}
|
||||
|
||||
console.log("data: ", data);
|
||||
|
||||
const url = `${globalUrl}/api/v1/orgs/${orgId}/cache/config`;
|
||||
|
||||
fetch(url, {
|
||||
@@ -975,98 +972,41 @@ const CacheView = memo((props) => {
|
||||
};
|
||||
|
||||
|
||||
const cacheDistributionModal = showDistributionPopup ? (
|
||||
<Dialog
|
||||
open={showDistributionPopup}
|
||||
onClose={() => setShowDistributionPopup(false)}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
borderRadius: theme?.palette?.DialogStyle?.borderRadius,
|
||||
border: theme?.palette?.DialogStyle?.border,
|
||||
fontFamily: theme?.typography?.fontFamily,
|
||||
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
||||
zIndex: 1000,
|
||||
minWidth: "600px",
|
||||
minHeight: "320px",
|
||||
overflow: "auto",
|
||||
'& .MuiDialogContent-root': {
|
||||
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
||||
},
|
||||
'& .MuiDialogTitle-root': {
|
||||
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
||||
},
|
||||
'& .MuiDialogActions-root': {
|
||||
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DialogTitle>
|
||||
<Typography variant="h5" color="textPrimary">
|
||||
Select sub-org to distribute Datastore key
|
||||
</Typography>
|
||||
</DialogTitle>
|
||||
<DialogContent style={{ color: "rgba(255,255,255,0.65)" }}>
|
||||
<MenuItem value="none" onClick={()=> {handleSelectSubOrg(null, "none")}}>None</MenuItem>
|
||||
<MenuItem value="all" onClick={()=> {handleSelectSubOrg(null, "all")}}>All</MenuItem>
|
||||
{userdata.orgs.map((data, index) => {
|
||||
if (data.creator_org !== userdata.active_org.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const imagesize = 22;
|
||||
const imageStyle = {
|
||||
width: imagesize,
|
||||
height: imagesize,
|
||||
pointerEvents: "none",
|
||||
marginRight: 10,
|
||||
marginLeft: data.id === userdata.active_org.id ? 0 : 20,
|
||||
};
|
||||
|
||||
const image = data.image === "" ? (
|
||||
<img alt={data.name} src={theme.palette.defaultImage} style={imageStyle} />
|
||||
) : (
|
||||
<img alt={data.name} src={data.image} style={imageStyle} />
|
||||
);
|
||||
const deleteConfirmDialog = (
|
||||
<DeleteConfirmDialog
|
||||
open={deleteConfirmOpen}
|
||||
onClose={() => { setDeleteConfirmOpen(false); setDeleteConfirmTarget(null); }}
|
||||
onConfirm={() => {
|
||||
if (deleteConfirmTarget?.bulk) {
|
||||
const itemsToDelete = selectedRows.map(rowId =>
|
||||
listCache.find(item => `${item.key}_${item.category || ""}` === rowId)
|
||||
).filter(Boolean);
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
})}
|
||||
const count = itemsToDelete.length;
|
||||
setSelectedRows([]);
|
||||
itemsToDelete.forEach(item => deleteEntry(orgId, item.key, item.category, false));
|
||||
|
||||
<div style={{ display: "flex", marginTop: 20 }}>
|
||||
<Button
|
||||
style={{ borderRadius: "2px", textTransform: 'none', fontSize:16, color: theme.palette.primary.main }}
|
||||
onClick={() => setShowDistributionPopup(false)}
|
||||
color="primary"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
style={{ borderRadius: "2px", textTransform: 'none', fontSize:16, marginLeft: 10 }}
|
||||
onClick={() => {
|
||||
changeDistribution(selectedCacheKey, selectedSubOrg);
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
) : null;
|
||||
setTimeout(() => {
|
||||
listOrgCache(orgId, selectedCategory, 0, pageSize, page);
|
||||
toast.success("Deleted " + count + " keys from datastore");
|
||||
}, 3000);
|
||||
} else {
|
||||
deleteEntry(orgId, deleteConfirmTarget.key, deleteConfirmTarget.category);
|
||||
}
|
||||
setDeleteConfirmOpen(false);
|
||||
setDeleteConfirmTarget(null);
|
||||
}}
|
||||
title={deleteConfirmTarget?.bulk ? `Delete ${selectedRows?.length} Key${selectedRows?.length > 1 ? "s" : ""}?` : "Delete Key?"}
|
||||
description={
|
||||
deleteConfirmTarget?.bulk
|
||||
? <>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>?</>
|
||||
}
|
||||
warningText="This cannot be undone. Any workflows using these keys will lose access."
|
||||
/>
|
||||
);
|
||||
|
||||
const saveAutomation = (allAutomation, settings) => {
|
||||
// Check if icon is a string. Otherwise make it empty.
|
||||
@@ -1759,6 +1699,10 @@ const CacheView = memo((props) => {
|
||||
enableClipboard={(copy) => {
|
||||
handleReactJsonClipboard(copy)
|
||||
}}
|
||||
onSelect={(select) => {
|
||||
//currentParams.set("category", selectedCategory);
|
||||
//HandleJsonCopy(validate.result, select, "exec");
|
||||
}}
|
||||
collapseStringsAfterLength={theme.palette.jsonCollapseStringsAfterLength}
|
||||
iconStyle={theme.palette.jsonIconStyle}
|
||||
displayDataTypes={false}
|
||||
@@ -1935,6 +1879,7 @@ const CacheView = memo((props) => {
|
||||
"workflow_id": data.workflow_id,
|
||||
"category": data.category,
|
||||
"tags": data.tags,
|
||||
"enrichments": data.enrichments,
|
||||
})
|
||||
setValue(newvalue)
|
||||
setModalOpen(true)
|
||||
@@ -2019,7 +1964,8 @@ const CacheView = memo((props) => {
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
deleteEntry(orgId, data.key, data.category)
|
||||
setDeleteConfirmTarget({ key: data.key, category: data.category })
|
||||
setDeleteConfirmOpen(true)
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
@@ -2094,13 +2040,21 @@ const CacheView = memo((props) => {
|
||||
style={{ margin: "auto" }}
|
||||
color="secondary"
|
||||
onClick={() => {
|
||||
setShowDistributionPopup(true)
|
||||
setShowDistributionPopup(true);
|
||||
let initialSelected = [];
|
||||
if(data?.suborg_distribution?.length > 0){
|
||||
setSelectedSubOrg(data.suborg_distribution)
|
||||
}else{
|
||||
setSelectedSubOrg([])
|
||||
initialSelected = data.suborg_distribution;
|
||||
}
|
||||
setSelectedCacheKey(data.key)
|
||||
setSelectedSubOrg(initialSelected);
|
||||
setSelectedCacheKey(data.key);
|
||||
const suborgs = (userdata?.orgs || []).filter(o => o.creator_org === userdata?.active_org?.id);
|
||||
const sorted = [...suborgs].sort((a, b) => {
|
||||
const aS = initialSelected.includes(a.id);
|
||||
const bS = initialSelected.includes(b.id);
|
||||
if (aS !== bS) return bS - aS;
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
setDistribOrgOrder(sorted.map(o => o.id));
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
@@ -2116,6 +2070,7 @@ const CacheView = memo((props) => {
|
||||
|
||||
var previousgroup = ""
|
||||
const isAutomating = categoryAutomations?.find((automation) => automation.enabled) !== undefined
|
||||
const isAutomatingAccess = categoryConfig?.settings?.timeout >= 60 || categoryConfig?.settings?.public === true ? true : false
|
||||
return (
|
||||
<div style={{
|
||||
minHeight: 2000,
|
||||
@@ -2183,7 +2138,17 @@ const CacheView = memo((props) => {
|
||||
apps={apps}
|
||||
/>
|
||||
|
||||
{cacheDistributionModal}
|
||||
<SubOrgDistributionDialog
|
||||
open={showDistributionPopup}
|
||||
onClose={() => { setShowDistributionPopup(false); setSelectedCacheKey(""); }}
|
||||
title="Distribute Datastore Key to Sub-Organizations"
|
||||
extraInfo={selectedCacheKey ? `Selected Key: ${selectedCacheKey}` : null}
|
||||
orgs={distribOrgOrder.map(id => (userdata?.orgs || []).find(o => o.id === id)).filter(Boolean)}
|
||||
selectedOrgIds={selectedSubOrg}
|
||||
onSelectionChange={setSelectedSubOrg}
|
||||
onSave={(ids) => { changeDistribution(selectedCacheKey, ids); }}
|
||||
/>
|
||||
{deleteConfirmDialog}
|
||||
|
||||
<div style={{height: "100%", overflowY: "auto", scrollbarColor: theme.palette.scrollbarColorTransparent, scrollbarWidth: 'thin'}}>
|
||||
<div style={{ height: "100%", width: "calc(100% - 20px)", scrollbarColor: theme.palette.scrollbarColorTransparent, scrollbarWidth: 'thin' }}>
|
||||
@@ -2204,7 +2169,6 @@ const CacheView = memo((props) => {
|
||||
height: 35,
|
||||
textTransform: 'none',
|
||||
|
||||
//border: isAutomating ? `1px solid ${theme.palette.primary.main}` : null,
|
||||
}}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
@@ -2272,12 +2236,12 @@ const CacheView = memo((props) => {
|
||||
datastoreCategories !== null &&
|
||||
datastoreCategories.length > 1 ? (
|
||||
|
||||
<FormControl style={{ minWidth: 250, maxWidth: 250, marginTop: 8, }}>
|
||||
<FormControl style={{ minWidth: 275, maxWidth: 275, marginTop: 8, }}>
|
||||
<Autocomplete
|
||||
labelId="category-choice"
|
||||
style={{
|
||||
minWidth: 250,
|
||||
maxWidth: 250,
|
||||
minWidth: 275,
|
||||
maxWidth: 275,
|
||||
}}
|
||||
ListboxProps={{
|
||||
style: {
|
||||
@@ -2557,13 +2521,13 @@ const CacheView = memo((props) => {
|
||||
marginLeft: 3,
|
||||
}}
|
||||
variant="outlined"
|
||||
color="secondary"
|
||||
color={isAutomatingAccess ? "primary" : "secondary"}
|
||||
disabled={selectedCategory === undefined || selectedCategory === "" || selectedCategory === "default"}
|
||||
onClick={() => {
|
||||
setShowSettingsMenu(true)
|
||||
}}
|
||||
>
|
||||
<SettingsIcon style={{color: theme.palette.secondary.main, }} />
|
||||
<SettingsIcon style={{}} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</ButtonGroup>
|
||||
@@ -2877,27 +2841,8 @@ const CacheView = memo((props) => {
|
||||
<Button
|
||||
style={{ marginLeft: 50, }}
|
||||
onClick={() => {
|
||||
setCachedLoaded(false)
|
||||
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)
|
||||
|
||||
setDeleteConfirmTarget({ bulk: true });
|
||||
setDeleteConfirmOpen(true);
|
||||
}}
|
||||
variant={"outlined"}
|
||||
color="secondary"
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
} from '../views/AngularWorkflow.jsx'
|
||||
|
||||
import algoliasearch from 'algoliasearch/lite';
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "33e4e3564f4f060e96e0531957bed552")
|
||||
|
||||
const CollectIngestModal = (props) => {
|
||||
const { globalUrl, open, setOpen, workflows, getWorkflows, apps, } = props;
|
||||
@@ -437,7 +437,7 @@ const CollectIngestModal = (props) => {
|
||||
|
||||
<Grid container>
|
||||
<IngestItem type="Ingest Tickets" appCategory={"cases"} webhook={true} index={1} />
|
||||
<IngestItem type="Enable Threat feeds" index={2} />
|
||||
<IngestItem type="Enable Threat feeds" index={2} webhook={true} />
|
||||
<IngestItem type="Ingest Assets" appCategory={"assets"} index={2} />
|
||||
<IngestItem type="Ingest Users " appCategory={"users"} index={2} />
|
||||
<IngestItem type="Enable Search" index={2} />
|
||||
|
||||
@@ -633,7 +633,7 @@ const ConfigureWorkflow = (props) => {
|
||||
if (aa !== undefined) {
|
||||
aa('init', {
|
||||
appId: "JNSS5CFDZZ",
|
||||
apiKey: "c8f882473ff42d41158430be09ec2b4e",
|
||||
apiKey: "33e4e3564f4f060e96e0531957bed552",
|
||||
})
|
||||
|
||||
const timestamp = new Date().getTime()
|
||||
|
||||
@@ -4,6 +4,7 @@ import ReactGA from 'react-ga4';
|
||||
import {Link} from 'react-router-dom';
|
||||
import theme from '../theme.jsx';
|
||||
import { removeQuery } from '../components/ScrollToTop.jsx';
|
||||
import SearchContactForm from '../components/SearchContactForm.jsx';
|
||||
|
||||
import {
|
||||
SkipNext as SkipNextIcon,
|
||||
@@ -38,18 +39,14 @@ import {
|
||||
} from "@mui/material"
|
||||
import { useDebouncedCallback } from "../utils/useDebouncedCallback.jsx";
|
||||
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "33e4e3564f4f060e96e0531957bed552")
|
||||
const CreatorGrid = props => {
|
||||
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, isHeader } = props
|
||||
const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows
|
||||
const xs = parsedXs === undefined || parsedXs === null ? isMobile ? 6 : 4 : parsedXs
|
||||
//const [apps, setApps] = 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 =
|
||||
window.location.host === "localhost:3002" ||
|
||||
@@ -59,44 +56,6 @@ const CreatorGrid = props => {
|
||||
const borderRadius = 3
|
||||
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}
|
||||
const SearchBox = ({currentRefinement, refine, isSearchStalled} ) => {
|
||||
var defaultSearch = ""
|
||||
@@ -249,62 +208,8 @@ const CreatorGrid = props => {
|
||||
<CustomHits hitsPerPage={100}/>
|
||||
</InstantSearch>
|
||||
{showSuggestion === true ?
|
||||
<div style={{maxWidth: isMobile ? "100%" : "60%", margin: "auto", paddingTop: 50, 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"}}>
|
||||
<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
|
||||
<SearchContactForm globalUrl={globalUrl} isMobile={isMobile} tabName="creators" />
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import React, { memo, useContext } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { getTheme } from '../theme.jsx';
|
||||
import { Context } from '../context/ContextApi.jsx';
|
||||
|
||||
const DeleteConfirmDialog = memo(({ open, onClose, onConfirm, title, description, warningText }) => {
|
||||
const { themeMode, brandColor } = useContext(Context);
|
||||
const theme = getTheme(themeMode, brandColor);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
borderRadius: theme?.palette?.DialogStyle?.borderRadius,
|
||||
border: theme?.palette?.DialogStyle?.border,
|
||||
fontFamily: theme?.typography?.fontFamily,
|
||||
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
||||
minWidth: 420,
|
||||
'& .MuiDialogContent-root': { backgroundColor: theme?.palette?.DialogStyle?.backgroundColor },
|
||||
'& .MuiDialogTitle-root': { backgroundColor: theme?.palette?.DialogStyle?.backgroundColor },
|
||||
'& .MuiDialogActions-root': { backgroundColor: theme?.palette?.DialogStyle?.backgroundColor },
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DialogTitle>
|
||||
<Typography variant="h6">{title}</Typography>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography variant="body1">{description}</Typography>
|
||||
{warningText && (
|
||||
<Typography variant="body2" color="textSecondary" style={{ marginTop: 8 }}>
|
||||
{warningText}
|
||||
</Typography>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ p: 2 }}>
|
||||
<Button style={{ textTransform: 'none' }} onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
style={{ textTransform: 'none', backgroundColor: theme?.palette?.deleteColor, color: '#fff' }}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
});
|
||||
|
||||
export default DeleteConfirmDialog;
|
||||
@@ -3,11 +3,8 @@ import algoliasearch from 'algoliasearch';
|
||||
import theme from '../theme.jsx';
|
||||
import { InstantSearch, connectSearchBox, connectHits } from 'react-instantsearch-dom';
|
||||
import {
|
||||
Grid,
|
||||
Paper,
|
||||
TextField,
|
||||
Typography,
|
||||
Button,
|
||||
InputAdornment,
|
||||
Avatar,
|
||||
List,
|
||||
@@ -16,6 +13,7 @@ import {
|
||||
ListItemText,
|
||||
} from '@mui/material';
|
||||
import { Search as SearchIcon } from '@mui/icons-material';
|
||||
import SearchContactForm from '../components/SearchContactForm.jsx';
|
||||
import useDebouncedCallback from '../utils/useDebouncedCallback.jsx';
|
||||
|
||||
|
||||
@@ -23,52 +21,9 @@ const searchClient = algoliasearch("JNSS5CFDZZ", "1e5f29b1550939855de5915eac3bf5
|
||||
|
||||
const DiscordChat = 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 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 [inputValue, setInputValue] = useState("");
|
||||
const debouncedRefine = useDebouncedCallback((value) => refine(value), 300);
|
||||
@@ -168,61 +123,7 @@ const DiscordChat = props => {
|
||||
<CustomHits />
|
||||
</div>
|
||||
</InstantSearch>
|
||||
<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>
|
||||
<SearchContactForm globalUrl={globalUrl} isMobile={isMobile} tabName="discord chats" />
|
||||
{/* <span style={{position: "absolute", display: "flex", textAlign: "right", float: "right", right: 0, bottom: 120, }}>
|
||||
<Typography variant="body2" color="textSecondary" style={{}}>
|
||||
Search by
|
||||
|
||||
@@ -4,6 +4,7 @@ import theme from '../theme.jsx';
|
||||
import ReactGA from 'react-ga4';
|
||||
import {Link} from 'react-router-dom';
|
||||
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 aa from 'search-insights'
|
||||
@@ -30,61 +31,19 @@ import { useDebouncedCallback } from "../utils/useDebouncedCallback.jsx";
|
||||
|
||||
|
||||
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "eb5fd80aa6ed5ab4730d836cff3ea283")
|
||||
const DocsGrid = props => {
|
||||
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, userdata, } = props
|
||||
const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows
|
||||
const xs = parsedXs === undefined || parsedXs === null ? isMobile ? 6 : 2 : parsedXs
|
||||
//const [apps, setApps] = 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 borderRadius = 3
|
||||
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} ) => {
|
||||
var defaultSearch = ""
|
||||
if (window !== undefined && window.location !== undefined && window.location.search !== undefined && window.location.search !== null) {
|
||||
@@ -124,8 +83,15 @@ const DocsGrid = props => {
|
||||
placeholder="Search our Documentation..."
|
||||
id="shuffle_search_field"
|
||||
onChange={(event) => {
|
||||
removeQuery("q")
|
||||
debouncedRefine(event.currentTarget.value)
|
||||
const value = event.currentTarget.value
|
||||
debouncedRefine(value)
|
||||
const urlSearchParams = new URLSearchParams(window.location.search)
|
||||
if (value) {
|
||||
urlSearchParams.set("q", value)
|
||||
} else {
|
||||
urlSearchParams.delete("q")
|
||||
}
|
||||
window.history.replaceState(null, "", value ? `?${urlSearchParams.toString()}` : window.location.pathname)
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if(event.key === "Enter") {
|
||||
@@ -291,62 +257,8 @@ const DocsGrid = props => {
|
||||
<CustomHits hitsPerPage={5}/>
|
||||
</InstantSearch>
|
||||
{showSuggestion === true ?
|
||||
<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>
|
||||
: null
|
||||
<SearchContactForm globalUrl={globalUrl} isMobile={isMobile} tabName="docs" />
|
||||
: null
|
||||
}
|
||||
|
||||
{/* <span style={{position: "absolute", display: "flex", textAlign: "right", float: "right", right: 0, bottom: 120, }}>
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
ExpandLess as ExpandLessIcon,
|
||||
ExpandMore as ExpandMoreIcon,
|
||||
Delete as DeleteIcon,
|
||||
Computer as ComputerIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { toast } from 'react-toastify';
|
||||
import { Context } from '../context/ContextApi.jsx';
|
||||
@@ -49,6 +50,7 @@ const EnvironmentTab = memo((props) => {
|
||||
const [modalUser, setModalUser] = React.useState({});
|
||||
const [loginInfo, setLoginInfo] = React.useState("");
|
||||
const [modalOpen, setModalOpen] = React.useState(false);
|
||||
const [sensorGroup, setSensorGroup] = React.useState(false);
|
||||
const [showLoader, setShowLoader] = useState(true)
|
||||
const [commandController, setCommandController] = React.useState({
|
||||
pipelines: false,
|
||||
@@ -535,13 +537,36 @@ const EnvironmentTab = memo((props) => {
|
||||
},
|
||||
},
|
||||
}}
|
||||
style={{
|
||||
}}
|
||||
>
|
||||
<DialogTitle>
|
||||
<Typography variant='h5' color="textPrimary" >Add Location</Typography>
|
||||
<DialogTitle style={{display: "flex", }}>
|
||||
<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>
|
||||
<DialogContent>
|
||||
<div>
|
||||
<Typography variant='body2' color="textPrimary">Location Name</Typography>
|
||||
<Typography variant='body1' color="textSecondary">
|
||||
{sensorGroup ?
|
||||
'With "Sensor Groups" enabled, Runtime Locations allows you to run a lightweight log-collector and response agent onprem.'
|
||||
:
|
||||
'Runtime Locations are a way to run automation in Shuffle. By default, it runs in Docker/Kubernetes and allows you to run AI Agents and Workflows in your designated datacenter.'
|
||||
}
|
||||
</Typography>
|
||||
<div style={{marginTop: 15, }}>
|
||||
<Typography color="textPrimary">
|
||||
{sensorGroup ?
|
||||
"Sensor Group name"
|
||||
:
|
||||
"Location Name"
|
||||
}
|
||||
</Typography>
|
||||
<TextField
|
||||
color="primary"
|
||||
style={{ backgroundColor: theme.palette.textFieldStyle.backgroundColor,}}
|
||||
@@ -555,13 +580,14 @@ const EnvironmentTab = memo((props) => {
|
||||
}}
|
||||
required
|
||||
fullWidth={true}
|
||||
placeholder="datacenter froglantern"
|
||||
placeholder="automation location 3"
|
||||
id="environment_name"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
onChange={(event) =>
|
||||
onChange={(event) => {
|
||||
changeModalData("environment", event.target.value)
|
||||
}
|
||||
setUpdate(Math.random())
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{loginInfo}
|
||||
@@ -576,12 +602,13 @@ const EnvironmentTab = memo((props) => {
|
||||
<Button
|
||||
variant="contained"
|
||||
style={{ borderRadius: "2px", fontSize: 16, textTransform: "none", }}
|
||||
disabled={modalUser.environment === undefined || modalUser.environment === ""}
|
||||
onClick={() => {
|
||||
submitEnvironment(modalUser); // Assuming modalUser is available
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
Submit
|
||||
Create {sensorGroup ? "Sensor Group" : "Location"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
@@ -956,7 +983,7 @@ const EnvironmentTab = memo((props) => {
|
||||
<ListItem
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "80px 80px 80px 150px 100px 80px 400px 100px",
|
||||
gridTemplateColumns: "80px 80px 70px 140px 100px 100px 400px 100px",
|
||||
width: "100%",
|
||||
minWidth: showLoader ? 800 : 0,
|
||||
paddingBottom: 0,
|
||||
@@ -1102,6 +1129,11 @@ const EnvironmentTab = memo((props) => {
|
||||
>
|
||||
<ListItemText
|
||||
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.name === "Cloud" ? (
|
||||
<Tooltip title="Cloud" placement="top">
|
||||
@@ -1275,6 +1307,9 @@ const EnvironmentTab = memo((props) => {
|
||||
<Tooltip title={`Make a new environment to set up a Datalake node. Please contact ${supportEmail} if this is something you want to see on Cloud directly.`} placement="top">
|
||||
<CancelIcon style={{ color: "rgba(255,255,255,0.3)" }} />
|
||||
</Tooltip>
|
||||
:
|
||||
environment?.sensor_group === true ?
|
||||
"N/A"
|
||||
:
|
||||
environment?.data_lake?.enabled && environment?.archived !== true ? (
|
||||
<a
|
||||
@@ -1333,7 +1368,7 @@ const EnvironmentTab = memo((props) => {
|
||||
/>
|
||||
|
||||
<ListItemText
|
||||
primary={environment.Type}
|
||||
primary={environment?.sensor_group === true ? "Sensor Group" : environment.Type}
|
||||
primaryTypographyProps={{
|
||||
style:{
|
||||
minWidth: 50,
|
||||
@@ -1520,6 +1555,16 @@ const EnvironmentTab = memo((props) => {
|
||||
<Grid container justifyContent="center" style={{minWidth: 850, maxWidth: 850, }}>
|
||||
<Grid item xs={12} sm={8} md={6}>
|
||||
<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, }}>
|
||||
<Typography variant="h6">
|
||||
Self-Hosted Orborus instance
|
||||
@@ -1703,6 +1748,7 @@ const EnvironmentTab = memo((props) => {
|
||||
}
|
||||
</Typography>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
{currentEnvQueue.length === 0 ? null :
|
||||
<List style={{ minWidth: 700, maxWidth: 700, maxHeight: 300, overflowY: "auto", scrollbarColor: theme.palette.scrollbarColorTransparent, scrollbarWidth: 'thin', }}>
|
||||
|
||||
@@ -29,6 +29,8 @@ import {
|
||||
Menu,
|
||||
Pagination,
|
||||
PaginationItem,
|
||||
Box,
|
||||
InputAdornment,
|
||||
} from "@mui/material";
|
||||
|
||||
import { DataGrid } from "@mui/x-data-grid";
|
||||
@@ -45,9 +47,12 @@ import {
|
||||
Clear as ClearIcon,
|
||||
Add as AddIcon,
|
||||
SelectAll,
|
||||
Search as SearchIcon,
|
||||
} from "@mui/icons-material";
|
||||
|
||||
import Dropzone from "../components/Dropzone.jsx";
|
||||
import SubOrgDistributionDialog from "./SubOrgDistributionDialog.jsx";
|
||||
import DeleteConfirmDialog from "./DeleteConfirmDialog.jsx";
|
||||
import ShuffleCodeEditor from "../components/ShuffleCodeEditor1.jsx";
|
||||
import {getTheme} from "../theme.jsx";
|
||||
import { Context } from "../context/ContextApi.jsx";
|
||||
@@ -82,6 +87,10 @@ const Files = memo((props) => {
|
||||
const [showDistributionPopup, setShowDistributionPopup] = useState(false)
|
||||
const [selectedSubOrg, setSelectedSubOrg] = 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 [page, setPage] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(50)
|
||||
@@ -326,7 +335,7 @@ const [filesLoaded, setFilesLoaded] = useState(false);
|
||||
navigator.clipboard.writeText(file.id);
|
||||
document.execCommand("copy");
|
||||
|
||||
toast(file.id + " copied to clipboard");
|
||||
toast.info(file.id + " copied to clipboard");
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
@@ -369,7 +378,8 @@ const [filesLoaded, setFilesLoaded] = useState(false);
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
deleteFile(file.id, true);
|
||||
setDeleteConfirmTarget({ id: file.id, filename: file.filename });
|
||||
setDeleteConfirmOpen(true);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
@@ -441,6 +451,12 @@ const [filesLoaded, setFilesLoaded] = useState(false);
|
||||
setSelectedSubOrg([])
|
||||
}
|
||||
setFileIdSelectedForDistribution(file.id)
|
||||
setFileNameSelectedForDistribution(file.filename)
|
||||
setDistribOrgOrder(
|
||||
(userdata?.orgs || [])
|
||||
.filter(o => o.creator_org === userdata.active_org.id)
|
||||
.map(o => o.id)
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
@@ -534,7 +550,7 @@ const [filesLoaded, setFilesLoaded] = useState(false);
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson.success === true) {
|
||||
toast("Successfully updated file");
|
||||
toast.success("Successfully updated file");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -848,125 +864,39 @@ const [filesLoaded, setFilesLoaded] = useState(false);
|
||||
</Dialog>
|
||||
: null
|
||||
|
||||
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 fileDistributionModal = 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 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 deleteConfirmDialog = (
|
||||
<DeleteConfirmDialog
|
||||
open={deleteConfirmOpen}
|
||||
onClose={() => { setDeleteConfirmOpen(false); setDeleteConfirmTarget(null); }}
|
||||
onConfirm={() => {
|
||||
if (deleteConfirmTarget?.bulk) {
|
||||
const toDelete = [...selectedRows];
|
||||
const count = toDelete.length;
|
||||
setSelectedRows([]);
|
||||
toDelete.forEach(fileId => deleteFile(fileId, false));
|
||||
setTimeout(() => {
|
||||
getFiles(selectedCategory);
|
||||
toast.success('Deleted ' + count + ' file' + (count > 1 ? 's' : ''));
|
||||
}, 3000);
|
||||
} else {
|
||||
deleteFile(deleteConfirmTarget.id, true);
|
||||
}
|
||||
setDeleteConfirmOpen(false);
|
||||
setDeleteConfirmTarget(null);
|
||||
}}
|
||||
title={
|
||||
deleteConfirmTarget?.bulk
|
||||
? '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>?</>
|
||||
: <>Are you sure you want to delete <b>{deleteConfirmTarget?.filename}</b>?</>
|
||||
}
|
||||
warningText="This cannot be undone."
|
||||
/>
|
||||
);
|
||||
|
||||
const deleteFile = (fileId, showSinglDeleteToast) => {
|
||||
|
||||
@@ -1295,7 +1225,17 @@ const [filesLoaded, setFilesLoaded] = useState(false);
|
||||
}}
|
||||
onDrop={uploadFile}
|
||||
>
|
||||
{fileDistributionModal}
|
||||
<SubOrgDistributionDialog
|
||||
open={showDistributionPopup}
|
||||
onClose={() => { setShowDistributionPopup(false); }}
|
||||
title="Distribute File to Sub-Organizations"
|
||||
extraInfo={fileNameSelectedForDistribution ? `Selected File: ${fileNameSelectedForDistribution}` : null}
|
||||
orgs={distribOrgOrder.map(id => (userdata?.orgs || []).find(o => o.id === id)).filter(Boolean)}
|
||||
selectedOrgIds={selectedSubOrg}
|
||||
onSelectionChange={setSelectedSubOrg}
|
||||
onSave={(ids) => { changeDistribution(fileIdSelectedForDistribution, ids); setShowDistributionPopup(false); }}
|
||||
/>
|
||||
{deleteConfirmDialog}
|
||||
<div style={{width: "100%", minHeight: 1100, boxSizing: 'border-box', padding: "27px 10px 19px 27px", height:"100%", backgroundColor: theme.palette.platformColor,borderTopRightRadius: '8px', borderBottomRightRadius: 8, borderLeft: theme.palette.defaultBorder, }}>
|
||||
|
||||
<div style={{height: "100%", }}>
|
||||
@@ -1559,17 +1499,20 @@ const [filesLoaded, setFilesLoaded] = useState(false);
|
||||
autoFocus
|
||||
/>}
|
||||
|
||||
<ShuffleCodeEditor
|
||||
isCloud={isCloud}
|
||||
expansionModalOpen={openEditor}
|
||||
setExpansionModalOpen={setOpenEditor}
|
||||
setcodedata = {setFileContent}
|
||||
codedata={fileContent}
|
||||
isFileEditor = {true}
|
||||
key = {fileContent} //https://reactjs.org/docs/reconciliation.html#recursing-on-children
|
||||
runUpdateText = {runUpdateText}
|
||||
contentLoading = {contentLoading}
|
||||
/>
|
||||
{openEditor === true && fileContent !== undefined && fileContent !== null && fileContent.length > 0 ?
|
||||
<ShuffleCodeEditor
|
||||
isCloud={isCloud}
|
||||
expansionModalOpen={openEditor}
|
||||
setExpansionModalOpen={setOpenEditor}
|
||||
setcodedata = {setFileContent}
|
||||
codedata={fileContent}
|
||||
isFileEditor = {true}
|
||||
key = {fileContent} //https://reactjs.org/docs/reconciliation.html#recursing-on-children
|
||||
runUpdateText = {runUpdateText}
|
||||
contentLoading = {contentLoading}
|
||||
/>
|
||||
: null}
|
||||
|
||||
{isSelectedFiles?null:
|
||||
<Divider
|
||||
style={{
|
||||
@@ -1672,22 +1615,9 @@ const [filesLoaded, setFilesLoaded] = useState(false);
|
||||
toast("Please select files to delete");
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}}
|
||||
setDeleteConfirmTarget({ bulk: true });
|
||||
setDeleteConfirmOpen(true);
|
||||
}}
|
||||
variant={"outlined"}
|
||||
color="secondary"
|
||||
startIcon={
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -52,6 +52,12 @@ const ShuffleLogo = "/images/Shuffle_logo.png";
|
||||
const detectionIcon = "/icons/detection.svg";
|
||||
const documentationIcon = "/icons/documentation.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 }) => {
|
||||
@@ -87,6 +93,17 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, SHUFFLE_V
|
||||
);
|
||||
const [activeOrgData, setActiveOrgData] = useState(null);
|
||||
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(() => {
|
||||
return orgOptions.find((option) => option.name === selectedOrg);
|
||||
}, [selectedOrg, orgOptions]);
|
||||
@@ -839,6 +856,8 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, SHUFFLE_V
|
||||
regiontag = "EU-2";
|
||||
} else if (regiontag === "ca"){
|
||||
regiontag = "CA";
|
||||
} else if (regiontag === "uk"){
|
||||
regiontag = "UK";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1022,7 +1041,7 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, SHUFFLE_V
|
||||
}}
|
||||
>
|
||||
<Tooltip
|
||||
title="Go to Home"
|
||||
title={showPartnerLogo ? "Go to Home" : "Switch Product"}
|
||||
placement="top"
|
||||
arrow
|
||||
componentsProps={{
|
||||
@@ -1042,16 +1061,194 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, SHUFFLE_V
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Link to={isCloud && !showPartnerLogo ? "/" : "/workflows"}>
|
||||
<img
|
||||
src={
|
||||
showPartnerLogo ? userdata?.active_org?.image : ShuffleLogo
|
||||
}
|
||||
alt="Shuffle Logo"
|
||||
style={{ width: showPartnerLogo ? 30 : 24, height: showPartnerLogo ? 30 : 24 }}
|
||||
/>
|
||||
</Link>
|
||||
</Tooltip>
|
||||
<Box
|
||||
onClick={showPartnerLogo ? undefined : handleProductClick}
|
||||
sx={{
|
||||
cursor: showPartnerLogo ? "default" : "pointer",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
gap: 0.5
|
||||
}}
|
||||
>
|
||||
<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 && (
|
||||
<Typography variant="body2" style={{fontSize: 16, color: themeMode === "dark" ? lightText : darkText, transition: "opacity 0.3s ease", fontWeight: 600, marginTop: -5, marginLeft: 3}}>
|
||||
@@ -1747,7 +1944,8 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, SHUFFLE_V
|
||||
<span style={{ display: "inline-block", width: "100%" }}>
|
||||
<Button
|
||||
component={Link}
|
||||
to="/partners"
|
||||
to={isCloud ? "/partners" : "https://shuffler.io/partners"}
|
||||
target={isCloud ? "_self" : "_blank"}
|
||||
onClick={(event) => {
|
||||
setCurrentOpenTab("partners");
|
||||
localStorage.setItem("lastTabOpenByUser", "partners");
|
||||
|
||||
@@ -330,7 +330,7 @@ const LicencePopup = (props) => {
|
||||
body: JSON.stringify({
|
||||
org_id: selectedOrganization.id,
|
||||
editing: "subscription_update",
|
||||
subscription_index: 0,
|
||||
subscription_index: subscription.id,
|
||||
subscription: subscription,
|
||||
}),
|
||||
mode: "cors",
|
||||
@@ -554,7 +554,7 @@ const LicencePopup = (props) => {
|
||||
const payload = {
|
||||
org_id: selectedOrganization.id,
|
||||
editing: "subscription_update",
|
||||
subscription_index: 0,
|
||||
subscription_index: subscription.id,
|
||||
subscription: {
|
||||
...form,
|
||||
// Ensure backend gets array of features
|
||||
@@ -696,6 +696,15 @@ const LicencePopup = (props) => {
|
||||
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
|
||||
label="Start date"
|
||||
type="date"
|
||||
@@ -1042,6 +1051,17 @@ const LicencePopup = (props) => {
|
||||
: localSub?.currency + localSub?.amount
|
||||
: "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) {
|
||||
return null;
|
||||
}
|
||||
@@ -1252,7 +1272,7 @@ const LicencePopup = (props) => {
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{isPaidPlan ? (
|
||||
{(isPaidPlan || localSub?.amount === "0") ? (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
@@ -1336,22 +1356,38 @@ const LicencePopup = (props) => {
|
||||
) : null}
|
||||
|
||||
{localSub.cancellationdate !== 0 ? (
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="textSecondary"
|
||||
style={{ marginTop: 2 }}
|
||||
>
|
||||
{`Cancelled on ${new Date(
|
||||
(localSub.cancellationdate || localSub.CancellationDate) *
|
||||
1000
|
||||
).toLocaleDateString(undefined, {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
})}`}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="textSecondary"
|
||||
style={{ marginTop: 2 }}
|
||||
>
|
||||
{`Cancelled on ${new Date(
|
||||
(localSub.cancellationdate || localSub.CancellationDate) *
|
||||
1000
|
||||
).toLocaleDateString(undefined, {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
})}`}
|
||||
</Typography>
|
||||
) : 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
|
||||
style={{
|
||||
marginTop: 12,
|
||||
@@ -1364,39 +1400,50 @@ const LicencePopup = (props) => {
|
||||
(isCloud || (!isCloud && selectedOrganization.cloud_sync)) && (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
||||
<Typography variant="body2" color="textSecondary" style={{}}>
|
||||
App Runs
|
||||
{localSub?.active ? "App Runs" : "App Runs included in plan"}
|
||||
</Typography>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
flexDirection: "column",
|
||||
gap: 10,
|
||||
}}
|
||||
>
|
||||
{localSub?.active ? (
|
||||
// Active plan - show current usage and progress bar
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
flexDirection: "column",
|
||||
gap: 10,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body1"
|
||||
style={{ minWidth: 140, fontWeight: 600 }}
|
||||
>
|
||||
{usedAppRuns?.toLocaleString?.() || usedAppRuns} of{" "}
|
||||
{appRunsLimit?.toLocaleString?.() || appRunsLimit}
|
||||
</Typography>
|
||||
<Box sx={{ width: "100%" }}>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={appRunsPct}
|
||||
sx={{
|
||||
height: 6,
|
||||
borderRadius: 6,
|
||||
backgroundColor: "#3a3a3a",
|
||||
"& .MuiLinearProgress-bar": {
|
||||
backgroundColor: "#ff8544",
|
||||
borderRadius: 6,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</div>
|
||||
) : (
|
||||
// Inactive plan - show only the plan's app runs capacity
|
||||
<Typography
|
||||
variant="body1"
|
||||
style={{ minWidth: 140, fontWeight: 600 }}
|
||||
style={{ minWidth: 140, fontWeight: 600, fontSize: 18 }}
|
||||
>
|
||||
{usedAppRuns?.toLocaleString?.() || usedAppRuns} of{" "}
|
||||
{appRunsLimit?.toLocaleString?.() || appRunsLimit}
|
||||
{calculateAppRunsFromPrice(localSub.amount)?.toLocaleString?.() || calculateAppRunsFromPrice(localSub.amount)}
|
||||
</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>
|
||||
)
|
||||
}
|
||||
@@ -1480,7 +1527,6 @@ const LicencePopup = (props) => {
|
||||
}}
|
||||
>
|
||||
{isCloud &&
|
||||
localSub.name.toLowerCase().includes("scale") &&
|
||||
localSub?.reference &&
|
||||
localSub.reference.length > 0 ? (
|
||||
<Button
|
||||
@@ -1490,8 +1536,11 @@ const LicencePopup = (props) => {
|
||||
onClick={() => {
|
||||
const url = `${globalUrl}/api/v1/orgs/${selectedOrganization.id}/manage_subscription`;
|
||||
fetch(url, {
|
||||
method: "GET",
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify({
|
||||
subscription_id: localSub.reference
|
||||
}),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})
|
||||
.then((r) => r.json())
|
||||
@@ -1575,8 +1624,8 @@ const LicencePopup = (props) => {
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
<Grid item maxWidth={licensePopup ? 400 : 450}>
|
||||
{isLoading ? (
|
||||
<Grid item maxWidth={licensePopup ? 400 : 800} style={{ display: "flex", flexDirection: "row", gap: 16, flexWrap: "wrap" }}>
|
||||
{isLoading ? (
|
||||
<SubscriptionSkeleton />
|
||||
) : (
|
||||
<>
|
||||
@@ -1584,8 +1633,14 @@ const LicencePopup = (props) => {
|
||||
selectedOrganization.subscriptions !== null &&
|
||||
selectedOrganization.subscriptions.length > 0
|
||||
? (selectedOrganization.subscriptions || [])
|
||||
.slice()
|
||||
.map((sub, index) => {
|
||||
.slice()
|
||||
.sort((a, b) => {
|
||||
// Active subscriptions first, then inactive
|
||||
if (a.active && !b.active) return -1;
|
||||
if (!a.active && b.active) return 1;
|
||||
return 0;
|
||||
})
|
||||
.map((sub, index) => {
|
||||
return (
|
||||
<SubscriptionObject
|
||||
key={sub.id || index}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Context } from "../context/ContextApi.jsx";
|
||||
import { toast } from 'react-toastify';
|
||||
import { useParams, useNavigate, Link } from "react-router-dom";
|
||||
import { getTheme } from '../theme.jsx';
|
||||
import ReactGA from 'react-ga4';
|
||||
//import { useAlert
|
||||
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
@@ -513,57 +514,71 @@ const AuthenticationOauth2 = (props) => {
|
||||
|
||||
// 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`
|
||||
|
||||
|
||||
console.log("URL: ", url)
|
||||
try {
|
||||
var newwin = window.open(url, "", "width=582,height=700");
|
||||
//console.log(newwin)
|
||||
var newwin = window.open(url, "", "width=582,height=700");
|
||||
|
||||
var open = true;
|
||||
const timer = setInterval(() => {
|
||||
if (newwin.closed) {
|
||||
console.log("Closing?")
|
||||
|
||||
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)
|
||||
// This happens if automatic clicks occurr without the user, which is
|
||||
// disallowed in most cases
|
||||
if (newwin === null || newwin === undefined) {
|
||||
setButtonClicked(false);
|
||||
if (setFinalized !== undefined) {
|
||||
setFinalized(true)
|
||||
}
|
||||
}
|
||||
|
||||
toast.info("Authentication window closed")
|
||||
} else {
|
||||
var open = true;
|
||||
const timer = setInterval(() => {
|
||||
if (newwin.closed) {
|
||||
console.log("Closing?")
|
||||
|
||||
// This is more a guess than anything
|
||||
// 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")
|
||||
}
|
||||
}, 1000);
|
||||
//do {
|
||||
// setTimeout(() => {
|
||||
// console.log(newwin)
|
||||
// console.log("CLOSED", newwin.closed)
|
||||
// if (newwin.closed) {
|
||||
setButtonClicked(false);
|
||||
clearInterval(timer);
|
||||
//alert('"Secure Payment" window closed!');
|
||||
|
||||
// open = false
|
||||
// }
|
||||
// }, 1000)
|
||||
//}
|
||||
//while(open === true)
|
||||
} catch (e) {
|
||||
toast("Failed authentication - probably bad credentials. Try again")
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -714,6 +729,13 @@ const AuthenticationOauth2 = (props) => {
|
||||
onClick={() => {
|
||||
// Hardcode some stuff?
|
||||
// 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()
|
||||
}}
|
||||
color="primary"
|
||||
@@ -1079,6 +1101,14 @@ const AuthenticationOauth2 = (props) => {
|
||||
"autoClose": 1500,
|
||||
})
|
||||
|
||||
if (isCloud) {
|
||||
ReactGA.event({
|
||||
category: "Integration",
|
||||
action: "Authenticate",
|
||||
label: `${selectedApp?.name} - Oauth2 manual`,
|
||||
})
|
||||
}
|
||||
|
||||
handleOauth2Request(clientId, clientSecret, oauthUrl, selectedScopes, undefined, true);
|
||||
}}
|
||||
color="primary"
|
||||
|
||||
@@ -710,7 +710,8 @@ const OrgHeaderexpandedNew = (props) => {
|
||||
/>
|
||||
</span>
|
||||
</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
|
||||
globalUrl={globalUrl}
|
||||
userdata={userdata}
|
||||
@@ -1100,6 +1101,9 @@ const RegionChangeModal = memo(({ selectedOrganization, setSelectedRegion, userd
|
||||
} else if (regiontag === "au") {
|
||||
regiontag = "AUS";
|
||||
regionCode = "au"
|
||||
} else if (regiontag === "uk") {
|
||||
regiontag = "UK";
|
||||
regionCode = "gb"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -846,6 +846,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
||||
setIsLoading(true);
|
||||
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 :)
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
// Load usecase data from API
|
||||
|
||||
@@ -1087,6 +1087,9 @@ const RuntimeDebugger = (props) => {
|
||||
options={[{
|
||||
"name": "Agent Runs",
|
||||
"id": "AGENT",
|
||||
},{
|
||||
"name": "Sensor Actions",
|
||||
"id": "SENSOR_ACTION",
|
||||
}].concat(workflows)}
|
||||
fullWidth
|
||||
style={{
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import React, { useState } from "react";
|
||||
import theme from "../theme.jsx";
|
||||
import { TextField, Typography, Button } from "@mui/material";
|
||||
|
||||
const SearchContactForm = ({ globalUrl, isMobile, tabName }) => {
|
||||
const [formMail, setFormMail] = useState("");
|
||||
const [message, setMessage] = useState("");
|
||||
const [formMessage, setFormMessage] = useState("");
|
||||
|
||||
const submitContact = (email, message) => {
|
||||
const data = {
|
||||
firstname: "",
|
||||
lastname: "",
|
||||
title: "",
|
||||
companyname: "",
|
||||
email: email,
|
||||
phone: "",
|
||||
message: message,
|
||||
};
|
||||
|
||||
const errorMessage =
|
||||
"Something went wrong. Please contact frikky@shuffler.io directly.";
|
||||
|
||||
fetch(globalUrl + "/api/v1/contact", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((response) => {
|
||||
setFormMessage(
|
||||
response?.success === true ? response.reason : errorMessage
|
||||
);
|
||||
setFormMail("");
|
||||
setMessage("");
|
||||
})
|
||||
.catch(() => {
|
||||
setFormMessage(errorMessage);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
paddingTop: 0,
|
||||
maxWidth: isMobile ? "100%" : "60%",
|
||||
margin: "auto",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" style={{ color: "white", marginTop: 50 }}>
|
||||
Can't find what you're looking for?
|
||||
</Typography>
|
||||
<div
|
||||
style={{
|
||||
flex: "1",
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
required
|
||||
style={{
|
||||
flex: "1",
|
||||
marginRight: 15,
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
}}
|
||||
InputProps={{ style: { color: "#ffffff" } }}
|
||||
color="primary"
|
||||
fullWidth={true}
|
||||
placeholder="Email (optional)"
|
||||
type="email"
|
||||
id="email-handler"
|
||||
autoComplete="email"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
value={formMail}
|
||||
onChange={(e) => setFormMail(e.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
required
|
||||
style={{ flex: "1", backgroundColor: theme.palette.inputColor }}
|
||||
InputProps={{ style: { color: "#ffffff" } }}
|
||||
color="primary"
|
||||
fullWidth={true}
|
||||
placeholder={tabName ? `What ${tabName} do you want to see?` : "What are we missing?"}
|
||||
type=""
|
||||
id="standard-required"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
autoComplete="off"
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
style={{
|
||||
borderRadius: 30,
|
||||
height: 50,
|
||||
width: 220,
|
||||
margin: isMobile ? "15px auto 15px auto" : 20,
|
||||
fontSize: 18,
|
||||
}}
|
||||
disabled={message.length === 0}
|
||||
onClick={() => submitContact(formMail, message)}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
<Typography style={{ color: "white" }} variant="body2">
|
||||
{formMessage}
|
||||
</Typography>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchContactForm;
|
||||
@@ -47,7 +47,7 @@ const chipStyle = {
|
||||
backgroundColor: "#3d3f43", height: 30, marginRight: 5, paddingLeft: 5, paddingRight: 5, height: 28, cursor: "pointer", borderColor: "#3d3f43", color: "white",
|
||||
}
|
||||
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "33e4e3564f4f060e96e0531957bed552")
|
||||
const SearchData = props => {
|
||||
const { serverside, globalUrl, userdata } = props
|
||||
let navigate = useNavigate();
|
||||
|
||||
@@ -38,7 +38,6 @@ import aa from 'search-insights'
|
||||
import { InstantSearch, Configure, 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/
|
||||
const chipStyle = {
|
||||
backgroundColor: "#3d3f43", height: 30, marginRight: 5, paddingLeft: 5, paddingRight: 5, height: 28, cursor: "pointer", borderColor: "#3d3f43", color: "white",
|
||||
|
||||
@@ -61,8 +61,6 @@ import PaperComponent from "../components/PaperComponent.jsx";
|
||||
import { padding, textAlign } from '@mui/system';
|
||||
import data from '../frameworkStyle.jsx';
|
||||
import { useNavigate, Link, useParams, useSearchParams } from "react-router-dom";
|
||||
import { tags as t } from '@lezer/highlight';
|
||||
|
||||
|
||||
import AceEditor from "react-ace";
|
||||
import ace from "ace-builds";
|
||||
@@ -149,7 +147,10 @@ const CodeEditor = (props) => {
|
||||
|
||||
// Auto-indent JSON-like content (with safety hehe)
|
||||
const autoIndentContent = React.useCallback((content) => {
|
||||
return content
|
||||
if (!isFileEditor) {
|
||||
console.log("Autoindent disabled")
|
||||
return content
|
||||
}
|
||||
|
||||
// Safety checks :)
|
||||
if (!content || typeof content !== 'string' || content.trim().length === 0) {
|
||||
@@ -238,6 +239,24 @@ const CodeEditor = (props) => {
|
||||
expectedOutput(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
|
||||
useEffect(() => {
|
||||
if (codedata && codedata !== localcodedata && typeof codedata === 'string') {
|
||||
@@ -1597,7 +1616,7 @@ const CodeEditor = (props) => {
|
||||
if (e.srcElement.className === "ace_content") {
|
||||
console.log("DRAG STOP IN CONTENT!", e.srcElement.className)
|
||||
|
||||
let usedposition = e.offsetY
|
||||
const usedposition = e.offsetY
|
||||
if (usedposition === undefined || usedposition === null) {
|
||||
toast.info(`Error: LayerY is undefined or null. Please contact ${supportEmail}`)
|
||||
return
|
||||
@@ -1784,8 +1803,8 @@ const CodeEditor = (props) => {
|
||||
// zIndex: 12501,
|
||||
pointerEvents: "auto",
|
||||
color: theme.palette.DialogStyle.color,
|
||||
minWidth: isMobile || isWorkflowEditor || fullScreenModeEnabled ? "100%" : isFileEditor ? "650px" : "80%",
|
||||
maxWidth: isMobile || isWorkflowEditor || fullScreenModeEnabled ? "100%" : isFileEditor ? "650px" : "1100px",
|
||||
minWidth: isMobile || isWorkflowEditor || fullScreenModeEnabled ? "100%" : isFileEditor ? 800 : "80%",
|
||||
maxWidth: isMobile || isWorkflowEditor || fullScreenModeEnabled ? "100%" : isFileEditor ? 800 : "1100px",
|
||||
minHeight: isMobile || isWorkflowEditor || fullScreenModeEnabled ? "100%" : "auto",
|
||||
maxHeight: isMobile || isWorkflowEditor || fullScreenModeEnabled ? "100%" : "700px",
|
||||
border: "3px solid rgba(255,255,255,0.3)",
|
||||
@@ -1981,7 +2000,8 @@ const CodeEditor = (props) => {
|
||||
paddingLeft: 10,
|
||||
}}
|
||||
>
|
||||
File Editor ({localcodedata.length})
|
||||
{/* cba positioning */}
|
||||
File Editor ({localcodedata.length}) {validation === true ? <span style={{ color: "lightgreen" }}>Valid JSON</span> : <span style={{ color: "red" }}>Invalid JSON</span>}
|
||||
</DialogTitle>
|
||||
</div>
|
||||
|
||||
@@ -2511,7 +2531,7 @@ const CodeEditor = (props) => {
|
||||
}
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
{(actionId || triggerId || conditionId) && !isWorkflowEditor && !isFileEditor ?
|
||||
{/*(actionId || triggerId || conditionId) && !isWorkflowEditor && !isFileEditor ?
|
||||
<>
|
||||
<IconButton
|
||||
style={{
|
||||
@@ -2557,7 +2577,7 @@ const CodeEditor = (props) => {
|
||||
</IconButton>
|
||||
</>
|
||||
: null
|
||||
}
|
||||
*/}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -2583,7 +2603,7 @@ const CodeEditor = (props) => {
|
||||
mode={isWorkflowEditor ? "yaml" : selectedAction === undefined ? "json" : selectedAction.name === "execute_python" ? "python" : selectedAction.name === "execute_bash" ? "bash" : "json"}
|
||||
theme="gruvbox"
|
||||
height={fullScreenModeEnabled ? "84vh" : isFileEditor ? 450 : isWorkflowEditor ? "90vh" : 550}
|
||||
width={isFileEditor ? 650 : fullScreenModeEnabled ? "50vw" : isWorkflowEditor ? "90vw" : "100%"}
|
||||
width={isFileEditor ? 800 : fullScreenModeEnabled ? isFileEditor ? "100%" : "50vw" : isWorkflowEditor ? "90vw" : "100%"}
|
||||
|
||||
markers={markers}
|
||||
highlightActiveLine={false}
|
||||
@@ -2717,7 +2737,7 @@ const CodeEditor = (props) => {
|
||||
</div>
|
||||
:
|
||||
<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" :
|
||||
triggerId ?
|
||||
`Output: ${triggerName?.replaceAll("_", " ").slice(0, 1).toUpperCase() + triggerName?.replaceAll("_", " ").slice(1)} (${triggerField})` :
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
import React, { useState, useContext } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
Box,
|
||||
TextField,
|
||||
Button,
|
||||
Typography,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Checkbox,
|
||||
InputAdornment,
|
||||
} from "@mui/material";
|
||||
import { Search as SearchIcon } from "@mui/icons-material";
|
||||
import { Context } from "../context/ContextApi.jsx";
|
||||
import { getTheme } from "../theme.jsx";
|
||||
|
||||
/**
|
||||
* A reusable dialog for selecting/distributing sub-organizations.
|
||||
*
|
||||
* Props:
|
||||
* open {boolean} - controls dialog visibility
|
||||
* onClose {function} - called on Cancel or backdrop click (no args)
|
||||
* title {string} - dialog title text
|
||||
* extraInfo {string} - secondary line below the title (e.g. "Selected Key: xxx")
|
||||
* orgs {Array} - ordered array of { id, name, image? } objects to display
|
||||
* selectedOrgIds {string[]} - currently selected org IDs (controlled)
|
||||
* onSelectionChange {function} - called with a updater fn (prev => next) when selection changes
|
||||
* onSave {function} - called with the final selectedOrgIds array when Save is clicked
|
||||
* disabled {boolean} - disables checkboxes and Save button (default: false)
|
||||
*/
|
||||
const SubOrgDistributionDialog = ({
|
||||
open,
|
||||
onClose,
|
||||
title,
|
||||
extraInfo = null,
|
||||
orgs = [],
|
||||
selectedOrgIds = [],
|
||||
onSelectionChange,
|
||||
onSave,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const { themeMode, brandColor } = useContext(Context);
|
||||
const theme = getTheme(themeMode, brandColor);
|
||||
|
||||
const safeOrgs = orgs || [];
|
||||
|
||||
const filteredOrgs = safeOrgs.filter(
|
||||
o => o && o.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
const handleSelectAll = () => {
|
||||
if (searchQuery) {
|
||||
const filteredIds = filteredOrgs.map(o => o.id);
|
||||
onSelectionChange(prev => [...new Set([...prev, ...filteredIds])]);
|
||||
} else {
|
||||
const allIds = safeOrgs.map(o => o.id);
|
||||
onSelectionChange(prev => [...new Set([...prev, ...allIds])]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeselectAll = () => {
|
||||
if (searchQuery) {
|
||||
const filteredIds = filteredOrgs.map(o => o.id);
|
||||
onSelectionChange(prev => prev.filter(id => !filteredIds.includes(id)));
|
||||
} else {
|
||||
onSelectionChange([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggle = (id) => {
|
||||
if (disabled) return;
|
||||
onSelectionChange(prev =>
|
||||
prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id]
|
||||
);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setSearchQuery("");
|
||||
onClose();
|
||||
};
|
||||
|
||||
const filteredSelected = filteredOrgs.filter(o => selectedOrgIds.includes(o.id)).length;
|
||||
const countText = searchQuery
|
||||
? `${filteredSelected} of ${filteredOrgs.length} filtered selected`
|
||||
: `${selectedOrgIds.length} of ${safeOrgs.length} selected`;
|
||||
|
||||
const imageSize = 22;
|
||||
const imageStyle = { width: imageSize, height: imageSize, pointerEvents: "none", marginRight: 10 };
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
PaperProps={{
|
||||
sx: {
|
||||
borderRadius: theme?.palette?.DialogStyle?.borderRadius,
|
||||
border: theme?.palette?.DialogStyle?.border,
|
||||
fontFamily: theme?.typography?.fontFamily,
|
||||
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
||||
zIndex: 1000,
|
||||
height: "80vh",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
"& .MuiDialogContent-root": {
|
||||
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
||||
},
|
||||
"& .MuiDialogTitle-root": {
|
||||
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
||||
},
|
||||
"& .MuiDialogActions-root": {
|
||||
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DialogTitle>
|
||||
<Typography variant="h6" color="textPrimary" style={{ textTransform: "none" }}>
|
||||
{title}
|
||||
</Typography>
|
||||
{extraInfo && (
|
||||
<Typography variant="body2" color="textSecondary" style={{ marginTop: 2 }}>
|
||||
{extraInfo}
|
||||
</Typography>
|
||||
)}
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent sx={{ display: "flex", flexDirection: "column", gap: 2, overflow: "hidden", pt: "8px !important" }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
placeholder="Search sub-organizations..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon style={{ opacity: 0.5 }} />
|
||||
</InputAdornment>
|
||||
),
|
||||
style: { color: theme.palette.textFieldStyle?.color },
|
||||
}}
|
||||
style={{ backgroundColor: theme.palette.textFieldStyle?.backgroundColor, flexShrink: 0 }}
|
||||
/>
|
||||
|
||||
<div style={{ display: "flex", gap: 8, alignItems: "center", flexShrink: 0 }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
style={{ textTransform: "none" }}
|
||||
onClick={handleSelectAll}
|
||||
>
|
||||
Select All{searchQuery ? " Filtered" : ""}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
style={{ textTransform: "none" }}
|
||||
onClick={handleDeselectAll}
|
||||
>
|
||||
Deselect All{searchQuery ? " Filtered" : ""}
|
||||
</Button>
|
||||
<Typography variant="body2" color="textSecondary" style={{ marginLeft: "auto" }}>
|
||||
{countText}
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
flex: 1,
|
||||
overflowY: "auto",
|
||||
border: theme.palette.defaultBorder,
|
||||
borderRadius: 4,
|
||||
scrollbarColor: theme.palette.scrollbarColorTransparent,
|
||||
scrollbarWidth: "thin",
|
||||
}}>
|
||||
<List dense sx={{ py: 0 }}>
|
||||
{filteredOrgs.map((org, index) => {
|
||||
const isSelected = selectedOrgIds.includes(org.id);
|
||||
const hasImage = org.image !== undefined;
|
||||
return (
|
||||
<ListItem
|
||||
key={org.id}
|
||||
onClick={() => handleToggle(org.id)}
|
||||
sx={{
|
||||
cursor: disabled ? "default" : "pointer",
|
||||
backgroundColor: index % 2 === 0
|
||||
? "transparent"
|
||||
: themeMode === "dark" ? "rgba(255,255,255,0.02)" : "rgba(0,0,0,0.02)",
|
||||
"&:hover": {
|
||||
backgroundColor: themeMode === "dark" ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.05)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
edge="start"
|
||||
checked={isSelected}
|
||||
tabIndex={-1}
|
||||
disableRipple
|
||||
color="primary"
|
||||
disabled={disabled}
|
||||
/>
|
||||
{hasImage && (
|
||||
org.image === "" ? (
|
||||
<img alt={org.name} src={theme.palette.defaultImage} style={imageStyle} />
|
||||
) : (
|
||||
<img alt={org.name} src={org.image} style={imageStyle} />
|
||||
)
|
||||
)}
|
||||
<ListItemText
|
||||
primary={org.name}
|
||||
/>
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
{filteredOrgs.length === 0 && (
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary="No sub-organizations found"
|
||||
style={{ textAlign: "center", opacity: 0.5 }}
|
||||
/>
|
||||
</ListItem>
|
||||
)}
|
||||
</List>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
alignItems: "center",
|
||||
p: 2,
|
||||
borderTop: theme.palette.defaultBorder,
|
||||
backgroundColor: theme.palette.platformColor,
|
||||
}}>
|
||||
<div style={{ display: "flex", gap: 8 }}>
|
||||
<Button
|
||||
onClick={handleClose}
|
||||
style={{ textTransform: "none", color: theme.palette.text.primary }}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={disabled}
|
||||
style={{ textTransform: "none" }}
|
||||
onClick={() => {
|
||||
onSave(selectedOrgIds);
|
||||
setSearchQuery("");
|
||||
}}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
</Box>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default SubOrgDistributionDialog;
|
||||
+1129
-1137
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,10 @@
|
||||
import React, { useState, useEffect, useContext, memo } from "react";
|
||||
import { toast } from 'react-toastify';
|
||||
import { Context } from "../context/ContextApi.jsx";
|
||||
import { Link } from "react-router-dom";
|
||||
import {
|
||||
FormControl,
|
||||
InputLabel,
|
||||
OutlinedInput,
|
||||
Checkbox,
|
||||
Tooltip,
|
||||
Typography,
|
||||
Select,
|
||||
@@ -31,23 +30,12 @@ import {
|
||||
import {
|
||||
Cached as CachedIcon,
|
||||
Edit as EditIcon,
|
||||
Style,
|
||||
} from "@mui/icons-material";
|
||||
import ModeEditOutlineOutlinedIcon from '@mui/icons-material/ModeEditOutlineOutlined';
|
||||
import ContentCopyOutlinedIcon from '@mui/icons-material/ContentCopyOutlined';
|
||||
|
||||
import {getTheme} from "../theme.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,
|
||||
};
|
||||
import SubOrgDistributionDialog from "./SubOrgDistributionDialog.jsx";
|
||||
|
||||
const logsViewModal = false;
|
||||
const userdata = "";
|
||||
@@ -76,10 +64,11 @@ const UserManagmentTab = memo((props) => {
|
||||
const [logsViewModal, setLogsViewModal] = React.useState(false);
|
||||
const [ipSelected, setIpSelected] = 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 theme = getTheme(themeMode, brandColor);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedOrganization?.mfa_required !== MFARequired) {
|
||||
setMFARequired(selectedOrganization?.mfa_required);
|
||||
@@ -247,30 +236,6 @@ const UserManagmentTab = memo((props) => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleOrgEditChange = (event) => {
|
||||
if (userdata.id === selectedUser.id) {
|
||||
toast("Can't remove orgs from yourself");
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.target.value.includes("ALL")) {
|
||||
toast.info("Adding to available all sub-organizations. This may take a minute.")
|
||||
event.target.value = selectedOrganization.child_orgs.map((org) => org.id)
|
||||
} else if (event.target.value.includes("None")) {
|
||||
toast.info("Removing from all sub-organizations. This may take a minute")
|
||||
event.target.value = []
|
||||
}
|
||||
|
||||
setMatchingOrganizations(event.target.value);
|
||||
// Workaround for empty orgs
|
||||
if (event.target.value.length === 0) {
|
||||
event.target.value.push("REMOVE");
|
||||
}
|
||||
|
||||
setUser(selectedUser.id, "suborgs", event.target.value);
|
||||
//setUser(selectedUser.id, "suborgs", matchingOrganizations)
|
||||
};
|
||||
|
||||
const userOrgEdit =
|
||||
selectedUser.id !== undefined &&
|
||||
selectedUser?.orgs !== undefined &&
|
||||
@@ -278,44 +243,44 @@ const UserManagmentTab = memo((props) => {
|
||||
selectedOrganization?.child_orgs !== undefined &&
|
||||
selectedOrganization?.child_orgs !== null &&
|
||||
selectedOrganization?.child_orgs?.length > 0 ? (
|
||||
<FormControl fullWidth sx={{ m: 1 }}>
|
||||
<InputLabel id="demo-multiple-checkbox-label" style={{ padding: 5 }}>
|
||||
Accessible Sub-Organizations (
|
||||
{selectedUser?.orgs ? selectedUser?.orgs?.length - 1 : 0})
|
||||
</InputLabel>
|
||||
<Select
|
||||
fullWidth
|
||||
style={{ width: "100%" }}
|
||||
disabled={selectedUser?.id === userdata?.id}
|
||||
labelId="demo-multiple-checkbox-label"
|
||||
id="demo-multiple-checkbox"
|
||||
multiple
|
||||
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>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
fullWidth
|
||||
disabled={selectedUser?.id === userdata?.id}
|
||||
onClick={() => {
|
||||
setPendingSubOrgs([...matchingOrganizations]);
|
||||
setSubOrgModalOpen(true);
|
||||
}}
|
||||
sx={{ m: 1, textTransform: 'none', justifyContent: 'space-between', py: 1.5, fontSize: 14 }}
|
||||
>
|
||||
Manage Sub-Organizations ({matchingOrganizations.length} assigned of {selectedOrganization?.child_orgs?.length || 0})
|
||||
</Button>
|
||||
) : 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 = () => {
|
||||
fetch(globalUrl + "/api/v1/getusers", {
|
||||
method: "GET",
|
||||
@@ -1117,6 +1082,8 @@ const UserManagmentTab = memo((props) => {
|
||||
});
|
||||
};
|
||||
|
||||
var previousreferrer = ""
|
||||
var nextreferrer = ""
|
||||
const logview = logsViewModal ? (
|
||||
<Dialog
|
||||
open={logsViewModal}
|
||||
@@ -1163,26 +1130,23 @@ const UserManagmentTab = memo((props) => {
|
||||
onChange={(event) => {
|
||||
setIpSelected(event.target.value);
|
||||
getLogs(event.target.value, userLogViewing.id);
|
||||
|
||||
|
||||
}}
|
||||
>
|
||||
{(() => {
|
||||
const uniqueIPs = new Set();
|
||||
console.log("Login info: ", userLogViewing.login_info)
|
||||
|
||||
return userLogViewing.login_info.map((data, index) => {
|
||||
if (
|
||||
data.ip.includes("127.0.0.1") ||
|
||||
uniqueIPs.has(data.ip)
|
||||
) {
|
||||
return null;
|
||||
console.log("Data: ", data)
|
||||
if (data.ip.includes("127.0.0.1") || uniqueIPs.has(data.ip)) {
|
||||
return null
|
||||
}
|
||||
|
||||
uniqueIPs.add(data.ip);
|
||||
uniqueIPs.add(data.ip)
|
||||
|
||||
return (
|
||||
<MenuItem key={index} value={data.ip}>
|
||||
{data.ip}
|
||||
{data?.timestamp ? new Date(data.timestamp * 1000).toLocaleString() : "N/A"} - {data?.ip}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
@@ -1229,12 +1193,13 @@ const UserManagmentTab = memo((props) => {
|
||||
minWidth: 700,
|
||||
maxWidth: 700,
|
||||
overflow: "hidden",
|
||||
marginLeft: 10,
|
||||
marginLeft: 50,
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
{logs.map((data, index) => {
|
||||
//console.log("LOG: ", data)
|
||||
previousreferrer = nextreferrer
|
||||
nextreferrer = data.referer
|
||||
|
||||
return (
|
||||
// redirect user to logs
|
||||
@@ -1243,6 +1208,8 @@ const UserManagmentTab = memo((props) => {
|
||||
key={index}
|
||||
style={{
|
||||
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
|
||||
@@ -1263,22 +1230,25 @@ const UserManagmentTab = memo((props) => {
|
||||
}}
|
||||
/>
|
||||
<ListItemText
|
||||
primary={data.referer}
|
||||
primary={data.referer.replace("https://shuffler.io", "")}
|
||||
style={{
|
||||
minWidth: 300,
|
||||
maxWidth: 300,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
/>
|
||||
<ListItemText
|
||||
primary={data.url}
|
||||
style={{
|
||||
minWidth: 700,
|
||||
maxWidth: 700,
|
||||
overflow: "hidden",
|
||||
marginLeft: 10,
|
||||
}}
|
||||
/>
|
||||
<Link to={data.url} target="_blank" style={{ textDecoration: "none", color: theme.palette.linkColor }}>
|
||||
<ListItemText
|
||||
primary={data.url.replace("https://shuffler.io", "")}
|
||||
style={{
|
||||
minWidth: 700,
|
||||
maxWidth: 700,
|
||||
overflow: "hidden",
|
||||
marginLeft: 50,
|
||||
backgroundColor: data.url.includes("/api/v1/register") ? "#d52b2b" : "inherit",
|
||||
}}
|
||||
/>
|
||||
</Link>
|
||||
</ListItem>
|
||||
)})}
|
||||
</List>
|
||||
@@ -1290,6 +1260,7 @@ const UserManagmentTab = memo((props) => {
|
||||
<div style={{ width: "100%", minHeight: 1100, boxSizing: 'border-box', padding: "27px 10px 19px 27px", height:"100%", backgroundColor: theme.palette.platformColor, borderTopRightRadius: '8px', borderBottomRightRadius: 8, borderLeft: "1px solid #494949", }}>
|
||||
{modalView}
|
||||
{editUserModal}
|
||||
{subOrgManagementDialog}
|
||||
{logview}
|
||||
<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' }}>
|
||||
|
||||
@@ -3,6 +3,7 @@ import React, { useEffect, useState } from 'react';
|
||||
import {Link} from 'react-router-dom';
|
||||
import theme from '../theme.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';
|
||||
|
||||
@@ -25,66 +26,23 @@ import { useDebouncedCallback } from "../utils/useDebouncedCallback.jsx";
|
||||
import WorkflowPaper from "../components/WorkflowPaper.jsx"
|
||||
import WorkflowPaperNew from "../components/WorkflowPaperNew.jsx"
|
||||
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "eb5fd80aa6ed5ab4730d836cff3ea283")
|
||||
const AppGrid = 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 xs = parsedXs === undefined || parsedXs === null ? isMobile ? 6 : 4 : parsedXs
|
||||
//const [apps, setApps] = 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 [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 borderRadius = 3
|
||||
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) => {
|
||||
console.log("Workflows: ", workflows)
|
||||
//workflows[0].category = ["detect"]
|
||||
@@ -229,10 +187,16 @@ const AppGrid = props => {
|
||||
placeholder="Find Workflows..."
|
||||
id="shuffle_search_field"
|
||||
onChange={(event) => {
|
||||
removeQuery("q")
|
||||
const value = event.currentTarget.value
|
||||
setInputValue(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) => {
|
||||
if(event.key === "Enter") {
|
||||
@@ -333,64 +297,10 @@ const AppGrid = props => {
|
||||
<CustomHits hitsPerPage={5}/>
|
||||
</InstantSearch>
|
||||
{showSuggestion === true ?
|
||||
<div style={{maxWidth: isMobile ? "100%" : "60%", margin: "auto", paddingTop: 0, 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"}}>
|
||||
<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
|
||||
<SearchContactForm globalUrl={globalUrl} isMobile={isMobile} tabName="workflows" />
|
||||
: null
|
||||
}
|
||||
{onlyResults === true ? null :
|
||||
{/* {onlyResults === true ? null :
|
||||
<span style={{position: "absolute", display: "flex", textAlign: "right", float: "right", right: 0, bottom: 120, }}>
|
||||
<Typography variant="body2" color="textSecondary" style={{}}>
|
||||
Search by
|
||||
@@ -399,7 +309,7 @@ const AppGrid = props => {
|
||||
<img src={"/images/logo-algolia-nebula-blue-full.svg"} alt="Algolia logo" style={{height: 17, marginLeft: 5, marginTop: 3,}} />
|
||||
</a>
|
||||
</span>
|
||||
}
|
||||
} */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import algoliasearch from 'algoliasearch';
|
||||
import { InstantSearch, connectSearchBox, connectHits } from 'react-instantsearch-dom';
|
||||
import { Grid, Paper, TextField, ButtonBase, InputAdornment, Typography, Button, Tooltip} from '@mui/material';
|
||||
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "33e4e3564f4f060e96e0531957bed552")
|
||||
const WorkflowSearch = props => {
|
||||
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, newSelectedApp, setNewSelectedApp, defaultSearch, showSearch, ConfiguredHits, selectAble, } = props
|
||||
const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
Checkbox
|
||||
} from "@mui/material";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Link, useSearchParams } from "react-router-dom";
|
||||
import theme from "../theme.jsx";
|
||||
import { toast } from "react-toastify";
|
||||
import { Context } from "../context/ContextApi.jsx";
|
||||
@@ -29,6 +29,12 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
|
||||
// Check if user is admin
|
||||
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
|
||||
const [users, setUsers] = React.useState([]);
|
||||
const [userSSOConnected, setUserSSOConnected] = React.useState(false);
|
||||
@@ -109,7 +115,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
|
||||
// Function to fetch users and check current user's SSO status
|
||||
const checkUserSSOStatus = () => {
|
||||
setCheckingSSOStatus(true);
|
||||
fetch(globalUrl + "/api/v1/getusers", {
|
||||
fetch(effectiveGlobalUrl + "/api/v1/getusers", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -309,7 +315,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
|
||||
};
|
||||
|
||||
const HandleTestSSO = () => {
|
||||
const url = `${globalUrl}/api/v1/orgs/${selectedOrganization?.id}/change`;
|
||||
const url = `${effectiveGlobalUrl}/api/v1/orgs/${selectedOrganization?.id}/change`;
|
||||
const data = {
|
||||
org_id: selectedOrganization?.id,
|
||||
sso: true,
|
||||
@@ -366,7 +372,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
|
||||
};
|
||||
|
||||
const HandleDisconnectSSO = () => {
|
||||
const url = `${globalUrl}/api/v1/disconnect_sso`;
|
||||
const url = `${effectiveGlobalUrl}/api/v1/disconnect_sso`;
|
||||
const data = {
|
||||
org_id: selectedOrganization?.id,
|
||||
};
|
||||
@@ -444,6 +450,11 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
|
||||
: "Connect your account with this org's SSO!"
|
||||
}
|
||||
</Typography>
|
||||
{regionUrlOverride && (
|
||||
<Typography variant="body2" style={{ margin: "5px 0px 5px 0px", fontSize: 14, color: "#f85a3e" }}>
|
||||
Using region override: {regionUrlOverride}
|
||||
</Typography>
|
||||
)}
|
||||
<Tooltip
|
||||
title={
|
||||
checkingSSOStatus
|
||||
|
||||
+85
-95
@@ -77,7 +77,7 @@ const theme = createTheme(adaptV4Theme({
|
||||
defaultImage: "/images/no_image.png",
|
||||
singulOrange: "/images/singul_orange.png",
|
||||
singulGreen: "/images/singul_green.png",
|
||||
singulBlackWhite: "/images/singul_black_white.png",
|
||||
singulBlackWhite: "/icons/workflow-page/shuffle_agent.png", // For AI Agent stuff
|
||||
},
|
||||
typography: {
|
||||
fontFamily: `"inter", "Roboto", "Helvetica", "Arial", sans-serif`,
|
||||
@@ -153,20 +153,10 @@ const theme = createTheme(adaptV4Theme({
|
||||
|
||||
export default theme;
|
||||
|
||||
export const getTheme = (themeMode, brandColor) => {
|
||||
// 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({
|
||||
export const getTheme = (themeMode, brandColor) =>
|
||||
createTheme({
|
||||
palette: {
|
||||
mode: resolvedMode,
|
||||
mode: themeMode,
|
||||
main: brandColor || "#FF8544",
|
||||
primary: {
|
||||
main: brandColor || "#FF8544",
|
||||
@@ -177,35 +167,36 @@ export const getTheme = (themeMode, brandColor) => {
|
||||
contrastText:"#000000",
|
||||
},
|
||||
text: {
|
||||
primary: resolvedMode === "dark" ? "#ffffff" : "#1A1A1A",
|
||||
secondary: resolvedMode === "dark" ? "#9E9E9E" : "#616161",
|
||||
primary: themeMode === "dark" ? "#ffffff" : "#1A1A1A",
|
||||
secondary: themeMode === "dark" ? "#9E9E9E" : "#616161",
|
||||
},
|
||||
type: resolvedMode,
|
||||
inputColor: resolvedMode === "dark" ? "rgba(39,41,45,1)" : "rgba(245, 245, 245, 1)",
|
||||
textColor: resolvedMode === "dark" ? "#F1F1F1" : "#1A1A1A",
|
||||
textPrimary: resolvedMode === "dark" ? "rgba(255, 255, 255, 0.8)" : "rgba(26, 26, 26, 0.8)",
|
||||
surfaceColor: resolvedMode === "dark" ? "#27292d" : "#EFEFEF",
|
||||
platformColor: resolvedMode === "dark" ? "#212121" : "#ffffff",
|
||||
backgroundColor: resolvedMode === "dark" ? "#1a1a1a" : "#f1f1f1",
|
||||
cytoscapeBackgroundColor: resolvedMode === "dark" ? "#161616" : "#f5f5f5",
|
||||
distributionColor: resolvedMode === "dark" ? "#40E0D0" : "#008080",
|
||||
cardBackgroundColor: resolvedMode === "dark" ? "#1e1e1e" : "#eaeaea",
|
||||
cardHoverColor: resolvedMode === "dark" ? "#323232" : "#F0F0F0",
|
||||
hoverColor: resolvedMode === "dark" ? "#323232" : "#D6D6D6",
|
||||
usecaseCardColor: resolvedMode === "dark" ? "#2f2f2f" : "rgba(245, 245, 245, 1)",
|
||||
usecaseCardHoverColor: resolvedMode === "dark" ? "#2F2F2F" : "rgba(245, 245, 245, 1)",
|
||||
usecaseDialogFieldColor: resolvedMode === "dark" ? "#2B2B2B" : "#F5F5F5",
|
||||
accentColor: resolvedMode === "dark" ? "#ff8544" : "#ff8544",
|
||||
green: resolvedMode === "dark" ? "#5cc879" : "#008000",
|
||||
defaultBorder: resolvedMode === "dark" ? '1px solid #494949' : '1px solid #CCCCCC',
|
||||
type: themeMode,
|
||||
inputColor: themeMode === "dark" ? "rgba(39,41,45,1)" : "rgba(245, 245, 245, 1)",
|
||||
textColor: themeMode === "dark" ? "#F1F1F1" : "#1A1A1A",
|
||||
textPrimary: themeMode === "dark" ? "rgba(255, 255, 255, 0.8)" : "rgba(26, 26, 26, 0.8)",
|
||||
surfaceColor: themeMode === "dark" ? "#27292d" : "#EFEFEF",
|
||||
platformColor: themeMode === "dark" ? "#212121" : "#ffffff",
|
||||
backgroundColor: themeMode === "dark" ? "#1a1a1a" : "#f1f1f1",
|
||||
cytoscapeBackgroundColor: themeMode === "dark" ? "#161616" : "#f5f5f5",
|
||||
distributionColor: themeMode === "dark" ? "#40E0D0" : "#008080",
|
||||
cardBackgroundColor: themeMode === "dark" ? "#1e1e1e" : "#eaeaea",
|
||||
cardHoverColor: themeMode === "dark" ? "#323232" : "#F0F0F0",
|
||||
hoverColor: themeMode === "dark" ? "#323232" : "#D6D6D6",
|
||||
usecaseCardColor: themeMode === "dark" ? "#2f2f2f" : "rgba(245, 245, 245, 1)",
|
||||
usecaseCardHoverColor: themeMode === "dark" ? "#2F2F2F" : "rgba(245, 245, 245, 1)",
|
||||
usecaseDialogFieldColor: themeMode === "dark" ? "#2B2B2B" : "#F5F5F5",
|
||||
accentColor: themeMode === "dark" ? "#ff8544" : "#ff8544",
|
||||
green: themeMode === "dark" ? "#5cc879" : "#008000",
|
||||
defaultBorder: themeMode === "dark" ? '1px solid #494949' : '1px solid #CCCCCC',
|
||||
linkColor: brandColor === "#ff8544" ? "#f86a3e" : brandColor,
|
||||
slateGrayColor: resolvedMode === "dark" ? "#494949" : "#CCCCCC",
|
||||
parsedAppPaperColor: resolvedMode === "dark" ? "#2f2f2f" : "#CCCCCC",
|
||||
|
||||
slateGrayColor: themeMode === "dark" ? "#494949" : "#CCCCCC",
|
||||
parsedAppPaperColor: themeMode === "dark" ? "#2f2f2f" : "#CCCCCC",
|
||||
welcomeCardSubtextColor: themeMode === "dark" ? "#C8C8C8" : "#2f2f2f",
|
||||
deleteColor: themeMode === "dark" ? "#FD4C62" : "#d32f2f",
|
||||
borderRadius: 10,
|
||||
loaderColor: resolvedMode === "dark" ? "#1a1a1a" : "#E0E0E0",
|
||||
loaderColor: themeMode === "dark" ? "#1a1a1a" : "#E0E0E0",
|
||||
jsonIconStyle: "round",
|
||||
jsonTheme: resolvedMode === "dark" ? "summerfruit" : {
|
||||
jsonTheme: themeMode === "dark" ? "summerfruit" : {
|
||||
base00: "#ffffff", // background
|
||||
base01: "#f0f0f0", // very light grey
|
||||
base02: "#f5f5f5", // light grey
|
||||
@@ -225,11 +216,11 @@ export const getTheme = (themeMode, brandColor) => {
|
||||
},
|
||||
jsonCollapseStringsAfterLength: 100,
|
||||
drawer: {
|
||||
backgroundColor: resolvedMode === "dark" ? "#262626" : "#f9f9f9"
|
||||
backgroundColor: themeMode === "dark" ? "#262626" : "#f9f9f9"
|
||||
},
|
||||
actionSidebarField: {
|
||||
backgroundColor: resolvedMode === "dark" ? "#2F2F2F" : "#F1F1F1",
|
||||
color: resolvedMode === "dark" ? "#ffffff" : "#000000",
|
||||
backgroundColor: themeMode === "dark" ? "#2F2F2F" : "#F1F1F1",
|
||||
color: themeMode === "dark" ? "#ffffff" : "#000000",
|
||||
borderRadius: 8,
|
||||
height: 40,
|
||||
border: "none",
|
||||
@@ -238,53 +229,53 @@ export const getTheme = (themeMode, brandColor) => {
|
||||
padding: 5,
|
||||
width: "98%",
|
||||
borderRadius: 5,
|
||||
border: resolvedMode === "dark" ? "1px solid rgba(255,255,255,0.7)" : "1px solid rgba(0,0,0,0.3)",
|
||||
backgroundColor: resolvedMode === "dark"
|
||||
border: themeMode === "dark" ? "1px solid rgba(255,255,255,0.7)" : "1px solid rgba(0,0,0,0.3)",
|
||||
backgroundColor: themeMode === "dark"
|
||||
? "#1A1A1A"
|
||||
: "#f1f1f1",
|
||||
color: resolvedMode === "dark"
|
||||
color: themeMode === "dark"
|
||||
? "#F1F1F1"
|
||||
: "#1A1A1A",
|
||||
overflowX: "auto",
|
||||
},
|
||||
textFieldStyle: {
|
||||
backgroundColor: resolvedMode === "dark" ? "#212121" : "#FFFFFF",
|
||||
color: resolvedMode === "dark" ? "#ffffff" : "#000000",
|
||||
backgroundColor: themeMode === "dark" ? "#212121" : "#FFFFFF",
|
||||
color: themeMode === "dark" ? "#ffffff" : "#000000",
|
||||
borderRadius: "5px",
|
||||
height: 40,
|
||||
border: resolvedMode === "dark" ? "1px solid #4D4D4D" : "1px solid #E0E0E0",
|
||||
border: themeMode === "dark" ? "1px solid #4D4D4D" : "1px solid #E0E0E0",
|
||||
},
|
||||
DialogStyle: {
|
||||
backgroundColor: resolvedMode === "dark" ? "#212121" : "#ffffff",
|
||||
backgroundColor: themeMode === "dark" ? "#212121" : "#ffffff",
|
||||
borderRadius: 2,
|
||||
boxShadow: resolvedMode === "dark" ? "0px 0px 10px 0px rgba(0,0,0,0.75)" : "0px 0px 10px 0px rgba(0,0,0,0.2)",
|
||||
border: resolvedMode === "dark" ? "1px solid #494949" : "1px solid #cccccc",
|
||||
boxShadow: themeMode === "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",
|
||||
},
|
||||
innerTextfieldStyle: {
|
||||
height: 40,
|
||||
fontSize: 16,
|
||||
backgroundColor: resolvedMode === "dark" ? "#212121" : "#f5f5f5",
|
||||
backgroundColor: themeMode === "dark" ? "#212121" : "#f5f5f5",
|
||||
},
|
||||
tooltip: {
|
||||
backgroundColor: resolvedMode === "dark" ? "#212121" : "#ffffff",
|
||||
color: resolvedMode === "dark" ? "#ffffff" : "#000000",
|
||||
border: resolvedMode === "dark" ? "1px solid #494949" : "1px solid #cccccc",
|
||||
backgroundColor: themeMode === "dark" ? "#212121" : "#ffffff",
|
||||
color: themeMode === "dark" ? "#ffffff" : "#000000",
|
||||
border: themeMode === "dark" ? "1px solid #494949" : "1px solid #cccccc",
|
||||
},
|
||||
chipStyle: {
|
||||
backgroundColor: resolvedMode === "dark" ? "#333333" : "#F5F5F5",
|
||||
borderColor: resolvedMode === "dark" ? "#444444" : "#E0E0E0",
|
||||
color: resolvedMode === "dark" ? "#FFFFFF" : "#333333",
|
||||
backgroundColor: themeMode === "dark" ? "#333333" : "#F5F5F5",
|
||||
borderColor: themeMode === "dark" ? "#444444" : "#E0E0E0",
|
||||
color: themeMode === "dark" ? "#FFFFFF" : "#333333",
|
||||
},
|
||||
defaultImage: "/images/no_image.png",
|
||||
singulOrange: "/images/singul_orange.png",
|
||||
singulGreen: "/images/singul_green.png",
|
||||
singulBlackWhite: "/icons/workflow-page/shuffle_agent.png",
|
||||
scrollbarColor: resolvedMode === "dark" ? "#494949 #2f2f2f": "#c1c1c1 #f1f1f1",
|
||||
scrollbarColorTransparent: resolvedMode === "dark" ? '#494949 transparent': "#c1c1c1 transparent",
|
||||
scrollbarColor: themeMode === "dark" ? "#494949 #2f2f2f": "#c1c1c1 #f1f1f1",
|
||||
scrollbarColorTransparent: themeMode === "dark" ? '#494949 transparent': "#c1c1c1 transparent",
|
||||
},
|
||||
typography: {
|
||||
fontFamily: `"inter", "Roboto", "Helvetica", "Arial", sans-serif`,
|
||||
color: resolvedMode === "dark" ? "#ffffff" : "#000000",
|
||||
color: themeMode === "dark" ? "#ffffff" : "#000000",
|
||||
useNextVariants: true,
|
||||
fontWeightLight: 300,
|
||||
fontWeightRegular: 400,
|
||||
@@ -292,36 +283,36 @@ export const getTheme = (themeMode, brandColor) => {
|
||||
fontWeightSemiBold: 600,
|
||||
fontWeightBold: 700,
|
||||
allVariants: {
|
||||
color: resolvedMode === "dark" ? "#ffffff" : "#1A1A1A",
|
||||
color: themeMode === "dark" ? "#ffffff" : "#1A1A1A",
|
||||
},
|
||||
h1: {
|
||||
fontSize: 40,
|
||||
color: resolvedMode === "dark" ? "#ffffff" : "#1A1A1A"
|
||||
color: themeMode === "dark" ? "#ffffff" : "#1A1A1A"
|
||||
},
|
||||
h2: {
|
||||
fontSize: 36,
|
||||
color: resolvedMode === "dark" ? "#ffffff" : "#1A1A1A"
|
||||
color: themeMode === "dark" ? "#ffffff" : "#1A1A1A"
|
||||
},
|
||||
h3: {
|
||||
fontSize: 32,
|
||||
color: resolvedMode === "dark" ? "#ffffff" : "#1A1A1A"
|
||||
color: themeMode === "dark" ? "#ffffff" : "#1A1A1A"
|
||||
},
|
||||
h4: {
|
||||
fontSize: 30,
|
||||
fontWeight: 500,
|
||||
color: resolvedMode === "dark" ? "#ffffff" : "#1A1A1A"
|
||||
color: themeMode === "dark" ? "#ffffff" : "#1A1A1A"
|
||||
},
|
||||
h6: {
|
||||
fontSize: 22,
|
||||
color: resolvedMode === "dark" ? "#ffffff" : "#1A1A1A"
|
||||
color: themeMode === "dark" ? "#ffffff" : "#1A1A1A"
|
||||
},
|
||||
body1: {
|
||||
fontSize: 16,
|
||||
color: resolvedMode === "dark" ? "#ffffff" : "#1A1A1A"
|
||||
color: themeMode === "dark" ? "#ffffff" : "#1A1A1A"
|
||||
},
|
||||
body2: {
|
||||
fontSize: 14,
|
||||
color: resolvedMode === "dark" ? "#ffffff" : "#1A1A1A"
|
||||
color: themeMode === "dark" ? "#ffffff" : "#1A1A1A"
|
||||
},
|
||||
},
|
||||
components: {
|
||||
@@ -336,7 +327,7 @@ export const getTheme = (themeMode, brandColor) => {
|
||||
{
|
||||
props: { variant: 'text', color: 'primary' },
|
||||
style: {
|
||||
color: resolvedMode === "dark" ? "#ffffff" : "#1A1A1A",
|
||||
color: themeMode === "dark" ? "#ffffff" : "#1A1A1A",
|
||||
whiteSpace: "nowrap",
|
||||
textWrap: "normal",
|
||||
},
|
||||
@@ -344,7 +335,7 @@ export const getTheme = (themeMode, brandColor) => {
|
||||
{
|
||||
props: { variant: 'text', color: 'secondary' },
|
||||
style: {
|
||||
color: resolvedMode === "dark" ? "#9E9E9E" : "#616161",
|
||||
color: themeMode === "dark" ? "#9E9E9E" : "#616161",
|
||||
whiteSpace: "nowrap",
|
||||
textWrap: "normal",
|
||||
},
|
||||
@@ -352,24 +343,24 @@ export const getTheme = (themeMode, brandColor) => {
|
||||
{
|
||||
props: { variant: 'contained', color: 'primary' },
|
||||
style: {
|
||||
backgroundColor: resolvedMode === "dark" ? brandColor || '#ff8544' : brandColor || '#FF7C35',
|
||||
color: resolvedMode === "dark" ? '#1a1a1a': '#FFFFFF',
|
||||
backgroundColor: themeMode === "dark" ? brandColor || '#ff8544' : brandColor || '#FF7C35',
|
||||
color: themeMode === "dark" ? '#1a1a1a': '#FFFFFF',
|
||||
borderRadius: '4px',
|
||||
whiteSpace: "nowrap",
|
||||
textWrap: "normal",
|
||||
transition: 'background-color 0.2s ease-in-out',
|
||||
'&:hover': {
|
||||
fontWeight: 600,
|
||||
backgroundColor: resolvedMode === 'dark' ? brandColor || "#ff955c" : brandColor || '#FF8D4F',
|
||||
color: resolvedMode === "dark" ? '#1a1a1a': '#FFFFFF',
|
||||
backgroundColor: themeMode === 'dark' ? brandColor || "#ff955c" : brandColor || '#FF8D4F',
|
||||
color: themeMode === "dark" ? '#1a1a1a': '#FFFFFF',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
props: { variant: 'contained', color: 'secondary' },
|
||||
style: {
|
||||
backgroundColor: resolvedMode === "dark" ? '#494949' : '#C9C9C9',
|
||||
color: resolvedMode === "dark" ? '#ffffff' : '#4C4C4C',
|
||||
backgroundColor: themeMode === "dark" ? '#494949' : '#C9C9C9',
|
||||
color: themeMode === "dark" ? '#ffffff' : '#4C4C4C',
|
||||
borderRadius: '4px',
|
||||
boxShadow: 'none',
|
||||
whiteSpace: "nowrap",
|
||||
@@ -377,23 +368,23 @@ export const getTheme = (themeMode, brandColor) => {
|
||||
textWrap: "normal",
|
||||
'&:hover': {
|
||||
fontWeight: 600,
|
||||
border: resolvedMode === "dark" ? '1px solid #f1f1f1' : 'none',
|
||||
backgroundColor: resolvedMode === "dark" ? '#494949' : '#C9C9C9',
|
||||
color: resolvedMode === "dark" ? '#ffffff' : '#4C4C4C',
|
||||
border: themeMode === "dark" ? '1px solid #f1f1f1' : 'none',
|
||||
backgroundColor: themeMode === "dark" ? '#494949' : '#C9C9C9',
|
||||
color: themeMode === "dark" ? '#ffffff' : '#4C4C4C',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
props: { variant: 'outlined', color: 'primary' },
|
||||
style: {
|
||||
borderColor: resolvedMode === "dark" ? brandColor || "#ff8544" : brandColor || "#cc5f1f",
|
||||
color: resolvedMode === "dark" ? brandColor || "#ff8544" : brandColor || "#cc5f1f",
|
||||
borderColor: themeMode === "dark" ? brandColor || "#ff8544" : brandColor || "#cc5f1f",
|
||||
color: themeMode === "dark" ? brandColor || "#ff8544" : brandColor || "#cc5f1f",
|
||||
whiteSpace: "nowrap",
|
||||
fontWeight: 'normal',
|
||||
textWrap: "normal",
|
||||
'&:hover': {
|
||||
backgroundColor: resolvedMode === "dark" ? brandColor || "#ff8544" : "#ffe8dc",
|
||||
color: resolvedMode === "dark" ? "#1a1a1a" : "#8a3d00",
|
||||
backgroundColor: themeMode === "dark" ? brandColor || "#ff8544" : "#ffe8dc",
|
||||
color: themeMode === "dark" ? "#1a1a1a" : "#8a3d00",
|
||||
fontWeight: 600,
|
||||
},
|
||||
},
|
||||
@@ -402,14 +393,14 @@ export const getTheme = (themeMode, brandColor) => {
|
||||
props: { variant: 'outlined', color: 'secondary' },
|
||||
style: {
|
||||
border: '1px solid #C5C5C5',
|
||||
color: resolvedMode === "dark" ? '#C5C5C5' : '#2D2D2D',
|
||||
color: themeMode === "dark" ? '#C5C5C5' : '#2D2D2D',
|
||||
whiteSpace: "nowrap",
|
||||
textWrap: "normal",
|
||||
'&:hover': {
|
||||
backgroundColor: resolvedMode === "dark" ? '#C5C5C5' : '#EFEFEF',
|
||||
borderColor: resolvedMode === "dark" ? '#C5C5C5' : '#2D2D2D',
|
||||
backgroundColor: themeMode === "dark" ? '#C5C5C5' : '#EFEFEF',
|
||||
borderColor: themeMode === "dark" ? '#C5C5C5' : '#2D2D2D',
|
||||
fontWeight: 600,
|
||||
color: resolvedMode === "dark" ? '#1a1a1a' : '#1A1A1A',
|
||||
color: themeMode === "dark" ? '#1a1a1a' : '#1A1A1A',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -432,8 +423,8 @@ export const getTheme = (themeMode, brandColor) => {
|
||||
background: 'linear-gradient(90deg, #e6743a 0%, #d4456e 50%, #8a4de8 100%)',
|
||||
},
|
||||
'&:disabled': {
|
||||
background: resolvedMode === "dark" ? '#494949' : '#C9C9C9',
|
||||
color: resolvedMode === "dark" ? '#9E9E9E' : '#616161',
|
||||
background: themeMode === "dark" ? '#494949' : '#C9C9C9',
|
||||
color: themeMode === "dark" ? '#9E9E9E' : '#616161',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -478,9 +469,9 @@ export const getTheme = (themeMode, brandColor) => {
|
||||
},
|
||||
'&:disabled': {
|
||||
background: 'transparent',
|
||||
color: resolvedMode === "dark" ? '#9E9E9E' : '#616161',
|
||||
color: themeMode === "dark" ? '#9E9E9E' : '#616161',
|
||||
'&::before': {
|
||||
background: resolvedMode === "dark" ? '#494949' : '#C9C9C9',
|
||||
background: themeMode === "dark" ? '#494949' : '#C9C9C9',
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -490,7 +481,7 @@ export const getTheme = (themeMode, brandColor) => {
|
||||
MuiTab: {
|
||||
styleOverrides: {
|
||||
root: {
|
||||
color: resolvedMode === "dark" ? "#C5C5C5" : "#1A1A1A",
|
||||
color: themeMode === "dark" ? "#C5C5C5" : "#1A1A1A",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -499,7 +490,7 @@ export const getTheme = (themeMode, brandColor) => {
|
||||
overrides: {
|
||||
MuiMenu: {
|
||||
list: {
|
||||
backgroundColor: resolvedMode === "dark" ? "#27292d" : "#ffffff",
|
||||
backgroundColor: themeMode === "dark" ? "#27292d" : "#ffffff",
|
||||
},
|
||||
},
|
||||
MuiCssBaseline: {
|
||||
@@ -545,5 +536,4 @@ export const getTheme = (themeMode, brandColor) => {
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+123
-39
@@ -44,6 +44,7 @@ import {
|
||||
Add as AddIcon,
|
||||
Warning as WarningIcon,
|
||||
Pause as PauseIcon,
|
||||
Chat as ChatIcon,
|
||||
} from '@mui/icons-material'
|
||||
|
||||
import {
|
||||
@@ -71,6 +72,7 @@ const AgentUI = (props) => {
|
||||
const [newSelectedApp, setNewSelectedApp] = React.useState({})
|
||||
const [appPickerAnchor, setAppPickerAnchor] = React.useState(null)
|
||||
const [chosenApps, setChosenApps] = useState([])
|
||||
const [planningEnabled, setPlanningEnabled] = useState([])
|
||||
|
||||
const activateApp = (appId) => {
|
||||
if (appId === undefined || appId === null || appId === "") {
|
||||
@@ -188,11 +190,11 @@ const AgentUI = (props) => {
|
||||
|
||||
const agentWrapperStyle = {
|
||||
width: "100%",
|
||||
maxHeight: "100vh",
|
||||
minHeight: "100vh",
|
||||
margin: "auto",
|
||||
backgroundColor: theme.palette.backgroundColor,
|
||||
|
||||
paddingBottom: showAgentStarter ? 0 : 1500,
|
||||
paddingBottom: showAgentStarter ? 0 : 50,
|
||||
|
||||
}
|
||||
|
||||
@@ -248,21 +250,25 @@ const AgentUI = (props) => {
|
||||
return
|
||||
}
|
||||
|
||||
// If no node_id provided, look for the AI Agent node
|
||||
if (node_id === undefined || node_id === null || node_id === "") {
|
||||
// Look for AI agent
|
||||
/*
|
||||
for (var key in execution_data.results) {
|
||||
const item = execution_data.results[key]
|
||||
if (item?.action?.app_name !== "AI Agent") {
|
||||
continue
|
||||
if (item?.action?.app_name === "AI Agent") {
|
||||
node_id = item?.action?.id
|
||||
break
|
||||
}
|
||||
|
||||
node_id = item?.action?.id
|
||||
break
|
||||
}
|
||||
*/
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -270,6 +276,11 @@ const AgentUI = (props) => {
|
||||
var found = false
|
||||
for (var key in execution_data.results) {
|
||||
const item = execution_data.results[key]
|
||||
|
||||
if (item?.action?.app_name !== "AI Agent") {
|
||||
continue
|
||||
}
|
||||
|
||||
if (item?.action?.id !== node_id) {
|
||||
continue
|
||||
}
|
||||
@@ -289,16 +300,6 @@ const AgentUI = (props) => {
|
||||
|
||||
if (found === false) {
|
||||
toast.warn("Failed to find the relevant AI Agent result")
|
||||
|
||||
if (execution_data?.results?.length === 1) {
|
||||
setAgentActionResult(execution_data.results[0])
|
||||
const validatedData = validateJson(execution_data.results[0].result)
|
||||
if (validatedData.valid) {
|
||||
setData(validatedData.result)
|
||||
} else {
|
||||
toast.warn("Action output result is not valid JSON!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -475,7 +476,7 @@ const AgentUI = (props) => {
|
||||
getAppAuth()
|
||||
}, [])
|
||||
|
||||
const maxTimelineWidth = 375
|
||||
const maxTimelineWidth = 275
|
||||
|
||||
const submitQuestions = (decisionId, questionAnswers, isContinuation) => {
|
||||
console.log("Submitting questions: ", decisionId, questionAnswers)
|
||||
@@ -521,8 +522,12 @@ const AgentUI = (props) => {
|
||||
const executionId = params.get("execution_id")
|
||||
const nodeId = params.get("node_id")
|
||||
const authorization = params.get("authorization")
|
||||
const workflowIdParam = params.get("workflow_id")
|
||||
|
||||
const url = `${globalUrl}/api/v1/workflows/${executionId}/run?reference_execution=${executionId}&authorization=${authorization}&answer=true¬e=${encodeURIComponent(JSON.stringify(newArgument))}&agentic=true&decision_id=${decisionId}`
|
||||
// workflow_id param > execution.workflow.id > executionId
|
||||
const foundWorkflowId = workflowIdParam !== undefined && workflowIdParam !== null && workflowIdParam !== "" ? workflowIdParam : execution?.workflow?.id !== undefined && execution?.workflow?.id !== null && execution?.workflow?.id !== "" ? execution.workflow.id : executionId
|
||||
|
||||
const url = `${globalUrl}/api/v1/workflows/${foundWorkflowId}/run?reference_execution=${executionId}&authorization=${authorization}&answer=true¬e=${encodeURIComponent(JSON.stringify(newArgument))}&agentic=true&decision_id=${decisionId}&node_id=${nodeId}`
|
||||
fetch(url, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
@@ -893,15 +898,32 @@ const AgentUI = (props) => {
|
||||
/>
|
||||
</div>
|
||||
<div style={{
|
||||
minWidth: 300,
|
||||
maxWidth: 300,
|
||||
minWidth: 400,
|
||||
maxWidth: 400,
|
||||
maxHeight: 150,
|
||||
overflowX: "hidden",
|
||||
overflowY: "auto",
|
||||
paddingTop: defaultTopPadding,
|
||||
paddingBottom: defaultTopPadding,
|
||||
}}>
|
||||
{itemLabel}
|
||||
<Markdown
|
||||
components={markdownComponents}
|
||||
id="markdown_wrapper"
|
||||
className={"style.reactMarkdown"}
|
||||
escapeHtml={false}
|
||||
skipHtml={false}
|
||||
remarkPlugins={[remarkGfm]}
|
||||
style={{
|
||||
minWidth: 400,
|
||||
maxWidth: 450,
|
||||
maxHeight: 150,
|
||||
overflowX: "hidden",
|
||||
overflowY: "auto",
|
||||
|
||||
paddingTop: defaultTopPadding,
|
||||
paddingBottom: defaultTopPadding,
|
||||
}}
|
||||
>
|
||||
{itemLabel}
|
||||
</Markdown>
|
||||
</div>
|
||||
|
||||
<Tooltip title={`Time taken: ${currentDuration} seconds. Started: ${new Date(itemStartTime * 1000).toLocaleString()}\nFinished: ${new Date(itemEndTime * 1000).toLocaleString()}`} placement="right"
|
||||
@@ -1084,7 +1106,7 @@ const AgentUI = (props) => {
|
||||
</div>
|
||||
: null}
|
||||
|
||||
{questions?.length > 0 && item?.status === "RUNNING" || item?.status === "WAITING" ?
|
||||
{item.category !== "agent" && questions?.length > 0 && (item?.status === "RUNNING" || item?.status === "WAITING") ?
|
||||
<div>
|
||||
{questions.map((q, questionIndex) => {
|
||||
return (
|
||||
@@ -1189,7 +1211,25 @@ const AgentUI = (props) => {
|
||||
|
||||
const [continuationText, setContinuationText] = useState("")
|
||||
|
||||
var actionResult = execution?.results?.length > 0 ? execution.results[0] : execution
|
||||
// Find the AI Agent result specifically, not just results[0]
|
||||
var actionResult = null
|
||||
if (execution?.results?.length > 0) {
|
||||
for (var key in execution.results) {
|
||||
const item = execution.results[key]
|
||||
if (item?.action?.app_name === "AI Agent") {
|
||||
actionResult = item
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to first result if no AI Agent found
|
||||
if (actionResult === null) {
|
||||
actionResult = execution.results[0]
|
||||
}
|
||||
} else {
|
||||
actionResult = execution
|
||||
}
|
||||
|
||||
const validate = validateJson(actionResult?.result)
|
||||
if (validate.valid === true) {
|
||||
actionResult.result = validate.result
|
||||
@@ -1246,6 +1286,11 @@ const AgentUI = (props) => {
|
||||
for (var key in agent_data?.decisions) {
|
||||
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) {
|
||||
item.run_details.started_at = originalStartTime
|
||||
}
|
||||
@@ -1495,6 +1540,7 @@ const AgentUI = (props) => {
|
||||
parsedAction = parsedAction.slice(0, -1) // Remove last comma
|
||||
}
|
||||
|
||||
/*
|
||||
const data = {
|
||||
"id": uuid,
|
||||
"name":"agent",
|
||||
@@ -1517,9 +1563,23 @@ const AgentUI = (props) => {
|
||||
"name":"action",
|
||||
"value": parsedAction,
|
||||
}
|
||||
]}
|
||||
|
||||
],
|
||||
"planning_mode": planningEnabled,
|
||||
}
|
||||
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, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
@@ -1604,8 +1664,8 @@ const AgentUI = (props) => {
|
||||
</ButtonGroup>
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{height: showAgentStarter ? "25vh" : "5vh", }}>
|
||||
<div style={{paddingBottom: 500, }}>
|
||||
<div style={{height: showAgentStarter ? "17vh" : "5vh", }}>
|
||||
</div>
|
||||
<div style={agentWrapperStyle}>
|
||||
<div style={{
|
||||
@@ -1667,10 +1727,14 @@ const AgentUI = (props) => {
|
||||
agentRequestLoading ?
|
||||
<CircularProgress size={24} style={{marginRight: 10, }} />
|
||||
:
|
||||
<Tooltip title="This is the input for the AI Agent. It can be any valid JSON.">
|
||||
<IconButton type="submit">
|
||||
<Tooltip title="Start the agent. Data can be anything, and it will run based on your selected tools">
|
||||
<IconButton
|
||||
type="submit"
|
||||
color={actionInput === "" || actionInput === undefined || actionInput === null ? "secondary" : "primary"}
|
||||
disabled={actionInput === "" || actionInput === undefined || actionInput === null}
|
||||
>
|
||||
<SendIcon
|
||||
color="primary"
|
||||
color={actionInput === "" || actionInput === undefined || actionInput === null ? "secondary" : "primary"}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
@@ -1678,12 +1742,29 @@ const AgentUI = (props) => {
|
||||
}}
|
||||
/>
|
||||
|
||||
<div style={{display: "flex", margin: "auto", marginTop: 30, minWidth: 300, maxWidth: 300, justifyContent: "center", overflowWrap: "wrap", }}>
|
||||
<div style={{display: "flex", margin: "auto", marginTop: 20, minWidth: 500, maxWidth: 500, justifyContent: "center", overflowWrap: "wrap", }}>
|
||||
<div style={{}}>
|
||||
{/*
|
||||
<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
|
||||
id="add_app_chip"
|
||||
icon={<AddIcon />} label="Select Apps / MCPs"
|
||||
style={chipStyle}
|
||||
variant={"outlined"}
|
||||
onClick={() => {
|
||||
setAppPickerAnchor(document.getElementById("add_app_chip"))
|
||||
}}
|
||||
@@ -1794,18 +1875,21 @@ const AgentUI = (props) => {
|
||||
<AvatarGroup>
|
||||
{chosenApps?.map((app, index) => {
|
||||
return(
|
||||
<Tooltip title={`Constraint: ${app?.name}`} key={index} placement="top">
|
||||
<Tooltip title={`MCP / App: ${app?.name}`} key={index} placement="top">
|
||||
<Avatar
|
||||
key={index}
|
||||
alt={app?.name}
|
||||
src={app?.large_image ? app.large_image : app?.image ? app.image : theme.palette.singulBlackWhite}
|
||||
onClick={() => {
|
||||
window.open(`/apps/${app.id}`, '_blank', 'noopener,noreferrer');
|
||||
if (app?.id !== undefined) {
|
||||
window.open(`/apps/${app.id}`, '_blank', 'noopener,noreferrer');
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
width: 30,
|
||||
height: 30,
|
||||
backgroundColor: theme.palette.backgroundColor,
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
@@ -231,7 +231,7 @@ export const triggers = [
|
||||
status: "uninitialized",
|
||||
trigger_type: "SCHEDULE",
|
||||
errors: null,
|
||||
large_image: "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OCA0OCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4IiByeD0iOCIgZmlsbD0iIzIxQTBCRCIvPgo8Y2lyY2xlIGN4PSIyNCIgY3k9IjI0IiByPSI4Ljc1IiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjEuNSIvPgo8cGF0aCBkPSJNMjguNSAyNEgyNC4yNUMyNC4xMTE5IDI0IDI0IDIzLjg4ODEgMjQgMjMuNzVWMjAuNSIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K",
|
||||
large_image: "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OCA0OCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4IiByeD0iOCIgZmlsbD0iI0UzQTQxQiIvPgo8cmVjdCB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEyIDEyKSIgZmlsbD0iI0UzQTQxQiIvPgo8Y2lyY2xlIGN4PSIyNCIgY3k9IjI0IiByPSI4Ljc1IiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjEuNSIvPgo8cGF0aCBkPSJNMjguNSAyNEgyNC4yNUMyNC4xMTE5IDI0IDI0IDIzLjg4ODEgMjQgMjMuNzVWMjAuNSIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4=",
|
||||
label: "Schedule",
|
||||
is_valid: true,
|
||||
environment: "onprem",
|
||||
@@ -300,6 +300,11 @@ export const triggers = [
|
||||
"name": "subflow",
|
||||
"example": "",
|
||||
"value": "",
|
||||
},
|
||||
{
|
||||
"name": "subflow_failure",
|
||||
"example": "",
|
||||
"value": "",
|
||||
}
|
||||
],
|
||||
status: "running",
|
||||
@@ -498,6 +503,31 @@ export function setActionState(actionId, updates, workflowId = null) {
|
||||
}
|
||||
}
|
||||
|
||||
// Will use this function to remove the action data when the node will get removed from the cytoscape.
|
||||
export function removeActionState(actionId, workflowId = null) {
|
||||
if (!actionId) return;
|
||||
|
||||
try {
|
||||
const stored = localStorage.getItem(ACTION_STATES_STORAGE_KEY);
|
||||
if (!stored) return;
|
||||
|
||||
const allStates = JSON.parse(stored);
|
||||
|
||||
if (workflowId && allStates[workflowId]) {
|
||||
delete allStates[workflowId][actionId];
|
||||
|
||||
// Clean up empty workflow objects
|
||||
if (Object.keys(allStates[workflowId]).length === 0) {
|
||||
delete allStates[workflowId];
|
||||
}
|
||||
}
|
||||
|
||||
localStorage.setItem(ACTION_STATES_STORAGE_KEY, JSON.stringify(allStates));
|
||||
} catch (e) {
|
||||
console.error("Failed to remove action state:", e);
|
||||
}
|
||||
}
|
||||
|
||||
const splitter = "|~|";
|
||||
const svgSize = 24;
|
||||
const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||
@@ -505,7 +535,7 @@ const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||
//const referenceUrl = "https://shuffler.io/functions/webhooks/"
|
||||
//const referenceUrl = window.location.origin+"/api/v1/hooks/"
|
||||
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "33e4e3564f4f060e96e0531957bed552")
|
||||
const AngularWorkflow = (defaultprops) => {
|
||||
const { globalUrl, setCookie, isLoggedIn, isLoaded, userdata, data_id, ReactGA, } = defaultprops;
|
||||
const {themeMode, supportEmail, brandColor} = useContext(Context)
|
||||
@@ -566,6 +596,8 @@ const AngularWorkflow = (defaultprops) => {
|
||||
const [originalWorkflow, setOriginalWorkflow] = React.useState({});
|
||||
const [originalSelectedEnvironment, setOriginalSelectedEnvironment] = React.useState({});
|
||||
const [subworkflow, setSubworkflow] = React.useState({});
|
||||
const [subworkflowFailure, setSubworkflowFailure] = React.useState({});
|
||||
const [subworkflowFailureStartnode, setSubworkflowFailureStartnode] = React.useState("");
|
||||
const [subworkflowStartnode, setSubworkflowStartnode] = React.useState("");
|
||||
const [leftViewOpen, setLeftViewOpen] = React.useState(isMobile ? false : true);
|
||||
const [leftBarSize, setLeftBarSize] = React.useState(isMobile ? 0 : 235)
|
||||
@@ -1130,7 +1162,14 @@ const AngularWorkflow = (defaultprops) => {
|
||||
"description": "Translates your JSON data into a standard formats, then stores it in the Shuffle Datastore",
|
||||
"label": "Translate standard",
|
||||
"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",
|
||||
"value": "",
|
||||
"required": true,
|
||||
@@ -1150,7 +1189,14 @@ const AngularWorkflow = (defaultprops) => {
|
||||
"name": "Cases",
|
||||
"description": "Available actions for case management",
|
||||
"label": "Cases",
|
||||
"parameters": [{
|
||||
"parameters": [
|
||||
{
|
||||
"name": "app_name",
|
||||
"value": "",
|
||||
"required": true,
|
||||
"multiline": false,
|
||||
},
|
||||
{
|
||||
"name": "action",
|
||||
"value": "list_tickets",
|
||||
"options": [
|
||||
@@ -1175,7 +1221,14 @@ const AngularWorkflow = (defaultprops) => {
|
||||
"name": "Communication",
|
||||
"description": "Available actions for communication",
|
||||
"label": "Communication",
|
||||
"parameters": [{
|
||||
"parameters": [
|
||||
{
|
||||
"name": "app_name",
|
||||
"value": "",
|
||||
"required": true,
|
||||
"multiline": false,
|
||||
},
|
||||
{
|
||||
"name": "action",
|
||||
"value": "list_messages",
|
||||
"options": [
|
||||
@@ -1210,7 +1263,8 @@ const AngularWorkflow = (defaultprops) => {
|
||||
"disable_user",
|
||||
"get_identity",
|
||||
"get_asset",
|
||||
"search_identity"
|
||||
"search_identity",
|
||||
"list_users",
|
||||
],
|
||||
"required": true,
|
||||
},
|
||||
@@ -2199,6 +2253,26 @@ const AngularWorkflow = (defaultprops) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (param.name === "subflow_failure" && param.value !== undefined && param.value !== null && param.value.length > 0) {
|
||||
if (param.value === workflow?.id) {
|
||||
setSubworkflowFailure(workflow);
|
||||
} else {
|
||||
const sub = responseJson.find((data) => data?.id === param.value);
|
||||
if (sub !== undefined) {
|
||||
setSubworkflowFailure(sub);
|
||||
|
||||
// Populate startnode if set
|
||||
const startnodeParam = trigger.parameters.find((p) => p.name === "subflow_failure_startnode");
|
||||
if (startnodeParam && startnodeParam.value && sub.actions) {
|
||||
const foundAction = sub.actions.find((a) => a?.id === startnodeParam.value);
|
||||
if (foundAction) {
|
||||
setSubworkflowFailureStartnode(foundAction);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (param.name === "startnode" && param.value !== undefined && param.value !== null) {
|
||||
|
||||
if (Object.getOwnPropertyNames(baseSubflow).length > 0) {
|
||||
@@ -2344,7 +2418,7 @@ const AngularWorkflow = (defaultprops) => {
|
||||
return
|
||||
}
|
||||
|
||||
setExecutionsLoading(true);
|
||||
setExecutionsLoading(true);
|
||||
|
||||
var url = `${globalUrl}/api/v2/workflows/${id}/executions`
|
||||
var method = "GET"
|
||||
@@ -2825,6 +2899,7 @@ const AngularWorkflow = (defaultprops) => {
|
||||
stop()
|
||||
return
|
||||
}
|
||||
|
||||
//console.log(responseJson)
|
||||
// Loop nodes and find results
|
||||
// Update on every interval? idk
|
||||
@@ -3971,6 +4046,8 @@ const AngularWorkflow = (defaultprops) => {
|
||||
if (actionAppname === appname) {
|
||||
workflow.actions[actionkey].selectedAuthentication = item;
|
||||
workflow.actions[actionkey].authentication_id = item.id;
|
||||
selectedAction.selectedAuthentication = item;
|
||||
selectedAction.authentication_id = item.id;
|
||||
appUpdates = true;
|
||||
}
|
||||
}
|
||||
@@ -5195,7 +5272,8 @@ const AngularWorkflow = (defaultprops) => {
|
||||
|
||||
|
||||
if (responseJson.public) {
|
||||
setAppAuthentication([])
|
||||
// Delay setting appAuthentication to prevent race condition with graph setup
|
||||
setTimeout(() => setAppAuthentication([]), 100)
|
||||
setLeftBarSize(300)
|
||||
|
||||
if (Object.getOwnPropertyNames(creatorProfile).length === 0) {
|
||||
@@ -5586,7 +5664,7 @@ const AngularWorkflow = (defaultprops) => {
|
||||
}
|
||||
|
||||
ReactDOM.unstable_batchedUpdates(() => {
|
||||
setRightSideBarOpen(true);
|
||||
// setRightSideBarOpen(true);
|
||||
setLastSaved(false);
|
||||
|
||||
/*
|
||||
@@ -6772,7 +6850,7 @@ const AngularWorkflow = (defaultprops) => {
|
||||
}
|
||||
|
||||
//event.target.unselect();
|
||||
setRightSideBarOpen(true);
|
||||
// setRightSideBarOpen(true);
|
||||
return
|
||||
|
||||
} else if (data.buttonType === "copy") {
|
||||
@@ -6864,7 +6942,7 @@ const AngularWorkflow = (defaultprops) => {
|
||||
if (sourcenode !== null && sourcenode !== undefined) {
|
||||
const sourcedata = sourcenode.data()
|
||||
|
||||
if (sourcedata.trigger_type !== "SUBFLOW" && sourcedata.trigger_type !== "USERINPUT") {
|
||||
if (sourcedata?.trigger_type !== "SUBFLOW" && sourcedata?.trigger_type !== "USERINPUT") {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -7129,12 +7207,12 @@ const AngularWorkflow = (defaultprops) => {
|
||||
|
||||
const tmpAuth = JSON.parse(JSON.stringify(newAppAuth));
|
||||
|
||||
const curappName = curapp.name.toLowerCase()
|
||||
const curappName = curapp.name.toLowerCase().replaceAll(" ", "_")
|
||||
for (let tmpAuthKey in tmpAuth) {
|
||||
var item = tmpAuth[tmpAuthKey];
|
||||
|
||||
const newfields = {};
|
||||
if (item.app.name.toLowerCase() !== curappName) {
|
||||
if (item.app.name.toLowerCase().replaceAll(" ", "_") !== curappName) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -7516,7 +7594,6 @@ const AngularWorkflow = (defaultprops) => {
|
||||
|
||||
setSelectedTriggerIndex(trigger_index)
|
||||
setSelectedTrigger(data)
|
||||
//setSelectedActionEnvironment(data.env)
|
||||
}, 25)
|
||||
} else if (data.type === "COMMENT") {
|
||||
if (selectedNodes?.length > 1) {
|
||||
@@ -7829,7 +7906,7 @@ const AngularWorkflow = (defaultprops) => {
|
||||
continue
|
||||
}
|
||||
|
||||
const paramname = param.name.toLowerCase().trim().replaceAll("_", " ");
|
||||
const paramname = param.name?.toLowerCase()?.trim()?.replaceAll("_", " ");
|
||||
|
||||
const foundresult = GetParamMatch(paramname, exampledata, "");
|
||||
if (foundresult.length > 0) {
|
||||
@@ -7866,10 +7943,7 @@ const AngularWorkflow = (defaultprops) => {
|
||||
continue
|
||||
}
|
||||
|
||||
const paramname = param.name
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replaceAll("_", " ");
|
||||
const paramname = param.name?.toLowerCase()?.trim()?.replaceAll("_", " ");
|
||||
|
||||
const foundresult = GetParamMatch(paramname, exampledata, "");
|
||||
if (foundresult.length > 0) {
|
||||
@@ -8169,11 +8243,11 @@ const AngularWorkflow = (defaultprops) => {
|
||||
if (workflow.branches[branchkey].destination_id === edge.target && workflow.branches[branchkey].source_id === edge.source) {
|
||||
|
||||
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) {
|
||||
console.log("Removing branch: ", foundbranch.data())
|
||||
|
||||
event.target.remove()
|
||||
//event.target.remove()
|
||||
|
||||
found = true
|
||||
break
|
||||
@@ -8545,6 +8619,10 @@ const AngularWorkflow = (defaultprops) => {
|
||||
|
||||
workflow.actions = workflow.actions.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) {
|
||||
// FIXME - should check branches connected to startnode, as picking random
|
||||
// is just confusing
|
||||
@@ -8654,7 +8732,7 @@ const AngularWorkflow = (defaultprops) => {
|
||||
if ((event.ctrlKey || event.metaKey) && !event.shiftKey) {
|
||||
|
||||
// If any modal/sidebar is open, let browser handle normal copy
|
||||
if (isAnyModalOrSidebarOpen) {
|
||||
if (isAnyModalOrSidebarOpen || event.target?.closest('.MuiDialog-root, .MuiModal-root, [role="dialog"]')) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -10105,8 +10183,8 @@ const AngularWorkflow = (defaultprops) => {
|
||||
// Calculates how a branch should curve (it's still weird~)
|
||||
// https://codepen.io/guillaumethomas/pen/xxbbBKO
|
||||
const calculateEdgeCurve = (sourcenodePosition, destinationnodePosition) => {
|
||||
const xParsed = destinationnodePosition.x - sourcenodePosition.x
|
||||
const yParsed = destinationnodePosition.y - sourcenodePosition.y
|
||||
const xParsed = destinationnodePosition?.x - sourcenodePosition?.x
|
||||
const yParsed = destinationnodePosition?.y - sourcenodePosition?.y
|
||||
|
||||
const z = Math.sqrt(xParsed * xParsed + yParsed * yParsed)
|
||||
const costheta = xParsed / z
|
||||
@@ -10273,7 +10351,7 @@ const AngularWorkflow = (defaultprops) => {
|
||||
action.iconBackground = iconInfo.iconBackgroundColor
|
||||
action.fillstyle = "linear-gradient"
|
||||
}
|
||||
}else if(!action.isStartNode) {
|
||||
} else if(!action.isStartNode) {
|
||||
// This is to round the corners of the image
|
||||
// If action has no large_image (e.g. imported/synced workflow where it was stripped),
|
||||
// inject it from the available apps in the sidebar
|
||||
@@ -10283,6 +10361,7 @@ const AngularWorkflow = (defaultprops) => {
|
||||
apps.find((a) => a.name === action.app_name)
|
||||
imageSource = (foundApp && foundApp.large_image) ? foundApp.large_image : ""
|
||||
}
|
||||
|
||||
const originalBase64 = imageSource !== "" ? imageSource : theme.palette.defaultImage
|
||||
const roundedImage = await roundBase64Image(originalBase64, 16);
|
||||
action = {...action, large_image: roundedImage}
|
||||
@@ -11424,10 +11503,10 @@ const AngularWorkflow = (defaultprops) => {
|
||||
// No matter what, it's being stopped.
|
||||
if (!responseJson.success) {
|
||||
if (responseJson.reason !== undefined) {
|
||||
toast("Failed to stop schedule: " + responseJson.reason);
|
||||
toast.warn("Failed to stop schedule: " + responseJson.reason);
|
||||
}
|
||||
} else {
|
||||
toast("Successfully stopped schedule");
|
||||
toast.success("Successfully stopped schedule");
|
||||
}
|
||||
|
||||
if (triggerindex !== undefined && triggerindex !== null && triggerindex >= 0) {
|
||||
@@ -13161,7 +13240,7 @@ const AngularWorkflow = (defaultprops) => {
|
||||
if (queryID !== undefined && queryID !== null) {
|
||||
aa('init', {
|
||||
appId: "JNSS5CFDZZ",
|
||||
apiKey: "c8f882473ff42d41158430be09ec2b4e",
|
||||
apiKey: "33e4e3564f4f060e96e0531957bed552",
|
||||
})
|
||||
|
||||
const timestamp = new Date().getTime()
|
||||
@@ -16032,7 +16111,7 @@ const AngularWorkflow = (defaultprops) => {
|
||||
onClick={() => {
|
||||
// Change Direction of the branch target/source
|
||||
const foundBranch = cy.getElementById(selectedEdge.id)
|
||||
if (foundBranch !== undefined && foundBranch !== null) {
|
||||
if (foundBranch !== undefined && foundBranch !== null && foundBranch?.length > 0) {
|
||||
const source = foundBranch.data("source")
|
||||
const target = foundBranch.data("target")
|
||||
|
||||
@@ -19292,6 +19371,236 @@ const AngularWorkflow = (defaultprops) => {
|
||||
/>
|
||||
) : 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, }} />
|
||||
<b>Required Input-Questions</b>
|
||||
@@ -19978,6 +20287,7 @@ const AngularWorkflow = (defaultprops) => {
|
||||
"&.Mui-selected": {
|
||||
backgroundColor: themeMode === "dark" ? "#1e1e1e" : "#CCCCCC",
|
||||
color: theme.palette.text.primary,
|
||||
borderRadius: "6px !important",
|
||||
fontWeight: 600,
|
||||
"&:hover": {
|
||||
backgroundColor: themeMode === "dark" ? "rgba(0,0,0,0.3)" : "rgba(0,0,0,0.1)",
|
||||
@@ -20020,7 +20330,6 @@ const AngularWorkflow = (defaultprops) => {
|
||||
justifyContent: "space-between",
|
||||
width: "100%",
|
||||
position: "relative",
|
||||
minHeight: 80,
|
||||
}}>
|
||||
{/* Left: Workflow Name Container */}
|
||||
<div style={{
|
||||
@@ -20055,12 +20364,68 @@ const AngularWorkflow = (defaultprops) => {
|
||||
}}
|
||||
>
|
||||
{workflow?.name !== undefined && workflow?.name !== null && workflow?.name?.length > 0 ?
|
||||
<EditIcon style={{ position: "absolute", top: 7, height: 20, width: 20, }} />
|
||||
<EditIcon style={{ position: "absolute", top: 9, height: 20, width: 20, }} />
|
||||
:
|
||||
null
|
||||
}
|
||||
<span style={{ marginLeft: 30, }}>{workflow.name}</span>
|
||||
</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>
|
||||
|
||||
{/* Center: Build/Debug Toggle */}
|
||||
@@ -20237,6 +20602,7 @@ const AngularWorkflow = (defaultprops) => {
|
||||
saveWorkflow(workflow, undefined, undefined, e.target.value)
|
||||
|
||||
/* Standard re-loads */
|
||||
|
||||
setAllTriggers(undefined)
|
||||
setSelectedTriggerIndex(-1)
|
||||
getEnvironments(e.target.value)
|
||||
@@ -20561,7 +20927,7 @@ const AngularWorkflow = (defaultprops) => {
|
||||
id="execution_location"
|
||||
style={{ color: theme.palette.text.primary }}
|
||||
>
|
||||
Runtime Location
|
||||
Runtime Location ({selectedActionEnvironment?.Name})
|
||||
</InputLabel>
|
||||
<Select
|
||||
labelId="execution_location"
|
||||
@@ -20575,7 +20941,7 @@ const AngularWorkflow = (defaultprops) => {
|
||||
}}
|
||||
}}
|
||||
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={{
|
||||
style: {
|
||||
@@ -20700,65 +21066,7 @@ const AngularWorkflow = (defaultprops) => {
|
||||
</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>
|
||||
|
||||
{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>
|
||||
);
|
||||
};
|
||||
@@ -21382,6 +21690,9 @@ const AngularWorkflow = (defaultprops) => {
|
||||
setElements([])
|
||||
cy.edges().remove()
|
||||
cy.nodes().remove()
|
||||
|
||||
// Rebuild graph from new workflow data
|
||||
setTimeout(() => setupGraph(inputworkflow), 100)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21603,7 +21914,7 @@ const AngularWorkflow = (defaultprops) => {
|
||||
if (queryID !== undefined && queryID !== null) {
|
||||
aa('init', {
|
||||
appId: "JNSS5CFDZZ",
|
||||
apiKey: "c8f882473ff42d41158430be09ec2b4e",
|
||||
apiKey: "33e4e3564f4f060e96e0531957bed552",
|
||||
})
|
||||
const timestamp = new Date().getTime();
|
||||
aa('sendEvents', [
|
||||
@@ -22836,11 +23147,16 @@ const AngularWorkflow = (defaultprops) => {
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else if (execution.execution_source === "ShuffleGPT") {
|
||||
} else if (execution.execution_source === "ShuffleGPT" || execution.execution_source == "Singul") {
|
||||
return (
|
||||
<AutoAwesomeIcon
|
||||
color="secondary"
|
||||
style={{ paddingTop: 8, paddingLeft: 4, height: 25, width: 25, }}
|
||||
<img
|
||||
alt={"Singul"}
|
||||
src={theme.palette.singulGreen}
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius: borderRadius,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
} else if (execution.execution_source === "pipeline") {
|
||||
@@ -23334,9 +23650,14 @@ const AngularWorkflow = (defaultprops) => {
|
||||
getWorkflowExecution(workflow.id, "", executionFilter, workflow.org_id)
|
||||
}}
|
||||
color="secondary"
|
||||
disabled={executionsLoading}
|
||||
>
|
||||
<CachedIcon style={{ marginRight: 10 }} />
|
||||
Refresh Runs
|
||||
{executionsLoading ? (
|
||||
<CircularProgress size={20} style={{ marginRight: 10 }} />
|
||||
) : (
|
||||
<CachedIcon style={{ marginRight: 10 }} />
|
||||
)}
|
||||
{executionsLoading ? "Refreshing runs" : "Refresh Runs"}
|
||||
</Button>
|
||||
<ButtonGroup
|
||||
fullWidth
|
||||
@@ -23667,7 +23988,7 @@ const AngularWorkflow = (defaultprops) => {
|
||||
)}
|
||||
</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
|
||||
@@ -24513,7 +24834,7 @@ const AngularWorkflow = (defaultprops) => {
|
||||
|
||||
{data?.action?.name === "run_schemaless" || data?.action?.name === "run_singul" || data?.action?.name === "singul" && data?.action?.parameters?.length > 4 ?
|
||||
<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 || ""}`}>
|
||||
<a
|
||||
@@ -24792,7 +25113,7 @@ const AngularWorkflow = (defaultprops) => {
|
||||
}}
|
||||
edge="end"
|
||||
>
|
||||
<ContentCopyIcon style={{ heigth: 15, }} />
|
||||
<ContentCopyIcon style={{heigth: 15, }}/>
|
||||
</IconButton>
|
||||
</span>
|
||||
:
|
||||
@@ -24800,7 +25121,7 @@ const AngularWorkflow = (defaultprops) => {
|
||||
variant="body1"
|
||||
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}
|
||||
</span>
|
||||
</Typography>
|
||||
@@ -25056,8 +25377,8 @@ const AngularWorkflow = (defaultprops) => {
|
||||
<div style={{ display: "flex", marginBottom: 15, position: "sticky", top: -31, zIndex: 10000, }}>
|
||||
{curapp === null ? null : (
|
||||
<img
|
||||
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}
|
||||
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}
|
||||
style={{
|
||||
marginRight: 20,
|
||||
width: imgsize,
|
||||
@@ -25078,10 +25399,10 @@ const AngularWorkflow = (defaultprops) => {
|
||||
marginBottom: "auto",
|
||||
}}
|
||||
>
|
||||
<b>{selectedResult.action.label.replaceAll("_", " ")}</b>
|
||||
<b>{selectedResult?.action?.label?.replaceAll("_", " ")}</b>
|
||||
</div>
|
||||
<Typography variant="body2" color="textSecondary" style={{ }}>
|
||||
{selectedResult.action.name}
|
||||
{selectedResult?.action?.name}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
@@ -25189,6 +25510,14 @@ const AngularWorkflow = (defaultprops) => {
|
||||
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 (
|
||||
data.example !== undefined &&
|
||||
data.example !== null &&
|
||||
@@ -25354,15 +25683,18 @@ const AngularWorkflow = (defaultprops) => {
|
||||
Object.getOwnPropertyNames(selectedAction).length > 0 &&
|
||||
rightSideBarOpen &&
|
||||
Object.getOwnPropertyNames(selectedTrigger || {}).length === 0 &&
|
||||
Object.getOwnPropertyNames(selectedComment || {}).length === 0
|
||||
Object.getOwnPropertyNames(selectedComment || {}).length === 0 &&
|
||||
Object.getOwnPropertyNames(selectedEdge || {}).length === 0
|
||||
}
|
||||
hideBackdrop={true}
|
||||
transitionDuration={{ enter: 180, exit: 0 }}
|
||||
onClose={() => {
|
||||
setRightSideBarOpen(false);
|
||||
setSelectedAction({});
|
||||
}}
|
||||
ModalProps={{
|
||||
keepMounted: true,
|
||||
disableEnforceFocus: true,
|
||||
}}
|
||||
sx={{
|
||||
pointerEvents: "none",
|
||||
@@ -26237,12 +26569,20 @@ const AngularWorkflow = (defaultprops) => {
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
style={{ width: 150, margin: "auto", }}
|
||||
style={{ width: 150, margin: "auto", paddingBottom: 50, }}
|
||||
disabled={false}
|
||||
variant="outlined"
|
||||
onClick={() => {
|
||||
setAuthenticationOptions(authenticationOption)
|
||||
handleSubmitCheck()
|
||||
|
||||
if (isCloud) {
|
||||
ReactGA.event({
|
||||
category: "Integration",
|
||||
action: "Authenticate",
|
||||
label: `${selectedApp?.name} - Manual`,
|
||||
})
|
||||
}
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
@@ -26954,7 +27294,7 @@ const AngularWorkflow = (defaultprops) => {
|
||||
setTimeout(() => {
|
||||
//toast("Running setupgraph with new revision. Actions: " + newrevision.actions.length)
|
||||
setupGraph(newrevision)
|
||||
}, 250)
|
||||
}, 500)
|
||||
|
||||
|
||||
// Re-adding cytoscape triggers
|
||||
@@ -27276,7 +27616,7 @@ const AngularWorkflow = (defaultprops) => {
|
||||
};
|
||||
|
||||
// Check if event.target.value is an array. If it is, split with comma
|
||||
if (parametername !== undefined && parametername !== undefined && parametername?.startsWith("${") && parametername?.endsWith("}")) {
|
||||
if (parametername !== undefined && parametername !== null && typeof parametername === "string" && parametername?.startsWith("${") && parametername?.endsWith("}")) {
|
||||
var paramcheckIndex = selectedAction.parameters.findIndex(param => param.name === parametername)
|
||||
if (paramcheckIndex !== -1) {
|
||||
// Replace the value in the field
|
||||
@@ -27312,7 +27652,7 @@ const AngularWorkflow = (defaultprops) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (data.startsWith("${") && data.endsWith("}")) {
|
||||
if (typeof data === "string" && data.startsWith("${") && data.endsWith("}")) {
|
||||
console.log("Changing field with variable: ", data)
|
||||
|
||||
// PARAM FIX - Gonna use the ID field, even though it's a hack
|
||||
@@ -27385,7 +27725,7 @@ const AngularWorkflow = (defaultprops) => {
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedAction.app_name === "Shuffle Tools" && selectedAction.name === "filter_list" && count === 0) {
|
||||
if (selectedAction !== undefined && selectedAction?.app_name === "Shuffle Tools" && selectedAction?.name === "filter_list" && count === 0) {
|
||||
const parsedvalue = data
|
||||
if (parsedvalue.includes("#")) {
|
||||
const splitparsed = parsedvalue.split(".#.")
|
||||
@@ -27402,13 +27742,15 @@ const AngularWorkflow = (defaultprops) => {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (selectedAction.parameters !== undefined && selectedAction.parameters !== null && selectedAction.parameters.length > count) {
|
||||
if (selectedAction !== undefined && selectedAction !== null && selectedAction?.parameters !== undefined && selectedAction?.parameters !== null && selectedAction.parameters?.length > count) {
|
||||
selectedAction.parameters[count].autocompleted = false
|
||||
selectedAction.parameters[count].value = data
|
||||
}
|
||||
}
|
||||
|
||||
setSelectedAction(selectedAction)
|
||||
if (selectedAction !== undefined && selectedAction !== null) {
|
||||
setSelectedAction(selectedAction)
|
||||
}
|
||||
//setUpdate(Math.random())
|
||||
}
|
||||
|
||||
|
||||
@@ -47,10 +47,11 @@ import Markdown from "react-markdown";
|
||||
import AuthenticationOauth2 from "../components/Oauth2Auth.jsx";
|
||||
import algoliasearch from "algoliasearch/lite";
|
||||
import { green } from "../views/AngularWorkflow.jsx"
|
||||
import { setAppCache } from "../views/AppExplorer.jsx"
|
||||
|
||||
const searchClient = algoliasearch(
|
||||
"JNSS5CFDZZ",
|
||||
"c8f882473ff42d41158430be09ec2b4e"
|
||||
"363e707135ea95f5523ec1b262d96b03"
|
||||
)
|
||||
|
||||
// Lazy loading of ApiExplorer component to reduce initial load time
|
||||
@@ -139,57 +140,90 @@ const ApiExplorerWrapper = (props) => {
|
||||
}
|
||||
|
||||
const runAlgoliaAppSearch = (appname) => {
|
||||
const index = searchClient.initIndex("appsearch");
|
||||
if (appname === "HTTP" || appname === "http") {
|
||||
navigate("/apis")
|
||||
return
|
||||
}
|
||||
|
||||
if (appname === "HTTP" || appname === "http") {
|
||||
navigate("/apis")
|
||||
return
|
||||
}
|
||||
// Check if appname is likely an ID (hash/UUID) or null - skip cache entirely
|
||||
const isLikelyId = !appname || appname === "null" || (appname.length >= 32 && /^[a-f0-9\-]+$/i.test(appname));
|
||||
|
||||
index
|
||||
.search(appname)
|
||||
.then(({ hits }) => {
|
||||
// Skip cache for IDs/null, proceed directly to Algolia
|
||||
if (!isLikelyId) {
|
||||
// Check backend cache first to save Algolia costs
|
||||
fetch(globalUrl + "/api/v1/apps/" + appname + "/cache")
|
||||
.then((response) => {
|
||||
if (!response.ok) {
|
||||
return { success: false };
|
||||
}
|
||||
return response.json();
|
||||
})
|
||||
.catch((error) => {
|
||||
return { success: false };
|
||||
})
|
||||
.then((cacheData) => {
|
||||
if (cacheData && cacheData.success && (cacheData.id || cacheData.objectID)) {
|
||||
const appId = cacheData.id || cacheData.objectID;
|
||||
setAppCache(globalUrl, appId, appname);
|
||||
getAppData(appId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (hits !== undefined && hits !== null && hits.length > 0) {
|
||||
const appsearchname = appname.replaceAll("_", " ").toLowerCase()
|
||||
var found = false
|
||||
for (var key in hits) {
|
||||
const hit = hits[key]
|
||||
const newname = hit.name.replaceAll("_", " ").toLowerCase()
|
||||
// Cache miss, fall through to Algolia
|
||||
runAlgoliaSearch();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (newname?.includes(appsearchname)) {
|
||||
found = true
|
||||
// For IDs/null, skip cache and go straight to Algolia
|
||||
runAlgoliaSearch();
|
||||
|
||||
getAppData(hit.objectID)
|
||||
break
|
||||
}
|
||||
}
|
||||
function runAlgoliaSearch() {
|
||||
// Fallback to Algolia if not in cache
|
||||
const index = searchClient.initIndex("appsearch");
|
||||
index
|
||||
.search(appname)
|
||||
.then(({ hits }) => {
|
||||
if (hits !== undefined && hits !== null && hits.length > 0) {
|
||||
const appsearchname = appname.toLowerCase().replace(/[_ \-]/g, "");
|
||||
var found = false
|
||||
for (var key in hits) {
|
||||
const hit = hits[key]
|
||||
const newname = hit.name?.toLowerCase().replace(/[_ \-]/g, "");
|
||||
|
||||
if (!found) {
|
||||
toast.error(`Failed to get API data for '${appname}' (1). Contact ${supportEmail} if this persists.`, {
|
||||
"autoClose": 10000,
|
||||
})
|
||||
if (newname?.includes(appsearchname)) {
|
||||
found = true
|
||||
if (!isLikelyId) setAppCache(globalUrl, hit.objectID, appname);
|
||||
getAppData(hit.objectID, appname) // pass alias so backend caches 'outlook' → real app
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
setTimeout(()=>{
|
||||
navigate("/search?tab=apps");
|
||||
},3000)
|
||||
}
|
||||
} else {
|
||||
toast.error(`Failed to get API data for '${appname}' (2). Contact ${supportEmail} if this persists.`, {
|
||||
"autoClose": 10000,
|
||||
})
|
||||
setTimeout(()=>{
|
||||
navigate("/search?tab=apps");
|
||||
},3000)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
if (!found) {
|
||||
toast.error(`Failed to get API data for '${appname}' (1). Contact ${supportEmail} if this persists.`, {
|
||||
"autoClose": 10000,
|
||||
})
|
||||
setTimeout(()=>{
|
||||
navigate("/search?tab=apps");
|
||||
},3000)
|
||||
}
|
||||
} else {
|
||||
toast.error(`Failed to get API data for '${appname}' (2). Contact ${supportEmail} if this persists.`, {
|
||||
"autoClose": 10000,
|
||||
})
|
||||
setTimeout(()=>{
|
||||
navigate("/search?tab=apps");
|
||||
},3000)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch data when appid is available
|
||||
const getAppData = useCallback((appid) => {
|
||||
const getAppData = useCallback((appid, alias) => {
|
||||
if (appid === undefined || appid === null || appid.length === 0) {
|
||||
toast.warning("No app ID loaded. Showing default API testing window. ")
|
||||
setOpenapi({
|
||||
@@ -217,10 +251,10 @@ const ApiExplorerWrapper = (props) => {
|
||||
return
|
||||
}
|
||||
|
||||
if (appid.length !== 32) {
|
||||
runAlgoliaAppSearch(appid)
|
||||
return
|
||||
}
|
||||
if (appid.length !== 32 && appid.length !== 36) {
|
||||
runAlgoliaAppSearch(appid)
|
||||
return
|
||||
}
|
||||
|
||||
const url = `${globalUrl}/api/v1/apps/${appid}/config`
|
||||
|
||||
@@ -677,7 +711,7 @@ const ApiExplorerWrapper = (props) => {
|
||||
toast.info(`This API is being rebuilt due to missing functionality. Please wait a minute or two, then try again. If this persists, please report to ${supportEmail}`, {
|
||||
"autoClose": 90000,
|
||||
})
|
||||
} else if (data.result.includes("authentication") && data.result.includes("Oauth2")) {
|
||||
} else if (data.result.includes("authentication") && data.result.includes("Oauth2") && validate?.result?.status !== 200) {
|
||||
toast.error("Oauth2 apps require authentication")
|
||||
|
||||
setAuthHighlighted(true)
|
||||
|
||||
@@ -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"
|
||||
|
||||
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))
|
||||
@@ -4478,7 +4478,7 @@ const AppCreator = (defaultprops) => {
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
console.log(`Not found: '${label}' in ${currentActionLabels}`)
|
||||
//console.log(`Not found: '${label}' in ${currentActionLabels}`)
|
||||
currentActionLabels.push(data?.action_label[labelKey])
|
||||
}
|
||||
}
|
||||
@@ -4575,22 +4575,32 @@ const AppCreator = (defaultprops) => {
|
||||
|
||||
if (e.target.value.length === 0) {
|
||||
e.target.value = ["No Label"]
|
||||
}
|
||||
|
||||
const foundIndex = actions.findIndex((action) => action.name === data.name)
|
||||
if (foundIndex !== undefined && foundIndex !== null && foundIndex >= 0) {
|
||||
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
|
||||
} 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
|
||||
}
|
||||
}
|
||||
|
||||
console.log("Should change: ", e.target.value, " Index: ", index)
|
||||
actions[foundIndex].action_label = e.target.value
|
||||
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)
|
||||
if (foundActionIndex !== undefined && foundActionIndex !== null && foundActionIndex >= 0) {
|
||||
actions[foundActionIndex].action_label = e.target.value
|
||||
setActions(actions)
|
||||
setUpdate(Math.random())
|
||||
} else {
|
||||
console.log("Could not find action: ", data.name)
|
||||
}
|
||||
}}
|
||||
value={data?.action_label}
|
||||
|
||||
@@ -65,7 +65,6 @@ import { Context } from "../context/ContextApi.jsx";
|
||||
|
||||
import {
|
||||
SearchBox,
|
||||
StaticRefinementList,
|
||||
RefinementList,
|
||||
InstantSearch,
|
||||
connectSearchBox,
|
||||
@@ -93,9 +92,18 @@ import aa from "search-insights";
|
||||
// 2 = OpenAPI (Invalid)
|
||||
const searchClient = algoliasearch(
|
||||
"JNSS5CFDZZ",
|
||||
"c8f882473ff42d41158430be09ec2b4e"
|
||||
"363e707135ea95f5523ec1b262d96b03"
|
||||
)
|
||||
|
||||
export const setAppCache = (globalUrl, appId, alias) => {
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
|
||||
if (!isCloud) return;
|
||||
fetch(globalUrl + "/api/v1/apps/" + appId + "/cache?alias=" + encodeURIComponent(alias), {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
}).catch(() => {});
|
||||
};
|
||||
|
||||
const AppExplorer = (props) => {
|
||||
const {
|
||||
globalUrl,
|
||||
@@ -214,6 +222,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
|
||||
);
|
||||
const [relatedWorkflows, setRelatedWorkflows] = useState(0);
|
||||
const [relatedApps, setRelatedApps] = useState(0);
|
||||
const [hasCachedCounts, setHasCachedCounts] = useState(false);
|
||||
const [appAuthentication, setAppAuthentication] = React.useState([]);
|
||||
const [authLoaded, setAuthLoaded] = useState(false);
|
||||
const baseResult = "The execution result will show up here";
|
||||
@@ -301,7 +310,9 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
|
||||
if (serverside) {
|
||||
console.log("Not getting app because serverside.");
|
||||
} else {
|
||||
if (params.appid.length === 32 || params.appid.length === 36) {
|
||||
if (params.appid === "new" || params.appid === "edit") {
|
||||
return;
|
||||
} else if (params.appid.length === 32 || params.appid.length === 36) {
|
||||
handleEditApp(params.appid);
|
||||
runAlgoliaAppSearch(params.appid, false, true);
|
||||
} else {
|
||||
@@ -455,7 +466,6 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
href={data.url}
|
||||
target="_blank"
|
||||
style={{ textDecoration: "none", color: "#f85a3e" }}
|
||||
>
|
||||
<Tooltip title={data.url} placement="bottom">
|
||||
@@ -762,7 +772,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
|
||||
headers: {
|
||||
"Content-Type": "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",
|
||||
})
|
||||
@@ -818,7 +828,9 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
|
||||
|
||||
setAppId(appid)
|
||||
|
||||
fetch(globalUrl + "/api/v1/apps/" + appid + "/config", {
|
||||
const url = globalUrl + "/api/v1/apps/" + appid + "/config"
|
||||
|
||||
fetch(url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@@ -1450,25 +1462,71 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
|
||||
};
|
||||
|
||||
const runAlgoliaAppSearch = (appname, isOriginal, triggerOnly) => {
|
||||
const index = searchClient.initIndex("appsearch");
|
||||
|
||||
console.log("Running appsearch for: ", appname);
|
||||
if (appname === "integration" || appname == "singul") {
|
||||
// Redirect to https://singul.io
|
||||
window.location.href = "https://singul.io"
|
||||
} else if (appname === "shuffle_agent" || appname === "agent" || appname === "agents" || appname == "shuffle-agent") {
|
||||
navigate("/agents")
|
||||
return
|
||||
}
|
||||
if (appname === "integration" || appname == "singul") {
|
||||
// Redirect to https://singul.io
|
||||
window.location.href = "https://singul.io"
|
||||
} else if (appname === "shuffle_agent" || appname === "agent" || appname === "agents" || appname == "shuffle-agent" || appname == "ai_agents" || appname == "AI Agent") {
|
||||
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
|
||||
.search(appname)
|
||||
.then(({ hits }) => {
|
||||
const appsearchname = appname.replaceAll("_", " ").toLowerCase();
|
||||
const appsearchname = appname.toLowerCase().replace(/[_ \-]/g, "");
|
||||
var found = false
|
||||
|
||||
if (hits !== undefined && hits !== null && hits.length === 1) {
|
||||
found = true
|
||||
if (!isLikelyId) setCache(hits[0].objectID);
|
||||
if (isOriginal !== false) {
|
||||
handleEditApp(hits[0].objectID)
|
||||
} else {
|
||||
@@ -1483,7 +1541,8 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (hit["name"].replaceAll("_", " ").toLowerCase().includes(appsearchname) || hit["objectID"] === appname) {
|
||||
const hitNameClean = hit["name"].toLowerCase().replace(/[_ \-]/g, "");
|
||||
if (hitNameClean.includes(appsearchname) || hit["objectID"] === appname) {
|
||||
/*
|
||||
if (hit.triggers !== undefined && hit.triggers !== null && hit.triggers.length > 0) {
|
||||
var parsedtriggers = []
|
||||
@@ -1505,9 +1564,11 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
|
||||
} else {
|
||||
if (isOriginal !== false) {
|
||||
found = true
|
||||
if (!isLikelyId) setCache(hit.objectID);
|
||||
handleEditApp(hit.objectID);
|
||||
} else {
|
||||
console.log("Found second app: ", hit);
|
||||
if (!isLikelyId) setCache(hit.objectID);
|
||||
hit.name = hit.name.charAt(0).toUpperCase() + hit.name.slice(1);
|
||||
setSecondaryApp(hit);
|
||||
}
|
||||
@@ -1519,6 +1580,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
|
||||
|
||||
if (!found) {
|
||||
if (hits.length > 0) {
|
||||
setCache(hits[0].objectID);
|
||||
if (isOriginal !== false) {
|
||||
handleEditApp(hits[0].objectID)
|
||||
} else {
|
||||
@@ -1536,6 +1598,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
|
||||
.catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -3153,7 +3216,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
|
||||
<Tab style={{color: theme.palette.text.primary, textTransform: "none", }} icon={appType === 0 || appType === 2 ? <OpenInNewIcon /> : <AppsIcon />} label={appType === 0 || appType === 2 ? "Try the API" : "Try it out"} />
|
||||
|
||||
<Tab icon={<ShowChartIcon />} style={{color: theme.palette.text.primary, textTransform: "none", }} label="Stats & Downloads" />
|
||||
<Tab icon={<PolylineIcon />} style={{color: theme.palette.text.primary, textTransform: "none", }} disabled style={{color: theme.palette.text.secondary}} label="Integrations" />
|
||||
<Tab icon={<PolylineIcon />} style={{color: theme.palette.text.secondary, textTransform: "none", }} disabled label="Integrations" />
|
||||
<Tab icon={<PersonIcon />} disabled={userdata.support !== true} style={{color: userdata.support !== true ? theme.palette.text.secondary: theme.palette.text.primary, textTransform: "none", }} label="Creator" value={4} />
|
||||
</Tabs>
|
||||
<div style={{ marginTop: 25 }}>
|
||||
@@ -3420,7 +3483,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
|
||||
</div>
|
||||
<div style={{ textAlign: "center", marginTop: 25 }}>
|
||||
<Link
|
||||
rel="noopener noreferrer"
|
||||
rel="noopener noreferrer nofollow"
|
||||
to={`/register?app_one=${app.name}&app_two=${secondaryApp.name}&message=You need to login first to connect ${app.name} and ${secondaryApp.name}&view=/apps/${params.appid}/integrations/${secondaryApp.name}`}
|
||||
style={{ textDecoration: "none" }}
|
||||
>
|
||||
@@ -3710,7 +3773,6 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
|
||||
|
||||
color="primary"
|
||||
id="checkbox-search"
|
||||
variant="body1"
|
||||
style={{
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
borderRadius: theme.palette?.borderRadius,
|
||||
@@ -4136,7 +4198,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
|
||||
Activate for all suborgs
|
||||
</MenuItem>
|
||||
|
||||
{userdata.orgs.map((data, index) => {
|
||||
{userdata?.orgs?.map((data, index) => {
|
||||
if (data.creator_org !== userdata.active_org.id) return null;
|
||||
|
||||
const imageSize = 28;
|
||||
@@ -4296,7 +4358,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
|
||||
if (queryID !== undefined && queryID !== null) {
|
||||
aa("init", {
|
||||
appId: "JNSS5CFDZZ",
|
||||
apiKey: "c8f882473ff42d41158430be09ec2b4e",
|
||||
apiKey: "26986891ddd374051039420cd77b0fff",
|
||||
});
|
||||
|
||||
const timestamp = new Date().getTime();
|
||||
@@ -4385,7 +4447,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
|
||||
if (queryID !== undefined && queryID !== null) {
|
||||
aa("init", {
|
||||
appId: "JNSS5CFDZZ",
|
||||
apiKey: "c8f882473ff42d41158430be09ec2b4e",
|
||||
apiKey: "363e707135ea95f5523ec1b262d96b03",
|
||||
});
|
||||
|
||||
const timestamp = new Date().getTime();
|
||||
@@ -4573,9 +4635,9 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
|
||||
maxWidth: height,
|
||||
maxHeight: height,
|
||||
}}
|
||||
onClick={() => {
|
||||
upload.click();
|
||||
}}
|
||||
// onClick={() => {
|
||||
// upload.click();
|
||||
// }}
|
||||
>
|
||||
{imageInfo}
|
||||
</div>
|
||||
@@ -4787,7 +4849,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
|
||||
>
|
||||
<div style={{ padding: "50px 0" }}>
|
||||
<Typography variant="h4">
|
||||
{relatedWorkflows !== 0 ? (
|
||||
{hasCachedCounts || relatedWorkflows !== 0 ? (
|
||||
relatedWorkflows
|
||||
) : (
|
||||
<InstantSearch
|
||||
@@ -4881,7 +4943,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
|
||||
>
|
||||
<div style={{ padding: "50px 0" }}>
|
||||
<Typography variant="h4">
|
||||
{relatedApps !== 0 ? (
|
||||
{hasCachedCounts || relatedApps !== 0 ? (
|
||||
relatedApps
|
||||
) : (
|
||||
<InstantSearch
|
||||
|
||||
@@ -70,10 +70,11 @@ const inputColor = "#383B40";
|
||||
|
||||
const chipStyle = {
|
||||
backgroundColor: "#3d3f43",
|
||||
height: 28,
|
||||
height: 30,
|
||||
marginRight: 5,
|
||||
paddingLeft: 5,
|
||||
paddingRight: 5,
|
||||
height: 28,
|
||||
cursor: "pointer",
|
||||
borderColor: "#3d3f43",
|
||||
color: "white",
|
||||
@@ -279,7 +280,7 @@ export const GetParsedPaths = (inputdata, basekey) => {
|
||||
return parsedValues;
|
||||
};
|
||||
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "33e4e3564f4f060e96e0531957bed552")
|
||||
const Apps = (props) => {
|
||||
const { globalUrl, isLoggedIn, isLoaded, userdata, serverside, } = props;
|
||||
|
||||
@@ -633,7 +634,7 @@ const Apps = (props) => {
|
||||
// dropdown with copy etc I guess
|
||||
const AppPaper = (props) => {
|
||||
const { app } = props
|
||||
let data = app
|
||||
const data = app
|
||||
|
||||
if (data.name === "" && data.id === "") {
|
||||
return null;
|
||||
@@ -801,6 +802,7 @@ const Apps = (props) => {
|
||||
justifyContent: "center",
|
||||
overflow: "hidden",
|
||||
maxHeight: 43,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" color="textSecondary">
|
||||
@@ -1163,6 +1165,7 @@ const Apps = (props) => {
|
||||
position: "absolute",
|
||||
top: -10,
|
||||
right: isCloud ? 50 : 0,
|
||||
backgroundColor: theme.palette.surfaceColor,
|
||||
backgroundColor: inputColor,
|
||||
color: "white",
|
||||
height: 35,
|
||||
@@ -1302,7 +1305,7 @@ const Apps = (props) => {
|
||||
if (queryID !== undefined && queryID !== null) {
|
||||
aa("init", {
|
||||
appId: "JNSS5CFDZZ",
|
||||
apiKey: "c8f882473ff42d41158430be09ec2b4e",
|
||||
apiKey: "33e4e3564f4f060e96e0531957bed552",
|
||||
});
|
||||
|
||||
const timestamp = new Date().getTime();
|
||||
@@ -1508,6 +1511,7 @@ const Apps = (props) => {
|
||||
borderRadius: 17 / 2,
|
||||
backgroundColor: itemColor,
|
||||
marginRight: 10,
|
||||
marginTop: 2,
|
||||
marginTop: "auto",
|
||||
marginBottom: "auto",
|
||||
}}
|
||||
@@ -1941,7 +1945,7 @@ const Apps = (props) => {
|
||||
const baseImage = <LibraryBooksIcon />
|
||||
|
||||
return (
|
||||
<div style={{ position: "absolute", marginTop: 15, marginLeft: 0, marginRight: 10, color: "white", zIndex: 1001, backgroundColor: theme.palette.inputColor, minWidth: leftBarSize - 10, maxWidth: leftBarSize - 10, boxShadows: "none", overflowX: "hidden", }}>
|
||||
<div style={{ position: "relative", marginTop: 15, marginLeft: 0, marginRight: 10, position: "absolute", color: "white", zIndex: 1001, backgroundColor: theme.palette.inputColor, minWidth: leftBarSize - 10, maxWidth: leftBarSize - 10, boxShadows: "none", overflowX: "hidden", }}>
|
||||
<List style={{ backgroundColor: theme.palette.inputColor, }}>
|
||||
{hits.length === 0 ?
|
||||
<ListItem style={outerlistitemStyle}>
|
||||
@@ -2032,7 +2036,7 @@ const Apps = (props) => {
|
||||
if (queryID !== undefined && queryID !== null) {
|
||||
aa('init', {
|
||||
appId: "JNSS5CFDZZ",
|
||||
apiKey: "c8f882473ff42d41158430be09ec2b4e",
|
||||
apiKey: "33e4e3564f4f060e96e0531957bed552",
|
||||
})
|
||||
|
||||
const timestamp = new Date().getTime()
|
||||
@@ -3011,7 +3015,7 @@ const Apps = (props) => {
|
||||
<FormControl>
|
||||
<DialogTitle>
|
||||
<div style={{ color: "rgba(255,255,255,0.9)" }}>
|
||||
Generate an app based on documentation (beta)
|
||||
Generate an app based on documentation
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<DialogContent style={{ color: "rgba(255,255,255,0.65)" }}>
|
||||
|
||||
@@ -34,7 +34,7 @@ import {
|
||||
|
||||
import InputAdornment from '@mui/material/InputAdornment';
|
||||
|
||||
import { ClearRefinements, connectHits, connectSearchBox, connectStateResults, InstantSearch, RefinementList, connectRefinementList, Configure } from "react-instantsearch-dom";
|
||||
import { ClearRefinements, connectInfiniteHits, connectSearchBox, connectStateResults, InstantSearch, RefinementList, connectRefinementList, Configure } from "react-instantsearch-dom";
|
||||
import { removeQuery } from "../components/ScrollToTop.jsx";
|
||||
import { toast } from "react-toastify";
|
||||
import algoliasearch from "algoliasearch/lite";
|
||||
@@ -47,7 +47,7 @@ import Dropzone from "../components/Dropzone.jsx";
|
||||
|
||||
const searchClient = algoliasearch(
|
||||
"JNSS5CFDZZ",
|
||||
"c8f882473ff42d41158430be09ec2b4e"
|
||||
"eb5fd80aa6ed5ab4730d836cff3ea283"
|
||||
);
|
||||
|
||||
// AppCard Component
|
||||
@@ -297,6 +297,8 @@ const AppCard = ({ data, index, mouseHoverIndex, setMouseHoverIndex, globalUrl,
|
||||
const Hits = ({
|
||||
userdata,
|
||||
hits,
|
||||
hasMore,
|
||||
refineNext,
|
||||
handleAppClick,
|
||||
setIsAnyAppActivated,
|
||||
searchQuery,
|
||||
@@ -310,13 +312,37 @@ const Hits = ({
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
|
||||
const [deactivatedIndexes, setDeactivatedIndexes] = React.useState([]);
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
||||
const loadMoreRef = useRef(null);
|
||||
const isFetchingMore = useRef(false);
|
||||
const { themeMode } = useContext(Context);
|
||||
const theme = getTheme(themeMode);
|
||||
|
||||
useEffect(() => {
|
||||
var baseurl = globalUrl;
|
||||
isFetchingMore.current = false;
|
||||
setIsLoadingMore(false);
|
||||
}, [hits.length]);
|
||||
|
||||
useEffect(() => {
|
||||
return; // infinite scroll disabled
|
||||
if (!loadMoreRef.current) return;
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting && hasMore && !isFetchingMore.current) {
|
||||
isFetchingMore.current = true;
|
||||
setIsLoadingMore(true);
|
||||
refineNext();
|
||||
}
|
||||
},
|
||||
{ rootMargin: "200px" }
|
||||
);
|
||||
observer.observe(loadMoreRef.current);
|
||||
return () => observer.disconnect();
|
||||
}, [hasMore, refineNext]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true)
|
||||
fetch(baseurl + "/api/v1/me", {
|
||||
fetch(globalUrl + "/api/v1/me", {
|
||||
credentials: "include",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -332,7 +358,7 @@ const Hits = ({
|
||||
.catch(error => {
|
||||
console.log("Failed login check: ", error);
|
||||
});
|
||||
}, [currTab]);
|
||||
}, []);
|
||||
|
||||
const normalizedString = (name) => {
|
||||
if (typeof name === 'string') {
|
||||
@@ -348,6 +374,16 @@ const Hits = ({
|
||||
}
|
||||
}, [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
|
||||
const handleActivateButton = (event, data, type) => {
|
||||
|
||||
@@ -384,7 +420,7 @@ const Hits = ({
|
||||
} else {
|
||||
//toast.success(`App ${type}d Successfully!`);
|
||||
if (type === 'activate') {
|
||||
setAllActivatedAppIds(prev => [...prev, data.objectID]);
|
||||
setAllActivatedAppIds(prev => [...(prev ?? []), data.objectID]);
|
||||
setIsAnyAppActivated(true);
|
||||
}
|
||||
if (type === 'deactivate') {
|
||||
@@ -405,8 +441,7 @@ const Hits = ({
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!isLoading ?
|
||||
(
|
||||
{(
|
||||
<div>
|
||||
{hits?.length === 0 && searchQuery.length >= 0 ? (
|
||||
<div style={{
|
||||
@@ -452,7 +487,7 @@ const Hits = ({
|
||||
paddingBottom: 40
|
||||
}}
|
||||
>
|
||||
{hits?.map((data, index) => {
|
||||
{sortedHits.map((data, index) => {
|
||||
const appUrl =
|
||||
isCloud
|
||||
? `/apps/${data.objectID}?queryID=${data.__queryID}`
|
||||
@@ -704,13 +739,18 @@ const Hits = ({
|
||||
</Zoom>
|
||||
);
|
||||
})}
|
||||
<div ref={loadMoreRef} style={{ gridColumn: "1 / -1", height: 10 }} />
|
||||
{isLoadingMore && (
|
||||
<>
|
||||
{[...Array(3)].map((_, i) => (
|
||||
<AppSkeleton key={`skeleton-more-${i}`} />
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<LoadingGrid />
|
||||
)
|
||||
}
|
||||
)}
|
||||
</div >
|
||||
);
|
||||
}
|
||||
@@ -811,7 +851,7 @@ const SearchBox = ({ refine, searchQuery, setSearchQuery }) => {
|
||||
};
|
||||
|
||||
const CustomSearchBox = connectSearchBox(SearchBox);
|
||||
const CustomHits = connectHits(Hits);
|
||||
const CustomHits = connectInfiniteHits(Hits);
|
||||
|
||||
// Custom Category Dropdown Component
|
||||
const CategoryDropdown = ({ items, currentRefinement, refine }) => {
|
||||
@@ -1108,7 +1148,7 @@ const Apps2 = (props) => {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [selectedCategory, setSelectedCategory] = useState([]);
|
||||
const [selectedLabel, setSelectedLabel] = useState([]);
|
||||
const [currTab, setCurrTab] = useState(0);
|
||||
const [currTab, setCurrTab] = useState(isLoggedIn ? 0 : 2);
|
||||
const [categories, setCategories] = useState([]);
|
||||
const [labels, setLabels] = useState([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@@ -1142,6 +1182,21 @@ const Apps2 = (props) => {
|
||||
const {themeMode, brandColor} = useContext(Context);
|
||||
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 isCloud =
|
||||
@@ -1165,6 +1220,17 @@ const Apps2 = (props) => {
|
||||
}
|
||||
}, [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(() => {
|
||||
|
||||
const getFramework = () => {
|
||||
@@ -1900,7 +1966,7 @@ const Apps2 = (props) => {
|
||||
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, }}>
|
||||
<InstantSearch searchClient={searchClient} indexName="appsearch">
|
||||
<InstantSearch key={currTab === 2 ? "active" : "inactive"} searchClient={conditionalSearchClient} indexName="appsearch">
|
||||
<AppModal
|
||||
open={openModal}
|
||||
onClose={handleAppModalClose}
|
||||
@@ -2412,24 +2478,26 @@ const Apps2 = (props) => {
|
||||
}
|
||||
|
||||
{
|
||||
currTab === 2 &&
|
||||
<CustomHits
|
||||
isLoggedIn={isLoggedIn}
|
||||
userdata={userdata}
|
||||
handleAppClick={handleAppClick}
|
||||
setIsAnyAppActivated={setIsAnyAppActivated}
|
||||
hitsPerPage={5}
|
||||
globalUrl={globalUrl}
|
||||
searchQuery={searchQuery}
|
||||
mouseHoverIndex={mouseHoverIndex}
|
||||
setMouseHoverIndex={setMouseHoverIndex}
|
||||
currTab={currTab}
|
||||
leftSideBarOpenByClick={leftSideBarOpenByClick}
|
||||
/>
|
||||
currTab === 2 && (
|
||||
<>
|
||||
<Configure clickAnalytics hitsPerPage={20} />
|
||||
<CustomHits
|
||||
isLoggedIn={isLoggedIn}
|
||||
userdata={userdata}
|
||||
handleAppClick={handleAppClick}
|
||||
setIsAnyAppActivated={setIsAnyAppActivated}
|
||||
globalUrl={globalUrl}
|
||||
searchQuery={searchQuery}
|
||||
mouseHoverIndex={mouseHoverIndex}
|
||||
setMouseHoverIndex={setMouseHoverIndex}
|
||||
currTab={currTab}
|
||||
leftSideBarOpenByClick={leftSideBarOpenByClick}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
</div >
|
||||
<Configure clickAnalytics />
|
||||
</InstantSearch>
|
||||
</div>
|
||||
</Dropzone>
|
||||
|
||||
@@ -473,6 +473,7 @@ const Docs = (defaultprops) => {
|
||||
const [hasRedirected, setHasRedirected] = useState(false)
|
||||
var isArticlePage = window.location.pathname.includes("/articles/") || window.location.pathname === "/articles" ? true : false;
|
||||
const searchFieldRef = useRef(null);
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io" || window.location.host === "migration.shuffler.io";
|
||||
|
||||
const handleDocRedirectForPartners = () => {
|
||||
if (hasRedirected) return;
|
||||
@@ -1279,6 +1280,7 @@ const Docs = (defaultprops) => {
|
||||
overflow: "hidden",
|
||||
paddingBottom: 100,
|
||||
marginLeft: mobile ? 0 : 50,
|
||||
marginTop: 50,
|
||||
textAlign: "center",
|
||||
margin: "auto",
|
||||
marginTop: 50,
|
||||
@@ -1289,6 +1291,9 @@ const Docs = (defaultprops) => {
|
||||
{showPartnerLogo === true ? null :
|
||||
<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" />
|
||||
{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" />
|
||||
</div>
|
||||
}
|
||||
@@ -1566,9 +1571,21 @@ const Docs = (defaultprops) => {
|
||||
</Button>
|
||||
</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 ?
|
||||
(
|
||||
<h4 style={{ fontWeight: 600, margin: 0, fontSize: "16px", marginBottom: "8px", color: theme.palette.text.primary, }}>Table Of Content</h4>
|
||||
<h4 style={{ marginTop: 20, fontWeight: 600, margin: 0, fontSize: "16px", marginBottom: "8px", color: theme.palette.text.primary, }}>Table Of Content</h4>
|
||||
|
||||
) : null}
|
||||
<div
|
||||
|
||||
@@ -160,6 +160,21 @@ const FreePlanCard = ({ classes }) => {
|
||||
|
||||
const MarketplaceCard = ({ classes, isCloud }) => {
|
||||
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",
|
||||
logo: "https://static.cdnlogo.com/logos/g/69/github-icon.svg",
|
||||
@@ -168,23 +183,11 @@ const MarketplaceCard = ({ classes, isCloud }) => {
|
||||
|
||||
link: "https://github.com/shuffle/shuffle/blob/main/.github/install-guide.md"
|
||||
},
|
||||
{
|
||||
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",
|
||||
logo: "https://cdn.cdnlogo.com/logos/a/12/azure.svg",
|
||||
tooltipText: "Coming soon to Azure Marketplace!",
|
||||
valid: false,
|
||||
},
|
||||
{
|
||||
name: "Google Cloud Platform",
|
||||
logo: "https://cdn.cdnlogo.com/logos/g/75/google-cloud.svg",
|
||||
tooltipText: "Coming soon to Google Cloud Marketplace!",
|
||||
valid: false,
|
||||
}
|
||||
];
|
||||
|
||||
@@ -251,6 +254,7 @@ const MarketplaceCard = ({ classes, isCloud }) => {
|
||||
className={classes.marketplaceIcon}
|
||||
style={{
|
||||
filter: option.valid === true ? null : "grayscale(1)",
|
||||
...(option.logoStyle || {}),
|
||||
}}
|
||||
/>
|
||||
<Typography className={classes.marketplaceText}>
|
||||
@@ -584,6 +588,7 @@ const LoginPage = props => {
|
||||
});
|
||||
}
|
||||
setLoginInfo(responseJson["reason"])
|
||||
setLoginLoading(false)
|
||||
} else {
|
||||
if (responseJson?.region_url !== undefined && responseJson?.region_url !== null && responseJson?.region_url !== "") {
|
||||
toast.info("Set region to " + responseJson.region_url)
|
||||
@@ -607,6 +612,7 @@ const LoginPage = props => {
|
||||
}
|
||||
else if (responseJson["reason"] !== undefined && responseJson["reason"] !== null && responseJson["reason"].includes("error")) {
|
||||
setLoginInfo(responseJson["reason"])
|
||||
setLoginLoading(false)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -687,6 +693,7 @@ const LoginPage = props => {
|
||||
});
|
||||
}
|
||||
setLoginInfo(responseJson["reason"])
|
||||
setLoginLoading(false)
|
||||
} else {
|
||||
if (responseJson["reason"] === "shuffle_account") {
|
||||
window.location.href = "/login?message=Please+login+with+your+Shuffle+account"
|
||||
@@ -812,7 +819,7 @@ const LoginPage = props => {
|
||||
width: "max-content",
|
||||
}}
|
||||
>
|
||||
<form onSubmit={onSubmit} style={{ margin: 15, width: "max-content", overflow: "hidden", textAlign: "center", }}>
|
||||
<form onSubmit={onSubmit} style={{ margin: 15, width: isMobile ? "100%" : "360px", width: "max-content", overflow: "hidden", textAlign: "center", }}>
|
||||
<img
|
||||
style={{
|
||||
height: isMobile ? 44 : 60,
|
||||
@@ -1047,7 +1054,7 @@ const LoginPage = props => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ marginTop: "10px", color: "white" }}>
|
||||
<div style={{ marginTop: 30, color: "white" }}>
|
||||
{loginInfo}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -375,7 +375,7 @@ const NewDashboard = (props) => {
|
||||
|
||||
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' }}>
|
||||
{(onboardingOpen && !loadingSelectedOrgStats) && (
|
||||
{/*(onboardingOpen && !loadingSelectedOrgStats) && (
|
||||
<DashboardOnboarding
|
||||
open={onboardingOpen}
|
||||
globalUrl={globalUrl}
|
||||
@@ -396,7 +396,7 @@ const NewDashboard = (props) => {
|
||||
headerTitle="Unlock your Dashboard"
|
||||
headerSubtitle="Complete these steps to start seeing insights."
|
||||
/>
|
||||
)}
|
||||
)*/}
|
||||
|
||||
{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 }}>
|
||||
|
||||
@@ -95,7 +95,7 @@ const RunWorkflow = (defaultprops) => {
|
||||
const answer = searchParams.get("answer")
|
||||
const execution_id = searchParams.get("reference_execution")
|
||||
const authorization = searchParams.get("authorization")
|
||||
const sourceNode = searchParams.get("source_node")
|
||||
const sourceNode = searchParams.get("source_node") || searchParams.get("start")
|
||||
const decisionId = searchParams.get("decision_id") // ONLY for agentic workflows
|
||||
const backendUrl = searchParams.get("backend_url") || globalUrl
|
||||
|
||||
@@ -847,7 +847,9 @@ const RunWorkflow = (defaultprops) => {
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson.success === false) {
|
||||
toast.warn("Failed getting the workflow. Please contact support@shuffler.io if this persists.")
|
||||
if (workflow_id !== execution_id) {
|
||||
toast.warn("Failed getting the workflow. Please contact support@shuffler.io if this persists.")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1182,9 +1184,9 @@ const RunWorkflow = (defaultprops) => {
|
||||
if (props.match.params.key === undefined) {
|
||||
setExplorerUi(true)
|
||||
|
||||
if (isLoggedIn) {
|
||||
loadForms(userdata.active_org.id)
|
||||
handleGetOrg(userdata.active_org.id)
|
||||
if (isLoggedIn && userdata?.active_org?.id) {
|
||||
loadForms(userdata?.active_org?.id)
|
||||
handleGetOrg(userdata?.active_org?.id)
|
||||
}
|
||||
|
||||
return
|
||||
@@ -1269,24 +1271,28 @@ const RunWorkflow = (defaultprops) => {
|
||||
|
||||
//console.log("IMG: ", image, "ORG: ", selectedOrganization)
|
||||
|
||||
if (!disabledButtons && answer !== undefined && answer !== null && organization !== "Unknown" && buttonClicked.length === 0) {
|
||||
console.log("Finding button!")
|
||||
// Find the button
|
||||
var buttonid = ""
|
||||
useEffect(() => {
|
||||
if (disabledButtons || answer === undefined || answer === null || organization === "Unknown" || buttonClicked.length > 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// Show rejection form for answer=false instead of auto-clicking
|
||||
if (answer === "false") {
|
||||
buttonid = "abort_execution"
|
||||
} else if (answer === "true") {
|
||||
return
|
||||
}
|
||||
|
||||
var buttonid = ""
|
||||
if (answer === "true") {
|
||||
buttonid = "continue_execution"
|
||||
}
|
||||
|
||||
if (buttonid !== "") {
|
||||
const foundButton = document.getElementById(buttonid)
|
||||
console.log("Button: ", foundButton)
|
||||
if (foundButton !== undefined && foundButton !== null) {
|
||||
foundButton.click()
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [disabledButtons, answer, organization, buttonClicked])
|
||||
|
||||
const FormList = () => {
|
||||
return (
|
||||
@@ -1697,61 +1703,103 @@ const RunWorkflow = (defaultprops) => {
|
||||
}
|
||||
|
||||
{disabledButtons ? null :
|
||||
<Typography variant="body2" color="textSecondary" style={{textAlign: "center", marginTop: 10, }}>
|
||||
What do you want to do?
|
||||
</Typography>
|
||||
answer === "false" ?
|
||||
<Typography variant="body2" color="textSecondary" style={{textAlign: "center", marginTop: 10, }}>
|
||||
Why are you declining?
|
||||
</Typography>
|
||||
:
|
||||
<Typography variant="body2" color="textSecondary" style={{textAlign: "center", marginTop: 10, }}>
|
||||
What do you want to do?
|
||||
</Typography>
|
||||
}
|
||||
|
||||
<div fullWidth style={{width: "100%", marginTop: 10, marginBottom: 10, display: "flex", }}>
|
||||
<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)
|
||||
{answer === "false" ?
|
||||
<div style={{width: "100%", marginTop: 10, marginBottom: 10, }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
minRows={3}
|
||||
maxRows={6}
|
||||
variant="outlined"
|
||||
placeholder="Provide a reason for declining (optional)"
|
||||
value={executionArgument || ""}
|
||||
onChange={(e) => {
|
||||
setExecutionArgument(e.target.value)
|
||||
}}
|
||||
style={{marginBottom: 10, }}
|
||||
/>
|
||||
<Button
|
||||
fullWidth
|
||||
id="abort_execution"
|
||||
variant="contained"
|
||||
disabled={disabledButtons}
|
||||
color="primary"
|
||||
style={{textTransform: "none", }}
|
||||
onClick={() => {
|
||||
setTimeout(() => {
|
||||
setButtonClicked("ABORTED")
|
||||
setExecutionData({
|
||||
status: "ABORTED",
|
||||
})
|
||||
}, 2500)
|
||||
|
||||
onSubmit(null, execution_id, authorization, true)
|
||||
onSubmit(null, execution_id, authorization, false)
|
||||
}}>
|
||||
Confirm Decline
|
||||
</Button>
|
||||
</div>
|
||||
:
|
||||
<div fullWidth style={{width: "100%", marginTop: 10, marginBottom: 10, display: "flex", }}>
|
||||
<Button
|
||||
fullWidth
|
||||
id="continue_execution"
|
||||
variant="contained"
|
||||
disabled={!handleValidateForm(executionArgument) || disabledButtons}
|
||||
color="primary"
|
||||
style={{
|
||||
flex: 1,
|
||||
textTransform: "none",
|
||||
}}
|
||||
onClick={() => {
|
||||
// Timeout 2500 just in case
|
||||
setTimeout(() => {
|
||||
setButtonClicked("FINISHED")
|
||||
setExecutionData({
|
||||
status: "FINISHED",
|
||||
})
|
||||
}, 2500)
|
||||
|
||||
onSubmit(null, execution_id, authorization, true)
|
||||
}}>
|
||||
Continue</Button>
|
||||
<Typography variant="body1" style={{marginLeft: 3, marginRight: 3, marginTop: 3, }}>
|
||||
or
|
||||
</Typography>
|
||||
<Button
|
||||
fullWidth
|
||||
id="abort_execution"
|
||||
variant="outlined"
|
||||
disabled={!handleValidateForm(executionArgument) || disabledButtons}
|
||||
color="primary"
|
||||
style={{
|
||||
flex: 1,
|
||||
textTransform: "none",
|
||||
}} onClick={() => {
|
||||
setTimeout(() => {
|
||||
setButtonClicked("ABORTED")
|
||||
setExecutionData({
|
||||
status: "ABORTED",
|
||||
})
|
||||
}, 2500)
|
||||
|
||||
onSubmit(null, execution_id, authorization, false)
|
||||
}}>
|
||||
Continue</Button>
|
||||
<Typography variant="body1" style={{marginLeft: 3, marginRight: 3, marginTop: 3, }}>
|
||||
or
|
||||
</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)
|
||||
Stop
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
|
||||
onSubmit(null, execution_id, authorization, false)
|
||||
}}>
|
||||
Stop
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{handleValidateForm(executionArgument) === false && disabledButtons === false ?
|
||||
{answer !== "false" && handleValidateForm(executionArgument) === false && disabledButtons === false ?
|
||||
<Typography variant="body2" color="textSecondary" style={{textAlign: "center", marginTop: 10, underline: "1px solid grey", }}>
|
||||
All required questions have not been answered yet.
|
||||
</Typography>
|
||||
|
||||
@@ -25,6 +25,8 @@ const SetAuthentication = (props) => {
|
||||
const [loadFail, setLoadFail] = 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 alert = useAlert();
|
||||
|
||||
@@ -107,10 +109,16 @@ const SetAuthentication = (props) => {
|
||||
// Set the orgsession to be a cookie for __session cookie
|
||||
|
||||
setTimeout(() => {
|
||||
// Not sure this does anything lol
|
||||
document.cookie = "__session=" + orgsession + "; path=/; max-age=1800; SameSite=Lax"; // Cookie expires in 30 min
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const fromSource = urlParams.get("source")
|
||||
if (fromSource === "shuffle") {
|
||||
setShowShuffle(true)
|
||||
}
|
||||
|
||||
if (appid === null) {
|
||||
setLoadFail(
|
||||
<span>
|
||||
@@ -146,23 +154,29 @@ const SetAuthentication = (props) => {
|
||||
const appname = app.name !== undefined ? app.name : ""
|
||||
const appLink = "/apps/" + app.id || ""
|
||||
|
||||
console.log("App: ", app)
|
||||
|
||||
return (
|
||||
<div style={{width: 1000, margin: "auto", paddingTop: 50, }}>
|
||||
<div style={{width: 1000, margin: "auto", paddingTop: 50, backgroundColor: theme.palette.backgroundColor, height: "200vh",}}>
|
||||
{loadFail !== "" ?
|
||||
loadFail
|
||||
:
|
||||
<><div>
|
||||
<Typography variant="h4" style={{ marginBottom: 20, }}>
|
||||
You are invited to: Configure <a href={appLink} target="_blank" style={{ color: '#FF8444', textDecoration: 'none' }}>{appname}</a> Authentication
|
||||
{showShuffle ?
|
||||
<span><a href={"https://shuffler.io"} target="_blank" style={{color: "#ff8444", textDecoration: "none", }}>Shuffle</a> Core handles this type of login to keep it securely stored.</span>
|
||||
:
|
||||
<span>You are invited to: Configure <a href={appLink} target="_blank" style={{ color: '#FF8444', textDecoration: 'none' }}>{appname}</a> Authentication</span>
|
||||
}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="h6" style={{ marginBottom: 20, }}>
|
||||
What does this mean?
|
||||
</Typography>
|
||||
<Typography variant="body1" style={{ marginBottom: 20, color: "rgba(255,255,255,0.4)", }}>
|
||||
A Shuffle Organization has invited you to configure authentication for this app so that they can use this authentication in one of their workflows.
|
||||
{showShuffle ?
|
||||
`By authenticating, you make it possible for Shuffle Security to use ${appname} in your security automation. Click the button below to get started. After authentication is done, you may close this window.`
|
||||
:
|
||||
"A Shuffle Organization has invited you to configure authentication for this app so that they can use this authentication in one of their workflows."
|
||||
}
|
||||
</Typography>
|
||||
|
||||
<Typography variant="body1" style={{ marginBottom: 20, }}>
|
||||
@@ -171,26 +185,38 @@ const SetAuthentication = (props) => {
|
||||
:
|
||||
app.authentication.type === "oauth2" || app.authentication.type === "oauth2-app" ?
|
||||
<AuthenticationOauth2
|
||||
selectedApp={app}
|
||||
selectedApp={{
|
||||
...app,
|
||||
"name": app.name?.replaceAll(" ", "_")
|
||||
}}
|
||||
selectedAction={{
|
||||
"app_name": app.name,
|
||||
"app_name": app.name?.replaceAll(" ", "_"),
|
||||
"app_id": app.id,
|
||||
"app_version": app.version,
|
||||
"app_version": app.app_version,
|
||||
"large_image": app.large_image,
|
||||
}}
|
||||
authenticationType={app.authentication}
|
||||
isCloud={true}
|
||||
authButtonOnly={true}
|
||||
getAppAuthentication={undefined} />
|
||||
getAppAuthentication={undefined}
|
||||
autoAuth={true}
|
||||
/>
|
||||
:
|
||||
<AuthenticationWindow
|
||||
globalUrl={globalUrl}
|
||||
selectedApp={app}
|
||||
selectedApp={{
|
||||
...app,
|
||||
"name": app.name?.toLowerCase()?.replaceAll(" ", "_"),
|
||||
}}
|
||||
authFieldsOnly={true}
|
||||
getAppAuthentication={undefined}
|
||||
appAuthentication={appAuthentication} />}
|
||||
</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, }}>
|
||||
What can they do with this?
|
||||
|
||||
@@ -751,12 +751,15 @@ const UsecaseListComponent = (props) => {
|
||||
return (
|
||||
<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, }}>
|
||||
<b>{index+1}. {usecase.name.slice(3, 100)}</b>
|
||||
<b>{index+1}. {usecase[1]?.name === "." ? usecase?.name.slice(3, 100) : usecase.name}</b>
|
||||
</Typography>
|
||||
<Grid container spacing={1}>
|
||||
|
||||
{usecase.list.map((subcase, subindex) => {
|
||||
const selectedItem = subindex === expandedItem && index === expandedIndex
|
||||
if (subcase?.disabled === true) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (subcase.matches === undefined || subcase.matches === null) {
|
||||
subcase.matches = []
|
||||
|
||||
+184
-82
@@ -4,7 +4,7 @@ import WelcomeForm2 from "../components/WelcomeForm2.jsx";
|
||||
import AppFramework from "../components/AppFramework.jsx";
|
||||
import {isMobile} from "react-device-detect";
|
||||
import {
|
||||
ArrowForwardIos as ArrowForwardIosIcon,
|
||||
HelpOutline as HelpOutlineIcon,
|
||||
} from '@mui/icons-material';
|
||||
|
||||
import {
|
||||
@@ -20,13 +20,17 @@ import {
|
||||
Stepper,
|
||||
Step,
|
||||
StepLabel,
|
||||
Tooltip,
|
||||
} from '@mui/material';
|
||||
import theme from '../theme.jsx';
|
||||
import { useNavigate, Link } from "react-router-dom";
|
||||
import { getTheme } from '../theme.jsx';
|
||||
import { useNavigate, Link, useLocation } from "react-router-dom";
|
||||
import ArrowBackIosIcon from '@mui/icons-material/ArrowBackIos';
|
||||
import Drift from "react-driftjs";
|
||||
import { Context } from "../context/ContextApi.jsx";
|
||||
|
||||
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 [skipped, setSkipped] = React.useState(new Set());
|
||||
const [inputUsecase, setInputUsecase] = useState({});
|
||||
@@ -41,6 +45,8 @@ const Welcome = (props) => {
|
||||
const [workflows, setWorkflows] = React.useState([]);
|
||||
|
||||
let navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
useEffect(() => {
|
||||
if (checkLogin !== undefined) {
|
||||
checkLogin()
|
||||
@@ -268,9 +274,11 @@ const Welcome = (props) => {
|
||||
getFramework()
|
||||
getApps()
|
||||
getAvailableWorkflows()
|
||||
}, [])
|
||||
|
||||
if (window.location.search !== undefined && window.location.search !== null) {
|
||||
const urlSearchParams = new URLSearchParams(window.location.search);
|
||||
useEffect(() => {
|
||||
if (location.search !== undefined && location.search !== null) {
|
||||
const urlSearchParams = new URLSearchParams(location.search);
|
||||
const params = Object.fromEntries(urlSearchParams.entries());
|
||||
const foundTab = params["tab"];
|
||||
|
||||
@@ -288,9 +296,13 @@ const Welcome = (props) => {
|
||||
|
||||
//setRenderDone(
|
||||
|
||||
} else {
|
||||
setShowWelcome(false)
|
||||
}
|
||||
} else {
|
||||
setShowWelcome(false)
|
||||
}
|
||||
}, [])
|
||||
}, [location.search])
|
||||
|
||||
const isStepSkipped = step => {
|
||||
return skipped.has(step)
|
||||
@@ -339,6 +351,59 @@ const Welcome = (props) => {
|
||||
|
||||
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 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 (
|
||||
<div style={{ margin: "auto", paddingBottom: 150, minHeight: 1500, paddingTop: 50, }}>
|
||||
{/*
|
||||
@@ -382,6 +447,33 @@ const Welcome = (props) => {
|
||||
})}
|
||||
</Stepper>
|
||||
</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 item xs={window.location.href.includes("tab=2") ? 6 : 12}>
|
||||
<div>
|
||||
@@ -423,87 +515,97 @@ const Welcome = (props) => {
|
||||
</div>
|
||||
:
|
||||
<Fade in={true}>
|
||||
<div style={{maxWidth: isMobile ? null : 590, margin: "auto", marginTop: 50,textAlign : isMobile ? "center" : null }}>
|
||||
<Typography variant="h4" style={{color: "#F1F1F1", marginTop: 50, fontSize: 32}}>
|
||||
Help us get to know you
|
||||
<div style={{maxWidth: isMobile ? null : 620, margin: "auto", marginTop: 70, padding: isMobile ? "0 16px" : 0}}>
|
||||
<Typography variant="h4" style={{color: theme.palette.textColor, marginTop: 50, fontSize: isMobile ? 26 : 34, fontWeight: 700, lineHeight: 1.25}}>
|
||||
What do you want to do with Shuffle?
|
||||
</Typography>
|
||||
<Typography variant="body1" style={{color: "#9E9E9E", marginTop: 12 ,marginBottom: 50, fontSize: 16}}>
|
||||
Let us help you create a smoother journey.
|
||||
<Typography variant="body1" style={{color: theme.palette.welcomeCardSubtextColor, marginTop: 12, marginBottom: 36, fontSize: 16, fontFamily: theme?.typography?.fontFamily}}>
|
||||
Let us help you create a smoother journey.
|
||||
</Typography>
|
||||
{/* <Typography variant="body1" style={{color: "#9E9E9E", marginTop: 12 ,marginBottom: 50, fontSize: 16}}>
|
||||
We will use this information to personalize your automation
|
||||
</Typography> */}
|
||||
<div style={{display: isMobile ? null : "flex", marginTop: 70, width: isMobile ? 280 : 700, margin: "auto",}}>
|
||||
<div style={{border: "2px solid #806BFF", borderRadius: theme.palette?.borderRadius, }}>
|
||||
<Card style={paperObject} onClick={() => {
|
||||
if (isCloud) {
|
||||
ReactGA.event({
|
||||
category: "welcome",
|
||||
action: "click_welcome_continue",
|
||||
label: "",
|
||||
})
|
||||
} else {
|
||||
//setActiveStep(1)
|
||||
}
|
||||
|
||||
navigate("/welcome?tab=2")
|
||||
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: "",
|
||||
})
|
||||
}
|
||||
<div style={{display: "flex", flexDirection: "column", gap: 20, marginTop : 50}}>
|
||||
|
||||
navigate("/workflows?message=Skipped intro")
|
||||
}}>
|
||||
<CardActionArea style={actionObject}>
|
||||
<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 }} />
|
||||
<Typography variant="h4" style={{color: "#F1F1F1"}}>
|
||||
Experienced
|
||||
</Typography>
|
||||
<Typography variant="body1" style={{marginTop: 10, color: "rgba(255,255,255,0.8)"}}>
|
||||
Head to Shuffle right away
|
||||
</Typography>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
</div>
|
||||
{welcomeCardsOptions.map((card) => (
|
||||
<div key={card?.title?.replace(" ", "-")} style={{position: "relative"}}>
|
||||
{card.badge && (
|
||||
<div style={{
|
||||
position: "absolute",
|
||||
top: -14,
|
||||
left: 18,
|
||||
backgroundColor: "#F2653B",
|
||||
color: "#fff",
|
||||
borderRadius: 20,
|
||||
padding: "4px 14px",
|
||||
fontSize: 13,
|
||||
fontWeight: 600,
|
||||
zIndex: 1,
|
||||
letterSpacing: "0.01em",
|
||||
}}>
|
||||
{card.badge}
|
||||
</div>
|
||||
)}
|
||||
<Card
|
||||
style={{
|
||||
backgroundColor: theme.palette.platformColor,
|
||||
backgroundImage: "none",
|
||||
borderRadius: 8,
|
||||
boxShadow: "none",
|
||||
}}
|
||||
onClick={card.onClick}
|
||||
>
|
||||
<CardActionArea
|
||||
style={{
|
||||
padding: "20px 20px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-start",
|
||||
borderRadius: 12,
|
||||
position: "relative",
|
||||
}}
|
||||
>
|
||||
<div style={{display: "flex", alignItems: "center", gap: 18}}>
|
||||
<div style={{
|
||||
width: 52,
|
||||
height: 52,
|
||||
minWidth: 52,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
borderRadius: 10,
|
||||
}}>
|
||||
{card.icon}
|
||||
</div>
|
||||
<div style={{textAlign: "left"}}>
|
||||
<Typography style={{color: theme.palette?.actionSidebarField?.color, fontWeight: 700, fontSize: 18, lineHeight: 1.3}}>
|
||||
{card.title}
|
||||
</Typography>
|
||||
<Typography style={{color: theme.palette.welcomeCardSubtextColor, fontSize: 13, marginTop: 6}}>
|
||||
{card.subtitle}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
<Tooltip title={card.tooltip} placement="right"
|
||||
componentsProps={{
|
||||
tooltip: {
|
||||
sx: {
|
||||
backgroundColor: "rgba(33, 33, 33, 1)",
|
||||
color: "rgba(241, 241, 241, 1)",
|
||||
fontSize: 14,
|
||||
border: "1px solid rgba(73, 73, 73, 1)",
|
||||
fontFamily: theme?.typography?.fontFamily,
|
||||
}
|
||||
},
|
||||
}}
|
||||
arrow>
|
||||
<div style={{ position: "absolute", top: 16, right: 16, display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||
<HelpOutlineIcon style={{color: theme.palette.welcomeCardSubtextColor, fontSize: 22}} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</CardActionArea>
|
||||
</Card>
|
||||
</div>
|
||||
))}
|
||||
|
||||
|
||||
<div style={{ flexDirection: "row", textAlign: isMobile ? "center" : null, margin: isMobile ? "auto" : null }}>
|
||||
<Button variant="contained" type="submit" fullWidth style={buttonStyle} onClick={() => {
|
||||
navigate(`/welcome?tab=2`)
|
||||
setActiveStep(1)
|
||||
setShowWelcome(true)
|
||||
ReactGA.event({
|
||||
category: "welcome",
|
||||
action: "click_welcome_continue",
|
||||
label: "",
|
||||
})
|
||||
}}>
|
||||
Continue
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Fade>
|
||||
|
||||
@@ -134,7 +134,7 @@ import {green, yellow, red, grey, triggers as wfTriggers, } from "../views/Angul
|
||||
import Licensed from "../components/Licensed.jsx";
|
||||
|
||||
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e");
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "33e4e3564f4f060e96e0531957bed552");
|
||||
|
||||
const svgSize = 24;
|
||||
const imagesize = 23;
|
||||
@@ -633,10 +633,13 @@ export const GetIconInfo = (action) => {
|
||||
svgViewBox: "0 0 48 48",
|
||||
},
|
||||
search: {
|
||||
icon: "M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z",
|
||||
icon: "M21 29C25.4183 29 29 25.4183 29 21C29 16.5817 25.4183 13 21 13C16.5817 13 13 16.5817 13 21C13 25.4183 16.5817 29 21 29Z M33 33L26.657 26.657",
|
||||
iconColor: "white",
|
||||
iconBackgroundColor: "green",
|
||||
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: {
|
||||
icon: "",
|
||||
@@ -1211,6 +1214,16 @@ const Workflows2 = (props) => {
|
||||
};
|
||||
|
||||
const handleCreateWorkflow = () => {
|
||||
|
||||
if (isCloud) {
|
||||
ReactGA.event({
|
||||
category: "Workflow",
|
||||
action: "Create_Workflow",
|
||||
label: "Create New Workflow Button"
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
setModalOpen(true)
|
||||
setIsEditing(false)
|
||||
setNewWorkflowName("")
|
||||
@@ -2932,6 +2945,16 @@ const Workflows2 = (props) => {
|
||||
}
|
||||
|
||||
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, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
@@ -3158,6 +3181,14 @@ const Workflows2 = (props) => {
|
||||
sx={{ backgroundColor: theme.palette.textFieldStyle.backgroundColor, color: theme.palette.text.primary, "&:hover": {backgroundColor: theme.palette.hoverColor} }}
|
||||
disabled={isDistributed}
|
||||
onClick={(event) => {
|
||||
if (isCloud) {
|
||||
ReactGA.event({
|
||||
category: "Workflow",
|
||||
action: "Edit_Workflow_Details",
|
||||
label: "Editing a workflow from the workflow list",
|
||||
});
|
||||
}
|
||||
|
||||
event.stopPropagation()
|
||||
if (data.actions !== undefined && data.actions !== null && data.actions.length > 0 && data.image !== "") {
|
||||
setEditing(data)
|
||||
@@ -3451,6 +3482,15 @@ const Workflows2 = (props) => {
|
||||
|
||||
|
||||
<Tooltip arrow
|
||||
onClick={() => {
|
||||
if (isCloud) {
|
||||
ReactGA.event({
|
||||
category: "Workflow",
|
||||
action: "View_Page",
|
||||
label: "Workflow Editor",
|
||||
});
|
||||
}
|
||||
}}
|
||||
onMouseEnter={() => {
|
||||
/*
|
||||
if (data.image === undefined || data.image === null || data.image === "" && !loadingWorkflows.includes(data.id)) {
|
||||
@@ -4048,7 +4088,7 @@ const Workflows2 = (props) => {
|
||||
|
||||
setLoadWorkflowsModalOpen(false);
|
||||
|
||||
if (workflowids.length > 0) {
|
||||
if (workflowids?.length > 0) {
|
||||
setHighlightIds(workflowids)
|
||||
}
|
||||
};
|
||||
@@ -5751,7 +5791,7 @@ const Workflows2 = (props) => {
|
||||
view === "grid" && currTab !== 2 ? (
|
||||
<>
|
||||
{currTab === 4 && backgroundWorkflows.map((data, index) => {
|
||||
if (data.triggers.length === 0) {
|
||||
if (data?.triggers === null || data?.triggers === undefined || data?.triggers?.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user