shaffuru files sync
This commit is contained in:
@@ -142,6 +142,10 @@ const AdminNavBar = (props) => {
|
||||
// navigate(`?tab=organization`, { replace: true });
|
||||
// }
|
||||
// }
|
||||
const adminTab = queryParams.get('admin_tab');
|
||||
if (adminTab) {
|
||||
setSelectedItem("Organization");
|
||||
}
|
||||
if (partnerTab) {
|
||||
setSelectedItem("Partner");
|
||||
}
|
||||
|
||||
@@ -1977,7 +1977,7 @@ const Action = memo((
|
||||
const actionId = action.name.replace(/ /g, "-").replace(/_/g, "-");
|
||||
window.history.pushState(null, "", `#${actionId}`);
|
||||
setExampleBody(action?.example_response);
|
||||
document.getElementById(`action-list-${nextSelectedActionIndex}`).scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
document.getElementById(`action-list-${nextSelectedActionIndex}`)?.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import { isMobile } from "react-device-detect"
|
||||
import PaperComponent from "../components/PaperComponent.jsx";
|
||||
import { CodeHandler, Img, OuterLink, } from '../views/Docs.jsx'
|
||||
import { v4 as uuidv4} from "uuid";
|
||||
import DeleteConfirmDialog from "./DeleteConfirmDialog.jsx";
|
||||
|
||||
import {
|
||||
Divider,
|
||||
@@ -63,10 +64,11 @@ import {
|
||||
} from "react-instantsearch-dom";
|
||||
import aa from "search-insights";
|
||||
import { Context } from '../context/ContextApi.jsx';
|
||||
import SubOrgDistributionDialog from './SubOrgDistributionDialog.jsx';
|
||||
|
||||
const searchClient = algoliasearch(
|
||||
"JNSS5CFDZZ",
|
||||
"c8f882473ff42d41158430be09ec2b4e"
|
||||
"33e4e3564f4f060e96e0531957bed552"
|
||||
)
|
||||
|
||||
const AppAuthTab = memo((props) => {
|
||||
@@ -89,9 +91,13 @@ const AppAuthTab = memo((props) => {
|
||||
const [searchQuery, setSearchQuery] = React.useState("");
|
||||
const [showAppModal, setShowAppModal] = useState(false)
|
||||
const [selectedAuthId, setSelectedAuthId] = useState("");
|
||||
const [selectedAuthName, setSelectedAuthName] = useState("");
|
||||
const [showDistributionPopup, setShowDistributionPopup] = useState(false);
|
||||
const [showAuthenticationLoader, setShowAuthenticationLoader] = useState(true)
|
||||
const [showAppAuthGroupLoader, setShowAppAuthGroupLoader] = useState(true)
|
||||
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
||||
const [deleteConfirmTarget, setDeleteConfirmTarget] = useState(null);
|
||||
const [distribOrgOrder, setDistribOrgOrder] = useState([]);
|
||||
const { themeMode, supportEmail, brandColor } = useContext(Context)
|
||||
const theme = getTheme(themeMode, brandColor)
|
||||
|
||||
@@ -183,7 +189,7 @@ const AppAuthTab = memo((props) => {
|
||||
};
|
||||
|
||||
const deleteAuthentication = (data) => {
|
||||
toast("Deleting auth " + data.label);
|
||||
toast("Deleting auth " + data?.label);
|
||||
|
||||
// Just use this one?
|
||||
const url = globalUrl + "/api/v1/apps/authentication/" + data.id;
|
||||
@@ -213,33 +219,6 @@ const AppAuthTab = memo((props) => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleSelectSubOrg = (id, action) => {
|
||||
if (action === "all") {
|
||||
const childOrgs = userdata.orgs.filter(
|
||||
(data) => data.creator_org === userdata.active_org.id
|
||||
);
|
||||
setSelectedSubOrg((prev) => {
|
||||
if (prev.length === childOrgs.length) {
|
||||
// If all child orgs are already selected, clear the selection
|
||||
return [];
|
||||
} else {
|
||||
// Otherwise, select all child org IDs
|
||||
return childOrgs.map((data) => data.id);
|
||||
}
|
||||
});
|
||||
} else if (action === "none") {
|
||||
setSelectedSubOrg([]);
|
||||
} else {
|
||||
setSelectedSubOrg((prev) => {
|
||||
if (prev.includes(id)) {
|
||||
return prev.filter((data) => data !== id);
|
||||
} else {
|
||||
return [...prev, id];
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const editAuthenticationConfig = (id, parentAction, selectedSuborgs) => {
|
||||
const data = {
|
||||
id: id,
|
||||
@@ -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));
|
||||
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -708,7 +708,7 @@ const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud, startOpenA
|
||||
fontWeight: 500,
|
||||
fontFamily: theme?.typography?.fontFamily,
|
||||
}}>
|
||||
Generate an app based on documentation (beta)
|
||||
Generate an app based on documentation
|
||||
</Typography>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import React, { useEffect, useState, useRef } from "react";
|
||||
import React, { useEffect, useState, useRef, useMemo } from "react";
|
||||
|
||||
import theme from "../theme.jsx";
|
||||
import ReactGA from "react-ga4";
|
||||
import { Link } from "react-router-dom";
|
||||
import { removeQuery } from "../components/ScrollToTop.jsx";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { Tabs, Tab, Collapse } from "@mui/material";
|
||||
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
|
||||
@@ -27,7 +26,7 @@ import {
|
||||
InstantSearch,
|
||||
Configure,
|
||||
connectSearchBox,
|
||||
connectHits,
|
||||
connectInfiniteHits,
|
||||
connectHitInsights,
|
||||
RefinementList,
|
||||
ClearRefinements,
|
||||
@@ -39,6 +38,7 @@ import aa from "search-insights";
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import "./FilterCSS.css";
|
||||
import SearchContactForm from "../components/SearchContactForm.jsx";
|
||||
|
||||
import {
|
||||
Zoom,
|
||||
@@ -54,7 +54,7 @@ import {
|
||||
|
||||
const searchClient = algoliasearch(
|
||||
"JNSS5CFDZZ",
|
||||
"c8f882473ff42d41158430be09ec2b4e"
|
||||
"eb5fd80aa6ed5ab4730d836cff3ea283"
|
||||
);
|
||||
//const searchClient = algoliasearch("L55H18ZINA", "a19be455e7e75ee8f20a93d26b9fc6d6")
|
||||
|
||||
@@ -77,62 +77,13 @@ const AppGrid = (props) => {
|
||||
const xs =
|
||||
parsedXs === undefined || parsedXs === null ? (isMobile ? 6 : 3) : parsedXs;
|
||||
|
||||
const [formMail, setFormMail] = React.useState("");
|
||||
const [message, setMessage] = React.useState("");
|
||||
const [formMessage, setFormMessage] = React.useState("");
|
||||
const [deactivatedIndexes, setDeactivatedIndexes] = React.useState([]);
|
||||
|
||||
const buttonStyle = {
|
||||
borderRadius: 30,
|
||||
height: 50,
|
||||
width: 220,
|
||||
margin: isMobile ? "15px auto 15px auto" : 20,
|
||||
fontSize: 18,
|
||||
};
|
||||
const innerColor = "rgba(255,255,255,0.65)";
|
||||
const borderRadius = 3;
|
||||
window.title = "Shuffle | Apps | Find and integrate any app";
|
||||
const noImage = "/public/no_image.png";
|
||||
|
||||
const submitContact = (email, message) => {
|
||||
const data = {
|
||||
firstname: "",
|
||||
lastname: "",
|
||||
title: "",
|
||||
companyname: "",
|
||||
email: email,
|
||||
phone: "",
|
||||
message: message,
|
||||
};
|
||||
|
||||
const errorMessage =
|
||||
"Something went wrong. Please contact frikky@shuffler.io directly.";
|
||||
|
||||
fetch(globalUrl + "/api/v1/contact", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((response) => {
|
||||
if (response?.success === true) {
|
||||
setFormMessage(response.reason);
|
||||
//toast("Thanks for submitting!")
|
||||
} else {
|
||||
setFormMessage(errorMessage);
|
||||
}
|
||||
|
||||
setFormMail("");
|
||||
setMessage("");
|
||||
})
|
||||
.catch((error) => {
|
||||
setFormMessage(errorMessage);
|
||||
console.log(error);
|
||||
});
|
||||
};
|
||||
|
||||
const SearchBox = ({ currentRefinement, refine, isSearchStalled, searchQuery, setSearchQuery }) => {
|
||||
var defaultSearch = "";
|
||||
|
||||
@@ -148,10 +99,11 @@ const AppGrid = (props) => {
|
||||
const params = Object.fromEntries(urlSearchParams.entries());
|
||||
const foundQuery = params["q"];
|
||||
if (foundQuery !== null && foundQuery !== undefined) {
|
||||
console.log("Got query: ", foundQuery);
|
||||
refine(foundQuery);
|
||||
defaultSearch = foundQuery;
|
||||
searchQuery = foundQuery
|
||||
if (searchQuery !== foundQuery) {
|
||||
setSearchQuery(foundQuery);
|
||||
}
|
||||
}
|
||||
}
|
||||
//}, [])
|
||||
@@ -234,7 +186,13 @@ const AppGrid = (props) => {
|
||||
onChange={(event) => {
|
||||
const value = event.currentTarget.value;
|
||||
setSearchQuery(value);
|
||||
removeQuery("q");
|
||||
const urlSearchParams = new URLSearchParams(window.location.search);
|
||||
if (value) {
|
||||
urlSearchParams.set("q", value);
|
||||
} else {
|
||||
urlSearchParams.delete("q");
|
||||
}
|
||||
window.history.replaceState({}, '', `${window.location.pathname}?${urlSearchParams.toString()}`);
|
||||
debouncedRefine(value);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
@@ -252,6 +210,21 @@ const AppGrid = (props) => {
|
||||
const [currTab, setCurrTab] = useState(0);
|
||||
const location = useLocation();
|
||||
|
||||
const conditionalSearchClient = useMemo(() => ({
|
||||
...searchClient,
|
||||
search(requests) {
|
||||
if (currTab !== 0) {
|
||||
return Promise.resolve({
|
||||
results: requests.map(() => ({
|
||||
hits: [], nbHits: 0, page: 0, nbPages: 0, hitsPerPage: 0,
|
||||
processingTimeMS: 0, exhaustiveNbHits: true, query: "", params: "",
|
||||
})),
|
||||
});
|
||||
}
|
||||
return searchClient.search(requests);
|
||||
},
|
||||
}), [currTab]);
|
||||
|
||||
useEffect(() => {
|
||||
const queryParams = new URLSearchParams(location.search);
|
||||
const tabParam = queryParams.get('tab');
|
||||
@@ -269,7 +242,6 @@ const AppGrid = (props) => {
|
||||
const newQueryParam = newTab === 0 ? 'all_apps' : newTab === 1 ? 'org_apps' : 'my_apps';
|
||||
const queryParams = new URLSearchParams(location.search);
|
||||
queryParams.set('tab', newQueryParam);
|
||||
queryParams.delete('q');
|
||||
window.history.replaceState({}, '', `${location.pathname}?${queryParams.toString()}`);
|
||||
};
|
||||
|
||||
@@ -281,6 +253,8 @@ const AppGrid = (props) => {
|
||||
// Component to fetch all public app from the algolia.
|
||||
const Hits = ({
|
||||
hits,
|
||||
hasMore,
|
||||
refineNext,
|
||||
insights,
|
||||
setIsAnyAppActivated,
|
||||
searchQuery
|
||||
@@ -288,6 +262,32 @@ const AppGrid = (props) => {
|
||||
const [mouseHoverIndex, setMouseHoverIndex] = useState(-1);
|
||||
var counted = 0;
|
||||
const [hoverEffect, setHoverEffect] = useState(-1);
|
||||
const [isLoadingMore, setIsLoadingMore] = useState(false);
|
||||
const loadMoreRef = useRef(null);
|
||||
const scrollContainerRef = useRef(null);
|
||||
const isFetchingMore = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
isFetchingMore.current = false;
|
||||
setIsLoadingMore(false);
|
||||
}, [hits.length]);
|
||||
|
||||
useEffect(() => {
|
||||
return; // infinite scroll disabled
|
||||
if (!loadMoreRef.current || !scrollContainerRef.current) return;
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
if (entries[0].isIntersecting && hasMore && !isFetchingMore.current) {
|
||||
isFetchingMore.current = true;
|
||||
setIsLoadingMore(true);
|
||||
refineNext();
|
||||
}
|
||||
},
|
||||
{ root: scrollContainerRef.current, rootMargin: "200px" }
|
||||
);
|
||||
observer.observe(loadMoreRef.current);
|
||||
return () => observer.disconnect();
|
||||
}, [hasMore, refineNext]);
|
||||
|
||||
const normalizedString = (name) => {
|
||||
if (typeof name === 'string') {
|
||||
@@ -359,11 +359,11 @@ const AppGrid = (props) => {
|
||||
} else {
|
||||
//toast.success(`App ${type}d Successfully!`);
|
||||
if (type === 'activate') {
|
||||
setAllActivatedAppIds(prev => [...prev, data.objectID]);
|
||||
setAllActivatedAppIds(prev => [...(prev || []), data.objectID]);
|
||||
setIsAnyAppActivated(true);
|
||||
}
|
||||
if (type === 'deactivate') {
|
||||
const updatedIds = allActivatedAppIds.filter(id => id !== data.objectID);
|
||||
const updatedIds = (allActivatedAppIds || []).filter(id => id !== data.objectID);
|
||||
setAllActivatedAppIds(updatedIds);
|
||||
}
|
||||
}
|
||||
@@ -373,6 +373,16 @@ const AppGrid = (props) => {
|
||||
});
|
||||
}
|
||||
|
||||
const sortedHits = useMemo(() => {
|
||||
const list = [...(hits || [])];
|
||||
if (!allActivatedAppIds?.length) return list;
|
||||
return list.sort((a, b) => {
|
||||
const aActive = allActivatedAppIds.includes(a.objectID) ? 1 : 0;
|
||||
const bActive = allActivatedAppIds.includes(b.objectID) ? 1 : 0;
|
||||
return bActive - aActive;
|
||||
});
|
||||
}, [hits, hits?.length, allActivatedAppIds]);
|
||||
|
||||
let workflowDelay = 0;
|
||||
const isHeader = true;
|
||||
const paperStyle = {
|
||||
@@ -404,6 +414,7 @@ const AppGrid = (props) => {
|
||||
) : (
|
||||
<Grid item spacing={2} justifyContent="flex-start">
|
||||
<div
|
||||
ref={scrollContainerRef}
|
||||
style={{
|
||||
gap: 16,
|
||||
marginTop: 16,
|
||||
@@ -418,7 +429,7 @@ const AppGrid = (props) => {
|
||||
scrollbarColor: "#494949 #2f2f2f",
|
||||
}}
|
||||
>
|
||||
{hits?.map((data, index) => {
|
||||
{sortedHits.map((data, index) => {
|
||||
const appUrl =
|
||||
isCloud === true ?
|
||||
`/apps/${data.objectID}`
|
||||
@@ -629,6 +640,12 @@ const AppGrid = (props) => {
|
||||
);
|
||||
})
|
||||
}
|
||||
<div ref={loadMoreRef} style={{ width: "100%", height: 10 }} />
|
||||
{isLoadingMore && (
|
||||
<div style={{ width: "100%", display: "flex", justifyContent: "center", padding: 10 }}>
|
||||
<CircularProgress size={24} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Grid >
|
||||
)}
|
||||
@@ -926,7 +943,7 @@ const AppGrid = (props) => {
|
||||
|
||||
//Component to display all apps.
|
||||
const AllApps = ({ setIsAnyAppActivated }) => {
|
||||
var [searchQuery, setSearchQuery] = useState("");
|
||||
var [searchQuery, setSearchQuery] = useState(() => new URLSearchParams(window.location.search).get('q') || "");
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -987,6 +1004,7 @@ const AppGrid = (props) => {
|
||||
}}
|
||||
onClick={() => {
|
||||
setSearchQuery('');
|
||||
removeQuery("q");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
@@ -1015,7 +1033,15 @@ const AppGrid = (props) => {
|
||||
placeholder="Search your Activated or self-built apps"
|
||||
id="shuffle_search_field"
|
||||
onChange={(event) => {
|
||||
setSearchQuery(event.currentTarget.value);
|
||||
const value = event.currentTarget.value;
|
||||
setSearchQuery(value);
|
||||
const urlSearchParams = new URLSearchParams(window.location.search);
|
||||
if (value) {
|
||||
urlSearchParams.set("q", value);
|
||||
} else {
|
||||
urlSearchParams.delete("q");
|
||||
}
|
||||
window.history.replaceState({}, '', `${window.location.pathname}?${urlSearchParams.toString()}`);
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if(event.key === "Enter") {
|
||||
@@ -1592,7 +1618,7 @@ const AppGrid = (props) => {
|
||||
//Component to fetch all apps created by user and Org
|
||||
const UserAndOrgApps = ({ selectedCategoryForUsersAndOgsApps, selectedTagsForUserAndOrgApps, selectedOptionOfCreatedWith, setselectedCategoryForUsersAndOgsApps, setSelectedTagsForUserAndOrgApps, setSelectedOptionOfCreatedWith }) => {
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [searchQuery, setSearchQuery] = useState(() => new URLSearchParams(window.location.search).get('q') || "");
|
||||
const [appsToShow, setAppsToShow] = useState([]);
|
||||
useEffect(() => {
|
||||
if (currTab === 1) {
|
||||
@@ -2003,7 +2029,7 @@ const AppGrid = (props) => {
|
||||
};
|
||||
|
||||
const CustomSearchBox = connectSearchBox(SearchBox);
|
||||
const CustomHits = connectHits(Hits);
|
||||
const CustomHits = connectInfiniteHits(Hits);
|
||||
|
||||
const DisplayAllAppsTab = () => {
|
||||
const [selectedCategoryForUsersAndOgsApps, setselectedCategoryForUsersAndOgsApps] = useState([]);
|
||||
@@ -2012,7 +2038,7 @@ const AppGrid = (props) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<InstantSearch searchClient={searchClient} indexName="appsearch">
|
||||
<InstantSearch key={currTab === 0 ? "active" : "inactive"} searchClient={conditionalSearchClient} indexName="appsearch">
|
||||
<div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'center', paddingRight: 215 }}>
|
||||
{currTab === 0 ? (
|
||||
<FilterForAllApps />
|
||||
@@ -2037,7 +2063,7 @@ const AppGrid = (props) => {
|
||||
/>
|
||||
|
||||
</div>
|
||||
<Configure clickAnalytics />
|
||||
{currTab === 0 && <Configure clickAnalytics hitsPerPage={20} />}
|
||||
</InstantSearch>
|
||||
</div>
|
||||
);
|
||||
@@ -2060,80 +2086,7 @@ const AppGrid = (props) => {
|
||||
>
|
||||
<DisplayAllAppsTab />
|
||||
{showSuggestion === true ? (
|
||||
<div
|
||||
style={{
|
||||
paddingTop: 0,
|
||||
maxWidth: isMobile ? "100%" : "60%",
|
||||
margin: "auto",
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" style={{ color: "white", marginTop: 50 }}>
|
||||
Can't find what you're looking for?
|
||||
</Typography>
|
||||
<div
|
||||
style={{
|
||||
flex: "1",
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
required
|
||||
style={{
|
||||
flex: "1",
|
||||
marginRight: 15,
|
||||
backgroundColor: theme.palette.inputColor,
|
||||
}}
|
||||
InputProps={{
|
||||
style: {
|
||||
color: "#ffffff",
|
||||
},
|
||||
}}
|
||||
color="primary"
|
||||
fullWidth={true}
|
||||
placeholder="Email (optional)"
|
||||
type="email"
|
||||
id="email-handler"
|
||||
autoComplete="email"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
onChange={(e) => setFormMail(e.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
required
|
||||
style={{ flex: "1", backgroundColor: theme.palette.inputColor }}
|
||||
InputProps={{
|
||||
style: {
|
||||
color: "#ffffff",
|
||||
},
|
||||
}}
|
||||
color="primary"
|
||||
fullWidth={true}
|
||||
placeholder="What apps do you want to see?"
|
||||
type=""
|
||||
id="standard-required"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
autoComplete="off"
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
style={buttonStyle}
|
||||
disabled={message.length === 0}
|
||||
onClick={() => {
|
||||
submitContact(formMail, message);
|
||||
}}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
<Typography style={{ color: "white" }} variant="body2">
|
||||
{formMessage}
|
||||
</Typography>
|
||||
</div>
|
||||
<SearchContactForm globalUrl={globalUrl} isMobile={isMobile} tabName="apps" />
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -35,7 +35,7 @@ import { Context } from '../context/ContextApi.jsx';
|
||||
|
||||
const searchClient = algoliasearch(
|
||||
"JNSS5CFDZZ",
|
||||
"c8f882473ff42d41158430be09ec2b4e"
|
||||
"33e4e3564f4f060e96e0531957bed552"
|
||||
);;
|
||||
|
||||
const AppModal = ({ open, onClose, app, globalUrl, getApps}) => {
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
InputAdornment,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "33e4e3564f4f060e96e0531957bed552")
|
||||
const Appsearch = props => {
|
||||
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, newSelectedApp, setNewSelectedApp, defaultSearch, showSearch, ConfiguredHits, userdata, cy, isCreatorPage, actionImageList, setActionImageList, setUserSpecialzedApp, placeholder,
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
} from '@mui/material';
|
||||
|
||||
import aa from 'search-insights'
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "33e4e3564f4f060e96e0531957bed552")
|
||||
const Appsearch = props => {
|
||||
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, newSelectedApp, setNewSelectedApp, defaultSearch, showSearch, ConfiguredHits, userdata, cy, isCreatorPage, actionImageList, setActionImageList, setUserSpecialzedApp, inputHeight, apps, } = props
|
||||
const { themeMode } = useContext(Context)
|
||||
|
||||
@@ -52,6 +52,7 @@ import {
|
||||
Cancel as CancelIcon,
|
||||
Shield as ShieldIcon,
|
||||
Cancel as XCircleIcon,
|
||||
LockOutlined as LockIcon,
|
||||
FlashOn as ZapIcon,
|
||||
People as UsersIcon,
|
||||
FmdGoodOutlined as FmdGoodOutlinedIcon,
|
||||
@@ -203,7 +204,7 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) =>
|
||||
>
|
||||
{isProdStatusOn
|
||||
? 'Your organization has full access to all enterprise features and capabilities.'
|
||||
: 'View your current limits and available features. Upgrade to unlock enterprise capabilities.'}
|
||||
: 'Your organization is running on the open-source plan. Upgrade to Enterprise to remove limits and unlock advanced capabilities.'}
|
||||
</Typography>
|
||||
|
||||
{/* Features Grid */}
|
||||
@@ -223,8 +224,8 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) =>
|
||||
})
|
||||
.map((feature, index) => {
|
||||
const Icon = feature.icon;
|
||||
const isAvailable = isProdStatusOn && feature.isActive;
|
||||
const statusColor = isAvailable ? colors.success : colors.disabled;
|
||||
const isAvailable = isProdStatusOn;
|
||||
const statusColor = isAvailable ? colors.success : colors.warning;
|
||||
const bgColor = isAvailable ? themeMode === "dark" ? "#212121" : "#ffffff" : colors.disabledBg;
|
||||
|
||||
return (
|
||||
@@ -235,19 +236,34 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) =>
|
||||
alignItems: 'center',
|
||||
gap: 14,
|
||||
padding: '14px 16px',
|
||||
paddingLeft: !isAvailable ? 20 : 16,
|
||||
borderRadius: 10,
|
||||
background: bgColor,
|
||||
border: `1px solid ${isAvailable ? colors.success + '40' : colors.border}`,
|
||||
transition: 'all 0.2s',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
>
|
||||
{!isAvailable && (
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: 3,
|
||||
background: colors.warning,
|
||||
borderRadius: '10px 0 0 10px',
|
||||
}} />
|
||||
)}
|
||||
|
||||
{/* Icon */}
|
||||
<div
|
||||
style={{
|
||||
width: 40,
|
||||
height: 40,
|
||||
borderRadius: 8,
|
||||
background: isAvailable ? colors.success + '20' : colors.disabledBg,
|
||||
background: isAvailable ? colors.success + '20' : `${colors.warning}20`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
@@ -264,7 +280,7 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) =>
|
||||
fontSize: 15,
|
||||
fontWeight: 600,
|
||||
color: colors.textPrimary,
|
||||
marginBottom: 4,
|
||||
marginBottom: 3,
|
||||
}}
|
||||
>
|
||||
{feature.label}
|
||||
@@ -272,7 +288,7 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) =>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 13,
|
||||
color: colors.textSecondary,
|
||||
color: !isAvailable ? colors.warning : colors.textSecondary,
|
||||
lineHeight: 1.4,
|
||||
}}
|
||||
>
|
||||
@@ -285,8 +301,8 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) =>
|
||||
style={{ color: colors.success, fontSize: 20, flexShrink: 0 }}
|
||||
/>
|
||||
) : (
|
||||
<XCircleIcon
|
||||
style={{ color: colors.disabled, fontSize: 20, flexShrink: 0 }}
|
||||
<LockIcon
|
||||
style={{ color: colors.warning, fontSize: 20, flexShrink: 0 }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -304,80 +320,323 @@ const ProductionStatus = ({ selectedOrganization, userdata, isCloud, theme }) =>
|
||||
/>
|
||||
|
||||
{!isProdStatusOn && (
|
||||
<div
|
||||
style={{
|
||||
padding: 20,
|
||||
<div style={{
|
||||
borderRadius: 12,
|
||||
background: themeMode == "dark" ? "#212121" : "#ffffff",
|
||||
border: `1px solid ${colors.border}`,
|
||||
marginBottom: 20,
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', gap: 12, marginBottom: 12 }}>
|
||||
<InfoIcon style={{ color: colors.accent, flexShrink: 0, marginTop: 2, fontSize: 20 }} />
|
||||
<div>
|
||||
<Typography variant="h6" style={{ fontWeight: 600, margin: '0 0 8px 0' }}>
|
||||
About Shuffle Enterprise
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="textSecondary"
|
||||
style={{
|
||||
margin: 0,
|
||||
lineHeight: 1.6,
|
||||
}}
|
||||
>
|
||||
Shuffle Enterprise is designed for organizations that require scalability, high
|
||||
availability, dedicated support, and robust infrastructure to run mission-critical
|
||||
workflows in production environments. <a href="https://shuffler.io/articles/Shuffle_Open_Source" target="_blank" rel="noreferrer" style={{color: "#FF8544" }}>learn more</a>
|
||||
</Typography>
|
||||
overflow: 'hidden',
|
||||
border: `1px solid ${colors.accent}40`,
|
||||
marginBottom: 24,
|
||||
}}>
|
||||
{/* Header */}
|
||||
<div style={{
|
||||
background: `linear-gradient(135deg, ${colors.accent}14 0%, ${colors.accent}06 100%)`,
|
||||
padding: '16px 20px',
|
||||
borderBottom: `1px solid ${colors.accent}20`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 12,
|
||||
}}>
|
||||
<div style={{
|
||||
width: 36,
|
||||
height: 36,
|
||||
borderRadius: 8,
|
||||
background: `${colors.accent}20`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
<ShieldIcon style={{ color: colors.accent, fontSize: 20 }} />
|
||||
</div>
|
||||
<div>
|
||||
<Typography variant="h6" style={{ fontWeight: 700, margin: 0, fontSize: 16 }}>
|
||||
Unlock Shuffle Enterprise
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" style={{ margin: 0, fontSize: 12, lineHeight: 1.4 }}>
|
||||
Scale your security operations without limits
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>)}
|
||||
|
||||
{/* CTA Section */}
|
||||
{!isProdStatusOn && (
|
||||
<div
|
||||
style={{
|
||||
padding: 20,
|
||||
borderRadius: 12,
|
||||
background: `linear-gradient(135deg, #FF854415 0%, #FF854408 100%)`,
|
||||
border: `1px solid #FF854440`,
|
||||
marginBottom: 24,
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Typography variant="h6" style={{ fontWeight: 600, margin: '0 0 8px 0' }}>
|
||||
Ready to upgrade?
|
||||
{/* Body */}
|
||||
<div style={{
|
||||
padding: '16px 20px 20px',
|
||||
background: themeMode === "dark" ? "#212121" : "#ffffff",
|
||||
}}>
|
||||
<div style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: '1fr 1fr',
|
||||
gap: 8,
|
||||
marginBottom: 16,
|
||||
}}>
|
||||
{[
|
||||
{ icon: ZapIcon, text: 'Higher App Run Limits' },
|
||||
{ icon: UsersIcon, text: 'Multi-Tenant Support' },
|
||||
{ icon: ShieldIcon, text: 'Enterprise SLA' },
|
||||
{ icon: FmdGoodOutlinedIcon, text: 'Multi-Location Deploy' },
|
||||
].map((item, i) => {
|
||||
const ItemIcon = item.icon;
|
||||
return (
|
||||
<div key={i} style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
padding: '8px 12px',
|
||||
borderRadius: 8,
|
||||
background: themeMode === "dark" ? `${colors.accent}08` : `${colors.accent}06`,
|
||||
}}>
|
||||
<ItemIcon style={{ color: colors.accent, fontSize: 16 }} />
|
||||
<span style={{ fontSize: 13, color: colors.textPrimary, fontWeight: 500 }}>
|
||||
{item.text}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<Typography variant="body2" color="textSecondary" style={{
|
||||
marginBottom: 16,
|
||||
lineHeight: 1.6,
|
||||
fontSize: 13,
|
||||
}}>
|
||||
Purpose-built for security teams that need scalability, high availability, and dedicated
|
||||
expert support to run mission-critical workflows in production.{' '}
|
||||
<a href="https://shuffler.io/articles/Shuffle_Open_Source" target="_blank" rel="noreferrer" style={{ color: colors.accent }}>
|
||||
Learn more
|
||||
</a>
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="textSecondary"
|
||||
style={{
|
||||
margin: 0,
|
||||
lineHeight: 1.6,
|
||||
}}
|
||||
|
||||
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
|
||||
<Button
|
||||
href="https://shuffler.io/pricing?env=Self-Hosted"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
endIcon={<ArrowRightIcon sx={{ fontSize: 16 }} />}
|
||||
>
|
||||
Upgrade Now
|
||||
</Button>
|
||||
<Button
|
||||
href="https://shuffler.io/contact?category=talk_to_sales"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
variant="outlined"
|
||||
startIcon={<MailIcon sx={{ fontSize: 16 }} />}
|
||||
>
|
||||
Contact our team to learn more about enterprise features and pricing.
|
||||
</Typography>
|
||||
Talk to Sales
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
href="https://shuffler.io/contact?category=talk_to_sales"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
startIcon={<MailIcon sx={{ fontSize: 16}} />}
|
||||
endIcon={<ArrowRightIcon sx={{ fontSize: 16 }} />}
|
||||
>
|
||||
Contact Sales
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const AppRunsQueueCard = memo(({ environment, isAirGapped, isCloudSynching, totalRuns, limit, theme, navigate }) => {
|
||||
|
||||
const usagePct = limit > 0 ? (totalRuns / limit) * 100 : 0;
|
||||
const hardPauseLimit = limit * 2;
|
||||
const hardPausePct = hardPauseLimit > 0 ? Math.min((totalRuns / hardPauseLimit) * 100, 100) : 0;
|
||||
const mainBarPct = Math.min(usagePct, 100);
|
||||
|
||||
const queueSize = environment?.queue !== undefined && environment?.queue !== null
|
||||
? Math.max(0, environment.queue)
|
||||
: 0;
|
||||
|
||||
const isThrottled = isCloudSynching ? false : (isAirGapped ? hardPausePct >= 100 : usagePct >= 100);
|
||||
|
||||
let status, statusColor, statusBg;
|
||||
if (isThrottled) {
|
||||
status = 'Throttled';
|
||||
statusColor = '#ef4444';
|
||||
statusBg = 'rgba(239, 68, 68, 0.12)';
|
||||
} else if (!isCloudSynching && usagePct >= 80) {
|
||||
status = 'Warning';
|
||||
statusColor = '#f59e0b';
|
||||
statusBg = 'rgba(245, 158, 11, 0.12)';
|
||||
} else {
|
||||
status = 'Healthy';
|
||||
statusColor = theme.palette.green;
|
||||
statusBg = `${theme.palette.green}1f`;
|
||||
}
|
||||
|
||||
const throttleRate = isThrottled ? '1/min' : '\u2014';
|
||||
const estClearTime = isThrottled && queueSize > 0 ? `${queueSize} min` : '\u2014';
|
||||
const mainBarColor = isThrottled ? '#ef4444' : usagePct >= 80 && !isCloudSynching ? '#f59e0b' : theme.palette.green;
|
||||
|
||||
const envTypeLabel = environment?.run_type === 'cloud' ? 'Cloud' : 'On-prem';
|
||||
const envName = environment?.Name || environment?.name || 'Default';
|
||||
|
||||
const borderColor = theme.palette.slateGrayColor;
|
||||
const trackBg = theme.palette.slateGrayColor;
|
||||
const mutedText = theme.palette.text.secondary;
|
||||
|
||||
return (
|
||||
<div style={{
|
||||
border: `1px solid ${borderColor}`,
|
||||
borderRadius: 12,
|
||||
padding: '20px 24px',
|
||||
backgroundColor: theme.palette.platformColor,
|
||||
marginBottom: 16,
|
||||
maxWidth: 800,
|
||||
}}>
|
||||
{/* Title row */}
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 6 }}>
|
||||
<Typography variant="body2" style={{ color: mutedText, fontSize: 13, fontWeight: 500 }}>
|
||||
{envTypeLabel} - {envName} · App runs / month
|
||||
</Typography>
|
||||
<div style={{
|
||||
padding: '4px 14px',
|
||||
borderRadius: 20,
|
||||
backgroundColor: statusBg,
|
||||
border: `1px solid ${statusColor}60`,
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
gap: 6,
|
||||
}}>
|
||||
<span style={{ width: 7, height: 7, borderRadius: '50%', backgroundColor: statusColor, display: 'inline-block', boxShadow: `0 0 6px ${statusColor}` }} />
|
||||
<span style={{ color: statusColor, fontWeight: 700, fontSize: 13 }}>{status}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main number */}
|
||||
<div style={{ display: 'flex', alignItems: 'baseline', gap: 6, marginBottom: 2 }}>
|
||||
<span style={{ fontSize: 28, fontWeight: 700, color: theme.palette.text.primary }}>
|
||||
{totalRuns.toLocaleString()}
|
||||
</span>
|
||||
<span style={{ fontSize: 18, color: mutedText }}>
|
||||
/ {limit.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{isAirGapped && !isCloudSynching && (
|
||||
<Typography variant="caption" style={{ color: mutedText, fontSize: 12, display: 'block', marginBottom: 14 }}>
|
||||
No throttle until {hardPauseLimit.toLocaleString()} runs · 2× your plan limit
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{/* Main usage bar */}
|
||||
<div style={{ position: 'relative', height: 8, borderRadius: 4, backgroundColor: trackBg, marginBottom: 4 }}>
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
left: 0, top: 0, bottom: 0,
|
||||
width: `${mainBarPct}%`,
|
||||
borderRadius: 4,
|
||||
backgroundColor: mainBarColor,
|
||||
transition: 'width 0.4s ease',
|
||||
}} />
|
||||
{/* 80% threshold marker */}
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
left: '80%',
|
||||
top: -3,
|
||||
bottom: -3,
|
||||
width: 2,
|
||||
backgroundColor: borderColor,
|
||||
borderRadius: 1,
|
||||
}} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 14 }}>
|
||||
<Typography variant="caption" style={{ color: mutedText, fontSize: 11 }}>0</Typography>
|
||||
<Typography variant="caption" style={{ color: mutedText, fontSize: 11 }}>80% threshold</Typography>
|
||||
<Typography variant="caption" style={{ color: mutedText, fontSize: 11 }}>{limit.toLocaleString()}</Typography>
|
||||
</div>
|
||||
|
||||
{/* Throttle limit row */}
|
||||
{isAirGapped && (
|
||||
<>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 6 }}>
|
||||
<Typography variant="caption" style={{ color: mutedText, fontSize: 12 }}>
|
||||
Burst throttle threshold (2× limit) · workflows throttle to 1/min above this
|
||||
</Typography>
|
||||
<Typography variant="caption" style={{ color: mutedText, fontSize: 12 }}>
|
||||
{totalRuns.toLocaleString()} / {hardPauseLimit.toLocaleString()}
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
<div style={{ position: 'relative', height: 6, borderRadius: 3, backgroundColor: trackBg, marginBottom: 16 }}>
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
left: 0, top: 0, bottom: 0,
|
||||
width: `${hardPausePct}%`,
|
||||
borderRadius: 3,
|
||||
backgroundColor: '#ef4444',
|
||||
transition: 'width 0.4s ease',
|
||||
}} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Alert box for Warning / Throttled */}
|
||||
{status !== 'Healthy' && (
|
||||
<div style={{
|
||||
borderRadius: 8,
|
||||
padding: '14px 16px',
|
||||
backgroundColor: 'rgba(245, 158, 11, 0.08)',
|
||||
border: '1px solid rgba(245, 158, 11, 0.3)',
|
||||
marginBottom: 16,
|
||||
}}>
|
||||
<Typography variant="body2" style={{ color: '#f59e0b', fontWeight: 700, marginBottom: 6, fontSize: 14 }}>
|
||||
{isThrottled ? 'Running slow \u2014 workflows are still running' : 'Approaching your monthly limit'}
|
||||
</Typography>
|
||||
<Typography variant="body2" style={{ color: theme.palette.text.primary, fontSize: 13, lineHeight: 1.6, marginBottom: 12 }}>
|
||||
{isThrottled
|
||||
? isAirGapped
|
||||
? `You've exceeded the burst threshold of ${hardPauseLimit.toLocaleString()} runs (2\u00d7 your plan limit). Your workflows are still running \u2014 there is no hard stop. Executions slow to 1 per minute until next month.`
|
||||
: `You've exceeded your ${limit.toLocaleString()} monthly limit. Your instance keeps running \u2014 executions slow to 1 per minute until next month. Nothing is lost. You can view or clear the queue from the Locations tab.`
|
||||
: isAirGapped
|
||||
? `You've used ${totalRuns.toLocaleString()} of ${limit.toLocaleString()} app runs. Workflows run normally \u2014 slowdown only begins at ${hardPauseLimit.toLocaleString()} runs (2\u00d7 your plan limit). No action needed.`
|
||||
: `You've used ${totalRuns.toLocaleString()} of ${limit.toLocaleString()} app runs (${Math.max(0, limit - totalRuns).toLocaleString()} remaining). If you reach 100%, executions continue at a reduced rate of 1 per minute, nothing stops or is lost.`
|
||||
}
|
||||
</Typography>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
sx={{
|
||||
borderColor: '#f59e0b',
|
||||
color: '#f59e0b',
|
||||
textTransform: 'none',
|
||||
fontSize: 13,
|
||||
borderRadius: '6px',
|
||||
padding: '5px 14px',
|
||||
}}
|
||||
onClick={() => {
|
||||
if (isThrottled) navigate('/admin?tab=locations');
|
||||
else window.open('https://shuffler.io/contact', '_blank', 'noopener,noreferrer');
|
||||
}}
|
||||
>
|
||||
{isThrottled ? 'Manage queue' : 'Contact Us'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stats row */}
|
||||
<div style={{ display: 'flex', borderTop: `1px solid ${borderColor}`, paddingTop: 16 }}>
|
||||
{[
|
||||
{ label: 'Queued jobs', value: queueSize },
|
||||
{ label: 'Throttle rate', value: throttleRate },
|
||||
{ label: 'Est. clear time', value: estClearTime },
|
||||
].map((stat, i) => (
|
||||
<div key={i} style={{
|
||||
flex: 1,
|
||||
paddingRight: i < 2 ? 16 : 0,
|
||||
borderRight: i < 2 ? `1px solid ${borderColor}` : 'none',
|
||||
marginRight: i < 2 ? 16 : 0,
|
||||
}}>
|
||||
<Typography variant="caption" style={{ color: mutedText, fontSize: 12, display: 'block', marginBottom: 4 }}>
|
||||
{stat.label}
|
||||
</Typography>
|
||||
<Typography style={{ fontWeight: 700, fontSize: 18, color: theme.palette.text.primary }}>
|
||||
{stat.value}
|
||||
</Typography>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const Billing = memo((props) => {
|
||||
const { globalUrl, userdata, serverside, billingInfo, stripeKey,isLoaded, selectedOrganization, handleGetOrg, clickedFromOrgTab, removeCookie} = props;
|
||||
//const alert = useAlert();
|
||||
@@ -413,7 +672,7 @@ const Billing = memo((props) => {
|
||||
const [statistics, setStatistics] = useState([])
|
||||
const [monthlyAppRunsParent, setMonthlyAppRunsParent] = useState(0)
|
||||
const [monthlyAllSuborgExecutions, setMonthlyAllSuborgExecutions] = useState(0)
|
||||
|
||||
const [billingEnvironments, setBillingEnvironments] = useState([])
|
||||
useEffect(() => {
|
||||
if (monthlyAppRunsParent > 0 || monthlyAllSuborgExecutions > 0) {
|
||||
const percentage = ((monthlyAppRunsParent + monthlyAllSuborgExecutions) / userdata.app_execution_limit) * 100;
|
||||
@@ -525,6 +784,29 @@ const Billing = memo((props) => {
|
||||
}, [])
|
||||
|
||||
|
||||
const getBillingEnvironments = () => {
|
||||
fetch(globalUrl + "/api/v1/getenvironments", {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status !== 200) return;
|
||||
return response.json();
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson && Array.isArray(responseJson)) {
|
||||
setBillingEnvironments(responseJson);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log("Error fetching environments for billing:", error);
|
||||
});
|
||||
};
|
||||
|
||||
const getStats = (orgid) => {
|
||||
|
||||
if (orgid === undefined || orgid === null) {
|
||||
@@ -563,6 +845,9 @@ const Billing = memo((props) => {
|
||||
useEffect(() => {
|
||||
if (selectedOrganization && selectedOrganization?.id?.length > 0) {
|
||||
getStats(selectedOrganization.id);
|
||||
if (!isCloud) {
|
||||
getBillingEnvironments();
|
||||
}
|
||||
}
|
||||
}, [selectedOrganization]);
|
||||
|
||||
@@ -2387,6 +2672,17 @@ const Billing = memo((props) => {
|
||||
|
||||
const isChildOrg = userdata?.active_org?.creator_org !== "" && userdata?.active_org?.creator_org !== undefined && userdata?.active_org?.creator_org !== null
|
||||
|
||||
const activeQueueEnvs = Array.isArray(billingEnvironments) ? billingEnvironments.filter(env => env != null && !env.archived && env.Type !== 'cloud') : [];
|
||||
const totalQueueSize = activeQueueEnvs.reduce((sum, env) => sum + Math.max(0, env?.queue || 0), 0);
|
||||
const aggregatedQueueEnv = {
|
||||
Name: `${activeQueueEnvs.length} Runtime Location${activeQueueEnvs.length !== 1 ? 's' : ''}`,
|
||||
run_type: 'on-prem',
|
||||
queue: totalQueueSize,
|
||||
};
|
||||
const appExecLimit = selectedOrganization?.sync_features?.app_executions?.limit ?? 0;
|
||||
const isAirGapped = selectedOrganization != null && (selectedOrganization.cloud_sync_active === true || selectedOrganization.cloud_sync === true) ? false : appExecLimit < 300000 ? false : true;
|
||||
const isCloudSynching = selectedOrganization != null && selectedOrganization.cloud_sync === true && appExecLimit >= 300000;
|
||||
|
||||
useEffect(() => {
|
||||
if (isChildOrg && currentTab === 0) {
|
||||
setCurrentTab(1);
|
||||
@@ -2800,6 +3096,28 @@ const Billing = memo((props) => {
|
||||
|
||||
</div>
|
||||
) : null*/}
|
||||
|
||||
{/* Queue Management */}
|
||||
{!isCloud && activeQueueEnvs.length > 0 && !isChildOrg && (
|
||||
<div style={{ maxWidth: 800, marginTop: 32, marginBottom: 8 }}>
|
||||
<Typography variant="h6" style={{ marginBottom: 6, fontSize: 20, fontWeight: 600 }}>
|
||||
Queue Management
|
||||
</Typography>
|
||||
<Typography variant="body2" color="textSecondary" style={{ marginBottom: 16, fontSize: 14 }}>
|
||||
Real-time status of your app run usage and workflow queue across all runtime locations.
|
||||
</Typography>
|
||||
<AppRunsQueueCard
|
||||
environment={aggregatedQueueEnv}
|
||||
totalRuns={Number(monthlyAppRunsParent ?? 0) + Number(monthlyAllSuborgExecutions ?? 0)}
|
||||
limit={selectedOrganization?.sync_features?.app_executions?.limit || 25000}
|
||||
theme={theme}
|
||||
navigate={navigate}
|
||||
isAirGapped={isAirGapped}
|
||||
isCloudSynching={isCloudSynching}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!isChildOrg && isCloud && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', marginTop: 50, maxWidth: 860 }} id="professional-services">
|
||||
<Typography variant="h6" style={{ marginBottom: 5, fontSize: 24, fontWeight: 500 }}>
|
||||
|
||||
@@ -92,6 +92,11 @@ const AppStats = (defaultprops) => {
|
||||
const statKey = syncStats === true ? "onprem_stats" : "daily_statistics"
|
||||
const dailyStats = inputdata[statKey]
|
||||
if (dailyStats === undefined || dailyStats === null) {
|
||||
setAppruns(undefined)
|
||||
setWorkflowRuns(undefined)
|
||||
setSubflowRuns(undefined)
|
||||
setChildOrgsAppRuns(undefined)
|
||||
setApprunCosts(undefined)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -378,6 +383,17 @@ const AppStats = (defaultprops) => {
|
||||
return
|
||||
}
|
||||
|
||||
if (syncStats && (statistics[statKey] === undefined || statistics[statKey] === null)) {
|
||||
setOnpremAppRuns(0)
|
||||
setFilteredStatistics(statistics)
|
||||
setAppruns(undefined)
|
||||
setWorkflowRuns(undefined)
|
||||
setSubflowRuns(undefined)
|
||||
setChildOrgsAppRuns(undefined)
|
||||
setApprunCosts(undefined)
|
||||
return
|
||||
}
|
||||
|
||||
// Calculate month to date cost
|
||||
var mtd_cost = 0
|
||||
for (let key in statistics[statKey]) {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import React, { useState, useEffect, useContext, memo } from "react";
|
||||
import { makeStyles } from "@mui/styles";
|
||||
import { getTheme } from "../theme.jsx";
|
||||
import SubOrgDistributionDialog from "./SubOrgDistributionDialog.jsx";
|
||||
import DeleteConfirmDialog from "./DeleteConfirmDialog.jsx";
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import ReactJson from "react-json-view-ssr";
|
||||
@@ -17,8 +19,6 @@ import {
|
||||
Button,
|
||||
Tabs,
|
||||
Tab,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
IconButton,
|
||||
Dialog,
|
||||
@@ -82,6 +82,7 @@ import {
|
||||
Hub as HubIcon,
|
||||
Key as KeyIcon,
|
||||
FlashOn as FlashOnIcon,
|
||||
Search as SearchIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { Context } from "../context/ContextApi.jsx";
|
||||
|
||||
@@ -127,6 +128,9 @@ const CacheView = memo((props) => {
|
||||
const [showDistributionPopup, setShowDistributionPopup] = useState(false);
|
||||
const [selectedSubOrg, setSelectedSubOrg] = useState([]);
|
||||
const [selectedCacheKey, setSelectedCacheKey] = useState("");
|
||||
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
||||
const [deleteConfirmTarget, setDeleteConfirmTarget] = useState(null);
|
||||
const [distribOrgOrder, setDistribOrgOrder] = useState([]);
|
||||
const [totalAmount, setTotalAmount] = useState(0);
|
||||
const [page, setPage] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(50)
|
||||
@@ -233,7 +237,7 @@ const CacheView = memo((props) => {
|
||||
|
||||
{
|
||||
"name": "Enrich",
|
||||
"description": "Enriches the data. Only runs on valid JSON data AND if the 'enrichment' field does not exist.",
|
||||
"description": "Enriches the data. Uses regex keys and runs a workflow in the background. Added to the 'enrichments' key.",
|
||||
"type": "singul",
|
||||
"options": [{
|
||||
"key": "",
|
||||
@@ -331,6 +335,14 @@ const CacheView = memo((props) => {
|
||||
|
||||
// In order to make linking weird urls from workflow page work.
|
||||
if (urlParams.get("src") == "workflow") {
|
||||
if (categoryParam === "OCSF") {
|
||||
const newParam = "shuffle-security incidents"
|
||||
|
||||
urlParams.set("category", newParam)
|
||||
window.history.replaceState({}, '', `${window.location.pathname}?${urlParams.toString()}`)
|
||||
categoryParam = newParam
|
||||
}
|
||||
|
||||
if (categoryParam?.toLowerCase().startsWith("list")) {
|
||||
const newParam = categoryParam.substring(5).replaceAll("%20", "_")
|
||||
|
||||
@@ -570,17 +582,26 @@ const CacheView = memo((props) => {
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
if (refreshList === undefined || refreshList === null || refreshList === true) {
|
||||
|
||||
toast.success("Deleted datastore entry");
|
||||
setTimeout(() => {
|
||||
listOrgCache(orgId, selectedCategory, 0, pageSize, page)
|
||||
}, 1000);
|
||||
}
|
||||
} else {
|
||||
if (refreshList === undefined || refreshList === null || refreshList === true) {
|
||||
setTimeout(() => {
|
||||
listOrgCache(orgId, selectedCategory, 0, pageSize, page)
|
||||
}, 1000);
|
||||
}
|
||||
toast.error(`Failed deleting entry ${key} in category ${itemCategory || selectedCategory}. If this persists, please contact support@shuffler.io.`)
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
if (refreshList === undefined || refreshList === null || refreshList === true) {
|
||||
setTimeout(() => {
|
||||
listOrgCache(orgId, selectedCategory, 0, pageSize, page)
|
||||
}, 1000);
|
||||
}
|
||||
toast(error.toString());
|
||||
});
|
||||
};
|
||||
@@ -831,6 +852,11 @@ const CacheView = memo((props) => {
|
||||
Category: {dataValue.category}
|
||||
</Typography>
|
||||
: null}
|
||||
{dataValue?.enrichments !== undefined && dataValue?.enrichments !== null && dataValue.enrichments.length > 0 ?
|
||||
<Typography variant="body2" color="textSecondary" style={{ }}>
|
||||
Enrichments: {dataValue.enrichments.length}
|
||||
</Typography>
|
||||
: null}
|
||||
{dataValue?.tags !== undefined && dataValue?.tags !== null && dataValue?.tags?.length > 0 ?
|
||||
<div style={{display: "flex", marginTop: 12, }}>
|
||||
<Typography variant="body2" color="textSecondary" style={{ marginRight: 10, marginTop: 4, }}>
|
||||
@@ -900,33 +926,6 @@ const CacheView = memo((props) => {
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
const handleSelectSubOrg = (id, action) => {
|
||||
if (action === "all") {
|
||||
const childOrgs = userdata.orgs.filter(
|
||||
(data) => data.creator_org === userdata.active_org.id
|
||||
);
|
||||
setSelectedSubOrg((prev) => {
|
||||
if (prev.length === childOrgs.length) {
|
||||
// If all child orgs are already selected, clear the selection
|
||||
return [];
|
||||
} else {
|
||||
// Otherwise, select all child org IDs
|
||||
return childOrgs.map((data) => data.id);
|
||||
}
|
||||
});
|
||||
} else if (action === "none") {
|
||||
setSelectedSubOrg([]);
|
||||
} else {
|
||||
setSelectedSubOrg((prev) => {
|
||||
if (prev.includes(id)) {
|
||||
return prev.filter((data) => data !== id);
|
||||
} else {
|
||||
return [...prev, id];
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const changeDistribution = (id, selectedSubOrg) => {
|
||||
|
||||
editFileConfig(id, [...new Set(selectedSubOrg)], selectedCategory)
|
||||
@@ -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,6 +4,7 @@ import ReactGA from 'react-ga4';
|
||||
import {Link} from 'react-router-dom';
|
||||
import theme from '../theme.jsx';
|
||||
import { removeQuery } from '../components/ScrollToTop.jsx';
|
||||
import SearchContactForm from '../components/SearchContactForm.jsx';
|
||||
|
||||
import {
|
||||
SkipNext as SkipNextIcon,
|
||||
@@ -38,18 +39,14 @@ import {
|
||||
} from "@mui/material"
|
||||
import { useDebouncedCallback } from "../utils/useDebouncedCallback.jsx";
|
||||
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "33e4e3564f4f060e96e0531957bed552")
|
||||
const CreatorGrid = props => {
|
||||
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, isHeader } = props
|
||||
const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows
|
||||
const xs = parsedXs === undefined || parsedXs === null ? isMobile ? 6 : 4 : parsedXs
|
||||
//const [apps, setApps] = React.useState([]);
|
||||
//const [filteredApps, setFilteredApps] = React.useState([]);
|
||||
const [formMail, setFormMail] = React.useState("");
|
||||
const [message, setMessage] = React.useState("");
|
||||
const [formMessage, setFormMessage] = React.useState("");
|
||||
|
||||
const buttonStyle = {borderRadius: 30, height: 50, width: 220, margin: isMobile ? "15px auto 15px auto" : 20, fontSize: 18,}
|
||||
|
||||
const isCloud =
|
||||
window.location.host === "localhost:3002" ||
|
||||
@@ -59,44 +56,6 @@ const CreatorGrid = props => {
|
||||
const borderRadius = 3
|
||||
window.title = "Shuffle | Workflows | Discover your use-case"
|
||||
|
||||
const submitContact = (email, message) => {
|
||||
const data = {
|
||||
"firstname": "",
|
||||
"lastname": "",
|
||||
"title": "",
|
||||
"companyname": "",
|
||||
"email": email,
|
||||
"phone": "",
|
||||
"message": message,
|
||||
}
|
||||
|
||||
const errorMessage = "Something went wrong. Please contact frikky@shuffler.io directly."
|
||||
|
||||
fetch(globalUrl+"/api/v1/contact", {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(response => {
|
||||
if (response.success === true) {
|
||||
setFormMessage(response.reason)
|
||||
//toast("Thanks for submitting!")
|
||||
} else {
|
||||
setFormMessage(errorMessage)
|
||||
}
|
||||
|
||||
setFormMail("")
|
||||
setMessage("")
|
||||
})
|
||||
.catch(error => {
|
||||
setFormMessage(errorMessage)
|
||||
console.log(error)
|
||||
});
|
||||
}
|
||||
|
||||
// value={currentRefinement}
|
||||
const SearchBox = ({currentRefinement, refine, isSearchStalled} ) => {
|
||||
var defaultSearch = ""
|
||||
@@ -249,62 +208,8 @@ const CreatorGrid = props => {
|
||||
<CustomHits hitsPerPage={100}/>
|
||||
</InstantSearch>
|
||||
{showSuggestion === true ?
|
||||
<div style={{maxWidth: isMobile ? "100%" : "60%", margin: "auto", paddingTop: 50, textAlign: "center",}}>
|
||||
<Typography variant="h6" style={{color: "white", marginTop: 50,}}>
|
||||
Can't find what you're looking for?
|
||||
</Typography>
|
||||
<div style={{flex: "1", display: "flex", flexDirection: "row"}}>
|
||||
<TextField
|
||||
required
|
||||
style={{flex: "1", marginRight: "15px", backgroundColor: theme.palette.inputColor}}
|
||||
InputProps={{
|
||||
style:{
|
||||
color: "#ffffff",
|
||||
},
|
||||
}}
|
||||
color="primary"
|
||||
fullWidth={true}
|
||||
placeholder="Email (optional)"
|
||||
type="email"
|
||||
id="email-handler"
|
||||
autoComplete="email"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
onChange={e => setFormMail(e.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
required
|
||||
style={{flex: "1", backgroundColor: theme.palette.inputColor}}
|
||||
InputProps={{
|
||||
style:{
|
||||
color: "#ffffff",
|
||||
},
|
||||
}}
|
||||
color="primary"
|
||||
fullWidth={true}
|
||||
placeholder="What are we missing?"
|
||||
type=""
|
||||
id="standard-required"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
autoComplete="off"
|
||||
onChange={e => setMessage(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
style={buttonStyle}
|
||||
disabled={message.length === 0}
|
||||
onClick={() => {
|
||||
submitContact(formMail, message)
|
||||
}}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
<Typography style={{color: "white"}} variant="body2">{formMessage}</Typography>
|
||||
</div>
|
||||
: null
|
||||
<SearchContactForm globalUrl={globalUrl} isMobile={isMobile} tabName="creators" />
|
||||
: null
|
||||
}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
import React, { memo, useContext } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { getTheme } from '../theme.jsx';
|
||||
import { Context } from '../context/ContextApi.jsx';
|
||||
|
||||
const DeleteConfirmDialog = memo(({ open, onClose, onConfirm, title, description, warningText }) => {
|
||||
const { themeMode, brandColor } = useContext(Context);
|
||||
const theme = getTheme(themeMode, brandColor);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
borderRadius: theme?.palette?.DialogStyle?.borderRadius,
|
||||
border: theme?.palette?.DialogStyle?.border,
|
||||
fontFamily: theme?.typography?.fontFamily,
|
||||
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
||||
minWidth: 420,
|
||||
'& .MuiDialogContent-root': { backgroundColor: theme?.palette?.DialogStyle?.backgroundColor },
|
||||
'& .MuiDialogTitle-root': { backgroundColor: theme?.palette?.DialogStyle?.backgroundColor },
|
||||
'& .MuiDialogActions-root': { backgroundColor: theme?.palette?.DialogStyle?.backgroundColor },
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DialogTitle>
|
||||
<Typography variant="h6">{title}</Typography>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography variant="body1">{description}</Typography>
|
||||
{warningText && (
|
||||
<Typography variant="body2" color="textSecondary" style={{ marginTop: 8 }}>
|
||||
{warningText}
|
||||
</Typography>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ p: 2 }}>
|
||||
<Button style={{ textTransform: 'none' }} onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
style={{ textTransform: 'none', backgroundColor: theme?.palette?.deleteColor, color: '#fff' }}
|
||||
onClick={onConfirm}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
});
|
||||
|
||||
export default DeleteConfirmDialog;
|
||||
@@ -3,11 +3,8 @@ import algoliasearch from 'algoliasearch';
|
||||
import theme from '../theme.jsx';
|
||||
import { InstantSearch, connectSearchBox, connectHits } from 'react-instantsearch-dom';
|
||||
import {
|
||||
Grid,
|
||||
Paper,
|
||||
TextField,
|
||||
Typography,
|
||||
Button,
|
||||
InputAdornment,
|
||||
Avatar,
|
||||
List,
|
||||
@@ -16,6 +13,7 @@ import {
|
||||
ListItemText,
|
||||
} from '@mui/material';
|
||||
import { Search as SearchIcon } from '@mui/icons-material';
|
||||
import SearchContactForm from '../components/SearchContactForm.jsx';
|
||||
import useDebouncedCallback from '../utils/useDebouncedCallback.jsx';
|
||||
|
||||
|
||||
@@ -23,52 +21,9 @@ const searchClient = algoliasearch("JNSS5CFDZZ", "1e5f29b1550939855de5915eac3bf5
|
||||
|
||||
const DiscordChat = props => {
|
||||
const { isMobile, globalUrl } = props
|
||||
const [value, setValue] = useState("");
|
||||
const [formMail, setFormMail] = React.useState("");
|
||||
const [message, setMessage] = React.useState("");
|
||||
const [formMessage, setFormMessage] = React.useState("");
|
||||
const buttonStyle = {borderRadius: 30, height: 50, width: 220, margin: isMobile ? "15px auto 15px auto" : 20, fontSize: 18,}
|
||||
|
||||
const borderRadius = 3
|
||||
|
||||
const submitContact = (email, message) => {
|
||||
const data = {
|
||||
"firstname": "",
|
||||
"lastname": "",
|
||||
"title": "",
|
||||
"companyname": "",
|
||||
"email": email,
|
||||
"phone": "",
|
||||
"message": message,
|
||||
}
|
||||
|
||||
const errorMessage = "Something went wrong. Please contact frikky@shuffler.io directly."
|
||||
|
||||
fetch(globalUrl+"/api/v1/contact", {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(response => {
|
||||
if (response.success === true) {
|
||||
setFormMessage(response.reason)
|
||||
//toast("Thanks for submitting!")
|
||||
} else {
|
||||
setFormMessage(errorMessage)
|
||||
}
|
||||
|
||||
setFormMail("")
|
||||
setMessage("")
|
||||
})
|
||||
.catch(error => {
|
||||
setFormMessage(errorMessage)
|
||||
console.log(error)
|
||||
});
|
||||
}
|
||||
|
||||
const SearchBox = ({ currentRefinement, refine }) => {
|
||||
const [inputValue, setInputValue] = useState("");
|
||||
const debouncedRefine = useDebouncedCallback((value) => refine(value), 300);
|
||||
@@ -168,61 +123,7 @@ const DiscordChat = props => {
|
||||
<CustomHits />
|
||||
</div>
|
||||
</InstantSearch>
|
||||
<div style={{paddingTop: 0, maxWidth: isMobile ? "100%" : "60%", margin: "auto"}}>
|
||||
<Typography variant="h6" style={{color: "white", marginTop: 50,}}>
|
||||
Can't find what you're looking for?
|
||||
</Typography>
|
||||
<div style={{flex: "1", display: "flex", flexDirection: "row", textAlign: "center",}}>
|
||||
<TextField
|
||||
required
|
||||
style={{flex: "1", marginRight: "15px", backgroundColor: theme.palette.inputColor}}
|
||||
InputProps={{
|
||||
style:{
|
||||
color: "#ffffff",
|
||||
},
|
||||
}}
|
||||
color="primary"
|
||||
fullWidth={true}
|
||||
placeholder="Email (optional)"
|
||||
type="email"
|
||||
id="email-handler"
|
||||
autoComplete="email"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
onChange={e => setFormMail(e.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
required
|
||||
style={{flex: "1", backgroundColor: theme.palette.inputColor}}
|
||||
InputProps={{
|
||||
style:{
|
||||
color: "#ffffff",
|
||||
},
|
||||
}}
|
||||
color="primary"
|
||||
fullWidth={true}
|
||||
placeholder="What are we missing?"
|
||||
type=""
|
||||
id="standard-required"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
autoComplete="off"
|
||||
onChange={e => setMessage(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
style={buttonStyle}
|
||||
disabled={message.length === 0}
|
||||
onClick={() => {
|
||||
submitContact(formMail, message)
|
||||
}}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
<Typography style={{color: "white"}} variant="body2">{formMessage}</Typography>
|
||||
</div>
|
||||
<SearchContactForm globalUrl={globalUrl} isMobile={isMobile} tabName="discord chats" />
|
||||
{/* <span style={{position: "absolute", display: "flex", textAlign: "right", float: "right", right: 0, bottom: 120, }}>
|
||||
<Typography variant="body2" color="textSecondary" style={{}}>
|
||||
Search by
|
||||
|
||||
@@ -4,6 +4,7 @@ import theme from '../theme.jsx';
|
||||
import ReactGA from 'react-ga4';
|
||||
import {Link} from 'react-router-dom';
|
||||
import { removeQuery } from '../components/ScrollToTop.jsx';
|
||||
import SearchContactForm from '../components/SearchContactForm.jsx';
|
||||
|
||||
import { Search as SearchIcon, CloudQueue as CloudQueueIcon, Code as CodeIcon, Close as CloseIcon, Folder as FolderIcon, LibraryBooks as LibraryBooksIcon } from '@mui/icons-material';
|
||||
import aa from 'search-insights'
|
||||
@@ -30,61 +31,19 @@ import { useDebouncedCallback } from "../utils/useDebouncedCallback.jsx";
|
||||
|
||||
|
||||
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "eb5fd80aa6ed5ab4730d836cff3ea283")
|
||||
const DocsGrid = props => {
|
||||
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, userdata, } = props
|
||||
const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows
|
||||
const xs = parsedXs === undefined || parsedXs === null ? isMobile ? 6 : 2 : parsedXs
|
||||
//const [apps, setApps] = React.useState([]);
|
||||
//const [filteredApps, setFilteredApps] = React.useState([]);
|
||||
const [formMail, setFormMail] = React.useState("");
|
||||
const [message, setMessage] = React.useState("");
|
||||
const [formMessage, setFormMessage] = React.useState("");
|
||||
|
||||
const buttonStyle = {borderRadius: 30, height: 50, width: 220, margin: isMobile ? "15px auto 15px auto" : 20, fontSize: 18,}
|
||||
|
||||
const innerColor = "rgba(255,255,255,0.65)"
|
||||
const borderRadius = 3
|
||||
window.title = "Shuffle | Apps | Find and integrate any app"
|
||||
|
||||
const submitContact = (email, message) => {
|
||||
const data = {
|
||||
"firstname": "",
|
||||
"lastname": "",
|
||||
"title": "",
|
||||
"companyname": "",
|
||||
"email": email,
|
||||
"phone": "",
|
||||
"message": message,
|
||||
}
|
||||
|
||||
const errorMessage = "Something went wrong. Please contact frikky@shuffler.io directly."
|
||||
|
||||
fetch(globalUrl+"/api/v1/contact", {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(response => {
|
||||
if (response.success === true) {
|
||||
setFormMessage(response.reason)
|
||||
//toast("Thanks for submitting!")
|
||||
} else {
|
||||
setFormMessage(errorMessage)
|
||||
}
|
||||
|
||||
setFormMail("")
|
||||
setMessage("")
|
||||
})
|
||||
.catch(error => {
|
||||
setFormMessage(errorMessage)
|
||||
console.log(error)
|
||||
});
|
||||
}
|
||||
|
||||
const SearchBox = ({currentRefinement, refine, isSearchStalled} ) => {
|
||||
var defaultSearch = ""
|
||||
if (window !== undefined && window.location !== undefined && window.location.search !== undefined && window.location.search !== null) {
|
||||
@@ -124,8 +83,15 @@ const DocsGrid = props => {
|
||||
placeholder="Search our Documentation..."
|
||||
id="shuffle_search_field"
|
||||
onChange={(event) => {
|
||||
removeQuery("q")
|
||||
debouncedRefine(event.currentTarget.value)
|
||||
const value = event.currentTarget.value
|
||||
debouncedRefine(value)
|
||||
const urlSearchParams = new URLSearchParams(window.location.search)
|
||||
if (value) {
|
||||
urlSearchParams.set("q", value)
|
||||
} else {
|
||||
urlSearchParams.delete("q")
|
||||
}
|
||||
window.history.replaceState(null, "", value ? `?${urlSearchParams.toString()}` : window.location.pathname)
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if(event.key === "Enter") {
|
||||
@@ -291,62 +257,8 @@ const DocsGrid = props => {
|
||||
<CustomHits hitsPerPage={5}/>
|
||||
</InstantSearch>
|
||||
{showSuggestion === true ?
|
||||
<div style={{paddingTop: 0, maxWidth: isMobile ? "100%" : "60%", margin: "auto"}}>
|
||||
<Typography variant="h6" style={{color: "white", marginTop: 50,}}>
|
||||
Can't find what you're looking for?
|
||||
</Typography>
|
||||
<div style={{flex: "1", display: "flex", flexDirection: "row", textAlign: "center",}}>
|
||||
<TextField
|
||||
required
|
||||
style={{flex: "1", marginRight: "15px", backgroundColor: theme.palette.inputColor}}
|
||||
InputProps={{
|
||||
style:{
|
||||
color: "#ffffff",
|
||||
},
|
||||
}}
|
||||
color="primary"
|
||||
fullWidth={true}
|
||||
placeholder="Email (optional)"
|
||||
type="email"
|
||||
id="email-handler"
|
||||
autoComplete="email"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
onChange={e => setFormMail(e.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
required
|
||||
style={{flex: "1", backgroundColor: theme.palette.inputColor}}
|
||||
InputProps={{
|
||||
style:{
|
||||
color: "#ffffff",
|
||||
},
|
||||
}}
|
||||
color="primary"
|
||||
fullWidth={true}
|
||||
placeholder="What are we missing?"
|
||||
type=""
|
||||
id="standard-required"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
autoComplete="off"
|
||||
onChange={e => setMessage(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
style={buttonStyle}
|
||||
disabled={message.length === 0}
|
||||
onClick={() => {
|
||||
submitContact(formMail, message)
|
||||
}}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
<Typography style={{color: "white"}} variant="body2">{formMessage}</Typography>
|
||||
</div>
|
||||
: null
|
||||
<SearchContactForm globalUrl={globalUrl} isMobile={isMobile} tabName="docs" />
|
||||
: null
|
||||
}
|
||||
|
||||
{/* <span style={{position: "absolute", display: "flex", textAlign: "right", float: "right", right: 0, bottom: 120, }}>
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
ExpandLess as ExpandLessIcon,
|
||||
ExpandMore as ExpandMoreIcon,
|
||||
Delete as DeleteIcon,
|
||||
Computer as ComputerIcon,
|
||||
} from "@mui/icons-material";
|
||||
import { toast } from 'react-toastify';
|
||||
import { Context } from '../context/ContextApi.jsx';
|
||||
@@ -49,6 +50,7 @@ const EnvironmentTab = memo((props) => {
|
||||
const [modalUser, setModalUser] = React.useState({});
|
||||
const [loginInfo, setLoginInfo] = React.useState("");
|
||||
const [modalOpen, setModalOpen] = React.useState(false);
|
||||
const [sensorGroup, setSensorGroup] = React.useState(false);
|
||||
const [showLoader, setShowLoader] = useState(true)
|
||||
const [commandController, setCommandController] = React.useState({
|
||||
pipelines: false,
|
||||
@@ -535,13 +537,36 @@ const EnvironmentTab = memo((props) => {
|
||||
},
|
||||
},
|
||||
}}
|
||||
style={{
|
||||
}}
|
||||
>
|
||||
<DialogTitle>
|
||||
<Typography variant='h5' color="textPrimary" >Add Location</Typography>
|
||||
<DialogTitle style={{display: "flex", }}>
|
||||
<Typography variant='h5' color="textPrimary">Add Location</Typography>
|
||||
<Button
|
||||
style={{ marginLeft: 485, }}
|
||||
variant={sensorGroup ? "contained" : "outlined"}
|
||||
color={sensorGroup ? "primary" : "secondary"}
|
||||
onClick={() => setSensorGroup(!sensorGroup)}
|
||||
>
|
||||
Sensor Group
|
||||
</Button>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<div>
|
||||
<Typography variant='body2' color="textPrimary">Location Name</Typography>
|
||||
<Typography variant='body1' color="textSecondary">
|
||||
{sensorGroup ?
|
||||
'With "Sensor Groups" enabled, Runtime Locations allows you to run a lightweight log-collector and response agent onprem.'
|
||||
:
|
||||
'Runtime Locations are a way to run automation in Shuffle. By default, it runs in Docker/Kubernetes and allows you to run AI Agents and Workflows in your designated datacenter.'
|
||||
}
|
||||
</Typography>
|
||||
<div style={{marginTop: 15, }}>
|
||||
<Typography color="textPrimary">
|
||||
{sensorGroup ?
|
||||
"Sensor Group name"
|
||||
:
|
||||
"Location Name"
|
||||
}
|
||||
</Typography>
|
||||
<TextField
|
||||
color="primary"
|
||||
style={{ backgroundColor: theme.palette.textFieldStyle.backgroundColor,}}
|
||||
@@ -555,13 +580,14 @@ const EnvironmentTab = memo((props) => {
|
||||
}}
|
||||
required
|
||||
fullWidth={true}
|
||||
placeholder="datacenter froglantern"
|
||||
placeholder="automation location 3"
|
||||
id="environment_name"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
onChange={(event) =>
|
||||
onChange={(event) => {
|
||||
changeModalData("environment", event.target.value)
|
||||
}
|
||||
setUpdate(Math.random())
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{loginInfo}
|
||||
@@ -576,12 +602,13 @@ const EnvironmentTab = memo((props) => {
|
||||
<Button
|
||||
variant="contained"
|
||||
style={{ borderRadius: "2px", fontSize: 16, textTransform: "none", }}
|
||||
disabled={modalUser.environment === undefined || modalUser.environment === ""}
|
||||
onClick={() => {
|
||||
submitEnvironment(modalUser); // Assuming modalUser is available
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
Submit
|
||||
Create {sensorGroup ? "Sensor Group" : "Location"}
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
@@ -956,7 +983,7 @@ const EnvironmentTab = memo((props) => {
|
||||
<ListItem
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "80px 80px 80px 150px 100px 80px 400px 100px",
|
||||
gridTemplateColumns: "80px 80px 70px 140px 100px 100px 400px 100px",
|
||||
width: "100%",
|
||||
minWidth: showLoader ? 800 : 0,
|
||||
paddingBottom: 0,
|
||||
@@ -1102,6 +1129,11 @@ const EnvironmentTab = memo((props) => {
|
||||
>
|
||||
<ListItemText
|
||||
primary={
|
||||
environment?.sensor_group === true ?
|
||||
<Tooltip title={`Sensor group for Shuffle Security monitors. Total: ${environment?.sensor_hosts?.length || 0}`}>
|
||||
<ComputerIcon />
|
||||
</Tooltip>
|
||||
:
|
||||
environment.run_type === "cloud" ||
|
||||
environment.name === "Cloud" ? (
|
||||
<Tooltip title="Cloud" placement="top">
|
||||
@@ -1275,6 +1307,9 @@ const EnvironmentTab = memo((props) => {
|
||||
<Tooltip title={`Make a new environment to set up a Datalake node. Please contact ${supportEmail} if this is something you want to see on Cloud directly.`} placement="top">
|
||||
<CancelIcon style={{ color: "rgba(255,255,255,0.3)" }} />
|
||||
</Tooltip>
|
||||
:
|
||||
environment?.sensor_group === true ?
|
||||
"N/A"
|
||||
:
|
||||
environment?.data_lake?.enabled && environment?.archived !== true ? (
|
||||
<a
|
||||
@@ -1333,7 +1368,7 @@ const EnvironmentTab = memo((props) => {
|
||||
/>
|
||||
|
||||
<ListItemText
|
||||
primary={environment.Type}
|
||||
primary={environment?.sensor_group === true ? "Sensor Group" : environment.Type}
|
||||
primaryTypographyProps={{
|
||||
style:{
|
||||
minWidth: 50,
|
||||
@@ -1520,6 +1555,16 @@ const EnvironmentTab = memo((props) => {
|
||||
<Grid container justifyContent="center" style={{minWidth: 850, maxWidth: 850, }}>
|
||||
<Grid item xs={12} sm={8} md={6}>
|
||||
<div style={{minWidth: 750, maxWidth: 750, minHeight: 350, display: 'flex', justifyContent: "center", backgroundColor: "transparent", }}>
|
||||
{environment?.sensor_group === true ?
|
||||
<div style={{ paddingTop: 50, paddingBottom: 100, }}>
|
||||
<Typography variant="h6">
|
||||
Sensor Group - Host controls available in <a href="https://security.shuffler.io/monitors" target="_blank" rel="noopener noreferrer" style={{textDecoration: "none", color: "#f85a3e",}}>Shuffle Security</a>
|
||||
</Typography>
|
||||
<Typography>
|
||||
Total registered hosts: {environment?.sensor_hosts?.length || 0}.<br/>Host management and response actions is done in Shuffle Security. Click the link above to manage.
|
||||
</Typography>
|
||||
</div>
|
||||
:
|
||||
<div style={{ paddingTop: 50, paddingBottom: 100, }}>
|
||||
<Typography variant="h6">
|
||||
Self-Hosted Orborus instance
|
||||
@@ -1703,6 +1748,7 @@ const EnvironmentTab = memo((props) => {
|
||||
}
|
||||
</Typography>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
{currentEnvQueue.length === 0 ? null :
|
||||
<List style={{ minWidth: 700, maxWidth: 700, maxHeight: 300, overflowY: "auto", scrollbarColor: theme.palette.scrollbarColorTransparent, scrollbarWidth: 'thin', }}>
|
||||
|
||||
@@ -29,6 +29,8 @@ import {
|
||||
Menu,
|
||||
Pagination,
|
||||
PaginationItem,
|
||||
Box,
|
||||
InputAdornment,
|
||||
} from "@mui/material";
|
||||
|
||||
import { DataGrid } from "@mui/x-data-grid";
|
||||
@@ -45,9 +47,12 @@ import {
|
||||
Clear as ClearIcon,
|
||||
Add as AddIcon,
|
||||
SelectAll,
|
||||
Search as SearchIcon,
|
||||
} from "@mui/icons-material";
|
||||
|
||||
import Dropzone from "../components/Dropzone.jsx";
|
||||
import SubOrgDistributionDialog from "./SubOrgDistributionDialog.jsx";
|
||||
import DeleteConfirmDialog from "./DeleteConfirmDialog.jsx";
|
||||
import ShuffleCodeEditor from "../components/ShuffleCodeEditor1.jsx";
|
||||
import {getTheme} from "../theme.jsx";
|
||||
import { Context } from "../context/ContextApi.jsx";
|
||||
@@ -82,6 +87,10 @@ const Files = memo((props) => {
|
||||
const [showDistributionPopup, setShowDistributionPopup] = useState(false)
|
||||
const [selectedSubOrg, setSelectedSubOrg] = useState([])
|
||||
const [fileIdSelectedForDistribution, setFileIdSelectedForDistribution] = useState("")
|
||||
const [fileNameSelectedForDistribution, setFileNameSelectedForDistribution] = useState("")
|
||||
const [distribOrgOrder, setDistribOrgOrder] = useState([])
|
||||
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false)
|
||||
const [deleteConfirmTarget, setDeleteConfirmTarget] = useState(null)
|
||||
const [totalAmount, setTotalAmount] = useState(0);
|
||||
const [page, setPage] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(50)
|
||||
@@ -326,7 +335,7 @@ const [filesLoaded, setFilesLoaded] = useState(false);
|
||||
navigator.clipboard.writeText(file.id);
|
||||
document.execCommand("copy");
|
||||
|
||||
toast(file.id + " copied to clipboard");
|
||||
toast.info(file.id + " copied to clipboard");
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
@@ -369,7 +378,8 @@ const [filesLoaded, setFilesLoaded] = useState(false);
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
deleteFile(file.id, true);
|
||||
setDeleteConfirmTarget({ id: file.id, filename: file.filename });
|
||||
setDeleteConfirmOpen(true);
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
@@ -441,6 +451,12 @@ const [filesLoaded, setFilesLoaded] = useState(false);
|
||||
setSelectedSubOrg([])
|
||||
}
|
||||
setFileIdSelectedForDistribution(file.id)
|
||||
setFileNameSelectedForDistribution(file.filename)
|
||||
setDistribOrgOrder(
|
||||
(userdata?.orgs || [])
|
||||
.filter(o => o.creator_org === userdata.active_org.id)
|
||||
.map(o => o.id)
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
@@ -534,7 +550,7 @@ const [filesLoaded, setFilesLoaded] = useState(false);
|
||||
})
|
||||
.then((responseJson) => {
|
||||
if (responseJson.success === true) {
|
||||
toast("Successfully updated file");
|
||||
toast.success("Successfully updated file");
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -848,125 +864,39 @@ const [filesLoaded, setFilesLoaded] = useState(false);
|
||||
</Dialog>
|
||||
: null
|
||||
|
||||
const handleSelectSubOrg = (id, action) => {
|
||||
if (action === "all") {
|
||||
const childOrgs = userdata.orgs.filter(
|
||||
(data) => data.creator_org === userdata.active_org.id
|
||||
);
|
||||
setSelectedSubOrg((prev) => {
|
||||
if (prev.length === childOrgs.length) {
|
||||
// If all child orgs are already selected, clear the selection
|
||||
return [];
|
||||
} else {
|
||||
// Otherwise, select all child org IDs
|
||||
return childOrgs.map((data) => data.id);
|
||||
}
|
||||
});
|
||||
} else if (action === "none") {
|
||||
setSelectedSubOrg([]);
|
||||
} else {
|
||||
setSelectedSubOrg((prev) => {
|
||||
if (prev.includes(id)) {
|
||||
return prev.filter((data) => data !== id);
|
||||
} else {
|
||||
return [...prev, id];
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const fileDistributionModal = showDistributionPopup ? (
|
||||
<Dialog
|
||||
open={showDistributionPopup}
|
||||
onClose={() => setShowDistributionPopup(false)}
|
||||
PaperProps={{
|
||||
sx: {
|
||||
borderRadius: theme?.palette?.DialogStyle?.borderRadius,
|
||||
border: theme?.palette?.DialogStyle?.border,
|
||||
fontFamily: theme?.typography?.fontFamily,
|
||||
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
||||
zIndex: 1000,
|
||||
minWidth: "600px",
|
||||
minHeight: "320px",
|
||||
overflow: "auto",
|
||||
'& .MuiDialogContent-root': {
|
||||
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
||||
},
|
||||
'& .MuiDialogTitle-root': {
|
||||
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
||||
},
|
||||
'& .MuiDialogActions-root': {
|
||||
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DialogTitle>
|
||||
<Typography variant="h5" color="textPrimary" >
|
||||
Select sub-org to distribute files
|
||||
</Typography>
|
||||
</DialogTitle>
|
||||
<DialogContent style={{ color: "rgba(255,255,255,0.65)" }}>
|
||||
<MenuItem value="none" onClick={()=> {handleSelectSubOrg(null, "none")}}>None</MenuItem>
|
||||
<MenuItem value="all" onClick={()=> {handleSelectSubOrg(null, "all")}}>All</MenuItem>
|
||||
{userdata.orgs.map((data, index) => {
|
||||
if (data.creator_org !== userdata.active_org.id) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const imagesize = 22;
|
||||
const imageStyle = {
|
||||
width: imagesize,
|
||||
height: imagesize,
|
||||
pointerEvents: "none",
|
||||
marginRight: 10,
|
||||
marginLeft: data.id === userdata.active_org.id ? 0 : 20,
|
||||
};
|
||||
|
||||
const image = data.image === "" ? (
|
||||
<img alt={data.name} src={theme.palette.defaultImage} style={imageStyle} />
|
||||
) : (
|
||||
<img alt={data.name} src={data.image} style={imageStyle} />
|
||||
);
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
key={index}
|
||||
value={data.id}
|
||||
onClick={() => handleSelectSubOrg(data.id)}
|
||||
style={{ display: "flex", alignItems: "center" }}
|
||||
>
|
||||
<Checkbox
|
||||
checked={selectedSubOrg.includes(data.id)}
|
||||
/>
|
||||
{image}
|
||||
<span style={{ marginLeft: 8 }}>{data.name}</span>
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
|
||||
<div style={{ display: "flex", marginTop: 20 }}>
|
||||
<Button
|
||||
style={{ borderRadius: "2px", textTransform: 'none', fontSize:16, color: theme.palette.primary.main }}
|
||||
onClick={() => setShowDistributionPopup(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
style={{ borderRadius: "2px", textTransform: 'none', fontSize:16, marginLeft: 10 }}
|
||||
onClick={() => {
|
||||
changeDistribution(fileIdSelectedForDistribution, selectedSubOrg);
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
): null
|
||||
const deleteConfirmDialog = (
|
||||
<DeleteConfirmDialog
|
||||
open={deleteConfirmOpen}
|
||||
onClose={() => { setDeleteConfirmOpen(false); setDeleteConfirmTarget(null); }}
|
||||
onConfirm={() => {
|
||||
if (deleteConfirmTarget?.bulk) {
|
||||
const toDelete = [...selectedRows];
|
||||
const count = toDelete.length;
|
||||
setSelectedRows([]);
|
||||
toDelete.forEach(fileId => deleteFile(fileId, false));
|
||||
setTimeout(() => {
|
||||
getFiles(selectedCategory);
|
||||
toast.success('Deleted ' + count + ' file' + (count > 1 ? 's' : ''));
|
||||
}, 3000);
|
||||
} else {
|
||||
deleteFile(deleteConfirmTarget.id, true);
|
||||
}
|
||||
setDeleteConfirmOpen(false);
|
||||
setDeleteConfirmTarget(null);
|
||||
}}
|
||||
title={
|
||||
deleteConfirmTarget?.bulk
|
||||
? 'Delete ' + selectedRows?.length + ' File' + (selectedRows?.length > 1 ? 's' : '') + '?'
|
||||
: 'Delete File?'
|
||||
}
|
||||
description={
|
||||
deleteConfirmTarget?.bulk
|
||||
? <>Are you sure you want to delete <b>{selectedRows?.length} file{selectedRows?.length > 1 ? 's' : ''}</b>?</>
|
||||
: <>Are you sure you want to delete <b>{deleteConfirmTarget?.filename}</b>?</>
|
||||
}
|
||||
warningText="This cannot be undone."
|
||||
/>
|
||||
);
|
||||
|
||||
const deleteFile = (fileId, showSinglDeleteToast) => {
|
||||
|
||||
@@ -1295,7 +1225,17 @@ const [filesLoaded, setFilesLoaded] = useState(false);
|
||||
}}
|
||||
onDrop={uploadFile}
|
||||
>
|
||||
{fileDistributionModal}
|
||||
<SubOrgDistributionDialog
|
||||
open={showDistributionPopup}
|
||||
onClose={() => { setShowDistributionPopup(false); }}
|
||||
title="Distribute File to Sub-Organizations"
|
||||
extraInfo={fileNameSelectedForDistribution ? `Selected File: ${fileNameSelectedForDistribution}` : null}
|
||||
orgs={distribOrgOrder.map(id => (userdata?.orgs || []).find(o => o.id === id)).filter(Boolean)}
|
||||
selectedOrgIds={selectedSubOrg}
|
||||
onSelectionChange={setSelectedSubOrg}
|
||||
onSave={(ids) => { changeDistribution(fileIdSelectedForDistribution, ids); setShowDistributionPopup(false); }}
|
||||
/>
|
||||
{deleteConfirmDialog}
|
||||
<div style={{width: "100%", minHeight: 1100, boxSizing: 'border-box', padding: "27px 10px 19px 27px", height:"100%", backgroundColor: theme.palette.platformColor,borderTopRightRadius: '8px', borderBottomRightRadius: 8, borderLeft: theme.palette.defaultBorder, }}>
|
||||
|
||||
<div style={{height: "100%", }}>
|
||||
@@ -1559,17 +1499,20 @@ const [filesLoaded, setFilesLoaded] = useState(false);
|
||||
autoFocus
|
||||
/>}
|
||||
|
||||
<ShuffleCodeEditor
|
||||
isCloud={isCloud}
|
||||
expansionModalOpen={openEditor}
|
||||
setExpansionModalOpen={setOpenEditor}
|
||||
setcodedata = {setFileContent}
|
||||
codedata={fileContent}
|
||||
isFileEditor = {true}
|
||||
key = {fileContent} //https://reactjs.org/docs/reconciliation.html#recursing-on-children
|
||||
runUpdateText = {runUpdateText}
|
||||
contentLoading = {contentLoading}
|
||||
/>
|
||||
{openEditor === true && fileContent !== undefined && fileContent !== null && fileContent.length > 0 ?
|
||||
<ShuffleCodeEditor
|
||||
isCloud={isCloud}
|
||||
expansionModalOpen={openEditor}
|
||||
setExpansionModalOpen={setOpenEditor}
|
||||
setcodedata = {setFileContent}
|
||||
codedata={fileContent}
|
||||
isFileEditor = {true}
|
||||
key = {fileContent} //https://reactjs.org/docs/reconciliation.html#recursing-on-children
|
||||
runUpdateText = {runUpdateText}
|
||||
contentLoading = {contentLoading}
|
||||
/>
|
||||
: null}
|
||||
|
||||
{isSelectedFiles?null:
|
||||
<Divider
|
||||
style={{
|
||||
@@ -1672,22 +1615,9 @@ const [filesLoaded, setFilesLoaded] = useState(false);
|
||||
toast("Please select files to delete");
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i = 0; i < selectedRows.length; i++) {
|
||||
const fileIdToDelete = selectedRows[i];
|
||||
deleteFile(fileIdToDelete, false);
|
||||
|
||||
if (i === selectedRows.length - 1) {
|
||||
setTimeout(() => {
|
||||
setSelectedRows([]);
|
||||
getFiles(selectedCategory);
|
||||
toast.success(
|
||||
`Deleted ${selectedRows.length} file${selectedRows.length === 1 ? "" : "s"}`
|
||||
);
|
||||
}, 2500);
|
||||
}
|
||||
}
|
||||
}}
|
||||
setDeleteConfirmTarget({ bulk: true });
|
||||
setDeleteConfirmOpen(true);
|
||||
}}
|
||||
variant={"outlined"}
|
||||
color="secondary"
|
||||
startIcon={
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -52,6 +52,12 @@ const ShuffleLogo = "/images/Shuffle_logo.png";
|
||||
const detectionIcon = "/icons/detection.svg";
|
||||
const documentationIcon = "/icons/documentation.svg";
|
||||
const ExpandMoreAndLessIcon = "/icons/expandMoreIcon.svg";
|
||||
const shuffleSecurityLogo = (
|
||||
<svg width="30" height="30" viewBox="0 0 56 56" fill="none">
|
||||
<path d="M14 14h28v6H20v16h16v-10h-8v-6h14v22H14V14z" fill="#FF6600" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
|
||||
|
||||
const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, SHUFFLE_VERSION }) => {
|
||||
@@ -87,6 +93,17 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, SHUFFLE_V
|
||||
);
|
||||
const [activeOrgData, setActiveOrgData] = useState(null);
|
||||
const [isProdStatusOn, setIsProdStatusOn] = useState(false);
|
||||
const [productAnchorEl, setProductAnchorEl] = useState(null);
|
||||
|
||||
const handleProductClick = (event) => {
|
||||
event.preventDefault();
|
||||
setProductAnchorEl((prev) => (prev ? null : event.currentTarget));
|
||||
};
|
||||
|
||||
const handleProductClose = () => {
|
||||
setProductAnchorEl(null);
|
||||
};
|
||||
|
||||
const userOrgs = React.useMemo(() => {
|
||||
return orgOptions.find((option) => option.name === selectedOrg);
|
||||
}, [selectedOrg, orgOptions]);
|
||||
@@ -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>
|
||||
)
|
||||
)
|
||||
});
|
||||
});
|
||||
|
||||
@@ -330,7 +330,7 @@ const LicencePopup = (props) => {
|
||||
body: JSON.stringify({
|
||||
org_id: selectedOrganization.id,
|
||||
editing: "subscription_update",
|
||||
subscription_index: 0,
|
||||
subscription_index: subscription.id,
|
||||
subscription: subscription,
|
||||
}),
|
||||
mode: "cors",
|
||||
@@ -554,7 +554,7 @@ const LicencePopup = (props) => {
|
||||
const payload = {
|
||||
org_id: selectedOrganization.id,
|
||||
editing: "subscription_update",
|
||||
subscription_index: 0,
|
||||
subscription_index: subscription.id,
|
||||
subscription: {
|
||||
...form,
|
||||
// Ensure backend gets array of features
|
||||
@@ -696,6 +696,15 @@ const LicencePopup = (props) => {
|
||||
helperText={errors.amount || "0 for Free"}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Stripe Sub ID"
|
||||
value={form.reference || ""}
|
||||
onChange={(e) => setForm({ ...form, reference: e.target.value })}
|
||||
fullWidth
|
||||
placeholder="sub_1234567890abcdef"
|
||||
helperText="Stripe subscription reference ID"
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Start date"
|
||||
type="date"
|
||||
@@ -1042,6 +1051,17 @@ const LicencePopup = (props) => {
|
||||
: localSub?.currency + localSub?.amount
|
||||
: "Free";
|
||||
|
||||
const calculateAppRunsFromPrice = (amount) => {
|
||||
const price = parseInt(amount) || 0;
|
||||
if (price === 0) return 2000; // Free plan
|
||||
|
||||
// Calculate Stripe quantity from price ($32 per unit)
|
||||
const stripeQuantity = Math.max(1, Math.round(price / 32));
|
||||
|
||||
// Backend logic: (quantity * 10000) + 2000
|
||||
return (stripeQuantity * 10000) + 2000;
|
||||
};
|
||||
|
||||
if (typeof window === "undefined" || window.location === undefined) {
|
||||
return null;
|
||||
}
|
||||
@@ -1252,7 +1272,7 @@ const LicencePopup = (props) => {
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{isPaidPlan ? (
|
||||
{(isPaidPlan || localSub?.amount === "0") ? (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
@@ -1336,22 +1356,38 @@ const LicencePopup = (props) => {
|
||||
) : null}
|
||||
|
||||
{localSub.cancellationdate !== 0 ? (
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="textSecondary"
|
||||
style={{ marginTop: 2 }}
|
||||
>
|
||||
{`Cancelled on ${new Date(
|
||||
(localSub.cancellationdate || localSub.CancellationDate) *
|
||||
1000
|
||||
).toLocaleDateString(undefined, {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
})}`}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="textSecondary"
|
||||
style={{ marginTop: 2 }}
|
||||
>
|
||||
{`Cancelled on ${new Date(
|
||||
(localSub.cancellationdate || localSub.CancellationDate) *
|
||||
1000
|
||||
).toLocaleDateString(undefined, {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
})}`}
|
||||
</Typography>
|
||||
) : null}
|
||||
|
||||
{localSub.amount !== "0" && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="textSecondary"
|
||||
style={{ marginTop: 2 }}
|
||||
>
|
||||
{`Purchased on ${new Date(
|
||||
(localSub.startdate || localSub.Startdate) * 1000
|
||||
).toLocaleDateString(undefined, {
|
||||
day: "2-digit",
|
||||
month: "short",
|
||||
year: "numeric",
|
||||
})}`}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<Divider
|
||||
style={{
|
||||
marginTop: 12,
|
||||
@@ -1364,39 +1400,50 @@ const LicencePopup = (props) => {
|
||||
(isCloud || (!isCloud && selectedOrganization.cloud_sync)) && (
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
|
||||
<Typography variant="body2" color="textSecondary" style={{}}>
|
||||
App Runs
|
||||
{localSub?.active ? "App Runs" : "App Runs included in plan"}
|
||||
</Typography>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
flexDirection: "column",
|
||||
gap: 10,
|
||||
}}
|
||||
>
|
||||
{localSub?.active ? (
|
||||
// Active plan - show current usage and progress bar
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
flexDirection: "column",
|
||||
gap: 10,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body1"
|
||||
style={{ minWidth: 140, fontWeight: 600 }}
|
||||
>
|
||||
{usedAppRuns?.toLocaleString?.() || usedAppRuns} of{" "}
|
||||
{appRunsLimit?.toLocaleString?.() || appRunsLimit}
|
||||
</Typography>
|
||||
<Box sx={{ width: "100%" }}>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={appRunsPct}
|
||||
sx={{
|
||||
height: 6,
|
||||
borderRadius: 6,
|
||||
backgroundColor: "#3a3a3a",
|
||||
"& .MuiLinearProgress-bar": {
|
||||
backgroundColor: "#ff8544",
|
||||
borderRadius: 6,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</div>
|
||||
) : (
|
||||
// Inactive plan - show only the plan's app runs capacity
|
||||
<Typography
|
||||
variant="body1"
|
||||
style={{ minWidth: 140, fontWeight: 600 }}
|
||||
style={{ minWidth: 140, fontWeight: 600, fontSize: 18 }}
|
||||
>
|
||||
{usedAppRuns?.toLocaleString?.() || usedAppRuns} of{" "}
|
||||
{appRunsLimit?.toLocaleString?.() || appRunsLimit}
|
||||
{calculateAppRunsFromPrice(localSub.amount)?.toLocaleString?.() || calculateAppRunsFromPrice(localSub.amount)}
|
||||
</Typography>
|
||||
<Box sx={{ width: "100%" }}>
|
||||
<LinearProgress
|
||||
variant="determinate"
|
||||
value={appRunsPct}
|
||||
sx={{
|
||||
height: 6,
|
||||
borderRadius: 6,
|
||||
backgroundColor: "#3a3a3a",
|
||||
"& .MuiLinearProgress-bar": {
|
||||
backgroundColor: "#ff8544",
|
||||
borderRadius: 6,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1480,7 +1527,6 @@ const LicencePopup = (props) => {
|
||||
}}
|
||||
>
|
||||
{isCloud &&
|
||||
localSub.name.toLowerCase().includes("scale") &&
|
||||
localSub?.reference &&
|
||||
localSub.reference.length > 0 ? (
|
||||
<Button
|
||||
@@ -1490,8 +1536,11 @@ const LicencePopup = (props) => {
|
||||
onClick={() => {
|
||||
const url = `${globalUrl}/api/v1/orgs/${selectedOrganization.id}/manage_subscription`;
|
||||
fetch(url, {
|
||||
method: "GET",
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
body: JSON.stringify({
|
||||
subscription_id: localSub.reference
|
||||
}),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
})
|
||||
.then((r) => r.json())
|
||||
@@ -1575,8 +1624,8 @@ const LicencePopup = (props) => {
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
<Grid item maxWidth={licensePopup ? 400 : 450}>
|
||||
{isLoading ? (
|
||||
<Grid item maxWidth={licensePopup ? 400 : 800} style={{ display: "flex", flexDirection: "row", gap: 16, flexWrap: "wrap" }}>
|
||||
{isLoading ? (
|
||||
<SubscriptionSkeleton />
|
||||
) : (
|
||||
<>
|
||||
@@ -1584,8 +1633,14 @@ const LicencePopup = (props) => {
|
||||
selectedOrganization.subscriptions !== null &&
|
||||
selectedOrganization.subscriptions.length > 0
|
||||
? (selectedOrganization.subscriptions || [])
|
||||
.slice()
|
||||
.map((sub, index) => {
|
||||
.slice()
|
||||
.sort((a, b) => {
|
||||
// Active subscriptions first, then inactive
|
||||
if (a.active && !b.active) return -1;
|
||||
if (!a.active && b.active) return 1;
|
||||
return 0;
|
||||
})
|
||||
.map((sub, index) => {
|
||||
return (
|
||||
<SubscriptionObject
|
||||
key={sub.id || index}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Context } from "../context/ContextApi.jsx";
|
||||
import { toast } from 'react-toastify';
|
||||
import { useParams, useNavigate, Link } from "react-router-dom";
|
||||
import { getTheme } from '../theme.jsx';
|
||||
import ReactGA from 'react-ga4';
|
||||
//import { useAlert
|
||||
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
@@ -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;
|
||||
@@ -47,7 +47,7 @@ const chipStyle = {
|
||||
backgroundColor: "#3d3f43", height: 30, marginRight: 5, paddingLeft: 5, paddingRight: 5, height: 28, cursor: "pointer", borderColor: "#3d3f43", color: "white",
|
||||
}
|
||||
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "33e4e3564f4f060e96e0531957bed552")
|
||||
const SearchData = props => {
|
||||
const { serverside, globalUrl, userdata } = props
|
||||
let navigate = useNavigate();
|
||||
|
||||
@@ -38,7 +38,6 @@ import aa from 'search-insights'
|
||||
import { InstantSearch, Configure, connectSearchBox, connectHits, Index } from 'react-instantsearch-dom';
|
||||
//import { InstantSearch, SearchBox, Hits, connectSearchBox, connectHits, Index } from 'react-instantsearch-dom';
|
||||
|
||||
import { HotKeys } from 'react-hotkeys';
|
||||
// https://www.algolia.com/doc/api-reference/widgets/search-box/react/
|
||||
const chipStyle = {
|
||||
backgroundColor: "#3d3f43", height: 30, marginRight: 5, paddingLeft: 5, paddingRight: 5, height: 28, cursor: "pointer", borderColor: "#3d3f43", color: "white",
|
||||
@@ -168,4 +167,4 @@ const SearchField = props => {
|
||||
)
|
||||
}
|
||||
|
||||
export default SearchField;
|
||||
export default SearchField;
|
||||
@@ -61,8 +61,6 @@ import PaperComponent from "../components/PaperComponent.jsx";
|
||||
import { padding, textAlign } from '@mui/system';
|
||||
import data from '../frameworkStyle.jsx';
|
||||
import { useNavigate, Link, useParams, useSearchParams } from "react-router-dom";
|
||||
import { tags as t } from '@lezer/highlight';
|
||||
|
||||
|
||||
import AceEditor from "react-ace";
|
||||
import ace from "ace-builds";
|
||||
@@ -149,7 +147,10 @@ const CodeEditor = (props) => {
|
||||
|
||||
// Auto-indent JSON-like content (with safety hehe)
|
||||
const autoIndentContent = React.useCallback((content) => {
|
||||
return content
|
||||
if (!isFileEditor) {
|
||||
console.log("Autoindent disabled")
|
||||
return content
|
||||
}
|
||||
|
||||
// Safety checks :)
|
||||
if (!content || typeof content !== 'string' || content.trim().length === 0) {
|
||||
@@ -238,6 +239,24 @@ const CodeEditor = (props) => {
|
||||
expectedOutput(localcodedata)
|
||||
}, [localcodedata])
|
||||
|
||||
useEffect(() => {
|
||||
if (!isFileEditor) {
|
||||
return
|
||||
}
|
||||
|
||||
if (codedata === undefined || codedata === null || typeof codedata !== 'string') {
|
||||
return
|
||||
}
|
||||
|
||||
const indentedContent = autoIndentContent(codedata);
|
||||
if (indentedContent !== undefined && indentedContent !== null) {
|
||||
console.log("SETTING: ", indentedContent)
|
||||
setlocalcodedata(indentedContent);
|
||||
} else {
|
||||
console.log("INDENT FAILED")
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Auto-indent when codedata prop changes
|
||||
useEffect(() => {
|
||||
if (codedata && codedata !== localcodedata && typeof codedata === 'string') {
|
||||
@@ -1597,7 +1616,7 @@ const CodeEditor = (props) => {
|
||||
if (e.srcElement.className === "ace_content") {
|
||||
console.log("DRAG STOP IN CONTENT!", e.srcElement.className)
|
||||
|
||||
let usedposition = e.offsetY
|
||||
const usedposition = e.offsetY
|
||||
if (usedposition === undefined || usedposition === null) {
|
||||
toast.info(`Error: LayerY is undefined or null. Please contact ${supportEmail}`)
|
||||
return
|
||||
@@ -1784,8 +1803,8 @@ const CodeEditor = (props) => {
|
||||
// zIndex: 12501,
|
||||
pointerEvents: "auto",
|
||||
color: theme.palette.DialogStyle.color,
|
||||
minWidth: isMobile || isWorkflowEditor || fullScreenModeEnabled ? "100%" : isFileEditor ? "650px" : "80%",
|
||||
maxWidth: isMobile || isWorkflowEditor || fullScreenModeEnabled ? "100%" : isFileEditor ? "650px" : "1100px",
|
||||
minWidth: isMobile || isWorkflowEditor || fullScreenModeEnabled ? "100%" : isFileEditor ? 800 : "80%",
|
||||
maxWidth: isMobile || isWorkflowEditor || fullScreenModeEnabled ? "100%" : isFileEditor ? 800 : "1100px",
|
||||
minHeight: isMobile || isWorkflowEditor || fullScreenModeEnabled ? "100%" : "auto",
|
||||
maxHeight: isMobile || isWorkflowEditor || fullScreenModeEnabled ? "100%" : "700px",
|
||||
border: "3px solid rgba(255,255,255,0.3)",
|
||||
@@ -1981,7 +2000,8 @@ const CodeEditor = (props) => {
|
||||
paddingLeft: 10,
|
||||
}}
|
||||
>
|
||||
File Editor ({localcodedata.length})
|
||||
{/* cba positioning */}
|
||||
File Editor ({localcodedata.length}) {validation === true ? <span style={{ color: "lightgreen" }}>Valid JSON</span> : <span style={{ color: "red" }}>Invalid JSON</span>}
|
||||
</DialogTitle>
|
||||
</div>
|
||||
|
||||
@@ -2511,7 +2531,7 @@ const CodeEditor = (props) => {
|
||||
}
|
||||
</Tooltip>
|
||||
</IconButton>
|
||||
{(actionId || triggerId || conditionId) && !isWorkflowEditor && !isFileEditor ?
|
||||
{/*(actionId || triggerId || conditionId) && !isWorkflowEditor && !isFileEditor ?
|
||||
<>
|
||||
<IconButton
|
||||
style={{
|
||||
@@ -2557,7 +2577,7 @@ const CodeEditor = (props) => {
|
||||
</IconButton>
|
||||
</>
|
||||
: null
|
||||
}
|
||||
*/}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -2583,7 +2603,7 @@ const CodeEditor = (props) => {
|
||||
mode={isWorkflowEditor ? "yaml" : selectedAction === undefined ? "json" : selectedAction.name === "execute_python" ? "python" : selectedAction.name === "execute_bash" ? "bash" : "json"}
|
||||
theme="gruvbox"
|
||||
height={fullScreenModeEnabled ? "84vh" : isFileEditor ? 450 : isWorkflowEditor ? "90vh" : 550}
|
||||
width={isFileEditor ? 650 : fullScreenModeEnabled ? "50vw" : isWorkflowEditor ? "90vw" : "100%"}
|
||||
width={isFileEditor ? 800 : fullScreenModeEnabled ? isFileEditor ? "100%" : "50vw" : isWorkflowEditor ? "90vw" : "100%"}
|
||||
|
||||
markers={markers}
|
||||
highlightActiveLine={false}
|
||||
@@ -2717,7 +2737,7 @@ const CodeEditor = (props) => {
|
||||
</div>
|
||||
:
|
||||
<span style={{ color: theme.palette.text.primary }}>
|
||||
{selectedAction.name === "execute_python" || selectedAction.name === "execute_bash" ?
|
||||
{selectedAction?.name === "execute_python" || selectedAction?.name === "execute_bash" ?
|
||||
"Code to run" :
|
||||
triggerId ?
|
||||
`Output: ${triggerName?.replaceAll("_", " ").slice(0, 1).toUpperCase() + triggerName?.replaceAll("_", " ").slice(1)} (${triggerField})` :
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
import React, { useState, useContext } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
Box,
|
||||
TextField,
|
||||
Button,
|
||||
Typography,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
Checkbox,
|
||||
InputAdornment,
|
||||
} from "@mui/material";
|
||||
import { Search as SearchIcon } from "@mui/icons-material";
|
||||
import { Context } from "../context/ContextApi.jsx";
|
||||
import { getTheme } from "../theme.jsx";
|
||||
|
||||
/**
|
||||
* A reusable dialog for selecting/distributing sub-organizations.
|
||||
*
|
||||
* Props:
|
||||
* open {boolean} - controls dialog visibility
|
||||
* onClose {function} - called on Cancel or backdrop click (no args)
|
||||
* title {string} - dialog title text
|
||||
* extraInfo {string} - secondary line below the title (e.g. "Selected Key: xxx")
|
||||
* orgs {Array} - ordered array of { id, name, image? } objects to display
|
||||
* selectedOrgIds {string[]} - currently selected org IDs (controlled)
|
||||
* onSelectionChange {function} - called with a updater fn (prev => next) when selection changes
|
||||
* onSave {function} - called with the final selectedOrgIds array when Save is clicked
|
||||
* disabled {boolean} - disables checkboxes and Save button (default: false)
|
||||
*/
|
||||
const SubOrgDistributionDialog = ({
|
||||
open,
|
||||
onClose,
|
||||
title,
|
||||
extraInfo = null,
|
||||
orgs = [],
|
||||
selectedOrgIds = [],
|
||||
onSelectionChange,
|
||||
onSave,
|
||||
disabled = false,
|
||||
}) => {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const { themeMode, brandColor } = useContext(Context);
|
||||
const theme = getTheme(themeMode, brandColor);
|
||||
|
||||
const safeOrgs = orgs || [];
|
||||
|
||||
const filteredOrgs = safeOrgs.filter(
|
||||
o => o && o.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
const handleSelectAll = () => {
|
||||
if (searchQuery) {
|
||||
const filteredIds = filteredOrgs.map(o => o.id);
|
||||
onSelectionChange(prev => [...new Set([...prev, ...filteredIds])]);
|
||||
} else {
|
||||
const allIds = safeOrgs.map(o => o.id);
|
||||
onSelectionChange(prev => [...new Set([...prev, ...allIds])]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeselectAll = () => {
|
||||
if (searchQuery) {
|
||||
const filteredIds = filteredOrgs.map(o => o.id);
|
||||
onSelectionChange(prev => prev.filter(id => !filteredIds.includes(id)));
|
||||
} else {
|
||||
onSelectionChange([]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggle = (id) => {
|
||||
if (disabled) return;
|
||||
onSelectionChange(prev =>
|
||||
prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id]
|
||||
);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setSearchQuery("");
|
||||
onClose();
|
||||
};
|
||||
|
||||
const filteredSelected = filteredOrgs.filter(o => selectedOrgIds.includes(o.id)).length;
|
||||
const countText = searchQuery
|
||||
? `${filteredSelected} of ${filteredOrgs.length} filtered selected`
|
||||
: `${selectedOrgIds.length} of ${safeOrgs.length} selected`;
|
||||
|
||||
const imageSize = 22;
|
||||
const imageStyle = { width: imageSize, height: imageSize, pointerEvents: "none", marginRight: 10 };
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={handleClose}
|
||||
maxWidth="md"
|
||||
fullWidth
|
||||
PaperProps={{
|
||||
sx: {
|
||||
borderRadius: theme?.palette?.DialogStyle?.borderRadius,
|
||||
border: theme?.palette?.DialogStyle?.border,
|
||||
fontFamily: theme?.typography?.fontFamily,
|
||||
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
||||
zIndex: 1000,
|
||||
height: "80vh",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
"& .MuiDialogContent-root": {
|
||||
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
||||
},
|
||||
"& .MuiDialogTitle-root": {
|
||||
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
||||
},
|
||||
"& .MuiDialogActions-root": {
|
||||
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DialogTitle>
|
||||
<Typography variant="h6" color="textPrimary" style={{ textTransform: "none" }}>
|
||||
{title}
|
||||
</Typography>
|
||||
{extraInfo && (
|
||||
<Typography variant="body2" color="textSecondary" style={{ marginTop: 2 }}>
|
||||
{extraInfo}
|
||||
</Typography>
|
||||
)}
|
||||
</DialogTitle>
|
||||
|
||||
<DialogContent sx={{ display: "flex", flexDirection: "column", gap: 2, overflow: "hidden", pt: "8px !important" }}>
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
placeholder="Search sub-organizations..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
InputProps={{
|
||||
startAdornment: (
|
||||
<InputAdornment position="start">
|
||||
<SearchIcon style={{ opacity: 0.5 }} />
|
||||
</InputAdornment>
|
||||
),
|
||||
style: { color: theme.palette.textFieldStyle?.color },
|
||||
}}
|
||||
style={{ backgroundColor: theme.palette.textFieldStyle?.backgroundColor, flexShrink: 0 }}
|
||||
/>
|
||||
|
||||
<div style={{ display: "flex", gap: 8, alignItems: "center", flexShrink: 0 }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
style={{ textTransform: "none" }}
|
||||
onClick={handleSelectAll}
|
||||
>
|
||||
Select All{searchQuery ? " Filtered" : ""}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outlined"
|
||||
size="small"
|
||||
style={{ textTransform: "none" }}
|
||||
onClick={handleDeselectAll}
|
||||
>
|
||||
Deselect All{searchQuery ? " Filtered" : ""}
|
||||
</Button>
|
||||
<Typography variant="body2" color="textSecondary" style={{ marginLeft: "auto" }}>
|
||||
{countText}
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
<div style={{
|
||||
flex: 1,
|
||||
overflowY: "auto",
|
||||
border: theme.palette.defaultBorder,
|
||||
borderRadius: 4,
|
||||
scrollbarColor: theme.palette.scrollbarColorTransparent,
|
||||
scrollbarWidth: "thin",
|
||||
}}>
|
||||
<List dense sx={{ py: 0 }}>
|
||||
{filteredOrgs.map((org, index) => {
|
||||
const isSelected = selectedOrgIds.includes(org.id);
|
||||
const hasImage = org.image !== undefined;
|
||||
return (
|
||||
<ListItem
|
||||
key={org.id}
|
||||
onClick={() => handleToggle(org.id)}
|
||||
sx={{
|
||||
cursor: disabled ? "default" : "pointer",
|
||||
backgroundColor: index % 2 === 0
|
||||
? "transparent"
|
||||
: themeMode === "dark" ? "rgba(255,255,255,0.02)" : "rgba(0,0,0,0.02)",
|
||||
"&:hover": {
|
||||
backgroundColor: themeMode === "dark" ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.05)",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Checkbox
|
||||
edge="start"
|
||||
checked={isSelected}
|
||||
tabIndex={-1}
|
||||
disableRipple
|
||||
color="primary"
|
||||
disabled={disabled}
|
||||
/>
|
||||
{hasImage && (
|
||||
org.image === "" ? (
|
||||
<img alt={org.name} src={theme.palette.defaultImage} style={imageStyle} />
|
||||
) : (
|
||||
<img alt={org.name} src={org.image} style={imageStyle} />
|
||||
)
|
||||
)}
|
||||
<ListItemText
|
||||
primary={org.name}
|
||||
/>
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
{filteredOrgs.length === 0 && (
|
||||
<ListItem>
|
||||
<ListItemText
|
||||
primary="No sub-organizations found"
|
||||
style={{ textAlign: "center", opacity: 0.5 }}
|
||||
/>
|
||||
</ListItem>
|
||||
)}
|
||||
</List>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
<Box sx={{
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
alignItems: "center",
|
||||
p: 2,
|
||||
borderTop: theme.palette.defaultBorder,
|
||||
backgroundColor: theme.palette.platformColor,
|
||||
}}>
|
||||
<div style={{ display: "flex", gap: 8 }}>
|
||||
<Button
|
||||
onClick={handleClose}
|
||||
style={{ textTransform: "none", color: theme.palette.text.primary }}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
disabled={disabled}
|
||||
style={{ textTransform: "none" }}
|
||||
onClick={() => {
|
||||
onSave(selectedOrgIds);
|
||||
setSearchQuery("");
|
||||
}}
|
||||
>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
</Box>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default SubOrgDistributionDialog;
|
||||
+1140
-1148
File diff suppressed because it is too large
Load Diff
@@ -1,11 +1,10 @@
|
||||
import React, { useState, useEffect, useContext, memo } from "react";
|
||||
import { toast } from 'react-toastify';
|
||||
import { Context } from "../context/ContextApi.jsx";
|
||||
import { Link } from "react-router-dom";
|
||||
import {
|
||||
FormControl,
|
||||
InputLabel,
|
||||
OutlinedInput,
|
||||
Checkbox,
|
||||
Tooltip,
|
||||
Typography,
|
||||
Select,
|
||||
@@ -31,23 +30,12 @@ import {
|
||||
import {
|
||||
Cached as CachedIcon,
|
||||
Edit as EditIcon,
|
||||
Style,
|
||||
} from "@mui/icons-material";
|
||||
import ModeEditOutlineOutlinedIcon from '@mui/icons-material/ModeEditOutlineOutlined';
|
||||
import ContentCopyOutlinedIcon from '@mui/icons-material/ContentCopyOutlined';
|
||||
|
||||
import {getTheme} from "../theme.jsx";
|
||||
const ITEM_HEIGHT = 48;
|
||||
const ITEM_PADDING_TOP = 8;
|
||||
const MenuProps = {
|
||||
PaperProps: {
|
||||
style: {
|
||||
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
|
||||
width: 500,
|
||||
},
|
||||
},
|
||||
getContentAnchorEl: () => null,
|
||||
};
|
||||
import SubOrgDistributionDialog from "./SubOrgDistributionDialog.jsx";
|
||||
|
||||
const logsViewModal = false;
|
||||
const userdata = "";
|
||||
@@ -76,10 +64,11 @@ const UserManagmentTab = memo((props) => {
|
||||
const [logsViewModal, setLogsViewModal] = React.useState(false);
|
||||
const [ipSelected, setIpSelected] = React.useState("");
|
||||
const [userLogViewing, setUserLogViewing] = React.useState({});
|
||||
const [subOrgModalOpen, setSubOrgModalOpen] = React.useState(false);
|
||||
const [pendingSubOrgs, setPendingSubOrgs] = React.useState([]);
|
||||
const { themeMode, supportEmail, brandColor } = useContext(Context);
|
||||
const theme = getTheme(themeMode, brandColor);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedOrganization?.mfa_required !== MFARequired) {
|
||||
setMFARequired(selectedOrganization?.mfa_required);
|
||||
@@ -247,30 +236,6 @@ const UserManagmentTab = memo((props) => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleOrgEditChange = (event) => {
|
||||
if (userdata.id === selectedUser.id) {
|
||||
toast("Can't remove orgs from yourself");
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.target.value.includes("ALL")) {
|
||||
toast.info("Adding to available all sub-organizations. This may take a minute.")
|
||||
event.target.value = selectedOrganization.child_orgs.map((org) => org.id)
|
||||
} else if (event.target.value.includes("None")) {
|
||||
toast.info("Removing from all sub-organizations. This may take a minute")
|
||||
event.target.value = []
|
||||
}
|
||||
|
||||
setMatchingOrganizations(event.target.value);
|
||||
// Workaround for empty orgs
|
||||
if (event.target.value.length === 0) {
|
||||
event.target.value.push("REMOVE");
|
||||
}
|
||||
|
||||
setUser(selectedUser.id, "suborgs", event.target.value);
|
||||
//setUser(selectedUser.id, "suborgs", matchingOrganizations)
|
||||
};
|
||||
|
||||
const userOrgEdit =
|
||||
selectedUser.id !== undefined &&
|
||||
selectedUser?.orgs !== undefined &&
|
||||
@@ -278,44 +243,44 @@ const UserManagmentTab = memo((props) => {
|
||||
selectedOrganization?.child_orgs !== undefined &&
|
||||
selectedOrganization?.child_orgs !== null &&
|
||||
selectedOrganization?.child_orgs?.length > 0 ? (
|
||||
<FormControl fullWidth sx={{ m: 1 }}>
|
||||
<InputLabel id="demo-multiple-checkbox-label" style={{ padding: 5 }}>
|
||||
Accessible Sub-Organizations (
|
||||
{selectedUser?.orgs ? selectedUser?.orgs?.length - 1 : 0})
|
||||
</InputLabel>
|
||||
<Select
|
||||
fullWidth
|
||||
style={{ width: "100%" }}
|
||||
disabled={selectedUser?.id === userdata?.id}
|
||||
labelId="demo-multiple-checkbox-label"
|
||||
id="demo-multiple-checkbox"
|
||||
multiple
|
||||
value={matchingOrganizations}
|
||||
onChange={handleOrgEditChange}
|
||||
input={<OutlinedInput label="Tag" />}
|
||||
renderValue={(selected) => {
|
||||
return selected.join(", ");
|
||||
}}
|
||||
MenuProps={MenuProps}
|
||||
>
|
||||
<MenuItem key={-2} value={"None"}>
|
||||
<Checkbox checked={false} />
|
||||
<ListItemText primary={"None"} />
|
||||
</MenuItem>
|
||||
<MenuItem key={-1} value={"ALL"}>
|
||||
<Checkbox checked={false} />
|
||||
<ListItemText primary={"ALL"} />
|
||||
</MenuItem>
|
||||
{selectedOrganization.child_orgs.map((org, index) => (
|
||||
<MenuItem key={index} value={org.id}>
|
||||
<Checkbox checked={matchingOrganizations.indexOf(org.id) > -1} />
|
||||
<ListItemText primary={org.name} />
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</FormControl>
|
||||
<Button
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
fullWidth
|
||||
disabled={selectedUser?.id === userdata?.id}
|
||||
onClick={() => {
|
||||
setPendingSubOrgs([...matchingOrganizations]);
|
||||
setSubOrgModalOpen(true);
|
||||
}}
|
||||
sx={{ m: 1, textTransform: 'none', justifyContent: 'space-between', py: 1.5, fontSize: 14 }}
|
||||
>
|
||||
Manage Sub-Organizations ({matchingOrganizations.length} assigned of {selectedOrganization?.child_orgs?.length || 0})
|
||||
</Button>
|
||||
) : null;
|
||||
|
||||
const subOrgManagementDialog = (
|
||||
<SubOrgDistributionDialog
|
||||
open={subOrgModalOpen}
|
||||
onClose={() => setSubOrgModalOpen(false)}
|
||||
title={`Manage Sub-Organizations for ${selectedUser?.username || ''}`}
|
||||
orgs={selectedOrganization?.child_orgs || []}
|
||||
selectedOrgIds={pendingSubOrgs}
|
||||
onSelectionChange={setPendingSubOrgs}
|
||||
onSave={(ids) => {
|
||||
if (userdata.id === selectedUser.id) {
|
||||
toast("Can't modify orgs for yourself");
|
||||
return;
|
||||
}
|
||||
const newValue = ids.length === 0 ? ["REMOVE"] : [...ids];
|
||||
setMatchingOrganizations([...ids]);
|
||||
setUser(selectedUser.id, "suborgs", newValue);
|
||||
setSubOrgModalOpen(false);
|
||||
setSelectedUserModalOpen(false);
|
||||
}}
|
||||
disabled={selectedUser?.id === userdata?.id}
|
||||
/>
|
||||
);
|
||||
|
||||
const getUsers = () => {
|
||||
fetch(globalUrl + "/api/v1/getusers", {
|
||||
method: "GET",
|
||||
@@ -1117,6 +1082,8 @@ const UserManagmentTab = memo((props) => {
|
||||
});
|
||||
};
|
||||
|
||||
var previousreferrer = ""
|
||||
var nextreferrer = ""
|
||||
const logview = logsViewModal ? (
|
||||
<Dialog
|
||||
open={logsViewModal}
|
||||
@@ -1163,26 +1130,23 @@ const UserManagmentTab = memo((props) => {
|
||||
onChange={(event) => {
|
||||
setIpSelected(event.target.value);
|
||||
getLogs(event.target.value, userLogViewing.id);
|
||||
|
||||
|
||||
}}
|
||||
>
|
||||
{(() => {
|
||||
const uniqueIPs = new Set();
|
||||
console.log("Login info: ", userLogViewing.login_info)
|
||||
|
||||
return userLogViewing.login_info.map((data, index) => {
|
||||
if (
|
||||
data.ip.includes("127.0.0.1") ||
|
||||
uniqueIPs.has(data.ip)
|
||||
) {
|
||||
return null;
|
||||
console.log("Data: ", data)
|
||||
if (data.ip.includes("127.0.0.1") || uniqueIPs.has(data.ip)) {
|
||||
return null
|
||||
}
|
||||
|
||||
uniqueIPs.add(data.ip);
|
||||
uniqueIPs.add(data.ip)
|
||||
|
||||
return (
|
||||
<MenuItem key={index} value={data.ip}>
|
||||
{data.ip}
|
||||
{data?.timestamp ? new Date(data.timestamp * 1000).toLocaleString() : "N/A"} - {data?.ip}
|
||||
</MenuItem>
|
||||
);
|
||||
});
|
||||
@@ -1229,12 +1193,13 @@ const UserManagmentTab = memo((props) => {
|
||||
minWidth: 700,
|
||||
maxWidth: 700,
|
||||
overflow: "hidden",
|
||||
marginLeft: 10,
|
||||
marginLeft: 50,
|
||||
}}
|
||||
/>
|
||||
</ListItem>
|
||||
{logs.map((data, index) => {
|
||||
//console.log("LOG: ", data)
|
||||
previousreferrer = nextreferrer
|
||||
nextreferrer = data.referer
|
||||
|
||||
return (
|
||||
// redirect user to logs
|
||||
@@ -1243,6 +1208,8 @@ const UserManagmentTab = memo((props) => {
|
||||
key={index}
|
||||
style={{
|
||||
backgroundColor: index % 2 === 0 ? "#1f2023" : "#27292d",
|
||||
paddingTop: data.referer !== previousreferrer ? 50 : 0,
|
||||
borderTop: data.referer !== previousreferrer ? `1px solid rgba(255,255,255,0.3)` : "none",
|
||||
}}
|
||||
>
|
||||
<ListItemText
|
||||
@@ -1263,22 +1230,25 @@ const UserManagmentTab = memo((props) => {
|
||||
}}
|
||||
/>
|
||||
<ListItemText
|
||||
primary={data.referer}
|
||||
primary={data.referer.replace("https://shuffler.io", "")}
|
||||
style={{
|
||||
minWidth: 300,
|
||||
maxWidth: 300,
|
||||
overflow: "hidden",
|
||||
}}
|
||||
/>
|
||||
<ListItemText
|
||||
primary={data.url}
|
||||
style={{
|
||||
minWidth: 700,
|
||||
maxWidth: 700,
|
||||
overflow: "hidden",
|
||||
marginLeft: 10,
|
||||
}}
|
||||
/>
|
||||
<Link to={data.url} target="_blank" style={{ textDecoration: "none", color: theme.palette.linkColor }}>
|
||||
<ListItemText
|
||||
primary={data.url.replace("https://shuffler.io", "")}
|
||||
style={{
|
||||
minWidth: 700,
|
||||
maxWidth: 700,
|
||||
overflow: "hidden",
|
||||
marginLeft: 50,
|
||||
backgroundColor: data.url.includes("/api/v1/register") ? "#d52b2b" : "inherit",
|
||||
}}
|
||||
/>
|
||||
</Link>
|
||||
</ListItem>
|
||||
)})}
|
||||
</List>
|
||||
@@ -1290,6 +1260,7 @@ const UserManagmentTab = memo((props) => {
|
||||
<div style={{ width: "100%", minHeight: 1100, boxSizing: 'border-box', padding: "27px 10px 19px 27px", height:"100%", backgroundColor: theme.palette.platformColor, borderTopRightRadius: '8px', borderBottomRightRadius: 8, borderLeft: "1px solid #494949", }}>
|
||||
{modalView}
|
||||
{editUserModal}
|
||||
{subOrgManagementDialog}
|
||||
{logview}
|
||||
<div style={{ height: "100%", maxHeight: 1700, overflowY: "auto", scrollbarColor: theme.palette.scrollbarColorTransparent, scrollbarWidth: 'thin'}}>
|
||||
<div style={{ height: "100%", width: "calc(100% - 20px)", scrollbarColor: theme.palette.scrollbarColorTransparent, scrollbarWidth: 'thin' }}>
|
||||
|
||||
@@ -3,6 +3,7 @@ import React, { useEffect, useState } from 'react';
|
||||
import {Link} from 'react-router-dom';
|
||||
import theme from '../theme.jsx';
|
||||
import { removeQuery } from '../components/ScrollToTop.jsx';
|
||||
import SearchContactForm from '../components/SearchContactForm.jsx';
|
||||
|
||||
import { Search as SearchIcon, CloudQueue as CloudQueueIcon, Code as CodeIcon } from '@mui/icons-material';
|
||||
|
||||
@@ -25,66 +26,23 @@ import { useDebouncedCallback } from "../utils/useDebouncedCallback.jsx";
|
||||
import WorkflowPaper from "../components/WorkflowPaper.jsx"
|
||||
import WorkflowPaperNew from "../components/WorkflowPaperNew.jsx"
|
||||
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "eb5fd80aa6ed5ab4730d836cff3ea283")
|
||||
const AppGrid = props => {
|
||||
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, alternativeView, onlyResults, inputsearch } = props
|
||||
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
|
||||
const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io";
|
||||
const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows
|
||||
const xs = parsedXs === undefined || parsedXs === null ? isMobile ? 6 : 4 : parsedXs
|
||||
//const [apps, setApps] = React.useState([]);
|
||||
//const [filteredApps, setFilteredApps] = React.useState([]);
|
||||
const [formMail, setFormMail] = React.useState("");
|
||||
const [message, setMessage] = React.useState("");
|
||||
const [formMessage, setFormMessage] = React.useState("");
|
||||
const [usecases, setUsecases] = React.useState([]);
|
||||
|
||||
const [localMessage, setLocalMessage] = React.useState("");
|
||||
|
||||
const buttonStyle = {borderRadius: 30, height: 50, width: 220, margin: isMobile ? "15px auto 15px auto" : 20, fontSize: 18,}
|
||||
|
||||
const innerColor = "rgba(255,255,255,0.65)"
|
||||
const borderRadius = 3
|
||||
window.title = "Shuffle | Workflows | Discover your use-case"
|
||||
|
||||
const submitContact = (email, message) => {
|
||||
const data = {
|
||||
"firstname": "",
|
||||
"lastname": "",
|
||||
"title": "",
|
||||
"companyname": "",
|
||||
"email": email,
|
||||
"phone": "",
|
||||
"message": message,
|
||||
}
|
||||
|
||||
const errorMessage = "Something went wrong. Please contact frikky@shuffler.io directly."
|
||||
|
||||
fetch(globalUrl+"/api/v1/contact", {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(data),
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(response => {
|
||||
if (response.success === true) {
|
||||
setFormMessage(response.reason)
|
||||
//toast("Thanks for submitting!")
|
||||
} else {
|
||||
setFormMessage(errorMessage)
|
||||
}
|
||||
|
||||
setFormMail("")
|
||||
setMessage("")
|
||||
})
|
||||
.catch(error => {
|
||||
setFormMessage(errorMessage)
|
||||
console.log(error)
|
||||
});
|
||||
}
|
||||
|
||||
const handleKeysetting = (categorydata, workflows) => {
|
||||
console.log("Workflows: ", workflows)
|
||||
//workflows[0].category = ["detect"]
|
||||
@@ -229,10 +187,16 @@ const AppGrid = props => {
|
||||
placeholder="Find Workflows..."
|
||||
id="shuffle_search_field"
|
||||
onChange={(event) => {
|
||||
removeQuery("q")
|
||||
const value = event.currentTarget.value
|
||||
setInputValue(value)
|
||||
debouncedRefine(value)
|
||||
const urlSearchParams = new URLSearchParams(window.location.search)
|
||||
if (value) {
|
||||
urlSearchParams.set("q", value)
|
||||
} else {
|
||||
urlSearchParams.delete("q")
|
||||
}
|
||||
window.history.replaceState(null, "", value ? `?${urlSearchParams.toString()}` : window.location.pathname)
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if(event.key === "Enter") {
|
||||
@@ -333,64 +297,10 @@ const AppGrid = props => {
|
||||
<CustomHits hitsPerPage={5}/>
|
||||
</InstantSearch>
|
||||
{showSuggestion === true ?
|
||||
<div style={{maxWidth: isMobile ? "100%" : "60%", margin: "auto", paddingTop: 0, textAlign: "center",}}>
|
||||
<Typography variant="h6" style={{color: "white", marginTop: 50,}}>
|
||||
Can't find what you're looking for?
|
||||
</Typography>
|
||||
<div style={{flex: "1", display: "flex", flexDirection: "row"}}>
|
||||
<TextField
|
||||
required
|
||||
style={{flex: "1", marginRight: "15px", backgroundColor: theme.palette.inputColor}}
|
||||
InputProps={{
|
||||
style:{
|
||||
color: "#ffffff",
|
||||
},
|
||||
}}
|
||||
color="primary"
|
||||
fullWidth={true}
|
||||
placeholder="Email (optional)"
|
||||
type="email"
|
||||
id="email-handler"
|
||||
autoComplete="email"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
onChange={e => setFormMail(e.target.value)}
|
||||
/>
|
||||
<TextField
|
||||
required
|
||||
style={{flex: "1", backgroundColor: theme.palette.inputColor}}
|
||||
InputProps={{
|
||||
style:{
|
||||
color: "#ffffff",
|
||||
},
|
||||
}}
|
||||
color="primary"
|
||||
fullWidth={true}
|
||||
placeholder="What apps do you want to see?"
|
||||
type=""
|
||||
id="standard-required"
|
||||
margin="normal"
|
||||
variant="outlined"
|
||||
autoComplete="off"
|
||||
onChange={e => setMessage(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
style={buttonStyle}
|
||||
disabled={message.length === 0}
|
||||
onClick={() => {
|
||||
submitContact(formMail, message)
|
||||
}}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
<Typography style={{color: "white"}} variant="body2">{formMessage}</Typography>
|
||||
</div>
|
||||
: null
|
||||
<SearchContactForm globalUrl={globalUrl} isMobile={isMobile} tabName="workflows" />
|
||||
: null
|
||||
}
|
||||
{onlyResults === true ? null :
|
||||
{/* {onlyResults === true ? null :
|
||||
<span style={{position: "absolute", display: "flex", textAlign: "right", float: "right", right: 0, bottom: 120, }}>
|
||||
<Typography variant="body2" color="textSecondary" style={{}}>
|
||||
Search by
|
||||
@@ -399,7 +309,7 @@ const AppGrid = props => {
|
||||
<img src={"/images/logo-algolia-nebula-blue-full.svg"} alt="Algolia logo" style={{height: 17, marginLeft: 5, marginTop: 3,}} />
|
||||
</a>
|
||||
</span>
|
||||
}
|
||||
} */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import algoliasearch from 'algoliasearch';
|
||||
import { InstantSearch, connectSearchBox, connectHits } from 'react-instantsearch-dom';
|
||||
import { Grid, Paper, TextField, ButtonBase, InputAdornment, Typography, Button, Tooltip} from '@mui/material';
|
||||
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
|
||||
const searchClient = algoliasearch("JNSS5CFDZZ", "33e4e3564f4f060e96e0531957bed552")
|
||||
const WorkflowSearch = props => {
|
||||
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, newSelectedApp, setNewSelectedApp, defaultSearch, showSearch, ConfiguredHits, selectAble, } = props
|
||||
const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user