shaffuru files sync

This commit is contained in:
Lalit Deore
2026-05-07 15:27:32 +05:30
parent 0bc92c9fa7
commit 4cff6addde
54 changed files with 5857 additions and 3687 deletions
+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)
+389 -71
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,
@@ -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"
@@ -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