Bumped shuffle-shared

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

After

Width:  |  Height:  |  Size: 4.7 KiB

+4
View File
@@ -142,6 +142,10 @@ const AdminNavBar = (props) => {
// navigate(`?tab=organization`, { replace: true });
// }
// }
const adminTab = queryParams.get('admin_tab');
if (adminTab) {
setSelectedItem("Organization");
}
if (partnerTab) {
setSelectedItem("Partner");
}
+1 -1
View File
@@ -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);
}
+55 -138
View File
@@ -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,
@@ -284,100 +263,22 @@ const AppAuthTab = memo((props) => {
editAuthenticationConfig(id, "suborg_distribute", [...new Set(selectedSubOrg)])
}
const cacheDistributionModal = showDistributionPopup ? (
<Dialog
open={showDistributionPopup}
onClose={() => {setShowDistributionPopup(false);setSelectedAuthId("")}}
PaperProps={{
sx: {
borderRadius: theme?.palette?.DialogStyle?.borderRadius,
border: theme?.palette?.DialogStyle?.border,
fontFamily: theme?.typography?.fontFamily,
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
zIndex: 1000,
minWidth: "600px",
minHeight: "320px",
overflow: "auto",
'& .MuiDialogContent-root': {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
},
'& .MuiDialogTitle-root': {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
},
'& .MuiDialogActions-root': {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
},
},
}}
>
<DialogTitle>
<Typography variant="h5" color="textPrimary">
Select sub-org to distribute Datastore key
</Typography>
</DialogTitle>
<DialogContent style={{ color: "rgba(255,255,255,0.65)" }}>
<MenuItem value="none" onClick={()=> {handleSelectSubOrg(null, "none")}}>None</MenuItem>
<MenuItem value="all" onClick={()=> {handleSelectSubOrg(null, "all")}}>All</MenuItem>
{userdata.orgs.map((data, index) => {
if (data.creator_org !== userdata.active_org.id) {
return null;
}
const imagesize = 22;
const imageStyle = {
width: imagesize,
height: imagesize,
pointerEvents: "none",
marginRight: 10,
marginLeft: data.id === userdata.active_org.id ? 0 : 20,
};
const image = data.image === "" ? (
<img alt={data.name} src={theme.palette.defaultImage} style={imageStyle} />
) : (
<img alt={data.name} src={data.image} style={imageStyle} />
);
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 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 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));
}}
/>
+1 -1
View File
@@ -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={() => {
+93 -140
View File
@@ -1,10 +1,9 @@
import React, { useEffect, useState, useRef } from "react";
import React, { useEffect, useState, useRef, useMemo } from "react";
import theme from "../theme.jsx";
import 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>
+1 -1
View File
@@ -35,7 +35,7 @@ import { Context } from '../context/ContextApi.jsx';
const searchClient = algoliasearch(
"JNSS5CFDZZ",
"c8f882473ff42d41158430be09ec2b4e"
"33e4e3564f4f060e96e0531957bed552"
);;
const AppModal = ({ open, onClose, app, globalUrl, getApps}) => {
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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)
+393 -75
View File
@@ -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,
@@ -81,7 +82,7 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) =>
}
const themeMode = theme.palette.mode;
const workflowActive = selectedOrganization?.sync_features?.workflow_executions?.active;
const workflowActive = selectedOrganization?.sync_features?.app_executions?.active;
const multiTenantActive = selectedOrganization?.sync_features?.multi_tenant?.active;
const multiEnvActive = selectedOrganization?.sync_features?.multi_env?.active;
const brandingActive = selectedOrganization?.sync_features?.branding?.active;
@@ -104,9 +105,9 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) =>
const features = [
{
icon: ZapIcon,
label: 'Workflow Executions',
licensed: `${selectedOrganization?.sync_features?.workflow_executions?.limit}/month limit`,
unlicensed: '10,000/month limit',
label: 'App Runs',
licensed: `${selectedOrganization?.sync_features?.app_executions?.limit}/month limit`,
unlicensed: '25,000/month limit',
isActive: workflowActive,
},
{
@@ -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 }}>
+16
View File
@@ -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]) {
+100 -155
View File
@@ -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)
@@ -940,8 +939,6 @@ const CacheView = memo((props) => {
selected_suborgs: selectedSubOrg,
category: category === undefined || category === "" || category === "default" ? "" : category,
}
console.log("data: ", data);
const url = `${globalUrl}/api/v1/orgs/${orgId}/cache/config`;
@@ -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"
+4
View File
@@ -777,6 +777,10 @@ const CloudSyncTab = (props) => {
newname = "app runs"
}
if (key === "onprem_app_executions" && userdata.support !== true) {
return null
}
const griditem = {
primary: newkey,
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 -99
View File
@@ -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;
+2 -101
View File
@@ -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
+13 -101
View File
@@ -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, }}>
+56 -10
View File
@@ -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', }}>
+80 -150
View File
@@ -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
+212 -14
View File
@@ -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]);
@@ -120,7 +137,7 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, SHUFFLE_V
setCurrentSelectedTheme(userdata?.theme);
}
}, [userdata]);
const CustomPopper = (props) => {
@@ -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");
@@ -2290,4 +2488,4 @@ const ModalView = memo(({searchBarModalOpen, setSearchBarModalOpen, globalUrl, s
</Dialog>
)
)
});
});
+106 -51
View File
@@ -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}
+76 -46
View File
@@ -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";
@@ -478,7 +479,7 @@ const AuthenticationOauth2 = (props) => {
if (prompt !== undefined && prompt !== null && prompt.length > 0) {
defaultPrompt = prompt
}
var url = `${authenticationType.redirect_uri}?client_id=${client_id}&redirect_uri=${redirectUri}&response_type=code&prompt=${defaultPrompt}&scope=${resources}&state=${state}&access_type=offline`;
if (admin_consent === true) {
console.log("Running Oauth2 WITH admin consent")
@@ -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")
setButtonClicked(false);
if (getAppAuthentication !== undefined) {
// This should be orgId, not action Id as to load auth properly
if (workflow !== undefined && workflow !== null && workflow.org_id !== undefined && workflow.org_id !== null && workflow.org_id.length > 0) {
getAppAuthentication(true, true, true, workflow.org_id)
} else {
getAppAuthentication(true, true, true)
}
}
toast.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;
+1 -1
View File
@@ -47,7 +47,7 @@ const chipStyle = {
backgroundColor: "#3d3f43", height: 30, marginRight: 5, paddingLeft: 5, paddingRight: 5, height: 28, cursor: "pointer", borderColor: "#3d3f43", color: "white",
}
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
const searchClient = algoliasearch("JNSS5CFDZZ", "33e4e3564f4f060e96e0531957bed552")
const SearchData = props => {
const { serverside, globalUrl, userdata } = props
let navigate = useNavigate();
+1 -2
View File
@@ -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",
@@ -168,4 +167,4 @@ const SearchField = props => {
)
}
export default SearchField;
export default SearchField;
+31 -11
View File
@@ -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})&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;{validation === true ? <span style={{ color: "lightgreen" }}>Valid JSON</span> : <span style={{ color: "red" }}>Invalid JSON</span>}
</DialogTitle>
</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;
File diff suppressed because it is too large Load Diff
+67 -96
View File
@@ -1,11 +1,10 @@
import React, { useState, useEffect, useContext, memo } from "react";
import { 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' }}>
+14 -104
View File
@@ -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>
)
}
+1 -1
View File
@@ -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
+23 -12
View File
@@ -1,16 +1,16 @@
import { useEffect, useContext } from "react";
import React from "react";
import {
Typography,
Switch,
Button,
Tooltip,
TextField,
Grid,
import {
Typography,
Switch,
Button,
Tooltip,
TextField,
Grid,
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";
@@ -28,7 +28,13 @@ 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
View File
@@ -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) => {
},
},
});
}
+126 -42
View File
@@ -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&note=${encodeURIComponent(JSON.stringify(newArgument))}&agentic=true&decision_id=${decisionId}`
// workflow_id param > execution.workflow.id > executionId
const foundWorkflowId = workflowIdParam !== undefined && workflowIdParam !== null && workflowIdParam !== "" ? workflowIdParam : execution?.workflow?.id !== undefined && execution?.workflow?.id !== null && execution?.workflow?.id !== "" ? execution.workflow.id : executionId
const url = `${globalUrl}/api/v1/workflows/${foundWorkflowId}/run?reference_execution=${executionId}&authorization=${authorization}&answer=true&note=${encodeURIComponent(JSON.stringify(newArgument))}&agentic=true&decision_id=${decisionId}&node_id=${nodeId}`
fetch(url, {
method: "GET",
credentials: "include",
@@ -893,15 +898,32 @@ const AgentUI = (props) => {
/>
</div>
<div style={{
minWidth: 300,
maxWidth: 300,
maxHeight: 150,
overflowX: "hidden",
overflowY: "auto",
paddingTop: defaultTopPadding,
paddingBottom: defaultTopPadding,
minWidth: 400,
maxWidth: 400,
maxHeight: 150,
overflowX: "hidden",
overflowY: "auto",
}}>
{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>
+457 -115
View File
@@ -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())
}
+81 -47
View File
@@ -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)
+24 -14
View File
@@ -4458,7 +4458,7 @@ const AppCreator = (defaultprops) => {
const hasFile = (data["file_field"] !== undefined && data["file_field"] !== null && data["file_field"].length > 0) || data["example_response"] === "shuffle_file_download"
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}
+90 -28
View File
@@ -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
+11 -7
View File
@@ -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)" }}>
+99 -31
View File
@@ -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>
+18 -1
View File
@@ -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
+21 -14
View File
@@ -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>
+2 -2
View File
@@ -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 }}>
+111 -63
View File
@@ -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, }}>
&nbsp;or&nbsp;
</Typography>
<Button
fullWidth
id="abort_execution"
variant="outlined"
disabled={!handleValidateForm(executionArgument) || disabledButtons}
color="primary"
style={{
flex: 1,
textTransform: "none",
}} onClick={() => {
setTimeout(() => {
setButtonClicked("ABORTED")
setExecutionData({
status: "ABORTED",
})
}, 2500)
onSubmit(null, execution_id, authorization, false)
}}>
Continue</Button>
<Typography variant="body1" style={{marginLeft: 3, marginRight: 3, marginTop: 3, }}>
&nbsp;or&nbsp;
</Typography>
<Button
fullWidth
id="abort_execution"
variant="outlined"
disabled={!handleValidateForm(executionArgument) || disabledButtons}
color="primary"
style={{
flex: 1,
textTransform: "none",
}} onClick={() => {
setTimeout(() => {
setButtonClicked("ABORTED")
setExecutionData({
status: "ABORTED",
})
}, 2500)
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>
+36 -10
View File
@@ -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>
@@ -145,24 +153,30 @@ 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?
+4 -1
View File
@@ -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
View File
@@ -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>
+45 -5
View File
@@ -134,7 +134,7 @@ import {green, yellow, red, grey, triggers as wfTriggers, } from "../views/Angul
import Licensed from "../components/Licensed.jsx";
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
}
+1 -1
View File
@@ -10,7 +10,7 @@ require (
github.com/gorilla/mux v1.8.1
github.com/satori/go.uuid v1.2.0
github.com/shuffle/shuffle-shared v1.2.45
github.com/shuffle/singul v0.0.30
github.com/shuffle/singul v0.0.32
k8s.io/api v0.34.2
k8s.io/apimachinery v0.34.2
k8s.io/client-go v0.34.2
+2 -2
View File
@@ -320,8 +320,8 @@ github.com/shuffle/opensearch-go/v4 v4.0.0 h1:Mh85CD1MwOgXiFFYlzS1llnvdqL3CztRdR
github.com/shuffle/opensearch-go/v4 v4.0.0/go.mod h1:gVLZKQE5khQWMb68XBtgKrhu78oLGL2zHwAGnFMDwC0=
github.com/shuffle/shuffle-shared v1.2.45 h1:d4gnF6qkrmmZVz4FEraEBzU7Kq8VbfAbiGlBoYeFD7o=
github.com/shuffle/shuffle-shared v1.2.45/go.mod h1:1DvYhS/4659FdHxzEpHTPuFUW0IfwQSuyluiGTwz9jo=
github.com/shuffle/singul v0.0.30 h1:xYTpGHWzZ9lX1P6/kJIz6kng5q6gUdua3IMhsK399e8=
github.com/shuffle/singul v0.0.30/go.mod h1:2dGXQMk8q4QOivomuJ9QkrpevpmTM1QYemJixbY5jW0=
github.com/shuffle/singul v0.0.32 h1:EoJjKCWfbtHqjo/UgofSvTp7rcYEH7k3eGXrZgXcMt8=
github.com/shuffle/singul v0.0.32/go.mod h1:An0DHl+E+bAO1ziIbqUJdLXhjixN6DOyv0GwlXwsKHc=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+190 -87
View File
@@ -193,6 +193,40 @@ func restoreActionConfig(ctx context.Context, executionID string, action *shuffl
}
}
func getAppProxyValue(primaryKey, fallbackKey string) string {
value := strings.TrimSpace(os.Getenv(primaryKey))
if len(value) > 0 {
return value
}
return strings.TrimSpace(os.Getenv(fallbackKey))
}
func appendAppProxyEnv(env []string) []string {
httpProxy := getAppProxyValue("HTTP_PROXY", "SHUFFLE_APP_HTTP_PROXY")
httpsProxy := getAppProxyValue("HTTPS_PROXY", "SHUFFLE_APP_HTTPS_PROXY")
noProxy := getAppProxyValue("NO_PROXY", "SHUFFLE_APP_NO_PROXY")
noProxyLower := getAppProxyValue("no_proxy", "SHUFFLE_APP_no_proxy")
if len(httpProxy) > 0 {
env = append(env, fmt.Sprintf("HTTP_PROXY=%s", httpProxy))
}
if len(httpsProxy) > 0 {
env = append(env, fmt.Sprintf("HTTPS_PROXY=%s", httpsProxy))
}
if len(noProxy) > 0 {
env = append(env, fmt.Sprintf("NO_PROXY=%s", noProxy))
}
if len(noProxyLower) > 0 {
env = append(env, fmt.Sprintf("no_proxy=%s", noProxyLower))
}
return env
}
// Images to be autodeployed in the latest version of Shuffle.
var autoDeploy = map[string]string{
"http:1.4.0": "frikky/shuffle:http_1.4.0",
@@ -1784,10 +1818,7 @@ func handleExecutionResult(workflowExecution shuffle.WorkflowExecution) {
if strings.ToLower(os.Getenv("SHUFFLE_PASS_APP_PROXY")) == "true" {
//log.Printf("APPENDING PROXY TO THE APP!")
env = append(env, fmt.Sprintf("HTTP_PROXY=%s", os.Getenv("HTTP_PROXY")))
env = append(env, fmt.Sprintf("HTTPS_PROXY=%s", os.Getenv("HTTPS_PROXY")))
env = append(env, fmt.Sprintf("NO_PROXY=%s", os.Getenv("NO_PROXY")))
env = append(env, fmt.Sprintf("no_proxy=%s", os.Getenv("no_proxy")))
env = appendAppProxyEnv(env)
}
overrideHttpProxy := os.Getenv("SHUFFLE_INTERNAL_HTTP_PROXY")
@@ -2280,11 +2311,6 @@ func handleSubflowPoller(ctx context.Context, workflowExecution shuffle.Workflow
}
}
if len(data) == 0 {
log.Printf("[WARNING] Stream result missing execution ID and authorization; injecting them from workflow execution")
data = fmt.Sprintf(`{"execution_id": "%s", "authorization": "%s"}`, workflowExecution.ExecutionId, workflowExecution.Authorization)
}
key := fmt.Sprintf("%s:%s", workflowExecution.ExecutionId, subflowId)
cacheKey := fmt.Sprintf("workflowexecution_%s", workflowExecution.ExecutionId)
usedCache := false
@@ -2337,6 +2363,11 @@ func handleSubflowPoller(ctx context.Context, workflowExecution shuffle.Workflow
return errors.New("Subflow status not found yet (cache)")
}
if len(data) == 0 {
log.Printf("[WARNING] Stream result missing execution ID and authorization; injecting them from workflow execution")
data = fmt.Sprintf(`{"execution_id": "%s", "authorization": "%s"}`, workflowExecution.ExecutionId, workflowExecution.Authorization)
}
req, err := http.NewRequest(
"POST",
streamResultUrl,
@@ -2601,6 +2632,112 @@ func arrayContains(visited []string, id string) bool {
return found
}
func isTerminalResultStatus(status string) bool {
return status == "SUCCESS" || status == "FINISHED" || status == "FAILURE" || status == "ABORTED"
}
func hasWaitForResultParameter(params []shuffle.WorkflowAppActionParameter) bool {
for _, param := range params {
if param.Name == "check_result" && strings.ToLower(param.Value) == "true" {
return true
}
}
return false
}
func getSubflowBarrierActionIDs(workflowExecution shuffle.WorkflowExecution) map[string]struct{} {
actionIDs := map[string]struct{}{}
for _, action := range workflowExecution.Workflow.Actions {
if action.AppName != "shuffle-subflow" && action.AppName != "shuffle-subflow-v2" && action.AppName != "Shuffle Workflow" {
continue
}
if !hasWaitForResultParameter(action.Parameters) {
continue
}
actionIDs[action.ID] = struct{}{}
}
for _, trigger := range workflowExecution.Workflow.Triggers {
if trigger.AppName != "shuffle-subflow" && trigger.AppName != "shuffle-subflow-v2" && trigger.AppName != "Shuffle Workflow" {
continue
}
if !hasWaitForResultParameter(trigger.Parameters) {
continue
}
actionIDs[trigger.ID] = struct{}{}
}
return actionIDs
}
func getSubflowBarrierProgress(workflowExecution shuffle.WorkflowExecution) (int, int, int) {
barrierActions := getSubflowBarrierActionIDs(workflowExecution)
expected := len(barrierActions)
if expected == 0 {
return 0, 0, 0
}
terminalByAction := map[string]string{}
for _, result := range workflowExecution.Results {
if _, ok := barrierActions[result.Action.ID]; !ok {
continue
}
if !isTerminalResultStatus(result.Status) {
continue
}
terminalByAction[result.Action.ID] = result.Status
}
completed := len(terminalByAction)
failed := 0
for _, status := range terminalByAction {
if status == "FAILURE" || status == "ABORTED" {
failed += 1
}
}
return expected, completed, failed
}
func enforceSubflowBarrier(workflowExecution *shuffle.WorkflowExecution) (int, int, int) {
if workflowExecution == nil {
return 0, 0, 0
}
if workflowExecution.Status == "FINISHED" || workflowExecution.Status == "FAILURE" || workflowExecution.Status == "ABORTED" {
return 0, 0, 0
}
expected, completed, failed := getSubflowBarrierProgress(*workflowExecution)
if expected == 0 {
return 0, 0, 0
}
if completed < expected {
workflowExecution.Status = "WAITING"
return expected, completed, failed
}
if workflowExecution.Status == "WAITING" {
if failed > 0 {
workflowExecution.Status = "FAILURE"
workflowExecution.Result = fmt.Sprintf("Subflow barrier failed: %d of %d subflows failed or aborted", failed, expected)
} else {
workflowExecution.Status = "EXECUTING"
}
}
return expected, completed, failed
}
func getResult(workflowExecution shuffle.WorkflowExecution, id string) shuffle.ActionResult {
for _, actionResult := range workflowExecution.Results {
if actionResult.Action.ID == id {
@@ -3712,10 +3849,7 @@ func deploySwarmService(dockercli *dockerclient.Client, name, image string, depl
}
if strings.ToLower(os.Getenv("SHUFFLE_PASS_APP_PROXY")) == "true" {
serviceSpec.TaskTemplate.ContainerSpec.Env = append(serviceSpec.TaskTemplate.ContainerSpec.Env, fmt.Sprintf("HTTP_PROXY=%s", os.Getenv("HTTP_PROXY")))
serviceSpec.TaskTemplate.ContainerSpec.Env = append(serviceSpec.TaskTemplate.ContainerSpec.Env, fmt.Sprintf("HTTPS_PROXY=%s", os.Getenv("HTTPS_PROXY")))
serviceSpec.TaskTemplate.ContainerSpec.Env = append(serviceSpec.TaskTemplate.ContainerSpec.Env, fmt.Sprintf("NO_PROXY=%s", os.Getenv("NO_PROXY")))
serviceSpec.TaskTemplate.ContainerSpec.Env = append(serviceSpec.TaskTemplate.ContainerSpec.Env, fmt.Sprintf("no_proxy=%s", os.Getenv("no_proxy")))
serviceSpec.TaskTemplate.ContainerSpec.Env = appendAppProxyEnv(serviceSpec.TaskTemplate.ContainerSpec.Env)
}
overrideHttpProxy := os.Getenv("SHUFFLE_INTERNAL_HTTP_PROXY")
@@ -4236,6 +4370,10 @@ func sendAppRequest(ctx context.Context, incomingUrl, appName string, port int,
callbackUrl := os.Getenv("SHUFFLE_WORKER_SERVER_URL")
if len(callbackUrl) > 0 {
parsedRequest.BaseUrl = callbackUrl
if parsedRequest.Action.AppName == "shuffle-subflow" || parsedRequest.Action.AppName == "shuffle-subflow-v2" || parsedRequest.Action.AppName == "User Input" {
parsedRequest.BaseUrl = fmt.Sprintf("http://%s:%d", hostname, baseport)
//parsedRequest.Url = parsedRequest.BaseUrl
}
} else if len(hostname) > 0 {
// Run with proper hostname, but set to shuffle-worker to avoid specific host target.
// This means running with VIP instead.
@@ -4462,10 +4600,7 @@ func baseDeploy() {
if strings.ToLower(os.Getenv("SHUFFLE_PASS_APP_PROXY")) == "true" {
//log.Printf("APPENDING PROXY TO THE APP!")
env = append(env, fmt.Sprintf("HTTP_PROXY=%s", os.Getenv("HTTP_PROXY")))
env = append(env, fmt.Sprintf("HTTPS_PROXY=%s", os.Getenv("HTTPS_PROXY")))
env = append(env, fmt.Sprintf("NO_PROXY=%s", os.Getenv("NO_PROXY")))
env = append(env, fmt.Sprintf("no_proxy=%s", os.Getenv("no_proxy")))
env = appendAppProxyEnv(env)
}
if len(os.Getenv("SHUFFLE_APP_SDK_TIMEOUT")) > 0 {
@@ -4932,7 +5067,7 @@ func main() {
}
}
func checkUnfinished(resp http.ResponseWriter, request *http.Request, execRequest shuffle.OrborusExecutionRequest) {
func checkUnfinished(execRequest shuffle.OrborusExecutionRequest) {
// Meant as a function that periodically checks whether previous executions have finished or not.
// Should probably be based on executedIds and finishedIds
// Schedule a check in the future instead?
@@ -4961,38 +5096,16 @@ func checkUnfinished(resp http.ResponseWriter, request *http.Request, execReques
sendResult(*exec, data)
}
func handleRunExecution(resp http.ResponseWriter, request *http.Request) {
defer request.Body.Close()
body, err := ioutil.ReadAll(request.Body)
if err != nil {
log.Printf("[WARNING] Failed reading body for stream result queue")
resp.WriteHeader(400)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err)))
return
}
//log.Printf("[DEBUG] In run execution with body length %d", len(body))
var execRequest shuffle.OrborusExecutionRequest
err = json.Unmarshal(body, &execRequest)
if err != nil {
log.Printf("[WARNING] Failed shuffle.WorkflowExecution unmarshaling: %s", err)
resp.WriteHeader(400)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err)))
return
}
func processRunExecution(execRequest shuffle.OrborusExecutionRequest) {
// Checks if a workflow is done 30 seconds later, and sends info to backend no matter what
go func() {
time.Sleep(time.Duration(30) * time.Second)
checkUnfinished(resp, request, execRequest)
checkUnfinished(execRequest)
}()
window.AddEvent(time.Now())
ctx := context.Background()
// FIXME: This should be PER EXECUTION
//if strings.ToLower(os.Getenv("SHUFFLE_PASS_APP_PROXY")) == "true" {
// Is it ok if these are standard? Should they be update-able after launch? Hmm
if len(execRequest.HTTPProxy) > 0 {
log.Printf("[DEBUG] Sending proxy info to child process")
os.Setenv("SHUFFLE_PASS_APP_PROXY", execRequest.ShufflePassProxyToApp)
@@ -5022,9 +5135,7 @@ func handleRunExecution(resp http.ResponseWriter, request *http.Request) {
baseUrl = execRequest.BaseUrl
}
// Setting to just have an auth available.
if len(execRequest.Authorization) > 0 && len(os.Getenv("AUTHORIZATION")) == 0 {
//log.Printf("[DEBUG] Sending proxy info to child process")
os.Setenv("AUTHORIZATION", execRequest.Authorization)
}
@@ -5037,71 +5148,47 @@ func handleRunExecution(resp http.ResponseWriter, request *http.Request) {
)
if err != nil {
log.Printf("[ERROR][%s] Failed to create a new request", execRequest.ExecutionId)
resp.WriteHeader(500)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err)))
log.Printf("[ERROR][%s] Failed to create stream results request: %s", execRequest.ExecutionId, err)
return
}
client := shuffle.GetExternalClient(streamResultUrl)
newresp, err := client.Do(req)
if err != nil {
log.Printf("[ERROR] Failed making request (2): %s", err)
resp.WriteHeader(500)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err)))
log.Printf("[ERROR][%s] Failed making stream results request: %s", execRequest.ExecutionId, err)
return
}
defer newresp.Body.Close()
body, err = ioutil.ReadAll(newresp.Body)
body, err := ioutil.ReadAll(newresp.Body)
if err != nil {
log.Printf("[ERROR][%s] Failed reading body (2): %s", execRequest.ExecutionId, err)
resp.WriteHeader(500)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err)))
log.Printf("[ERROR][%s] Failed reading stream results response body: %s", execRequest.ExecutionId, err)
return
}
if newresp.StatusCode != 200 {
log.Printf("[ERROR][%s] Bad statuscode: %d, %s", execRequest.ExecutionId, newresp.StatusCode, string(body))
if strings.Contains(string(body), "Workflowexecution is already finished") {
log.Printf("[DEBUG] Shutting down (19)")
//shutdown(workflowExecution, "", "", true)
}
resp.WriteHeader(500)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Bad statuscode: %d"}`, newresp.StatusCode)))
log.Printf("[ERROR][%s] Bad statuscode from stream results: %d, %s", execRequest.ExecutionId, newresp.StatusCode, string(body))
return
}
err = json.Unmarshal(body, &workflowExecution)
if err != nil {
log.Printf("[ERROR] Failed workflowExecution unmarshal: %s", err)
resp.WriteHeader(500)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err)))
log.Printf("[ERROR][%s] Failed workflowExecution unmarshal: %s", execRequest.ExecutionId, err)
return
}
//err = shuffle.SetWorkflowExecution(ctx, workflowExecution, true)
err = setWorkflowExecution(ctx, workflowExecution, true)
if err != nil {
log.Printf("[ERROR] Failed initializing execution saving for %s: %s", workflowExecution.ExecutionId, err)
log.Printf("[ERROR][%s] Failed initializing execution saving: %s", workflowExecution.ExecutionId, err)
}
if workflowExecution.Status == "FINISHED" || workflowExecution.Status == "SUCCESS" {
log.Printf("[DEBUG] Workflow %s is finished. Exiting worker.", workflowExecution.ExecutionId)
log.Printf("[DEBUG] Shutting down (20)")
resp.WriteHeader(200)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Bad status for execution - already %s. Returning with 200 OK"}`, workflowExecution.Status)))
log.Printf("[DEBUG] Workflow %s is finished before dispatch. Exiting async run setup.", workflowExecution.ExecutionId)
return
}
//startAction, extra, children, parents, visited, executed, nextActions, environments := shuffle.GetExecutionVariables(ctx, workflowExecution.ExecutionId)
extra := 0
for _, trigger := range workflowExecution.Workflow.Triggers {
//log.Printf("Appname trigger (0): %s", trigger.AppName)
if trigger.AppName == "User Input" || trigger.AppName == "Shuffle Workflow" {
extra += 1
}
@@ -5110,15 +5197,10 @@ func handleRunExecution(resp http.ResponseWriter, request *http.Request) {
log.Printf("[INFO][%s] (1) Status: %s, Results: %d, actions: %d", workflowExecution.ExecutionId, workflowExecution.Status, len(workflowExecution.Results), len(workflowExecution.Workflow.Actions)+extra)
if workflowExecution.Status != "EXECUTING" {
log.Printf("[WARNING] Exiting as worker execution has status %s!", workflowExecution.Status)
log.Printf("[DEBUG] Shutting down (38)")
resp.WriteHeader(400)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Bad status %s for the workflow execution %s"}`, workflowExecution.Status, workflowExecution.ExecutionId)))
log.Printf("[WARNING][%s] Exiting async run as execution status is %s", workflowExecution.ExecutionId, workflowExecution.Status)
return
}
//log.Printf("[DEBUG] Starting execution :O")
cacheKey := fmt.Sprintf("workflowexecution_%s", workflowExecution.ExecutionId)
execData, err := json.Marshal(workflowExecution)
if err != nil {
@@ -5133,15 +5215,36 @@ func handleRunExecution(resp http.ResponseWriter, request *http.Request) {
err = executionInit(workflowExecution)
if err != nil {
log.Printf("[DEBUG][%s] Shutting down (30) - Workflow setup failed: %s", workflowExecution.ExecutionId, err)
resp.WriteHeader(500)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "Error in execution init: %s"}`, err)))
return
//shutdown(workflowExecution, "", "", true)
}
handleExecutionResult(workflowExecution)
resp.WriteHeader(200)
resp.Write([]byte(fmt.Sprintf(`{"success": true}`)))
}
func handleRunExecution(resp http.ResponseWriter, request *http.Request) {
defer request.Body.Close()
body, err := ioutil.ReadAll(request.Body)
if err != nil {
log.Printf("[WARNING] Failed reading body for stream result queue")
resp.WriteHeader(400)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err)))
return
}
//log.Printf("[DEBUG] In run execution with body length %d", len(body))
var execRequest shuffle.OrborusExecutionRequest
err = json.Unmarshal(body, &execRequest)
if err != nil {
log.Printf("[WARNING] Failed shuffle.WorkflowExecution unmarshaling: %s", err)
resp.WriteHeader(400)
resp.Write([]byte(fmt.Sprintf(`{"success": false, "reason": "%s"}`, err)))
return
}
resp.WriteHeader(http.StatusAccepted)
resp.Write([]byte(`{"success": true, "accepted": true}`))
go processRunExecution(execRequest)
}
func handleDownloadImage(resp http.ResponseWriter, request *http.Request) {