Cloud sync for all edits in May->June

This commit is contained in:
Frikky
2025-06-17 19:23:15 +02:00
parent 7c15f58617
commit c1c6a9f128
30 changed files with 7290 additions and 1153 deletions
+1 -1
View File
@@ -23,7 +23,7 @@ require (
github.com/gorilla/mux v1.8.1
github.com/h2non/filetype v1.1.3
github.com/satori/go.uuid v1.2.0
github.com/shuffle/shuffle-shared v0.8.82
github.com/shuffle/shuffle-shared v0.8.84
golang.org/x/crypto v0.38.0
google.golang.org/api v0.236.0
google.golang.org/grpc v1.72.2
+2 -2
View File
@@ -350,8 +350,8 @@ github.com/sendgrid/sendgrid-go v3.16.1+incompatible h1:zWhTmB0Y8XCDzeWIm2/BIt1G
github.com/sendgrid/sendgrid-go v3.16.1+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/shuffle/shuffle-shared v0.8.82 h1:vwXG/NOZNoPMfbF14ZnygoY8+AI2QVsggWfgfpSbqzo=
github.com/shuffle/shuffle-shared v0.8.82/go.mod h1:RdfNxqCPI+zU4jQKy3E/p4Io2injm7LpSKQUCDHNtLk=
github.com/shuffle/shuffle-shared v0.8.84 h1:ElIMQYjKBVOiadbiGkSzt/lPU5xqaQwRxQvk9wx/xYM=
github.com/shuffle/shuffle-shared v0.8.84/go.mod h1:RdfNxqCPI+zU4jQKy3E/p4Io2injm7LpSKQUCDHNtLk=
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+5 -5
View File
@@ -1,6 +1,6 @@
services:
frontend:
image: ghcr.io/shuffle/shuffle-frontend:latest
image: ghcr.io/shuffle/shuffle-frontend:nightly
container_name: shuffle-frontend
hostname: shuffle-frontend
ports:
@@ -14,7 +14,7 @@ services:
depends_on:
- backend
backend:
image: ghcr.io/shuffle/shuffle-backend:latest
image: ghcr.io/shuffle/shuffle-backend:nightly
container_name: shuffle-backend
hostname: ${BACKEND_HOSTNAME}
# Here for debugging:
@@ -33,7 +33,7 @@ services:
- SHUFFLE_FILE_LOCATION=/shuffle-files
restart: unless-stopped
orborus:
image: ghcr.io/shuffle/shuffle-orborus:latest
image: ghcr.io/shuffle/shuffle-orborus:nightly
container_name: shuffle-orborus
hostname: shuffle-orborus
networks:
@@ -55,13 +55,13 @@ services:
- SHUFFLE_STATS_DISABLED=true
- SHUFFLE_LOGS_DISABLED=true
- SHUFFLE_SWARM_CONFIG=run
- SHUFFLE_WORKER_IMAGE=ghcr.io/shuffle/shuffle-worker:latest
- SHUFFLE_WORKER_IMAGE=ghcr.io/shuffle/shuffle-worker:nightly
env_file: .env
restart: unless-stopped
security_opt:
- seccomp:unconfined
opensearch:
image: opensearchproject/opensearch:2.19.1
image: opensearchproject/opensearch:3.0.0
hostname: shuffle-opensearch
container_name: shuffle-opensearch
environment:
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

+66 -26
View File
@@ -1,6 +1,7 @@
import React, { useState, useEffect, useContext, memo } from 'react';
import { useNavigate, useLocation } from 'react-router-dom';
import OrganizationTab from '../components/OrganizationTab.jsx';
import PartnerTab from '../components/PartnerTab.jsx';
import UserManagmentTab from '../components/UserManagmentTab.jsx';
import CacheView from "../components/CacheView.jsx";
import Files from "../components/Files.jsx";
@@ -25,6 +26,24 @@ import { Index } from 'react-instantsearch-dom';
import { Context } from '../context/ContextApi.jsx';
import { toast } from 'react-toastify';
const PartnerIcon = ({ strokeColor, fillColor = 'transparent', width = 22, height = 22 }) => (
<svg
width={width}
height={height}
viewBox="0 0 18 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M14.5327 4.49465C14.0102 4.60513 13.5779 4.90684 13.2905 5.31071L12.382 1L1.7665 3.23791C1.24516 3.34838 0.911462 3.86034 1.02072 4.38181L3.25807 15L7.57601 14.0901C7.16749 13.8026 6.85992 13.3679 6.74948 12.8393C6.51672 11.7334 7.22331 10.6477 8.32892 10.4149C9.43453 10.1821 10.52 10.8889 10.7527 11.9947C10.8643 12.5221 10.7587 13.0448 10.501 13.4724L14.8189 12.5625L13.9104 8.25182C14.3368 8.50484 14.8533 8.60699 15.3759 8.49652C16.4815 8.2637 17.1893 7.17801 16.9553 6.07212C16.7225 4.96623 15.6371 4.25827 14.5315 4.49228L14.5327 4.49465Z"
fill={fillColor}
stroke={strokeColor}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
const AdminNavBar = (props) => {
const location = useLocation();
const { globalUrl, userdata, isCloud,isOrgLoaded, isLoaded,removeCookie, handleStatusChange, selectedStatus, setSelectedStatus, handleEditOrg, serverside, notifications, handleGetOrg, orgId, checkLogin, setNotifications, stripeKey, setSelectedOrganization, selectedOrganization } = props;
@@ -37,17 +56,6 @@ const AdminNavBar = (props) => {
const [visibleItems, setVisibleItems] = useState([]);
const [isUserDataLoaded, setIsUserDataLoaded] = useState(false);
const items = [
{ iconSrc: <BusinessIcon />, alt: "Organization Icon", text: "Organization", component: OrganizationTab, props: { isIntegrationPartner, isChildOrg, isGlobalUser, globalUrl,removeCookie, selectedStatus, isLoaded, setSelectedStatus, handleStatusChange, handleEditOrg, handleGetOrg, userdata, isCloud, serverside, notifications, checkLogin, setNotifications, stripeKey, setSelectedOrganization, selectedOrganization } },
{ iconSrc: <PermIdentityIcon />, alt: "Users Icon", text: "Users", component: UserManagmentTab, props: { globalUrl, userdata, serverside, isCloud, selectedOrganization, setSelectedOrganization, handleEditOrg } },
{ iconSrc: <HttpsOutlinedIcon />, alt: "App Auth Icon", text: "App_auth", component: AppAuthTab, props: { globalUrl, userdata, isCloud, selectedOrganization } },
{ iconSrc: <StorageOutlinedIcon />, alt: "Datastore Icon", text: "Datastore", component: CacheView, props: { globalUrl, userdata, selectedOrganization, serverside, isSelectedDataStore, orgId , isCloud} },
{ iconSrc: <InsertDriveFileOutlinedIcon />, alt: "Files Icon", text: "Files", component: Files, props: { isCloud, globalUrl, userdata, serverside, selectedOrganization, isSelectedFiles } },
{ iconSrc: <AccessTimeOutlinedIcon />, alt: "Trigger Icon", text: "Triggers", component: SchedulesTab, props: { globalUrl, userdata, isCloud, serverside } },
{ iconSrc: <FmdGoodOutlinedIcon />, alt: "Environments Icon", text: "Locations", component: EnvironmentTab, props: { globalUrl, userdata, isCloud, selectedOrganization } },
{ iconSrc: <GroupOutlinedIcon />, alt: "Tenants Icon", text: "Tenants", component: TenantsTab, props: {isCloud, globalUrl, userdata, serverside, selectedOrganization, setSelectedOrganization, checkLogin } }
];
useEffect(() => {
if (userdata && userdata?.active_org?.id?.length > 0) {
@@ -119,7 +127,17 @@ const AdminNavBar = (props) => {
useEffect(() => {
const queryParams = new URLSearchParams(location.search);
const tabName = queryParams.get('tab');
const partnerTab = queryParams.get('partner_tab');
// if(!isCloud){
// if(tabName === "partner" || partnerTab !== null) {
// setSelectedItem("Organization");
// navigate(`?tab=organization`, { replace: true });
// }
// }
if (partnerTab) {
setSelectedItem("Partner");
}
if (tabName === "environments") {
setSelectedItem("Locations");
} else if (tabName === "suborgs") {
@@ -128,11 +146,24 @@ const AdminNavBar = (props) => {
setSelectedItem("Datastore");
}else if (tabName) {
setSelectedItem(tabName.charAt(0).toUpperCase() + tabName.slice(1));
} else {
setSelectedItem("Organization");
}else if (tabName === "partner") {
setSelectedItem("Partner");
}
}, [location.search]);
const items = [
{ iconSrc: <BusinessIcon />, alt: "Organization Icon", text: "Organization", component: OrganizationTab, props: { isIntegrationPartner, isChildOrg, isGlobalUser,globalUrl,removeCookie, selectedStatus, isLoaded, setSelectedStatus, handleStatusChange, handleEditOrg, handleGetOrg, userdata, isCloud, serverside, notifications, checkLogin, setNotifications, stripeKey, setSelectedOrganization, selectedOrganization } },
{ iconSrc: undefined, alt: "Partner Icon", text: "Partner", component: PartnerTab, props: { globalUrl,removeCookie, isLoaded, handleGetOrg, userdata, isCloud, serverside, checkLogin, setSelectedOrganization, selectedOrganization } },
{ iconSrc: <PermIdentityIcon />, alt: "Users Icon", text: "Users", component: UserManagmentTab, props: { globalUrl, userdata, serverside, isCloud, selectedOrganization, setSelectedOrganization, handleEditOrg } },
{ iconSrc: <HttpsOutlinedIcon />, alt: "App Auth Icon", text: "App_auth", component: AppAuthTab, props: { globalUrl, userdata, isCloud, selectedOrganization } },
{ iconSrc: <StorageOutlinedIcon />, alt: "Datastore Icon", text: "Datastore", component: CacheView, props: { globalUrl, userdata, selectedOrganization, serverside, isSelectedDataStore, orgId , isCloud} },
{ iconSrc: <InsertDriveFileOutlinedIcon />, alt: "Files Icon", text: "Files", component: Files, props: { isCloud, globalUrl, userdata, serverside, selectedOrganization, isSelectedFiles } },
{ iconSrc: <AccessTimeOutlinedIcon />, alt: "Trigger Icon", text: "Triggers", component: SchedulesTab, props: { globalUrl, userdata, isCloud, serverside } },
{ iconSrc: <FmdGoodOutlinedIcon />, alt: "Environments Icon", text: "Locations", component: EnvironmentTab, props: { globalUrl, userdata, isCloud, selectedOrganization } },
{ iconSrc: <GroupOutlinedIcon />, alt: "Tenants Icon", text: "Tenants", component: TenantsTab, props: {isCloud, globalUrl, userdata, serverside, selectedOrganization, setSelectedOrganization, checkLogin } }
].filter(Boolean);
const setConfig = (newValue) => {
setSelectedItem(newValue);
if (newValue === "App Auth") {
@@ -208,16 +239,16 @@ const AdminNavBar = (props) => {
const ComponentToRender = selectedItemData.component;
const componentProps = selectedItemData.props;
const updatedProps = {
...componentProps,
notifications: notifications,
setNotifications: setNotifications,
userdata: userdata,
selectedOrganization: selectedOrganization
};
const updatedProps = {
...componentProps,
notifications: notifications,
setNotifications: setNotifications,
userdata: userdata,
selectedOrganization: selectedOrganization
};
return <ComponentToRender {...updatedProps} />;
};
return <ComponentToRender {...updatedProps} />;
};
const defaultImage = "/images/logos/orange_logo.svg"
const imageData =
@@ -296,7 +327,7 @@ const AdminNavBar = (props) => {
disabled={
((item.text === "Users") || (item.text === "Files") || (item.text === "Triggers") || (item.text === "Locations")) && !(userdata?.support || userdata?.active_org?.role === "admin")
}
startIcon={item.iconSrc}
startIcon={item?.iconSrc === undefined ? <PartnerIcon strokeColor={selectedItem === item.text ? theme.palette.text.primary : theme.palette.text.secondary} /> : item?.iconSrc}
onClick={() => setConfig(item.text)}
>
{item.text.replace(/_/g, " ")}
@@ -308,7 +339,10 @@ const AdminNavBar = (props) => {
</nav>
</div>
<Wrapper2>{renderComponent()}</Wrapper2>
<Wrapper2>
{renderComponent()}
</Wrapper2>
</Wrapper>
);
};
@@ -424,7 +458,13 @@ const Loader = () => {
const PaddingWrapper2 = memo(({ children }) => {
return (
<div div style={{marginBottom: 30, width: "75%" , maxWidth: 1200, height: "100%", boxSizing: 'border-box'}}>
<div style={{
marginBottom: 30,
width: "75%" ,
maxWidth: 1200,
height: "100%",
boxSizing: 'border-box',
}}>
{children}
</div>
)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,205 @@
import React, { useState, useEffect, useContext, memo } from "react";
import { Context } from "../context/ContextApi.jsx";
import { getTheme } from "../theme.jsx";
import { GetIconInfo } from "../views/Workflows2.jsx";
import { toast } from 'react-toastify';
import {
Dialog,
DialogTitle,
DialogContent,
Typography,
Paper,
LinearProgress,
Grid,
Button,
} from '@mui/material';
import {
Rocket as RocketIcon,
FilterAlt as FilterAltIcon,
} from '@mui/icons-material';
const CollectIngestModal = (props) => {
const { globalUrl, open, setOpen } = props;
const { themeMode, brandColor } = useContext(Context);
const theme = getTheme(themeMode, brandColor);
if (open === undefined || open === null) {
console.error("CollectIngestModal: 'open' prop is required.");
return null
}
if (setOpen === undefined || setOpen === null) {
console.error("CollectIngestModal: 'setOpen' prop is required.");
return null
}
const startIngestion = (appname, index) => {
console.log("APPNAME:", appname, "INDEX:", index)
const body = {
"app_name": appname,
"label": appname,
}
const url = `${globalUrl}/api/v2/workflows/generate`
fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(body),
credentials: "include",
})
.then((response) => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then((data) => {
console.log("Ingestion started successfully:", data);
toast.success(`Ingestion for ${appname} started successfully!`);
})
.catch((error) => {
console.error("Error starting ingestion:", error);
toast.error(`Failed to start ingestion for ${appname}. Please try again.`);
});
}
const IngestItem = (props) => {
const { type, index } = props
const [hovering, setHovering] = useState(false);
const [isFinished, setIsFinished] = useState(false);
const appname = type
const ingestedAmount = 20
const iconDetails = GetIconInfo({
"app_name": appname,
"name": appname,
})
return (
//<Grid item xs={hovering ? 12 : 5.9}
<Grid item xs={12}
style={{
minHeight: hovering ? 225 : 145,
maxHeight: hovering ? 225 : 145,
cursor: "pointer",
position: "relative",
transition: "all 0.3s ease-in-out",
borderRadius: theme.palette.borderRadius,
border: hovering ? `2px solid ${theme.palette.primary.main}` : isFinished ? `2px solid ${theme.palette.success.main}` : `2px solid ${theme.palette.secondary.main}`,
textAlign: "center",
marginBottom: 5,
overflow: "hidden",
}}
onMouseEnter={() => setHovering(true)}
onMouseLeave={() => setHovering(false)}
>
<div style={{marginTop: 35, marginBottom: 35, }}>
{iconDetails?.originalIcon && (
iconDetails?.originalIcon
)}
<Typography variant="h4" style={{marginTop: 10, }}>
{appname}
</Typography>
</div>
<Button variant="contained" onClick={() => {
toast.info("Starting ingest for relevant apps")
startIngestion(appname, index)
}}>
Start Ingestion
</Button>
{hovering ?
<div>
</div>
: null}
{isFinished ?
<div>
<Typography variant="body1" style={{
position: "absolute",
bottom: 10,
left: 10,
color: theme.palette.text.secondary,
}}>
{ingestedAmount} / X
</Typography>
<LinearProgress
style={{
width: "100%",
position: "absolute",
bottom: 0,
}}
variant="determinate"
fullWidth value={{ingestedAmount}}
/>
</div>
: null}
</Grid>
)
}
return (
<Dialog
PaperProps={{
sx: {
borderRadius: theme?.palette?.DialogStyle?.borderRadius,
border: theme?.palette?.DialogStyle?.border,
minWidth: 500,
minHeight: 700,
fontFamily: theme?.typography?.fontFamily,
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
zIndex: 1000,
'& .MuiDialogContent-root': {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
},
'& .MuiDialogTitle-root': {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
},
'& .MuiDialogActions-root': {
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
},
},
}}
open={open}
onClose={() => {
setOpen(false)
}}
>
<DialogTitle>
</DialogTitle>
<DialogContent style={{ paddingLeft: "30px", paddingRight: '30px', backgroundColor: theme.palette.DialogStyle.backgroundColor, }}>
<Typography variant="h6" style={{ color: theme.palette.text.primary, marginBottom: 20 }}>
<FilterAltIcon style={{ verticalAlign: "middle", marginRight: 10 }} />
Collection and Ingestion
</Typography>
<Grid container>
<IngestItem type="Ingest Tickets" index={1} />
<IngestItem type="Enable Threat feeds" index={2} />
<IngestItem type="Track Assets" index={2} />
<IngestItem type="Enable Search" index={2} />
<IngestItem type="Enable Mitre Att&ck techniques" index={2} />
<IngestItem type="Enable Detection Rules" index={2} />
<IngestItem type="Ingest Logs" index={2} />
</Grid>
</DialogContent>
</Dialog>
)
}
export default CollectIngestModal
-8
View File
@@ -142,7 +142,6 @@ const countries = [
suggested: true,
},
{ code: 'GA', label: 'Gabon', phone: '241' },
{ code: 'GB', label: 'United Kingdom', phone: '44' },
{ code: 'GD', label: 'Grenada', phone: '1-473' },
{ code: 'GE', label: 'Georgia', phone: '995' },
{ code: 'GF', label: 'French Guiana', phone: '594' },
@@ -178,7 +177,6 @@ const countries = [
{ code: 'IE', label: 'Ireland', phone: '353' },
{ code: 'IL', label: 'Israel', phone: '972' },
{ code: 'IM', label: 'Isle of Man', phone: '44' },
{ code: 'IN', label: 'India', phone: '91' },
{
code: 'IO',
label: 'British Indian Ocean Territory',
@@ -390,12 +388,6 @@ const countries = [
},
{ code: 'UA', label: 'Ukraine', phone: '380' },
{ code: 'UG', label: 'Uganda', phone: '256' },
{
code: 'US',
label: 'United States',
phone: '1',
suggested: true,
},
{ code: 'UY', label: 'Uruguay', phone: '598' },
{ code: 'UZ', label: 'Uzbekistan', phone: '998' },
{
+48 -3
View File
@@ -681,7 +681,7 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
<Divider style={{ marginBottom: 10, }} />
<Typography color="textSecondary" align="center" style={{ marginTop: 5, marginBottom: 5, fontSize: 18 }}>
Version: 2.1.0-rc1
Version: 2.1.0-rc2
</Typography>
</Menu>
</span>
@@ -745,6 +745,8 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
localStorage.setItem("globalUrl", "");
localStorage.setItem("getting_started_sidebar", "open");
localStorage.removeItem("workflows");
localStorage.removeItem("apps");
fetch(`${globalUrl}/api/v1/orgs/${orgId}/change`, {
mode: "cors",
@@ -913,7 +915,8 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
"US": "us",
"EU": "eu",
"CA": "ca",
"UK": "gb"
"UK": "gb",
"au": "au",
};
region = regionMapping[region_url] || "eu";
@@ -1465,7 +1468,7 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
<Collapse in={openSecurityTab} timeout="auto" unmountOnExit >
<Box
style={{
maxHeight: openSecurityTab && expandLeftNav ? 185 : 0,
maxHeight: openSecurityTab && expandLeftNav ? 250 : 0,
overflow: "hidden",
transition: "max-height 0.3s ease, opacity 0.3s ease",
display: "flex",
@@ -1681,6 +1684,48 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
</span>
</Button>
</span>
<span style={{ display: "inline-block", width: "100%" }}>
<Button
component={Link}
to="/partners"
onClick={(event) => {
setCurrentOpenTab("partners");
localStorage.setItem("lastTabOpenByUser", "partners");
}}
variant="text"
sx={{
width: "100%",
height: 35,
color: themeMode === "dark" ? lightText : darkText,
justifyContent: "flex-start",
textTransform: "none",
backgroundColor:
currentOpenTab === "partners"
? themeMode === "dark" ? darkHoverColor : lightHoverColor
: "transparent",
"&:hover": {
backgroundColor: themeMode === "dark" ? darkHoverColor : lightHoverColor,
},
cursor: "pointer",
}}
>
<span style={{ position: "relative", left: !expandLeftNav ? 10 : 0, marginRight: 10, fontSize: 18 }}>
</span>
<span
style={{
display: expandLeftNav ? "inline" : "none",
opacity: expandLeftNav ? 1 : 0,
transition: "opacity 0.3s ease",
fontSize: 18,
color: themeMode === "dark" ? currentOpenTab === "locations" ? theme.palette.text.primary : lightText : darkText,
}}
>
Partners
</span>
</Button>
</span>
</>): null}
</Box>
</Collapse>
+147 -22
View File
@@ -61,9 +61,9 @@ const menuData = {
{
title: "Singul",
description:
"Connect and run actions seamlessly between different platforms.",
"Connect your favorite services with a singul line of code.",
icon: "/images/logos/singul.svg",
path: "https://singul-docs.gitbook.io/singul/getting-started",
path: "https://singul.io",
gaData: {
category: "navbar",
action: "products_click",
@@ -662,9 +662,9 @@ const MobileMenu = ({ anchorEl, handleClose, isLoggedIn, navigate, isCloud }) =>
action: "partners_click",
label: "go_to_partners",
})
handleItemClick('/partners')
handleItemClick('/become-partner')
}else{
window.open("https://shuffler.io/partners", '_blank');
window.open("https://shuffler.io/become-partner", '_blank');
return;
}
}}
@@ -1391,26 +1391,151 @@ const Navbar = (props) => {
>
Pricing
</Button>
<Button
component={Link}
to="/partners"
sx={buttonStyles}
onClick={() => {
if(isCloud) {
navigate("/partners");
ReactGA.event({
category: "navbar",
action: "partners_click",
label: "go_to_partners",
})
} else {
window.open("https://shuffler.io/partners", '_blank');
return;
}
<Box
className="partners-container"
sx={{
position: "relative",
"&::after": {
// This creates an invisible connection between button and dropdown
content: '""',
position: "absolute",
top: "100%",
left: 0,
width: "100%",
height: "20px", // Buffer zone height
backgroundColor: "transparent",
},
"&:hover .partners-dropdown": {
opacity: 1,
visibility: "visible",
pointerEvents: "auto",
transform: "translateY(0)",
transition: "opacity 0.3s ease, visibility 0s, transform 0.3s ease",
},
}}
>
Partners
</Button>
<Button
component={Link}
to="/become-partner"
sx={{
...buttonStyles,
}}
onClick={() => {
if(isCloud) {
navigate("/become-partner");
ReactGA.event({
category: "navbar",
action: "partners_click",
label: "go_to_become_partner",
})
} else {
window.open("https://shuffler.io/become-partner", '_blank');
return;
}
}}
>
Partners
</Button>
<Box
className="partners-dropdown"
sx={{
position: "absolute",
top: "140%",
left: 0,
backgroundColor: "#212121",
borderRadius: "4px",
boxShadow: "0px 5px 15px rgba(0, 0, 0, 0.2)",
width: "220px",
opacity: 0,
visibility: "hidden",
pointerEvents: "none",
transform: "translateY(-10px)",
transition: "opacity 0.3s ease, visibility 0s linear 0.3s, transform 0.3s ease",
zIndex: 10,
"&:hover": {
opacity: 1,
visibility: "visible",
pointerEvents: "auto",
transform: "translateY(0)",
},
// Add a pseudo-element to create a buffer zone above the dropdown
"&::before": {
content: '""',
position: "absolute",
top: "-20px", // Match the height of the buffer zone
left: 0,
width: "100%",
height: "20px",
backgroundColor: "transparent",
}
}}
>
<Box sx={{}}>
<Button
fullWidth
sx={{
color: "white",
justifyContent: "flex-start",
textTransform: "none",
py: 2,
px: 2,
fontSize: "14px",
fontFamily: theme.typography.fontFamily,
borderRadius: "4px",
"&:hover": {
backgroundColor: "rgba(255,255,255,0.05)"
}
}}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
if(isCloud) {
navigate("/become-partner");
ReactGA.event({
category: "navbar",
action: "partners_click",
label: "become_partner_click",
})
} else {
window.open("https://shuffler.io/become-partner", '_blank');
}
}}
>
Become a partner
</Button>
<Button
fullWidth
sx={{
color: "white",
justifyContent: "flex-start",
textTransform: "none",
fontSize: "14px",
fontFamily: theme.typography.fontFamily,
py: 2,
px: 2,
borderRadius: "4px",
"&:hover": {
backgroundColor: "rgba(255,255,255,0.05)"
}
}}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
navigate("/partners");
if(isCloud) {
ReactGA.event({
category: "navbar",
action: "partners_click",
label: "discover_partners_click",
})
}
}}
>
Discover partners
</Button>
</Box>
</Box>
</Box>
</Box>
);
+40 -9
View File
@@ -4,7 +4,7 @@ import { makeStyles, createStyles } from "@mui/styles";
import {getTheme} from '../theme.jsx';
import { useNavigate, Link, useParams } from "react-router-dom";
import { validateJson, GetIconInfo } from "../views/Workflows.jsx";
import { validateJson, GetIconInfo } from "../views/Workflows2.jsx";
import { GetParsedPaths } from "../views/Apps.jsx";
import { sortByKey } from "../views/AngularWorkflow.jsx";
import { NestedMenuItem } from "mui-nested-menu";
@@ -825,7 +825,7 @@ const ParsedAction = (props) => {
// Check if array, and if first item is object + success
if (Array.isArray(valid.result) && valid.result.length > 0 && typeof valid.result[0] === "object") {
if (valid.result[0].success !== false) {
exampleData = valid.result[0]
exampleData = valid.result
break
}
} else {
@@ -910,14 +910,48 @@ const ParsedAction = (props) => {
if (newActionList?.length > 0) {
let appParentActions = parentActionList?.map(action => "$" + action.name.toLowerCase());
let notPresentAction = actions?.filter((action) => !appParentActions?.includes(action))
// Extract all variable references from paramvalue
// Examples of what it matches:
// - Simple variables: $test, $myVar, $x
// - Variables with underscores: $my_variable, $ok_ok, $testing_nice, $nice_try_man
// - Workflow data access patterns: $not_customer_copy.body.subject.#, $not_customer.body.subject.text
// - Deep nested properties: $email_action.response.items[0].sender.address
const variablePattern = /\$[a-zA-Z0-9_]+(?=\.|,|;|:|\s|"|'|`|\)|\]|\}|$)/g;
const foundVariables = paramvalue.match(variablePattern) || [];
notPresentAction?.forEach((action) => {
action = action.replace(" ", "_");
if (paramvalue.includes(action)) {
// Check if the exact action is in the foundVariables list
if (foundVariables.includes(action)) {
errorVars.push(action);
// paramvalue = paramvalue.replace(action, "")
// paramvalue = paramvalue.replace(/^\s*[\r\n]/gm, "");
}
})
});
// Check for variables that don't exist in the workflow at all
foundVariables.forEach(variable => {
// Skip checking for $ followed by system vars as exec and shuffle_cache.
if (variable.match(/\$(exec|shuffle_cache)/)) {
return;
}
// Create list of all valid action variables from newActionList
const newActionNames = newActionList?.map(action => "$" + action.name.toLowerCase()) || [];
// Extract workflow and execution variables from newActionList
const workflowVars = newActionList
.filter(item => item.type === "workflow_variable" || item.type === "execution_variable")
.map(item => "$" + item.autocomplete.toLowerCase());
// If the variable doesn't exist in any workflow actions, parent actions, newActionList, or extracted variables, show error
if (!actions.includes(variable) &&
!appParentActions.includes(variable) &&
!newActionNames.includes(variable) &&
!workflowVars.includes(variable.toLowerCase())) {
errorVars.push(variable);
}
});
}
}
@@ -3085,9 +3119,6 @@ const ParsedAction = (props) => {
}
var isAppSelected = false
console.log("App found: ", app.name, app.categories, appIndex, newactionname)
const paramIndex = selectedAction.parameters.findIndex((param) => param.name === "app_name")
if (paramIndex > -1) {
// Check the actual value and if it's the same
+21
View File
@@ -0,0 +1,21 @@
import { Box, Typography } from '@mui/material'
import React from 'react'
const PartnerApps = () => {
return (
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "100%",
height: "100%",
minHeight: "500px",
}}
>
<Typography>Partner Apps</Typography>
</Box>
)
}
export default PartnerApps
+987
View File
@@ -0,0 +1,987 @@
import React, { memo, useEffect, useState, useContext } from "react";
import { makeStyles } from "@mui/styles";
import { toast } from "react-toastify";
import { getTheme } from "../theme.jsx";
import { Context } from "../context/ContextApi.jsx";
import countries from "./Countries.jsx";
import {
FormControl,
InputLabel,
Paper,
OutlinedInput,
Checkbox,
Card,
Tooltip,
FormControlLabel,
Chip,
Link,
Typography,
Switch,
Select,
MenuItem,
Divider,
ListItemText,
TextField,
Button,
Tabs,
Tab,
Grid,
Autocomplete,
Skeleton,
Box,
} from "@mui/material";
const useStyles = makeStyles({
notchedOutline: {
borderColor: "#f85a3e !important",
},
});
const PartnerDetails = (props) => {
const {
userdata,
selectedOrganization,
setSelectedOrganization,
globalUrl,
isCloud,
adminTab,
isEditOrgTab,
partnerData,
loadingPartnerData,
setPartnerData,
} = props;
const classes = useStyles();
const defaultBranch = "main";
const ITEM_HEIGHT = 48;
const ITEM_PADDING_TOP = 8;
const MenuProps = {
PaperProps: {
style: {
maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
width: 400,
borderRadius: 10,
overflowY: "scroll",
},
},
getContentAnchorEl: () => null,
};
const expertiseOptions = [
"Cybersecurity",
"Cloud Security",
"Managed Detection and Response (MDR)",
"Incident Response",
"Penetration Testing",
"Compliance and Governance",
"Network Security",
"Endpoint Security",
"Identity and Access Management (IAM)",
"SIEM",
];
const servicesOptions = [
"Cybersecurity Consulting",
"Managed Security",
"Incident Response",
"Penetration Testing",
"Cloud Security",
"Managed Detection",
"Endpoint Security",
"Network Security",
"Identity Management",
"Compliance",
"Security Training",
"Security Management",
"Threat Intelligence",
"Web Security",
"Database Security",
"Disaster Recovery",
"IT Security Audit",
"Cybersecurity Training",
"Security Automation",
"AI Security",
"IoT Security",
"Cloud Security Broker",
"Security Service",
"Managed Security Provider",
"Cybersecurity Service",
]
const solutionsOptions = [
"EDR",
"MDR",
"XDR",
"Case Management",
"SIEM",
"Vulnerability Management",
"IPS",
"IDS",
"Threat Intelligence",
"IAM",
"Data Security",
"Incident Response",
"Cloud Security",
"Network Security",
]
const { themeMode } = useContext(Context);
const theme = getTheme(themeMode);
const [isDisabled, setIsDisabled] = useState(isCloud ? userdata?.active_org?.is_partner ? false : true : true);
const handleExpertiseChange = (event) => {
const { value } = event.target;
setPartnerData({
...partnerData,
expertise: value,
});
};
useEffect(() => {
setIsDisabled(isCloud ? userdata?.active_org?.is_partner ? false : true : true);
if(userdata?.support) {
setIsDisabled(false);
}
}, [userdata, isCloud]);
const handleServicesChange = (event) => {
const { value } = event.target;
setPartnerData({
...partnerData,
services: Array.isArray(value) ? value : [],
});
};
const handleSolutionsChange = (event) => {
const { value } = event.target;
setPartnerData({
...partnerData,
solutions: Array.isArray(value) ? value : [],
});
};
const setSelectedRegion = (region) => {
// send a POST request to /api/v1/orgs/{org_id}/region with the region as the body
const regionMap = {
US: "us-west2",
EU: "europe-west2",
CA: "northamerica-northeast1",
UK: "europe-west2",
"EU-2": "europe-west3",
AUS: "australia-southeast1",
};
region = regionMap[region] || region;
var data = {
dst_region: region,
};
toast.info(
"Changing region to " + region + "...This may take a few minutes."
);
fetch(`${globalUrl}/api/v1/orgs/${selectedOrganization.id}/change/region`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
body: JSON.stringify(data),
timeOut: 1000,
}).then((response) => {
if (response.status !== 200) {
response
.json()
.then((reason) => {
toast.error("Failed to change region: " + reason.reason);
})
.catch((err) => {
toast.error("Failed to change region");
});
} else {
toast("Region changed successfully! Reloading in 5 seconds..");
// Reload the page in 2 seconds
setTimeout(() => {
window.location.reload();
}, 5000);
}
// return responseJson
});
};
if (loadingPartnerData) {
return (
<div style={{ textAlign: "left", maxWidth: "95%" }}>
<Grid container spacing={3} style={{ textAlign: "left", marginTop: 5 }}>
<Grid item xs={12}>
<span>
<div>
<div style={{ flex: "3", color: "white" }}>
<div style={{ marginTop: 8, display: "flex" }} />
<div style={{ display: "flex" }}>
<div style={{ width: "100%", maxWidth: 434, marginRight: 10 }}>
<Typography variant="text" style={{ color: theme?.palette?.text?.primary }}>
Name
</Typography>
<Skeleton
variant="rounded"
height={35}
width="100%"
style={{
marginTop: 5,
borderRadius: 4
}}
animation="wave"
/>
</div>
{/* <div style={{ alignItems: "center" }}>
<div style={{ marginRight: "12px", color: theme?.palette?.text?.primary }}>
Expertise
</div>
<Skeleton
variant="rounded"
height={35}
width={220}
style={{
marginTop: 5,
borderRadius: 4
}}
animation="wave"
/>
</div>
<div style={{ alignItems: "center", marginLeft: 12 }}>
<div style={{ marginRight: "12px", color: theme?.palette?.text?.primary }}>
Services
</div>
<Skeleton
variant="rounded"
height={35}
width={220}
style={{
marginTop: 5,
borderRadius: 4
}}
animation="wave"
/>
</div> */}
<div style={{ alignItems: "center" }}>
<div style={{ marginRight: "12px", color: theme?.palette?.text?.primary }}>
Solutions
</div>
<Skeleton
variant="rounded"
height={35}
width={220}
style={{
marginTop: 5,
borderRadius: 4
}}
animation="wave"
/>
</div>
<div style={{ marginLeft: 13, fontSize: 16, color: "#9E9E9E" }}>
<Typography variant="text" style={{ color: theme?.palette?.text?.primary }}>
Region
</Typography>
<Skeleton
variant="rounded"
height={35}
width={190}
style={{
marginTop: 5,
borderRadius: 4
}}
animation="wave"
/>
</div>
<div style={{ alignItems: "center", marginLeft: 12 }}>
<div style={{ marginRight: "12px", color: theme?.palette?.text?.primary }}>
Country
</div>
<Skeleton
variant="rounded"
height={35}
width={220}
style={{
marginTop: 5,
borderRadius: 4
}}
animation="wave"
/>
</div>
</div>
<div style={{ marginTop: "10px" }}>
<Typography variant="text" style={{ color: theme?.palette?.text?.primary }}>
Description
</Typography>
<Skeleton
variant="rounded"
height={89}
width="100%"
style={{
marginTop: 5,
borderRadius: 4
}}
animation="wave"
/>
</div>
<div>
<div style={{ width: "100%", maxWidth: 500, marginRight: 10, marginTop: 10 }}>
<Typography variant="text" style={{ color: theme?.palette?.text?.primary }}>
Website URL
</Typography>
<Skeleton
variant="rounded"
height={35}
width="100%"
style={{
marginTop: 5,
borderRadius: 4
}}
animation="wave"
/>
</div>
<div style={{ width: "100%", maxWidth: 500, marginRight: 10, marginTop: 20 }}>
<Typography variant="text" style={{ color: theme?.palette?.text?.primary }}>
Article URL
</Typography>
<Skeleton
variant="rounded"
height={35}
width="100%"
style={{
marginTop: 5,
borderRadius: 4
}}
animation="wave"
/>
</div>
</div>
</div>
</div>
</span>
</Grid>
</Grid>
</div>
);
}
return (
<div style={{ textAlign: "left", maxWidth: "95%" }}>
<Grid container spacing={3} style={{ textAlign: "left", marginTop: 5 }}>
<Grid item xs={12} style={{}}>
<span>
<div style={{}}>
<div style={{ flex: "3", color: "white" }}>
<div style={{ marginTop: 8, display: "flex" }} />
<div style={{ display: "flex" }}>
<div
style={{ width: "100%", maxWidth: 434, marginRight: 10 }}
>
<Typography
variant="text"
style={{ color: theme.palette.text.primary }}
>
Name
</Typography>
<TextField
required
disabled={isDisabled}
style={{
flex: "1",
display: "flex",
height: 35,
width: "100%",
maxWidth: 434,
marginTop: "5px",
marginRight: "15px",
color: theme.palette.textFieldStyle.color,
backgroundColor: isEditOrgTab
? theme.palette.textFieldStyle.backgroundColor
: theme.palette.inputColor,
cursor: isDisabled ? "not-allowed" : "pointer",
}}
fullWidth={true}
placeholder="Name"
type="name"
id="standard-required"
margin="normal"
variant="outlined"
value={partnerData?.name}
onBlur={() => {}}
onChange={(e) => {
if (e.target.value.length > 100) {
toast("Choose a shorter name.");
return;
}
setPartnerData({
...partnerData,
name: e.target.value,
});
}}
color="primary"
InputProps={{
style: {
color: theme.palette.textFieldStyle.color,
height: "35px",
fontSize: "1em",
borderRadius: 4,
backgroundColor:
theme.palette.textFieldStyle.backgroundColor,
},
classes: {
notchedOutline: isEditOrgTab
? null
: classes.notchedOutline,
},
}}
/>
</div>
{/* <div style={{ alignItems: "center" }}>
<div
style={{
marginRight: "12px",
color: theme.palette.text.primary,
}}
>
Expertise
</div>
<FormControl style={{ width: 220, height: 35 }}>
<Select
style={{
minWidth: 220,
marginTop: 5,
maxWidth: 220,
height: 35,
borderRadius: 4,
color: theme.palette.textFieldStyle.color,
cursor: isDisabled ? "not-allowed" : "pointer",
}}
disabled={isDisabled}
id="multiselect-status"
multiple
value={partnerData?.expertise}
onChange={(event) => {
handleExpertiseChange(event);
}}
input={<OutlinedInput />}
renderValue={(selected) => selected.join(", ")}
MenuProps={MenuProps}
>
{expertiseOptions?.map((name) => (
<MenuItem key={name} value={name}>
<Checkbox
checked={partnerData?.expertise?.indexOf(name) > -1}
/>
<ListItemText primary={name} />
</MenuItem>
))}
</Select>
</FormControl>
</div>
<div style={{ alignItems: "center", marginLeft: 12 }}>
<div
style={{
marginRight: "12px",
color: theme.palette.text.primary,
}}
>
Services
</div>
<FormControl style={{ width: 220, height: 35 }}>
<Select
style={{
minWidth: 220,
marginTop: 5,
maxWidth: 220,
height: 35,
borderRadius: 4,
color: theme.palette.textFieldStyle.color,
cursor: isDisabled ? "not-allowed" : "pointer",
}}
disabled={isDisabled}
id="multiselect-status"
multiple
value={partnerData?.services || []}
onChange={(event) => {
handleServicesChange(event);
}}
input={<OutlinedInput />}
renderValue={(selected) => selected.join(", ")}
MenuProps={MenuProps}
>
{servicesOptions?.map((name) => (
<MenuItem key={name} value={name}>
<Checkbox
checked={(partnerData?.services || []).indexOf(name) > -1}
/>
<ListItemText primary={name} />
</MenuItem>
))}
</Select>
</FormControl>
</div> */}
<div style={{ alignItems: "center" }}>
<div
style={{
marginRight: "12px",
color: theme.palette.text.primary,
}}
>
Solutions
</div>
<FormControl style={{ width: 190, height: 35 }}>
<Select
style={{
minWidth: 190,
marginTop: 5,
maxWidth: 190,
height: 35,
borderRadius: 4,
color: theme.palette.textFieldStyle.color,
cursor: isDisabled ? "not-allowed" : "pointer",
}}
disabled={isDisabled}
id="multiselect-status"
multiple
value={partnerData?.solutions || []}
onChange={(event) => {
handleSolutionsChange(event);
}}
input={<OutlinedInput />}
renderValue={(selected) => selected.join(", ")}
MenuProps={MenuProps}
>
{solutionsOptions?.map((name) => (
<MenuItem key={name} value={name}>
<Checkbox
checked={(partnerData?.solutions || []).indexOf(name) > -1}
/>
<ListItemText primary={name} />
</MenuItem>
))}
</Select>
</FormControl>
</div>
<div
style={{ marginLeft: 13, fontSize: 16, color: "#9E9E9E" }}
>
<Typography
variant="text"
style={{ color: theme.palette.text.primary }}
>
Region
</Typography>
<RegionChangeModal
isDisabled={isDisabled}
selectedOrganization={selectedOrganization}
setSelectedRegion={setSelectedRegion}
partnerData={partnerData}
setPartnerData={setPartnerData}
userdata={userdata}
/>
</div>
<div style={{ alignItems: "flex-start", marginLeft: 13, display: "flex", flexDirection: "column" }}>
<Typography
variant="text"
style={{ color: theme.palette.text.primary }}
>
Country
</Typography>
<FormControl style={{ width: 210, height: 35 }}>
<Autocomplete
id="country-select"
value={countries.find(country => country.label === partnerData?.country)}
style={{
minWidth: 210,
marginTop: 5,
maxWidth: 210,
height: 35,
borderRadius: 4,
color: theme.palette.textFieldStyle.color,
cursor: isDisabled ? "not-allowed" : "pointer",
}}
disabled={isDisabled}
options={countries}
autoHighlight
onClear={() => {
setPartnerData({
...partnerData,
country: "",
});
}}
getOptionLabel={(option) => option?.label || ""}
onChange={(event, newValue) => {
setPartnerData({
...partnerData,
country: newValue?.label,
});
}}
renderOption={(props, option) => (
<Box
component="li"
sx={{ "& > img": { mr: 2, flexShrink: 0 } }}
{...props}
>
<img
loading="lazy"
width="20"
src={`https://flagcdn.com/48x36/${option.code.toLowerCase()}.png`}
alt=""
/>
{option.label}
</Box>
)}
renderInput={(params) => (
<TextField
{...params}
placeholder="Select country"
variant="outlined"
InputProps={{
...params.InputProps,
startAdornment: partnerData?.country ? (
<img
loading="lazy"
width="20"
src={`https://flagcdn.com/48x36/${countries.find(country => country.label === partnerData.country)?.code.toLowerCase()}.png`}
alt=""
style={{ marginRight: 8 }}
/>
) : null,
style: {
color: theme.palette.textFieldStyle.color,
height: "35px",
fontSize: "1em",
borderRadius: 4,
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
},
classes: {
notchedOutline: isEditOrgTab ? null : classes.notchedOutline,
},
}}
/>
)}
/>
</FormControl>
</div>
</div>
<div style={{ marginTop: "10px", }} />
<Typography
variant="text"
style={{ color: theme.palette.text.primary }}
>
Description
</Typography>
<div style={{ display: "flex" }}>
<TextField
required
disabled={isDisabled}
multiline
rows={3}
style={{
flex: "1",
marginTop: "5px",
color: theme.palette.textFieldStyle.color,
backgroundColor: isEditOrgTab
? theme.palette.textFieldStyle.backgroundColor
: theme.palette.inputColor,
height: 89,
borderRadius: 4,
cursor: isDisabled ? "not-allowed" : "pointer",
}}
fullWidth={true}
type="name"
id="outlined-with-placeholder"
margin="normal"
variant="outlined"
placeholder="A description for the organization"
value={partnerData?.description}
onBlur={(e) => {
setPartnerData({
...partnerData,
description: e.target.value,
});
}}
onChange={(e) => {
setPartnerData({
...partnerData,
description: e.target.value,
});
}}
InputProps={{
classes: {
notchedOutline: isEditOrgTab
? null
: classes.notchedOutline,
},
style: {
color: theme.palette.textFieldStyle.color,
height: 89,
borderRadius: 4,
},
}}
/>
</div>
<div>
<div
style={{ width: "100%", maxWidth: 500, marginRight: 10, marginTop: 10 }}
>
<Typography
variant="text"
style={{ color: theme.palette.text.primary }}
>
Website URL
</Typography>
<TextField
required
disabled={isDisabled}
style={{
flex: "1",
display: "flex",
height: 35,
width: "100%",
maxWidth: 500,
marginTop: "5px",
marginRight: "15px",
color: theme.palette.textFieldStyle.color,
backgroundColor: isEditOrgTab
? theme.palette.textFieldStyle.backgroundColor
: theme.palette.inputColor,
cursor: isDisabled ? "not-allowed" : "pointer",
}}
fullWidth={true}
placeholder="https://www.example.com"
type="name"
id="standard-required"
margin="normal"
variant="outlined"
value={partnerData?.website_url}
onBlur={() => {}}
onChange={(e) => {
if (e.target.value.length > 100) {
toast("Choose a shorter website URL.");
return;
}
setPartnerData({
...partnerData,
website_url: e.target.value,
});
}}
color="primary"
InputProps={{
style: {
color: theme.palette.textFieldStyle.color,
height: "35px",
fontSize: "1em",
borderRadius: 4,
backgroundColor:
theme.palette.textFieldStyle.backgroundColor,
},
classes: {
notchedOutline: isEditOrgTab
? null
: classes.notchedOutline,
},
}}
/>
</div>
<div
style={{ width: "100%", maxWidth: 500, marginRight: 10, marginTop: 20 }}
>
<Typography
variant="text"
style={{ color: theme.palette.text.primary }}
>
Article URL
</Typography>
<TextField
required
disabled={isDisabled}
style={{
flex: "1",
display: "flex",
height: 35,
width: "100%",
maxWidth: 500,
marginTop: "5px",
marginRight: "15px",
color: theme.palette.textFieldStyle.color,
backgroundColor: isEditOrgTab
? theme.palette.textFieldStyle.backgroundColor
: theme.palette.inputColor,
cursor: isDisabled ? "not-allowed" : "pointer",
}}
fullWidth={true}
placeholder="https://www.example.com"
type="name"
id="standard-required"
margin="normal"
variant="outlined"
value={partnerData?.article_url}
onBlur={() => {}}
onChange={(e) => {
if (e.target.value.length > 100) {
toast("Choose a shorter article URL.");
return;
}
setPartnerData({
...partnerData,
article_url: e.target.value,
});
}}
color="primary"
InputProps={{
style: {
color: theme.palette.textFieldStyle.color,
height: "35px",
fontSize: "1em",
borderRadius: 4,
backgroundColor:
theme.palette.textFieldStyle.backgroundColor,
},
classes: {
notchedOutline: isEditOrgTab
? null
: classes.notchedOutline,
},
}}
/>
</div>
</div>
</div>
</div>
</span>
</Grid>
</Grid>
</div>
);
};
export default PartnerDetails;
const RegionChangeModal = memo(
({
isDisabled,
selectedOrganization,
setSelectedRegion,
userdata,
partnerData,
setPartnerData,
}) => {
// Show from options: "us-west2", "europe-west2", "europe-west3", "northamerica-northeast1"
// var regions = ["us-west2", "europe-west2", "europe-west3", "northamerica-northeast1"]
const regionMapping = {
"Africa": "af",
"Asia Pacific (APAC)": "",
"Europe": "eu",
"Latin America": "la",
"Middle East": "me",
"North America": "na",
"Global": ""
};
//let regiontag = "UK";
let regiontag = partnerData?.region || "UK";
let regionCode = "gb";
const regionsplit = selectedOrganization?.region_url?.split(".");
if (regionsplit?.length > 2 && !regionsplit[0]?.includes("shuffler")) {
const namesplit = regionsplit[0]?.split("/");
regiontag = namesplit[namesplit.length - 1];
if (regiontag === "california") {
regiontag = "US";
regionCode = "us";
} else if (regiontag === "frankfurt") {
regiontag = "EU-2";
regionCode = "eu";
} else if (regiontag === "ca") {
regiontag = "CA";
regionCode = "ca";
} else if (regiontag === "austrailia") {
regiontag = "AUS";
regionCode = "au";
}
}
return (
<FormControl
disabled={isDisabled}
style={{
display: "flex",
flexDirection: "column",
marginTop: 5,
alignItems: "center",
cursor: isDisabled ? "not-allowed" : "pointer",
}}
>
{/* <InputLabel id="demo-simple-select-label">Region</InputLabel> */}
<Select
labelId="demo-simple-select-label"
id="demo-simple-select"
value={partnerData?.region || "Europe"}
style={{ minWidth: 190, height: 35, borderRadius: 4 }}
onChange={(e) => {
// if (userdata?.support) {
// setSelectedRegion(e.target.value)
// } else {
// handleSendChangeRegionMail(e.target.value)
// }
setPartnerData({
...partnerData,
region: e.target.value,
})
// setSelectedRegion(e.target.value)
}}
>
{Object.keys(regionMapping).map((region, index) => {
const regionImageCode = regionMapping[region];
// Set the default region if selectedOrganization.region is not set
if (selectedOrganization.region === undefined) {
selectedOrganization.region = "europe-west2";
}
if (region === "AUS") {
region = "AUS (test)";
}
// Check if the current region matches the selected region
if (region === selectedOrganization.region) {
// If the region matches, set the MenuItem as selected
return (
<MenuItem value={region} key={index} disabled>
{/* show region image through cdn */}
{/* <img
src={`https://flagcdn.com/48x36/${regionImageCode}.png`}
alt={region}
style={{ marginRight: 10 }}
/> */}
{region}
</MenuItem>
);
} else {
return (
<MenuItem sx={{ display: "flex" }} key={index} value={region}>
{/* <img
src={`https://flagcdn.com/48x36/${regionImageCode}.png`}
alt={region}
style={{ marginRight: 10, width: 20, height: 18 }}
/> */}
{region}
</MenuItem>
);
}
})}
</Select>
</FormControl>
);
}
);
File diff suppressed because one or more lines are too long
+391
View File
@@ -0,0 +1,391 @@
import React, { useEffect, useState, useContext } from 'react';
import { toast } from "react-toastify";
import { Context } from '../context/ContextApi.jsx';
import { getTheme } from '../theme.jsx';
import {
Button,
Typography,
Box,
IconButton,
Tooltip,
} from "@mui/material";
import OpenInNewIcon from '@mui/icons-material/OpenInNew';
import { useNavigate } from 'react-router';
import PartnerHeader from '../components/PartnerHeader.jsx';
import PartnerDetails from '../components/PartnerDetails.jsx';
const PartnerSettings = (props) => {
const {
isCloud,
userdata,
globalUrl,
serverside,
loadingPartnerData,
selectedOrganization,
setSelectedOrganization,
handleGetOrg,
partnerData,
setPartnerData,
} = props;
const [isPartner, setIsPartner] = React.useState(isCloud ? userdata?.active_org?.is_partner ? true : false : false);
const [partnerTypes, setPartnerTypes] = React.useState({});
const [isPublishing, setIsPublishing] = React.useState(false);
const [isToggling, setIsToggling] = React.useState(false);
useEffect(() => {
setIsPartner(isCloud ? userdata?.active_org?.is_partner ? true : false : false);
if(userdata?.support){
setIsPartner(true);
}
}, [userdata, isCloud]);
// Partner Types handling : Getting from org status
useEffect(() => {
const partnerTypes = {};
userdata?.org_status.forEach(status => {
if (status.includes("_partner")) {
partnerTypes[status] = true;
}
});
setPartnerTypes(partnerTypes);
}, [selectedOrganization]);
// Partner Type Colors
const partnerTypeColors = {
"tech_partner": "#ff8544",
"distribution_partner": "#2BC07E",
"service_partner": "#a99cf9",
"integration_partner": "#fb47a0"
}
const handleSendUpdateRequest = () => {
toast("Your request has been sent to the support team. They will review your request and get back to you as soon as possible.")
}
// Open partner page in new tab
const handleOpenPartnerPage = () => {
if (partnerData?.id) {
if(partnerData?.public){
window.open(`${window.location.origin}/partners/${partnerData.name.toLowerCase().replaceAll(" ", "_")}`)
}else{
window.open(`${window.location.origin}/partners/${partnerData?.id}`, "_blank");
}
} else {
toast.error("Partner ID not found");
}
}
// Update Partner Details
const handleUpdatePartnerDetails = () => {
// Validate required fields before publishing
const requiredStringFields = [
{ field: partnerData?.name, name: "Partner Name" },
{ field: partnerData?.description, name: "Description" },
{ field: selectedOrganization?.id, name: "Organization Id" },
{ field: partnerData?.image_url, name: "Logo Image" },
{ field: partnerData?.landscape_image_url, name: "Landscape Image" },
{ field: partnerData?.website_url, name: "Website URL" },
{ field: partnerData?.article_url, name: "Article URL" },
{ field: partnerData?.country, name: "Country" },
{ field: partnerData?.region, name: "Region" },
];
// Required array fields (multi-select dropdowns)
const requiredArrayFields = [
{ field: partnerData?.solutions, name: "Solutions" },
];
// Check if any required string fields are empty
const emptyStringFields = requiredStringFields.filter(item =>
!item.field || item.field.trim() === ""
);
// Check if any required array fields are empty
const emptyArrayFields = requiredArrayFields.filter(item =>
!item.field || !Array.isArray(item.field) || item.field.length === 0
);
// Combine all empty fields
const allEmptyFields = [...emptyStringFields, ...emptyArrayFields];
// If there are empty required fields, show error and return
if (allEmptyFields.length > 0) {
const missingFields = allEmptyFields.map(item => item.name).join(", ");
toast.error(`Please fill in all required fields: ${missingFields}`);
return;
}
// Check if at least one partner type is selected
if (Object.keys(partnerTypes).length === 0) {
toast.error("There should be at least one partner type");
return;
}
setIsPublishing(true);
const url = globalUrl + "/api/v1/partners/" + userdata?.active_org?.id;
const data = {
id: partnerData.id?.trim() || null,
name: partnerData.name?.trim(),
org_id: selectedOrganization?.id?.trim(),
description: partnerData.description?.trim(),
website_url: partnerData.website_url?.trim(),
article_url: partnerData.article_url?.trim(),
partner_type: partnerTypes,
expertise: partnerData?.expertise || [],
services: partnerData?.services || [],
solutions: partnerData?.solutions || [],
country: partnerData?.country || "",
region: partnerData?.region || "",
image_url: partnerData?.image_url?.trim(),
landscape_image_url: partnerData?.landscape_image_url?.trim(),
public: partnerData?.public
}
fetch(url, {
mode: "cors",
method: "POST",
body: JSON.stringify(data),
credentials: "include",
headers: {
"Content-Type": "application/json; charset=utf-8",
},
})
.then((response) => {
setIsPublishing(false);
if (response.status !== 200) {
toast.error("Failed to publish partner");
}
return response.json();
})
.then((responseJson) => {
toast.success("Partner details successfully updated");
})
.catch((error) => {
setIsPublishing(false);
toast.error("Failed to update partner details: " + error?.message);
})
}
// Toggle Partner Publish Status
const handleTogglePublishStatus = () => {
setIsToggling(true);
const newPublishStatus = !partnerData?.public;
const url = globalUrl + "/api/v1/partners/" + userdata?.active_org?.id;
const data = {
id: partnerData.id?.trim() || null,
name: partnerData.name?.trim(),
org_id: selectedOrganization?.id?.trim(),
description: partnerData.description?.trim(),
website_url: partnerData.website_url?.trim(),
article_url: partnerData.article_url?.trim(),
partner_type: partnerTypes,
usecases: partnerData?.usecases || [],
expertise: partnerData?.expertise || [],
services: partnerData?.services || [],
solutions: partnerData?.solutions || [],
country: partnerData?.country || "",
region: partnerData?.region || "",
image_url: partnerData?.image_url?.trim(),
landscape_image_url: partnerData?.landscape_image_url?.trim(),
public: newPublishStatus
}
fetch(url, {
mode: "cors",
method: "POST",
body: JSON.stringify(data),
credentials: "include",
headers: {
"Content-Type": "application/json; charset=utf-8",
},
})
.then((response) => {
setIsToggling(false);
if (response.status !== 200) {
toast.error("Failed to update publish status");
}
return response.json();
})
.then((responseJson) => {
// Update local state
setPartnerData(prev => ({
...prev,
public: newPublishStatus
}));
toast.success(`Partner ${newPublishStatus ? 'published' : 'unpublished'} successfully`);
})
.catch((error) => {
setIsToggling(false);
toast.error("Failed to update publish status: " + error?.message);
})
}
const { themeMode } = useContext(Context);
const theme = getTheme(themeMode);
const navigate = useNavigate();
return (
<div style={{ width: "100%", boxSizing: 'border-box', padding: "27px 10px 19px 27px", height:"100%", backgroundColor: theme.palette.platformColor, borderRadius: '16px', }}>
<div style={{ height: "100%", width: "100%", overflowX: 'hidden', scrollbarColor: theme.palette.scrollbarColorTransparent, scrollbarWidth: 'thin'}} >
<div style={{ marginBottom: 40 }}>
<div style={{display:"flex", alignItems:"center", justifyContent:"space-between"}}>
<div style={{width:'100%'}}>
<Box
sx={{display:"flex", alignItems:"flex-start", gap:2, justifyContent:"flex-start"
}}>
<Typography variant='h3' sx={{ marginBottom: "15px", marginTop: 0, }}>Configuration</Typography>
{Object?.entries(partnerTypes)?.map(([key, value]) => (
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "999px",
py: 1.2,
px: 2.5,
fontSize: "13px",
fontWeight: 500,
fontFamily: theme.typography.fontFamily,
color: "#fff",
backgroundColor: "transparent",
border: `1.5px solid ${
partnerTypeColors[key]
}`,
transition: "all 0.2s ease",
textAlign: "center",
whiteSpace: "nowrap",
color: partnerTypeColors[key],
}}
>
{key.replace("_", " ").replace(/\b\w/g, char => char.toUpperCase())}
</Box>
))}
</Box>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
width: "100%",
marginTop: 0.5,
}}>
<Typography variant="body2" style={{ color: theme.palette.text.secondary, fontSize: 16, maxWidth: "60%" }}>
Set up and manage partner details and information to be displayed on the Partners page.
</Typography>
<Box sx={{ display: "flex", gap: 2, alignItems: "center" }}>
{isPartner && (
<>
{/* View Partner Page Button */}
<Tooltip title="View Partner Page">
<IconButton
sx={{
color: theme.palette.primary.main,
backgroundColor: theme.palette.action.hover,
"&:hover": {
backgroundColor: theme.palette.action.selected,
}
}}
onClick={handleOpenPartnerPage}
disabled={!partnerData?.id}
>
<OpenInNewIcon />
</IconButton>
</Tooltip>
{/* Update Details Button */}
<Button
sx={{
fontSize: 16,
textTransform: 'capitalize',
boxShadow: "none",
px: 4,
}}
variant="contained"
color="primary"
disabled={isPublishing || isToggling}
onClick={handleUpdatePartnerDetails}
>
{isPublishing ? "Updating..." : "Update Details"}
</Button>
{/* Toggle Publish/Unpublish Button */}
<Button
sx={{
fontSize: 16,
textTransform: 'capitalize',
boxShadow: "none",
marginRight: 4,
px: 3,
backgroundColor: partnerData?.public ? "#FD4C62" : "#4caf50",
"&:hover": {
backgroundColor: partnerData?.public ? "#FD4C62" : "#4caf50"
}
}}
variant="contained"
disabled={isToggling || isPublishing}
onClick={handleTogglePublishStatus}
>
{isToggling ? (partnerData?.public ? "Unpublishing..." : "Publishing...") : (partnerData?.public ? "Unpublish" : "Publish")}
</Button>
</>
)}
{!isPartner && (
<Button
sx={{
fontSize: 16,
textTransform: 'capitalize',
boxShadow: "none",
px: 5,
textDecoration: "none",
marginRight: 4
}}
variant="contained"
color="primary"
onClick={() => {
if(!isCloud){
navigate("/become-partner")
}else{
window.open("https://shuffler.io/become-partner")
}
}}
>
Become a partner
</Button>
)}
</Box>
</Box>
</div>
</div>
</div>
<PartnerHeader
isCloud={isCloud}
userdata={userdata}
isPublishing={isPublishing}
setSelectedOrganization={setSelectedOrganization}
globalUrl={globalUrl}
selectedOrganization={selectedOrganization}
isEditOrgTab={true}
loadingPartnerData={loadingPartnerData}
partnerData={partnerData}
setPartnerData={setPartnerData}
handleGetOrg={handleGetOrg}
/>
<PartnerDetails
isCloud={isCloud}
userdata={userdata}
partnerData={partnerData}
isPublishing={isPublishing}
loadingPartnerData={loadingPartnerData}
setPartnerData={setPartnerData}
setSelectedOrganization={setSelectedOrganization}
globalUrl={globalUrl}
selectedOrganization={selectedOrganization}
isEditOrgTab={true}
handleGetOrg={handleGetOrg}
serverside={serverside}
/>
</div>
</div >
)
}
export default PartnerSettings;
+245
View File
@@ -0,0 +1,245 @@
import React, { useEffect, useState, useCallback, useContext } from 'react';
import { useNavigate, useLocation } from "react-router-dom";
import Branding from "../components/Branding.jsx";
import { ToastContainer, toast } from "react-toastify";
import { Button } from '@mui/material';
import { getTheme } from '../theme.jsx';
import { Context } from '../context/ContextApi.jsx';
import PartnerSettings from '../components/PartnerSettings.jsx';
import PartnersUsecasesTab from '../components/PartnersUsecasesTab.jsx';
import PartnersApps from '../components/PartnerApps.jsx';
import PartnerArticles from '../components/PartnersArticles.jsx';
const PartnerTab = (props) => {
const location = useLocation();
const navigate = useNavigate();
const {
userdata,
globalUrl,
serverside,
isCloud,
setSelectedOrganization,
selectedOrganization, handleGetOrg,
handleStatusChange,
isLoaded,
removeCookie
} = props;
const [selectedTab, setSelectedTab] = useState('partner_settings');
const [loadingPartnerData, setLoadingPartnerData] = useState(false);
const [curIndex, setCurIndex] = React.useState(0);
const [partnerData, setPartnerData] = React.useState({
name: "",
description: "",
image_url: "",
landscape_image_url: "",
website_url: "",
article_url: "",
expertise: [],
usecases: [],
services: [],
solutions: [],
country: "",
region: "",
partner_type: {},
});
const tabsOnPartnerTab = [ 'Partner Settings', 'Usecases', 'Apps', 'AI Agents', 'Articles', 'Branding'];
const { themeMode } = useContext(Context);
const theme = getTheme(themeMode);
useEffect(() => {
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 :)
return;
}
setLoadingPartnerData(true);
const url = globalUrl + "/api/v1/partners/" + userdata?.active_org?.id;
fetch(url, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
toast("Failed to get partner data")
}
return response.json();
})
.then((responseJson) => {
if(responseJson.success) {
setPartnerData(responseJson?.partner);
console.log("responseJson", responseJson)
setLoadingPartnerData(false);
}else{
setLoadingPartnerData(false);
toast(responseJson?.reason)
}
})
.catch((error) => {
setLoadingPartnerData(false);
})
}, [globalUrl]);
// Used to auto select the tab based on the url : partner_tab
useEffect(() => {
const queryParams = new URLSearchParams(location.search);
const tabName = queryParams.get('partner_tab');
if (tabName) {
const decodedTabName = decodeURIComponent(tabName);
setSelectedTab(decodedTabName);
if (decodedTabName === 'partner_settings') {
setCurIndex(0);
} else if(decodedTabName === 'usecases'){
setCurIndex(1)
}else if (decodedTabName === 'apps'){
setCurIndex(2);
} else if (decodedTabName === 'aiagents') {
setCurIndex(3);
} else if (decodedTabName === 'articles') {
setCurIndex(4);
} else if (decodedTabName === 'branding') {
setCurIndex(5);
}
}
}, [location.search]);
// Tab click on partner tab
const handleTabClick = (tabName) => {
const formattedTabName = tabName.toLowerCase().replace(/[\s&]+/g, '');
const encodedTabName = encodeURIComponent(formattedTabName);
setSelectedTab(formattedTabName);
document.title = `Shuffle - partner - ${formattedTabName}`;
navigate(`?partner_tab=${encodedTabName}`);
};
// Rendering the content based on the selected tab
const renderContent = () => {
switch (selectedTab) {
case 'partner_settings':
return <PartnerSettings isCloud={isCloud} loadingPartnerData={loadingPartnerData} partnerData={partnerData} setPartnerData={setPartnerData} handleStatusChange={handleStatusChange} userdata={userdata} globalUrl={globalUrl} serverside={serverside} handleGetOrg={handleGetOrg} setSelectedOrganization={setSelectedOrganization} selectedOrganization={selectedOrganization} selectedTab={selectedTab} />;
case 'usecases':
return <PartnersUsecasesTab isCloud={isCloud} partnerData={partnerData} setPartnerData={setPartnerData} handleStatusChange={handleStatusChange} userdata={userdata} globalUrl={globalUrl} serverside={serverside} handleGetOrg={handleGetOrg} setSelectedOrganization={setSelectedOrganization} selectedOrganization={selectedOrganization} selectedTab={selectedTab} />;
case `apps`:
return <PartnersApps isCloud={isCloud} partnerData={partnerData} setPartnerData={setPartnerData} handleStatusChange={handleStatusChange} userdata={userdata} globalUrl={globalUrl} serverside={serverside} handleGetOrg={handleGetOrg} setSelectedOrganization={setSelectedOrganization} selectedOrganization={selectedOrganization} selectedTab={selectedTab} />;
case 'articles' :
return <PartnerArticles isCloud={isCloud} partnerData={partnerData} setPartnerData={setPartnerData} handleStatusChange={handleStatusChange} userdata={userdata} globalUrl={globalUrl} serverside={serverside} handleGetOrg={handleGetOrg} setSelectedOrganization={setSelectedOrganization} selectedOrganization={selectedOrganization} selectedTab={selectedTab} />;
case `ai_agents`:
return <PartnerArticles isCloud={isCloud} partnerData={partnerData} setPartnerData={setPartnerData} handleStatusChange={handleStatusChange} userdata={userdata} globalUrl={globalUrl} serverside={serverside} handleGetOrg={handleGetOrg} setSelectedOrganization={setSelectedOrganization} selectedOrganization={selectedOrganization} selectedTab={selectedTab} />;
case 'branding':
return <Branding
isCloud={isCloud}
userdata={userdata}
globalUrl={globalUrl}
handleGetOrg={handleGetOrg}
selectedOrganization={selectedOrganization}
clickedFromOrgTab={true}
setSelectedOrganization={setSelectedOrganization}
/>;
default:
return <PartnerSettings isCloud={isCloud} partnerData={partnerData} setPartnerData={setPartnerData} handleStatusChange={handleStatusChange} userdata={userdata} globalUrl={globalUrl} serverside={serverside} handleGetOrg={handleGetOrg} setSelectedOrganization={setSelectedOrganization} selectedOrganization={selectedOrganization} />;
}
};
const isTabDisabled = (tabName) => {
// If user is a support user, enable all tabs
if (userdata?.support) {
return false;
}
// For non-support users, apply the following restrictions:
// For onPrem only partner settings and branding are enabled
if (
!isCloud &&
(tabName === "Usecases" ||
tabName === "Apps" ||
tabName === "AI Agents" ||
tabName === "Articles")
) {
return true;
}
// Disable Apps, Articles, and AI Agents for all non-support users
if (
tabName === "Apps" ||
tabName === "Articles" ||
tabName === "AI Agents"
) {
return true;
}
// Disable Usecases tab for cloud users if not a partner or is in a sub-org
if (isCloud && tabName === "Usecases") {
if (
!userdata?.active_org?.is_partner ||
userdata?.active_org?.is_sub_org
) {
return true;
}
}
// Disable Branding tab if not an integration partner
const isIntegrationPartner =
userdata &&
userdata?.org_status?.includes("integration_partner") &&
!userdata?.org_status?.includes("sub_org");
if (tabName === "Branding" && !isIntegrationPartner) {
return true;
}
// Enable the tab by default
return false;
}
return (
<div style={{ height: "100%", width: "100%", color: theme.palette.platformColor, backgroundColor: theme.palette.platformColor, borderTopRightRadius: '8px', borderBottomRightRadius: 8, borderLeft: theme.palette.defaultBorder, boxSizing: 'border-box' }}>
<div style={{ display: 'flex', justifyContent: 'space-around', width: "100%", borderBottom: theme.palette.defaultBorder ,boxSizing: 'border-box' }}>
{tabsOnPartnerTab?.map((tabName, index) => (
<div style={{ pointerEvents: 'auto', width: '100%',}}>
<Button
key={tabName}
onClick={() => {
setCurIndex(index);
handleTabClick(index === 0 ? "partner_settings" : tabName.toLowerCase().replace(/[\s&]+/g, ''));
}}
disabled={isTabDisabled(tabName)}
variant="text"
sx={{
"&.MuiButton-root": {
padding: '28px 0',
borderBottom: index === curIndex ? '2px solid #FF8444' : 'none',
cursor: 'pointer',
fontWeight: index === curIndex ? 'bold' : 'normal',
color: index === curIndex ? "#FF8444" : theme.palette.text.primary,
textTransform: 'none',
fontSize: 16,
width: "100%",
height: "100%",
borderRadius: 0,
},
"&: hover": {
backgroundColor: theme.palette.hoverColor
},
"&.Mui-disabled": {
color: "#6F6F6F",
},
}}
>
{tabName}
</Button>
</div>
))}
</div>
<div style={{ display: 'flex', justifyContent: 'center', width: "100%", height: "100%", paddingBottom: 200, boxSizing:'border-box'}}>
{renderContent()}
</div>
</div>
);
};
export default PartnerTab;
@@ -0,0 +1,21 @@
import { Box, Typography } from '@mui/material'
import React from 'react'
const PartnersArticles = () => {
return (
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: "100%",
height: "100%",
minHeight: "500px",
}}
>
<Typography>Partner Articles</Typography>
</Box>
)
}
export default PartnersArticles
File diff suppressed because it is too large Load Diff
+45 -182
View File
@@ -89,49 +89,18 @@ const Priorities = memo((props) => {
useEffect(() => {
prepareNotificationAppList()
if (notifications === undefined || notifications === null || notifications.length === 0) {
getNotifications()
}
}, []);
let navigate = useNavigate();
const classes = useStyles();
const getAppIDs = async (appList) => {
fetch(globalUrl + "/api/v1/apps", {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
}).then((response) => {
if (response.status !== 200) {
toast("Failed getting app ids: ", response.reason);
console.log("Status not 200 for app ids :O!");
return;
}
return response.json();
}).then((responseJson) => {
if (responseJson !== undefined) {
// console.log("App ids: ", responseJson)
// console.log("App list: ", appList)
const filteredApps = responseJson.filter(app => appList.includes(app.name));
const appDetails = filteredApps.map(app => ({ name: app.name, id: app.id }));
return appDetails
// console.log("App IDs: ", appDetails)
}
}).catch((error) => {
console.log("Error getting app ids: " + error);
})
}
// getting comms & cases app from app framework
var notificationAppList = [];
// if (selectedOrganization.security_framework?.cases && selectedOrganization.security_framework?.cases.name.length > 0) {
// notificationAppList = notificationAppList.concat(selectedOrganization.security_framework?.cases);
// }
// if (selectedOrganization.security_framework?.communication && selectedOrganization.security_framework?.communication.name.length > 0) {
// notificationAppList = notificationAppList.concat(selectedOrganization.security_framework?.communication);
// }
const mergeAuthData = (result, responseJson) => {
// result is only getting: Jira AND discord.
@@ -146,7 +115,7 @@ const Priorities = memo((props) => {
return updatedResult;
};
const prepareNotificationAppList = async () => {
const prepareNotificationAppList = () => {
// getting App ID,Authentication fields and saved auths for each app
var result = []
fetch(globalUrl + "/api/v1/apps", {
@@ -187,18 +156,17 @@ const Priorities = memo((props) => {
return;
}
return response.json();
}).then(async (responseJson) => {
}).then((responseJson) => {
if (!responseJson.success) {
console.log("Could not get app auth")
return;
}
// console.log("responseJson of auth: ", responseJson.data)
result = await mergeAuthData(appDetails, responseJson.data)
result = mergeAuthData(appDetails, responseJson.data)
// console.log("merged auth data: ", result)
// console.log("result", result)
result.map(item => {
console.log("Getting app config for: ", item.name);
fetch(globalUrl + `/api/v1/apps/${item.id}/config`, {
method: "GET",
headers: {
@@ -235,9 +203,9 @@ const Priorities = memo((props) => {
setnotificationAppsDetails(newJSON);
console.log("notificationAppsDetails: ", newJSON)
}
}).then(async () => {
}).then(() => {
// removing this for now,
// await checkIfAlreadyGenerated(filteredApps, workflows);
// checkIfAlreadyGenerated(filteredApps, workflows);
}).catch((error) => {
console.log("Error getting app config: " + error);
@@ -280,143 +248,6 @@ const Priorities = memo((props) => {
console.log("Apps: ", notificationAppsDetails);
}, [notificationAppsDetails])
// const generateJiraNotificationWorkflow = async (app, appAuthId, projectId, issuetype) => {
// //currently only supports JIRA figure out a way to support more apps
// var appname = app.name;
// var appImage = app.large_image;
// var workflowName = `[GENERATED] ${appname} notification workflow`
// var workflowDescription = "Generated by Shuffle for sending info/error notifications."
// var data = {
// "name": workflowName,
// "description": workflowDescription,
// }
// fetch(globalUrl + "/api/v1/workflows", {
// method: "POST",
// headers: {
// "Content-Type": "application/json",
// Accept: "application/json",
// },
// body: JSON.stringify(data),
// credentials: "include",
// })
// .then((response) => {
// if (response.status !== 200) {
// toast("Failed setting notification workflow: ", response.reason);
// console.log("Status not 200 for workflows :O!");
// return;
// }
// return response.json();
// }).then((responseJson) => {
// if (responseJson !== undefined) {
// console.log("Notification workflow created successfully")
// var workflow_id = responseJson.id
// if (appname.toLowerCase() === "jira") {
// console.log("updating workflow for JIRA")
// var workflowBody = {
// "name": workflowName,
// "Description": workflowDescription,
// "id": workflow_id,
// "actions": [
// {
// "app_id": app.id,
// "app_name": "jira",
// "name": "post_create_issue",
// "authentication_id": appAuthId,
// "large_image": appImage,
// "isStartNode": true,
// "label": "create_issue",
// "app_version": "1.1.0",
// "parameters": [
// {
// "name": "body",
// "value": `{"fields": { "project": {"key": "${projectId}"},"summary": "$exec.title","issuetype": {"name": "${issuetype}"},"description": {"content": [{"content":[{"type":"text","text":"$exec.description"}],"type": "paragraph"}],"type": "doc","version": 1}}}`
// },
// {
// "name": "username_basic",
// "value": ""
// },
// {
// "name": "password_basic",
// "value": ""
// },
// {
// "name": "url",
// "value": ""
// },
// {
// "name": "headers",
// "value": "Content-type=application/json \nAccept=application/json"
// },
// {
// "name": "queries",
// "value": ""
// },
// {
// "name": "ssl_verify",
// "value": "False"
// }
// ]
// }
// ]
// }
// }
// fetch(globalUrl + `/api/v1/workflows/${workflow_id}`, {
// method: "PUT",
// headers: {
// "Content-Type": "application/json",
// Accept: "application/json",
// },
// body: JSON.stringify(workflowBody),
// credentials: "include",
// })
// .then((response) => {
// if (response.status !== 200) {
// toast("Failed setting notification workflow: ", response.reason);
// console.log("Status not 200 for workflows :O!");
// return;
// }
// return response.json();
// }).then((responseJson) => {
// if (responseJson !== undefined) {
// handleEditOrg(
// selectedOrganization.name,
// selectedOrganization.description,
// selectedOrganization.id,
// selectedOrganization.image,
// {
// documentation_reference: selectedOrganization?.defaults?.documentation_reference,
// workflow_upload_repo: selectedOrganization?.defaults?.workflow_upload_repo,
// workflow_upload_branch: selectedOrganization?.defaults?.workflow_upload_branch,
// workflow_upload_username: selectedOrganization?.defaults?.workflow_upload_username,
// workflow_upload_token: selectedOrganization?.defaults?.workflow_upload_token,
// newsletter: selectedOrganization?.defaults?.newsletter,
// weekly_recommendations: selectedOrganization?.defaults?.weekly_recommendations,
// notification_workflow: workflow_id,
// },
// {
// sso_entrypoint: selectedOrganization?.sso_config?.sso_entrypoint,
// sso_certificate: selectedOrganization?.sso_config?.sso_certificate,
// client_id: selectedOrganization?.sso_config?.client_id,
// client_secret: selectedOrganization?.sso_config?.client_secret,
// openid_authorization: selectedOrganization?.sso_config?.openid_authorization,
// openid_token: selectedOrganization?.sso_config?.openid_token,
// SSORequired: selectedOrganization?.sso_config?.SSORequired,
// auto_provision: selectedOrganization?.sso_config?.auto_provision,
// }
// )
// console.log("Notification workflow updated successfully")
// toast("Notification workflow updated successfully")
// }
// })
// }
// }).catch((error) => {
// console.log("Error setting workflows: " + error);
// })
// }
const generateCommsNotificationWorkflow = async (app, sender, recepient) => {
var appname = app.name
var appImage = app.large_image
@@ -984,12 +815,13 @@ print('"' + encoded + '"')
<FormControl>
<DialogTitle>
<div style={{ color: "rgba(255,255,255,0.9)" }}>
{`Configure ${selectedAppDetails.name} workflow`}
{`Configure ${selectedAppDetails?.name?.replaceAll("_", " ")} workflow`}
</div>
</DialogTitle>
<DialogContent style={{ color: "rgba(255,255,255,0.65)", overflowY: 'auto', maxHeight: '450px' }}>
<Divider style={{ marginBottom: '1em', backgroundColor: 'rgba(255,255,255,0.12)' }} />
{console.log("len Selected app details: ", selectedAppDetails)}
{(selectedAppDetails.authentication_data || (selectedAppDetails.auth_config && selectedAppDetails.auth_config.required == false) || (selectedAppDetails.authentication && selectedAppDetails.authentication.required == false)) ?
<>
<Box mb={2}>
@@ -1152,13 +984,15 @@ print('"' + encoded + '"')
const renderChips = useCallback(() => {
const appList = Object.values(notificationAppsDetails);
return (
<Stack spacing={2}>
<Stack direction="row" spacing={1}>
{appList.map((app) => (
<Chip
key={app.id}
label={app.name}
label={app?.name.replaceAll("_", " ")}
variant={generatedWorkflow.app_name === app.name ? "filled" : "outlined"}
disabled={generatedWorkflow.app_name === app.name ? true : false}
onClick={() => {
@@ -1176,6 +1010,7 @@ print('"' + encoded + '"')
avatar={<img src={app.large_image} alt={app.name} style={{ width: 24, height: 24, borderRadius: '50%' }} />}
/>
))}
</Stack>
<Box sx={{
@@ -1198,7 +1033,7 @@ print('"' + encoded + '"')
>
Set up app authentication
</Link>
to unlock additional workflow options.
to show additional workflow options.
</Typography>
</Box>
</Stack>
@@ -1283,10 +1118,38 @@ print('"' + encoded + '"')
})
}
const getNotifications = () => {
fetch(`${globalUrl}/api/v1/notifications`, {
credentials: "include",
method: "GET",
headers: {
"Content-Type": "application/json",
},
})
.then(function (response) {
if (response.status !== 200) {
console.log("Error in response");
}
return response.json();
})
.then(function (responseJson) {
if (responseJson?.success !== false && responseJson?.notifications !== undefined && responseJson?.notifications !== null) {
setNotifications(responseJson.notifications || [])
} else {
toast("Failed loading notifications. Please try again later.");
}
})
.catch((error) => {
console.log("error in notification loading: ", error);
});
}
const clearNotifications = () => {
// Don't really care about the logout
toast("Clearing notifications")
toast.info("Marking all notifications as read. This may take a while.")
fetch(`${globalUrl}/api/v1/notifications/clear`, {
credentials: "include",
method: "GET",
+21 -23
View File
@@ -64,6 +64,7 @@ import AceEditor from "react-ace";
import ace from "ace-builds";
import 'ace-builds/src-noconflict/mode-python';
import 'ace-builds/src-noconflict/mode-json';
import 'ace-builds/src-noconflict/mode-yaml';
//import 'ace-builds/src-noconflict/theme-twilight';
//import 'ace-builds/src-noconflict/theme-solarized_dark';
import 'ace-builds/src-noconflict/theme-gruvbox';
@@ -119,6 +120,7 @@ const CodeEditor = (props) => {
handleActionParamChange,
setcodedata,
isFileEditor,
isWorkflowEditor,
runUpdateText,
toolsAppId,
parameterName,
@@ -143,11 +145,6 @@ const CodeEditor = (props) => {
const [localcodedata, setlocalcodedata] = React.useState(codedata === undefined || codedata === null || codedata.length === 0 ? "" : codedata);
//const { setContainer } = useCodeMirror({
// container: editorRef.current,
// extensions,
// value: localcodedata,
//})
// const {codelang, setcodelang} = props
const {themeMode, supportEmail} = useContext(Context)
const theme = getTheme(themeMode)
@@ -1529,13 +1526,15 @@ const CodeEditor = (props) => {
// zIndex: 12501,
pointerEvents: "auto",
color: theme.palette.DialogStyle.color,
minWidth: isMobile ? "100%" : isFileEditor ? "650px" : "80%",
maxWidth: isMobile ? "100%" : isFileEditor ? "650px" : "1100px",
minHeight: isMobile ? "100%" : "auto",
maxHeight: isMobile ? "100%" : "700px",
minWidth: isMobile || isWorkflowEditor ? "100%" : isFileEditor ? "650px" : "80%",
maxWidth: isMobile || isWorkflowEditor ? "100%" : isFileEditor ? "650px" : "1100px",
minHeight: isMobile || isWorkflowEditor ? "100%" : "auto",
maxHeight: isMobile || isWorkflowEditor ? "100%" : "700px",
border: "3px solid rgba(255,255,255,0.3)",
padding: isMobile ? "25px 10px 25px 10px" : "25px",
padding: isMobile ? "25px 10px 25px 10px" : isWorkflowEditor ? "25px 10px 25px 200px" : "25px",
backgroundColor: themeMode === "dark" ? "black" : theme.palette.DialogStyle.backgroundColor,
opacity: isWorkflowEditor ? 0.93 : 1,
},
}}
>
@@ -1719,7 +1718,7 @@ const CodeEditor = (props) => {
Code Editor
</DialogTitle>
*/}
{isFileEditor ? null :
{isFileEditor || isWorkflowEditor ? null :
<div style={{ display: "flex", maxHeight: 40, }}>
<ButtonGroup style={{ borderRadius: theme.palette.borderRadius, }}>
{userdata !== undefined && userdata !== null && userdata.support === true ?
@@ -2190,15 +2189,15 @@ const CodeEditor = (props) => {
console.log("DROP: ", e)
}}
>
{(availableVariables !== undefined && availableVariables !== null && availableVariables.length > 0) || isFileEditor ? (
{(availableVariables !== undefined && availableVariables !== null && availableVariables.length > 0) || isFileEditor || isWorkflowEditor ? (
<AceEditor
id="shuffle-codeeditor"
name="shuffle-codeeditor"
value={localcodedata}
mode={selectedAction === undefined ? "json" : selectedAction.name === "execute_python" ? "python" : selectedAction.name === "execute_bash" ? "bash" : "json"}
mode={isWorkflowEditor ? "yaml" : selectedAction === undefined ? "json" : selectedAction.name === "execute_python" ? "python" : selectedAction.name === "execute_bash" ? "bash" : "json"}
theme="gruvbox"
height={isFileEditor ? 450 : 550}
width={isFileEditor ? 650 : "100%"}
height={isFileEditor ? 450 : isWorkflowEditor ? "90vh" : 550}
width={isFileEditor ? 650 : isWorkflowEditor ? "90vw" : "100%"}
markers={markers}
highlightActiveLine={false}
@@ -2259,7 +2258,7 @@ const CodeEditor = (props) => {
</div>
</div>
{isFileEditor ? null :
{isFileEditor || isWorkflowEditor ? null :
<div style={{
flex: sourceDataOpen ? 1.5 : 3,
marginLeft: 5,
@@ -2524,7 +2523,7 @@ const CodeEditor = (props) => {
</div>
<div style={{ display: 'flex' }}>
<div style={{ display: 'flex', width: isWorkflowEditor ? "90%" : "100%", }}>
<Button
style={{
height: 35,
@@ -2547,6 +2546,7 @@ const CodeEditor = (props) => {
<Button
variant="contained"
color="primary"
disabled={isWorkflowEditor}
style={{
height: 35,
flex: 1,
@@ -2554,13 +2554,11 @@ const CodeEditor = (props) => {
marginTop: 5,
}}
onClick={(event) => {
/*
const clickedFieldId = "rightside_field_" + fieldCount
const clickedField = document.getElementById(clickedFieldId)
if (clickedField !== undefined && clickedField !== null) {
clickedField.focus()
if (isWorkflowEditor === true) {
setExpansionModalOpen(false)
navigate("")
return
}
*/
if (isFileEditor !== true) {
navigate("")
+252 -2
View File
@@ -21,6 +21,9 @@ import {
DialogActions,
DialogContent,
Skeleton,
IconButton,
Modal,
Checkbox,
} from "@mui/material";
import {
@@ -32,6 +35,8 @@ import {
Business as BusinessIcon,
Flag,
ArrowDropDown as ArrowDropDownIcon,
VisibilityOff,
Visibility,
} from "@mui/icons-material";
@@ -66,7 +71,8 @@ const TenantsTab = memo((props) => {
const itemColor = "black";
const { themeMode, supportEmail, brandColor } = useContext(Context);
const theme = getTheme(themeMode, brandColor);
const [accountDeleteButtonClicked, setAccountDeleteButtonClicked] = useState(false);
const [selectedSuborg, setSelectedSuborg] = useState(null);
useEffect(() => {
if(parentOrg !== null && parentOrgFlag === null) {
let regiontag = "UK";
@@ -594,6 +600,221 @@ const TenantsTab = memo((props) => {
});
};
const DeleteAccountPopUp = () => {
const [userDeleteAccepted, setUserDeleteAccepted] = useState(false);
const [password, setPassword] = useState("");
const [showPassword, setShowPassword] = useState(false);
const [disabled, setDisabled] = useState(true);
const [open, setOpen] = useState(true);
const boxStyling = {
position: "relative",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
zIndex: "9999",
backgroundColor: theme.palette.backgroundColor,
color: theme.palette.text.primary,
padding: 20,
borderRadius: 5,
boxShadow: "0 0 10px rgba(0, 0, 0, 0.3)",
width: 430,
height: 430,
};
const closeIconButtonStyling = {
color: theme.palette.text.primary,
border: "none",
backgroundColor: "transparent",
position: 'relative',
width: 20,
height: 20,
cursor: "pointer",
left: "calc(100% - 30px)",
};
const handlePasswordVisibility = () => {
setShowPassword(!showPassword);
};
const buttonStyle = {
marginTop: 20,
height: 50,
border: "none",
width: "100%",
fontSize: 16,
backgroundColor: disabled ? "gray" : "red",
color: theme.palette.text.primary,
cursor: disabled === false && "pointer",
};
const handlePasswordChange = (e) => {
setPassword(e.target.value);
};
const handleCheckBoxEvent = () => {
setUserDeleteAccepted((prev) => !prev);
};
useEffect(() => {
if (password.length > 8 && userDeleteAccepted) {
setDisabled(false);
} else {
setDisabled(true);
}
}, [password, userDeleteAccepted]);
const handleDeleteAccount = () => {
const baseURL = globalUrl;
const url = `${baseURL}/api/v1/orgs/${selectedOrganization?.id}`;
const data = {
suborg_id : selectedSuborg?.id,
password: password,
}
fetch(url, {
mode: "cors",
method: "DELETE",
body: JSON.stringify(data),
credentials: "include",
crossDomain: true,
withCredentials: true,
headers: {
"Content-Type": "application/json",
},
})
.then((response) => response.json())
.then((data) => {
if (data.success) {
toast.success(
"Suborg deleted"
);
handleGetSubOrgs(userdata.active_org.id);
setAccountDeleteButtonClicked(false);
} else {
if (data.reason) {
toast.error(data.reason);
}else {
toast.error("Failed to delete suborg. Please try again or contact support@shuffler.io for help.");
}
}
})
.catch((error) => {
console.error(
"There was a problem with your fetch operation:",
error
);
});
};
return (
<Modal open= {open} >
<div style={boxStyling}>
<IconButton
style={closeIconButtonStyling}
onClick={() => {
setAccountDeleteButtonClicked(false);
setSelectedSuborg(null);
}}
>
<CloseIcon />
</IconButton>
<h1 style={{textAlign:"center", margin : "0 0 20px 0"}}>Sub-Organization</h1>
{/* <div style={{paddingLeft: 15}} > */}
<div
style={{
display: "flex",
flexDirection: "column",
textAlign: "left",
// paddingLeft: 15
// marginLeft: 20
}}
>
<label>If you delete your suborg then:</label>
<ul style={{ textAlign: "left" }}>
<li>
<label>
Your suborg information will be removed from our Database
permanently.
</label>
</li>
<li>
<label>This action cannot be reversed.</label>
</li>
</ul>
<div
style={{
display: "flex",
flexDirection: "row",
alignItems: "center",
gap: 10,
}}
>
<Checkbox
checked={userDeleteAccepted}
onChange={handleCheckBoxEvent}
size="medium"
/>
<label style={{ fontSize: "16px", color: theme.palette.text.primary }}>
I have read the above information and I agree to it completely
</label>
</div>
<div
style={{
marginTop: 20,
display: "flex",
flexDirection: "column",
gap: 8,
}}
>
<label>Enter password to delete suborg {" " + selectedSuborg?.name}</label>
<TextField
style={{
backgroundColor: theme.palette.inputColor,
flex: "1",
}}
type={showPassword ? "text" : "password"}
value={password}
onChange={handlePasswordChange}
required
placeholder="Password"
id="standard-required"
autoComplete="off"
InputProps={{
style: {
color: theme.palette.textFieldStyle.color,
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
height: "45px",
fontSize: "1em",
borderRadius: theme.palette.textFieldStyle.borderRadius,
},
endAdornment: (
<IconButton
onClick={handlePasswordVisibility}
style={{color:theme.palette.text.primary}}
>
{showPassword ? <VisibilityOff /> : <Visibility />}
</IconButton>
),
}}
/>
</div>
<button
style={buttonStyle}
disabled={disabled}
onClick={handleDeleteAccount}
>
Delete Suborg
</button>
</div>
</div>
</Modal>
);
};
const modalView = (
<Dialog
open={modalOpen}
@@ -795,6 +1016,7 @@ const TenantsTab = 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: theme.palette.defaultBorder,}}>
{modalView}
{cloudSyncModal}
{accountDeleteButtonClicked && <DeleteAccountPopUp/>}
<div style={{height: "100%", maxHeight: 1700, overflowY: "auto",overflowX: 'hidden', scrollbarColor: theme.palette.scrollbarColorTransparent, scrollbarWidth: 'thin'}}>
<div style={{ height: "100%", width: "calc(100% - 20px)", scrollbarColor: theme.palette.scrollbarColorTransparent, scrollbarWidth: 'thin' }}>
<div style={{ marginBottom: 20 }}>
@@ -1340,7 +1562,8 @@ const TenantsTab = memo((props) => {
<ListItemText
primary={
<Tooltip title={data.id === userdata?.active_org?.id ? "You are already in this organization." : ""} disableInteractive>
<>
<Tooltip title={data.id === userdata?.active_org?.id ? "You are already in this organization." : ""} disableInteractive>
<Button
color="primary"
variant='outlined'
@@ -1359,7 +1582,34 @@ const TenantsTab = memo((props) => {
>
Change Active Org
</Button>
</Tooltip>
{selectedOrganization?.creator_org?.length > 0 ? null :
<Button
variant="outlined"
sx={{
whiteSpace: "nowrap",
textTransform: "none",
fontSize: "16px",
color: "red",
marginLeft: "5px",
borderColor: "red",
"&:hover": {
color: theme.palette.text.primary,
backgroundColor: "red",
},
}}
disabled={data?.id === userdata?.active_org?.id}
onClick={() => {
setAccountDeleteButtonClicked(true);
setSelectedSuborg(data);
}}
>
Delete Org
</Button>}
</>
}
style={{ display: "table-cell", verticalAlign: "middle" }}
/>
+1 -26
View File
@@ -1458,33 +1458,8 @@ const UserManagmentTab = memo((props) => {
);
}
const getRegionFlag = (region_url) => {
let regiontag = "UK";
let regionCode = "gb";
const regionsplit = region_url?.split(".");
if (regionsplit?.length > 2 && !regionsplit[0]?.includes("shuffler")) {
const namesplit = regionsplit[0]?.split("/");
regiontag = namesplit[namesplit?.length - 1];
if (regiontag === "california") {
regiontag = "US";
regionCode = "us";
} else if (regiontag === "frankfurt") {
regiontag = "EU-2";
regionCode = "eu";
} else if (regiontag === "ca") {
regiontag = "CA";
regionCode = "ca";
}else if (regiontag === "au") {
regiontag = "AUS";
regionCode = "au"
}
}
return regionCode;
};
const userRegion = data?.user_geo_info?.country?.iso_code?.length > 0 ? data?.user_geo_info?.country?.iso_code : selectedOrganization?.region_url?.length > 0 ? getRegionFlag(selectedOrganization?.region_url) : "eu";
const userRegion = data?.user_geo_info?.country?.iso_code
return (
<ListItem key={index} style={{ backgroundColor: bgColor, display: 'table-row', borderBottomLeftRadius: users?.length - 1 === index ? 8 : 0, borderBottomRightRadius: users?.length - 1 === index ? 8 : 0 }}>
+4
View File
@@ -181,6 +181,10 @@ export const getTheme = (themeMode, brandColor) =>
cardBackgroundColor: themeMode === "dark" ? "#1e1e1e" : "#eaeaea",
cardHoverColor: themeMode === "dark" ? "#323232" : "#F0F0F0",
hoverColor: themeMode === "dark" ? "#323232" : "#D6D6D6",
usecaseCardColor: themeMode === "dark" ? "#2f2f2f" : "rgba(245, 245, 245, 1)",
usecaseCardHoverColor: themeMode === "dark" ? "#2F2F2F" : "rgba(245, 245, 245, 1)",
usecaseDialogFieldColor: themeMode === "dark" ? "#2B2B2B" : "#F5F5F5",
accentColor: themeMode === "dark" ? "#ff8544" : "#ff8544",
green: themeMode === "dark" ? "#5cc879" : "#008000",
defaultBorder: themeMode === "dark" ? '1px solid #494949' : '1px solid #CCCCCC',
linkColor: brandColor === "#ff8544" ? "#f86a3e" : brandColor,
+2 -2
View File
@@ -287,8 +287,8 @@ const Admin2 = (props) => {
const handleStatusChange = (event) => {
const { value } = event.target;
setSelectedStatus(value);
const { value } = event.target
setSelectedStatus(value)
handleEditOrg(
selectedOrganization?.name,
+334 -119
View File
@@ -6,6 +6,7 @@ import { getTheme } from "../theme.jsx";
import { useInterval } from "react-powerhooks";
import { makeStyles, } from "@mui/styles";
import YAML from "yaml";
import WorkflowTemplatePopup from "../components/WorkflowTemplatePopup.jsx"
import { v4 as uuidv4, v5 as uuidv5, validate as isUUID, } from "uuid";
import { useNavigate, Link, useParams } from "react-router-dom";
@@ -132,6 +133,7 @@ import {
ArrowForward as ArrowForwardIcon,
OpenInFull as OpenInFullIcon,
Difference as DifferenceIcon,
DataObject as DataObjectIcon,
} from "@mui/icons-material";
import SwapHorizIcon from '@mui/icons-material/SwapHoriz';
//import * as cytoscape from "cytoscape";
@@ -778,9 +780,9 @@ const AngularWorkflow = (defaultprops) => {
"name": "action",
"value": "list_tickets",
"options": [
"create_ticket",
"list_tickets",
"get_ticket",
"create_ticket",
"close_ticket",
"add_comment",
"update_ticket",
@@ -794,16 +796,6 @@ const AngularWorkflow = (defaultprops) => {
"required": false,
"multiline": true,
},
/*{
"name": "options",
"value": "deduplicate,enrich",
"required": false,
"multiselect": true,
"options": [
"deduplicate",
"enrich",
]
}*/
]
}, {
"name": "Communication",
@@ -813,9 +805,9 @@ const AngularWorkflow = (defaultprops) => {
"name": "action",
"value": "list_messages",
"options": [
"send_message",
"list_messages",
"get_message",
"send_message",
"search_messages",
"list_attachments",
"get_attachment",
@@ -838,15 +830,15 @@ const AngularWorkflow = (defaultprops) => {
"parameters": [{
"name": "action",
"value": "get_asset",
"options": [
"reset_password",
"enable_user",
"disable_user",
"get_identity",
"get_asset",
"search_identity"
],
"required": true,
"options": [
"reset_password",
"enable_user",
"disable_user",
"get_identity",
"get_asset",
"search_identity"
],
"required": true,
},
{
"name": "fields",
@@ -861,9 +853,9 @@ const AngularWorkflow = (defaultprops) => {
"label": "Assets",
"parameters": [{
"name": "action",
"value": "search_vulnerabilities",
"value": "list_assets",
"options": [
"list_assets",
"list_assets",
"get_asset",
"search_assets",
"search_users",
@@ -915,8 +907,8 @@ const AngularWorkflow = (defaultprops) => {
"value": "get_ioc",
"options": [
"get_ioc",
"search_ioc",
"create_ioc",
"search_ioc",
"update_ioc",
"delete_ioc"
],
@@ -975,28 +967,9 @@ const AngularWorkflow = (defaultprops) => {
"multiline": true,
}]
},
]
}]
/*
{
"name": "Email",
"label": "Email",
"parameters": [{
"name": "action",
"value": "list_email",
"options": [
"list_email",
"send_mail",
],
"required": true,
}],
}]
}]
*/
// For code editor
const [codeEditorModalOpen, setCodeEditorModalOpen] = React.useState(false);
const [codedata, setcodedata] = React.useState("");
@@ -1213,6 +1186,7 @@ const AngularWorkflow = (defaultprops) => {
if (workflow?.suborg_distribution !== undefined && workflow?.suborg_distribution !== null && workflow?.suborg_distribution.length > 0) {
getChildWorkflows(workflow.id)
}
}, [workflow]);
// Event for making sure app is correct
@@ -2506,9 +2480,11 @@ const AngularWorkflow = (defaultprops) => {
console.log("Should redirect to register with redirect.")
setTimeout(() => {
toast("You may not have access to this workflow.")
//window.location.href = `/register?view=/workflows/${props.match.params.key}&message=You need sign up to use workflows with Shuffle`
window.location.href = `/workflows`
// toast("You may not have access to this workflow.")
localStorage.setItem("redirectId", props.match.params.key)
navigate("/register?view=workflows&message=You need sign up to use workflows with Shuffle");
// window.location.href = `/register?view=/workflows/${props.match.params.key}&message=You need sign up to use workflows with Shuffle`
// window.location.href = `/workflows`
}, 2500)
return
@@ -2878,9 +2854,9 @@ const AngularWorkflow = (defaultprops) => {
setSavingState(0);
console.log("Workflow failed loading: ", responseJson);
if (responseJson.reason !== undefined && responseJson.reason !== null) {
toast("Failed to save: " + responseJson.reason);
toast.error("Failed to save: " + responseJson.reason);
} else {
toast(`Failed to save. Please contact your ${supportEmail} or your local admin if this is unexpected.`)
toast.error(`Failed to save. Please contact your ${supportEmail} or your local admin if this is unexpected.`)
}
} else {
setSavingState(1);
@@ -4590,6 +4566,9 @@ const AngularWorkflow = (defaultprops) => {
}
setTimeout(() => {
if(!isLoggedIn){
localStorage.setItem("redirectId", props.match.params.key);
}
window.location.pathname = "/workflows";
}, 2500);
@@ -4880,6 +4859,17 @@ const AngularWorkflow = (defaultprops) => {
setOriginalWorkflow(responseJson)
}
const tmpUi = new URLSearchParams(cursearch).get("ui");
if (
tmpUi !== undefined &&
tmpUi !== null &&
tmpUi === "yaml"
) {
setTimeout(() => {
setupWorkflowYaml(responseJson)
}, 2500)
}
setWorkflow(responseJson);
setWorkflowDone(true);
@@ -4898,6 +4888,9 @@ const AngularWorkflow = (defaultprops) => {
setConfigureWorkflowModalOpen(true)
}
}
})
.catch((error) => {
@@ -9959,6 +9952,191 @@ const AngularWorkflow = (defaultprops) => {
});
}
const setupWorkflowYaml = (inputworkflow) => {
toast.warn("YAML exploring is an experimental feature - only visible to support users. The goal of this is to make it EASY to edit the workflow as YAML instead of just using the UI")
if (userdata?.support !== true) {
console.log("Not support: ", userdata)
return
}
// This should be getting the workflow based on actual nodes in the workflow
// as the goal is to have it be live
var copiedWorkflow = JSON.parse(JSON.stringify(inputworkflow))
const removeObjects = [
"execution_org",
"categories",
"example_argument",
"public",
"contact_info",
"published_id",
"revision_id",
"usecase_ids",
"input_questions",
"form_control",
"blogpost",
"video",
"status",
"generated",
"hidden",
"updated_by",
"validated",
"validation",
"childorg_workflow_ids",
"backup_config",
"auth_groups",
"isValid",
"workflow_as_code",
"image",
"sharing",
"owner",
"configuration",
"created",
"edited",
"last_runtime",
"due_date",
"is_valid",
"execution_environment",
"default_return_value",
"visual_branches",
"previously_saved",
"workflow_type",
"parentorg_workflow",
"suborg_distribution",
"id",
"comments",
"org_id",
// Failing test
"spalabi",
]
for (var i = 0; i < removeObjects.length; i++) {
delete copiedWorkflow[removeObjects[i]]
}
const removeActionValues = [
"_id",
"id",
"large_image",
"description",
"is_valid",
"isStartNode",
"sharing",
"public",
"generated",
"execution_variable",
"position",
"category",
"reference_url",
"sub_action",
"run_magic_output",
"run_magic_input",
"category_label",
"suggestions",
"parent_controlled",
"source_workflow",
"source_executions",
"app_association",
"suggestion",
"small_image",
"long_description",
"tags",
"errors",
"source_executions",
"source_execution",
"required",
"example",
"type",
"test2",
]
const removeActionParamValues = [
"id",
"multiline",
"multiselect",
"options",
"action_field",
"variant",
"configuration",
"tags",
"schema",
"skip_multicheck",
"value_replace",
"unique_toggled",
"hidden",
"error",
"example",
"test3",
]
if (copiedWorkflow?.actions !== undefined && copiedWorkflow?.actions !== null && copiedWorkflow?.actions.length > 0) {
for (var key in copiedWorkflow.actions) {
for (var i = 0; i < removeActionValues.length; i++) {
delete copiedWorkflow.actions[key][removeActionValues[i]]
}
if (copiedWorkflow.actions[key].parameters === undefined || copiedWorkflow.actions[key].parameters === null) {
continue
}
for (var j = 0; j < copiedWorkflow.actions[key].parameters.length; j++) {
for (var k = 0; k < removeActionParamValues.length; k++) {
delete copiedWorkflow.actions[key].parameters[j][removeActionParamValues[k]]
}
}
}
}
if (copiedWorkflow?.triggers !== undefined && copiedWorkflow?.triggers !== null && copiedWorkflow?.triggers.length > 0) {
for (var key in copiedWorkflow.triggers) {
for (var i = 0; i < removeActionValues.length; i++) {
delete copiedWorkflow.triggers[key][removeActionValues[i]]
}
delete copiedWorkflow.triggers[key]["app_name"]
delete copiedWorkflow.triggers[key]["app_version"]
delete copiedWorkflow.triggers[key]["name"]
delete copiedWorkflow.triggers[key]["priority"]
delete copiedWorkflow.triggers[key]["replacement_for_trigger"]
if (copiedWorkflow.triggers[key].parameters === undefined || copiedWorkflow.triggers[key].parameters === null) {
continue
}
for (var j = 0; j < copiedWorkflow.triggers[key].parameters.length; j++) {
for (var k = 0; k < removeActionParamValues.length; k++) {
delete copiedWorkflow.triggers[key].parameters[j][removeActionParamValues[k]]
}
}
}
}
if (copiedWorkflow?.workflow_variables === undefined || copiedWorkflow?.workflow_variables === null || copiedWorkflow?.workflow_variables.length === 0) {
delete copiedWorkflow.workflow_variables
}
if (copiedWorkflow?.branches === undefined || copiedWorkflow?.branches === null || copiedWorkflow?.branches.length === 0) {
delete copiedWorkflow.branches
}
// YAML
const sampledata = YAML.stringify(copiedWorkflow)
navigate("?ui=yaml", { replace: true })
setCodeEditorModalOpen(true)
setEditorData({
"name": "workflow yaml",
"value": sampledata,
})
}
// eslint-disable-next-line react-hooks/exhaustive-deps
//useEffect(() => {
if (firstrequest) {
@@ -10000,6 +10178,7 @@ const AngularWorkflow = (defaultprops) => {
//navigate(`?execution_highlight=${parsed_url}`)
//props.history.push(curpath + newitem);
}
return;
}
@@ -12136,10 +12315,8 @@ const AngularWorkflow = (defaultprops) => {
}
if ((app.id === "integration" || app.id === "shuffle_agent") && userdata.support !== true) {
console.log("APPID: ", app.id, isCloud)
if (isCloud === false && app.id === "integration") {
} else {
console.log("RETURNING", app.id)
return null
}
}
@@ -12572,7 +12749,7 @@ const AngularWorkflow = (defaultprops) => {
// maxiter = max amount of parent nodes to loop
// also handles breaks if there are issues
var iterations = 0;
var maxiter = 10;
var maxiter = 100;
while (true) {
for (let parentkey in allkeys) {
var currentnode = cy.getElementById(allkeys[parentkey]);
@@ -18553,6 +18730,68 @@ const AngularWorkflow = (defaultprops) => {
zoom: isSafari ? undefined : 0.9,
}
const changeOrg = () => {
localStorage.setItem("globalUrl", "");
localStorage.setItem("getting_started_sidebar", "open");
fetch(`${globalUrl}/api/v1/orgs/${workflow.org_id}/change`, {
mode: "cors",
credentials: "include",
crossDomain: true,
method: "POST",
body: JSON.stringify({ "org_id": workflow.org_id }),
withCredentials: true,
headers: {
"Content-Type": "application/json; charset=utf-8",
},
})
.then(function (response) {
if (response.status !== 200) {
console.log("Error in response");
} else {
localStorage.removeItem("apps")
localStorage.removeItem("workflows")
localStorage.removeItem("userinfo")
}
return response.json();
})
.then(function (responseJson) {
console.log("In here?")
if (responseJson.success === true) {
if (responseJson.region_url !== undefined && responseJson.region_url !== null && responseJson.region_url.length > 0) {
console.log("Region Change: ", responseJson.region_url);
localStorage.setItem("globalUrl", responseJson.region_url);
//globalUrl = responseJson.region_url
}
if (responseJson["reason"] === "SSO_REDIRECT") {
setTimeout(() => {
toast.info("Redirecting to SSO login page as SSO is required for this organization.")
window.location.href = responseJson["url"]
return
}, 2000)
} else {
setTimeout(() => {
window.location.reload();
}, 2000);
}
toast.info("Successfully changed active organisation - refreshing!");
} else {
if (responseJson.reason !== undefined && responseJson.reason !== null && responseJson.reason.length > 0) {
toast(responseJson.reason);
} else {
toast(`Failed changing org. Try again or contact ${supportEmail} if this persists.`);
}
}
})
.catch((error) => {
console.log("error changing: ", error);
//removeCookie("session_token", {path: "/"})
})
}
const TopCytoscapeBar = (props) => {
const [hovered, setHovered] = useState(false)
@@ -18620,72 +18859,15 @@ const AngularWorkflow = (defaultprops) => {
</Tooltip>
)}
{!distributedFromParent ?
isCorrectOrg ? null :
{!distributedFromParent || userdata?.support === true ?
isCorrectOrg ? null :
<Typography variant="body2" style={{ marginLeft: 10, }}>
<b>Warning</b>: <span
style={{ color: "#FF8544", cursor: "pointer", pointerEvents: "auto", }}
onClick={() => {
toast("Changing to correct organisation. Please wait a few seconds.")
localStorage.setItem("globalUrl", "");
localStorage.setItem("getting_started_sidebar", "open");
fetch(`${globalUrl}/api/v1/orgs/${workflow.org_id}/change`, {
mode: "cors",
credentials: "include",
crossDomain: true,
method: "POST",
body: JSON.stringify({ "org_id": workflow.org_id }),
withCredentials: true,
headers: {
"Content-Type": "application/json; charset=utf-8",
},
})
.then(function (response) {
if (response.status !== 200) {
console.log("Error in response");
} else {
localStorage.removeItem("apps")
localStorage.removeItem("workflows")
localStorage.removeItem("userinfo")
}
return response.json();
})
.then(function (responseJson) {
console.log("In here?")
if (responseJson.success === true) {
if (responseJson.region_url !== undefined && responseJson.region_url !== null && responseJson.region_url.length > 0) {
console.log("Region Change: ", responseJson.region_url);
localStorage.setItem("globalUrl", responseJson.region_url);
//globalUrl = responseJson.region_url
}
if (responseJson["reason"] === "SSO_REDIRECT") {
setTimeout(() => {
toast.info("Redirecting to SSO login page as SSO is required for this organization.")
window.location.href = responseJson["url"]
return
}, 2000)
} else {
setTimeout(() => {
window.location.reload();
}, 2000);
}
toast("Successfully changed active organisation - refreshing!");
} else {
if (responseJson.reason !== undefined && responseJson.reason !== null && responseJson.reason.length > 0) {
toast(responseJson.reason);
} else {
toast(`Failed changing org. Try again or contact ${supportEmail} if this persists.`);
}
}
})
.catch((error) => {
console.log("error changing: ", error);
//removeCookie("session_token", {path: "/"})
})
changeOrg()
@@ -18707,6 +18889,11 @@ const AngularWorkflow = (defaultprops) => {
<div>
{originalWorkflow?.parentorg_workflow !== undefined && originalWorkflow?.parentorg_workflow !== null && originalWorkflow?.parentorg_workflow.length > 0 || workflow?.parentorg_workflow !== undefined && workflow?.parentorg_workflow !== null && workflow?.parentorg_workflow.length > 0 ?
null
/*
* Disabled because redirects occurred anyway
<Button
color="secondary"
variant="outlined"
@@ -18717,17 +18904,20 @@ const AngularWorkflow = (defaultprops) => {
marginLeft: 10,
}}
onClick={() => {
navigate(`/workflows/${workflow.parentorg_workflow}`)
//changeOrg()
// Reload the page
navigate(`/workflows/${workflow.parentorg_workflow}`)
window.location.reload()
}}
>
Go to parent org workflow
</Button>
*/
: null}
{userdata !== undefined && userdata !== null && userdata.orgs !== undefined && userdata.orgs !== null && userdata.orgs.length > 1 && workflow?.id !== undefined && workflow?.id && workflow?.id?.length > 0 ?
{userdata !== undefined && userdata !== null && userdata.orgs !== undefined && userdata.orgs !== null && userdata.orgs.length > 1 && workflow?.id !== undefined && workflow?.id && workflow?.id?.length > 0 && userdata?.active_org?.creator_org?.length === 0 && userdata?.active_org?.id == workflow?.org_id ?
<Button
color="secondary"
variant="outlined"
@@ -19575,7 +19765,7 @@ const AngularWorkflow = (defaultprops) => {
>
<Tooltip
title="Hide error messages. They will show up the next refresh."
title="Hides error messages this session."
placement="top"
>
<IconButton
@@ -19924,7 +20114,7 @@ const AngularWorkflow = (defaultprops) => {
.catch((error) => {
console.log("Dupe workflow for suborg error: ", error.toString())
})
}
}
const BottomCytoscapeBar = () => {
if (workflow.id === undefined || workflow.id === null || (!workflow.public && apps.length === 0)) {
@@ -20277,6 +20467,29 @@ const AngularWorkflow = (defaultprops) => {
</Button>
</span>
</Tooltip>
{userdata?.support === true ?
<Tooltip
color="secondary"
title="Show simplified YAML (support only)"
placement="top"
>
<span>
<Button
disabled={workflow.public}
color="secondary"
variant="text"
style={{ width: 65, height: buttonHeights, }}
onClick={() => {
setupWorkflowYaml(workflow)
}}
>
<DataObjectIcon />
</Button>
</span>
</Tooltip>
: null}
</ButtonGroup>
@@ -20408,10 +20621,11 @@ const AngularWorkflow = (defaultprops) => {
position,
backgroundcolor: "#1f2023",
color: "#ffffff",
textHalign: "center",
textValign: "center",
textMarginX: "0px",
textMarginY: "0px",
textHalign: "right",
textValign: "bottom",
textMarginX: "-250px",
textMarginY: "-150px",
},
position,
});
@@ -25509,6 +25723,7 @@ const AngularWorkflow = (defaultprops) => {
setAiQueryModalOpen={setAiQueryModalOpen}
isWorkflowEditor={editorData?.name === "workflow yaml"}
/>
: null}
+7 -2
View File
@@ -745,7 +745,7 @@ const AppCreator = (defaultprops) => {
}
setBasedata(data);
console.log("Info: ", data)
console.log("Loaded Info: ", data)
try {
if (data.info !== null && data.info !== undefined) {
@@ -759,7 +759,12 @@ const AppCreator = (defaultprops) => {
if (data.info.title.length > 29) {
setName(data.info.title.slice(0, 29));
} else {
setName(data.info.title);
// Check if fork=true in URL
if (window.location.search.includes("fork=true")) {
setName(data.info.title + " Fork");
} else {
setName(data.info.title)
}
}
}
+2 -2
View File
@@ -4066,7 +4066,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
</Button>
}
{isMobile ? null : (
{isMobile || app?.reference_org === userdata?.active_org?.id ? null : (
<Button
variant={userdata.active_apps !== undefined && userdata.active_apps !== null && userdata.active_apps.includes(appId) ? "outlined": "contained"}
component="label"
@@ -4142,7 +4142,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
component="label"
color="secondary"
onClick={() => {
navigate(`/apps/new?id=${appId}`)
navigate(`/apps/new?id=${appId}&fork=true`)
}}
style={{ height: 40, marginTop: 5, marginLeft: 5, }}
>
+3 -3
View File
@@ -154,7 +154,7 @@ const AppCard = ({ data, index, mouseHoverIndex, setMouseHoverIndex, globalUrl,
whiteSpace: "nowrap",
color: theme.palette.text.primary,
}}>
{data.name.replace(/_/g, ' ').replace(/\b\w/g, char => char.toUpperCase())}
{data?.name?.replace(/_/g, ' ')?.replace(/\b\w/g, char => char?.toUpperCase())}
</div>
</div>
@@ -165,7 +165,7 @@ const AppCard = ({ data, index, mouseHoverIndex, setMouseHoverIndex, globalUrl,
marginLeft: 8,
color: theme.palette.text.secondary,
}}>
{data.categories ? data.categories.join(", ") : "NA"}
{data?.categories ? data?.categories?.join(", ") : "NA"}
</div>
<div style={{
@@ -186,7 +186,7 @@ const AppCard = ({ data, index, mouseHoverIndex, setMouseHoverIndex, globalUrl,
whiteSpace: "nowrap",
color: "rgba(158, 158, 158, 1)",
}}>
{data.generated !== true && data.tags && data.tags.slice(0, 2).map((tag, tagIndex) => (
{data?.generated !== true && data?.tags && data?.tags?.slice(0, 2).map((tag, tagIndex) => (
<span key={tagIndex}>
{tag}
{tagIndex < data.tags.length - 1 ? ", " : ""}
+79 -59
View File
@@ -447,20 +447,27 @@ const LoginPage = props => {
}
if (isLoggedIn === true && serverside !== true) {
const tmpView = new URLSearchParams(window.location.search).get("view");
if (tmpView !== undefined && tmpView !== null) {
let pathOnly = tmpView.split("?")[0];
if (!pathOnly.startsWith("/")) pathOnly = "/" + pathOnly;
setTimeout(() => {
const tmpView = new URLSearchParams(window.location.search).get("view");
if (tmpView !== undefined && tmpView !== null) {
let pathOnly = tmpView.split("?")[0];
if (!pathOnly.startsWith("/")) pathOnly = "/" + pathOnly;
if (pathOnly === "/pricing" || pathOnly === "admin") {
window.location.replace(pathOnly + window.location.search);
} else {
window.location.replace(pathOnly);
if (pathOnly === "/pricing" || pathOnly === "admin") {
window.location.replace(pathOnly + window.location.search);
} else {
if(localStorage.getItem("redirectId") !== null && localStorage.getItem("redirectId") !== undefined) {
const redirectId = localStorage.getItem("redirectId")
localStorage.removeItem("redirectId")
pathOnly = pathOnly + "/" + redirectId
}
window.location.replace(pathOnly);
}
return;
}
return;
}
window.location.pathname = "/workflows"
window.location.pathname = "/workflows"
}, 2000);
}
const checkAdmin = () => {
@@ -537,51 +544,52 @@ const LoginPage = props => {
'Content-Type': 'application/json; charset=utf-8',
},
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for login:O!");
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for login:O!");
}
return response.json();
})
.then((responseJson) => {
setLoginLoading(false)
if (responseJson["success"] === false) {
setLoginInfo(responseJson["reason"])
} else {
if (responseJson?.region_url !== undefined && responseJson?.region_url !== null && responseJson?.region_url !== "") {
toast.info("Set region to " + responseJson.region_url)
localStorage.setItem("globalUrl", responseJson.region_url)
}
return response.json();
})
.then((responseJson) => {
if (responseJson["reason"] === "MFA_REDIRECT") {
setLoginInfo("Enter the 6-digit MFA code.")
setMFAField(true)
return
setLoginLoading(false)
console.log("Resp from backend: ", responseJson)
if (responseJson["success"] === false) {
}
else if (responseJson["reason"] === "MFA_SETUP") {
window.location.href = `/login/${responseJson.url}/mfa-setup`;
return;
}
else if (responseJson["reason"] === "SSO_REDIRECT") {
//navigate(responseJson["url"])
window.location.href = responseJson["url"]
return
}
else if (responseJson["reason"] !== undefined && responseJson["reason"] !== null && responseJson["reason"].includes("error")) {
setLoginInfo(responseJson["reason"])
return
}
else {
if (responseJson["reason"] === "MFA_REDIRECT") {
setLoginInfo("Enter the 6-digit MFA code.")
setMFAField(true)
return
}
else if (responseJson["reason"] === "MFA_SETUP") {
window.location.href = `/login/${responseJson.url}/mfa-setup`;
return;
}
else if (responseJson["reason"] === "SSO_REDIRECT") {
//navigate(responseJson["url"])
window.location.href = responseJson["url"]
return
}
else if (responseJson["reason"] !== undefined && responseJson["reason"] !== null && responseJson["reason"].includes("error")) {
setLoginInfo(responseJson["reason"])
return
}
setLoginInfo("Successful login! Redirecting you in 3 seconds...")
for (var key in responseJson["cookies"]) {
setCookie(responseJson["cookies"][key].key, responseJson["cookies"][key].value, { path: "/" })
}
setLoginInfo("Successful login! Redirecting you in 3 seconds...")
for (var key in responseJson["cookies"]) {
setCookie(responseJson["cookies"][key].key, responseJson["cookies"][key].value, { path: "/" })
}
setTimeout(() => {
const tmpView = new URLSearchParams(window.location.search).get("view");
if (tmpView !== undefined && tmpView !== null) {
let pathOnly = tmpView.split("?")[0];
@@ -590,18 +598,21 @@ const LoginPage = props => {
if (pathOnly === "/pricing" || pathOnly === "admin") {
window.location.replace(pathOnly + window.location.search);
} else {
if(localStorage.getItem("redirectId") !== null && localStorage.getItem("redirectId") !== undefined) {
const redirectId = localStorage.getItem("redirectId")
localStorage.removeItem("redirectId")
pathOnly = pathOnly + "/" + redirectId
}
window.location.replace(pathOnly);
}
return;
}
console.log("LOGIN DATA: ", responseJson)
if (responseJson.tutorials !== undefined && responseJson.tutorials !== null) {
// Find welcome in responseJson.tutorials under key name
const welcome = responseJson.tutorials.find(function (element) {
return element.name === "welcome";
})
console.log("Welcome: ", welcome)
if (welcome === undefined || welcome === null) {
console.log("RUN login Welcome!!")
@@ -613,12 +624,13 @@ const LoginPage = props => {
}
window.location.pathname = "/workflows"
}
})
.catch(error => {
setLoginInfo("Error from login API: " + error)
setLoginLoading(false)
});
}, 2000);
}
})
.catch(error => {
setLoginInfo("Error from login API: " + error)
setLoginLoading(false)
});
} else {
url = baseurl + '/api/v1/register';
fetch(url, {
@@ -641,11 +653,15 @@ const LoginPage = props => {
//setLoginInfo("Successful register!")
//var newpath = "/login?message=Successfully signed up. You can now sign in."
//const tmpMessage = new URLSearchParams(window.location.search).get("message")
setLoginInfo("Successful registration! Redirecting in 3 seconds...")
for (var key in responseJson["cookies"]) {
setCookie(responseJson["cookies"][key].key, responseJson["cookies"][key].value, { path: "/" })
}
setLoginLoading(false)
setLoginInfo("Successful registration! Redirecting in 3 seconds...")
setTimeout(() => {
console.log("LOGIN DATA: ", responseJson)
@@ -657,6 +673,11 @@ const LoginPage = props => {
if (pathOnly === "/pricing" || pathOnly === "admin") {
window.location.replace(pathOnly + window.location.search);
} else {
if(localStorage.getItem("redirectId") !== null && localStorage.getItem("redirectId") !== undefined) {
const redirectId = localStorage.getItem("redirectId")
localStorage.removeItem("redirectId")
pathOnly = pathOnly + "/" + redirectId
}
window.location.replace(pathOnly);
}
return
@@ -666,9 +687,8 @@ const LoginPage = props => {
console.log("RUN Welcome!!")
//window.location.pathname = "/welcome?tab=2"
window.location.href = "/welcome"
}, 1500);
}, 2000);
}
setLoginLoading(false)
}),
)
.catch(error => {
+147 -66
View File
@@ -3,6 +3,7 @@ import React, { useEffect, useContext, memo, useState, useRef } from "react";
import { useLocation, useNavigate, Link } from "react-router-dom";
import ReactDOM from "react-dom"
import { getTheme } from "../theme.jsx";
// Material UI Icons
import Add from '@mui/icons-material/Add';
import Search from '@mui/icons-material/Search';
@@ -12,6 +13,7 @@ import GridOnIcon from '@mui/icons-material/GridOn';
import ListIcon from '@mui/icons-material/List';
import PublishIcon from '@mui/icons-material/Publish';
import GetAppIcon from '@mui/icons-material/GetApp';
// Material UI & Components
import { makeStyles } from "@mui/styles";
import { Navigate } from "react-router-dom";
@@ -92,6 +94,11 @@ import {
Visibility as VisibilityIcon,
EditNote as EditNoteIcon,
ErrorOutline as ErrorOutlineIcon,
Coronavirus as CoronavirusIcon,
Fingerprint as FingerprintIcon,
Psychology as PsychologyIcon,
Wifi as WifiIcon,
Devices as DevicesIcon,
} from "@mui/icons-material";
// Additional Components
@@ -128,19 +135,20 @@ const imagesize = 22;
export const GetIconInfo = (action) => {
// Finds the icon based on the action. Should be verbs.
const iconList = [
{ key: "cases", values: ["cases"] },
{ key: "cases", values: ["cases", "ticket", "alert"] },
{ key: "cache_add", values: ["set_cache"] },
{ key: "cache_get", values: ["get_cache"] },
{ key: "filter", values: ["filter"] },
{ key: "merge", values: ["join", "merge", "route", "router"] },
{
key: "search",
values: ["search", "find", "locate", "index", "analyze", "anal", "match", "check cache", "check", "verify", "validate"],
values: ["search", "find", "locate", "index", "analyze", "anal", "match", "check cache", "check", "verify", "validate", "siem", ],
},
{ key: "list", values: ["list", "head", "options"] },
{
key: "download",
values: [
"ingest",
"capture",
"get",
"download",
@@ -154,20 +162,6 @@ export const GetIconInfo = (action) => {
},
{ key: "add", values: ["add", "accept",] },
{ key: "delete", values: ["delete", "remove", "clear", "clean", "dismiss",] },
{
key: "send",
values: [
"send",
"dispatch",
"mail",
"forward",
"post",
"submit",
"mark",
"set",
"release",
],
},
{
key: "repeat",
values: ["repeat", "retry", "pause", "skip", "copy", "replicat", "demo",],
@@ -197,8 +191,29 @@ export const GetIconInfo = (action) => {
key: "compare",
values: ["compare", "convert", "to", "filter", "translate", "parse"],
},
{ key: "assets", values: ["cmdb", "assets", "asset", "cmdb", "inventory", "host", "hosts", "device", "devices"] },
{ key: "close", values: ["close", "stop", "cancel", "block"] },
{ key: "communication", values: ["communication", "comms", "email", "mail",] },
{ key: "eradication", values: ["eradication", "edr", "xdr"] },
{ key: "iam", values: ["iam", "identity", "access", "auth", "authentication", "authorization", "oauth", "sso", "openid"] },
{ key: "intel", values: ["intel", "feed", "threat intel", "threat intelligence", "ti", "t.i.", "t.i", "ti.", "rule", "technique", "tactic", "techniques", "tactics", "ioc", "indicator",] },
{ key: "network", values: ["network", "net", "networking", "firewall", "proxy", "vpn", "sdwan", "sd-wan"] },
{
key: "send",
values: [
"send",
"dispatch",
"mail",
"forward",
"post",
"submit",
"mark",
"set",
"release",
],
},
];
var selectedKey = ""
@@ -357,7 +372,45 @@ export const GetIconInfo = (action) => {
iconBackgroundColor: "green",
originalIcon: <SearchIcon />,
},
};
eradication: {
icon: "",
iconColor: "white",
iconBackgroundColor: "green",
originalIcon: <CoronavirusIcon />,
},
iam: {
icon: "",
iconColor: "white",
iconBackgroundColor: "green",
originalIcon: <FingerprintIcon />,
},
intel: {
icon: "",
iconColor: "white",
iconBackgroundColor: "green",
originalIcon: <PsychologyIcon />,
},
network: {
icon: "",
iconColor: "white",
iconBackgroundColor: "green",
originalIcon: <WifiIcon />,
},
assets: {
icon: "",
iconColor: "white",
iconBackgroundColor: "green",
originalIcon: <DevicesIcon />,
},
}
/*
{ key: "eradication", values: ["eradication", "edr", "xdr"] },
{ key: "iam", values: ["iam", "identity", "access", "auth", "authentication", "authorization", "oauth", "sso", "openid"] },
{ key: "intel", values: ["intel", "threat intel", "threat intelligence", "ti", "t.i.", "t.i", "ti."] },
{ key: "network", values: ["network", "net", "networking", "firewall", "proxy", "vpn", "sdwan", "sd-wan"] },
{ key: "siem", values: ["siem", "security information and event management", "security information and event management", "security information and event management"] },
*/
var selectedItem = parsedIcons[selectedKey];
if (selectedItem === undefined || selectedItem === null) {
@@ -374,11 +427,8 @@ export const GetIconInfo = (action) => {
selectedItem.iconBackgroundColor = defaultColor;
}
if (selectedItem.icon === "" || selectedItem.icon === undefined) {
console.log(
`MISSING PATH FOR ${selectedKey} (find in scope): `,
selectedItem.originalIcon.type.type
);
if ((selectedItem.icon === "" || selectedItem.icon === undefined) && (selectedItem.originalIcon === undefined || selectedItem.originalIcon === "")) {
console.log(`MISSING PATH FOR ${selectedKey} (find in scope): `, selectedItem.originalIcon.type.type)
}
if (
@@ -753,6 +803,12 @@ const Workflows2 = (props) => {
const handleTabChange = (event, newValue) => {
setCurrTab(newValue);
//if (view === "list" && currTab > 0) {
// setView("grid")
// setUpdate(Math.random())
//}
// Update URL query params based on tab index
const tabMapping = {
0: 'org_workflows',
@@ -767,9 +823,6 @@ const Workflows2 = (props) => {
navigate(`${location.pathname}?${queryParams.toString()}`);
};
const handleCreateWorkflow = () => {
setModalOpen(true)
setIsEditing(false)
@@ -1854,9 +1907,9 @@ const Workflows2 = (props) => {
data = JSON.parse(JSON.stringify(data));
data = sanitizeWorkflow(data);
toast("Sanitizing and publishing " + data.name);
// This ALWAYS talks to Shuffle cloud
fetch(globalUrl + "/api/v1/workflows/" + data.id + "/publish", {
fetch(globalUrl + "/api/v1/workflows/" + data.id + "/publish" , {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -3414,6 +3467,8 @@ const Workflows2 = (props) => {
return obj;
})
console.log("ROWS: ", rows)
workflowData = (
<DataGrid
color="primary"
@@ -3851,6 +3906,8 @@ const Workflows2 = (props) => {
onClick={() => {
localStorage.setItem("view", "list");
setView("list");
setCurrTab(0)
}}
>
<ListIcon />
@@ -4005,20 +4062,30 @@ const Workflows2 = (props) => {
// }
useEffect(() => {
if (currTab === 2) return;
if (userdata !== undefined && userdata !== null && filteredWorkflows.length > 0) {
if (filteredWorkflows.length > 0) {
var categoryWorkflows = []
if (currTab === 0) {
categoryWorkflows = filteredWorkflows.filter(workflow => workflow?.org_id === userdata?.active_org?.id)
setOrgWorkflows(categoryWorkflows)
if (categoryWorkflows.length === 0) {
setOrgWorkflows(filteredWorkflows)
} else {
setOrgWorkflows(categoryWorkflows)
}
}
else if (currTab === 1) {
categoryWorkflows = filteredWorkflows.filter(workflow => workflow?.org_id === userdata?.active_org?.id && workflow?.owner === userdata?.id)
setMyWorkflows(categoryWorkflows)
}
setIsLoadingWorkflow(false);
}
} else {
if (currTab === 0 && workflows?.length > 0) {
setOrgWorkflows(workflows)
}
}
}, [currTab, workflows, userdata, filteredWorkflows, filters])
@@ -4607,13 +4674,17 @@ const Workflows2 = (props) => {
</IconButton>
</Tooltip>
<Tooltip title={view === "grid" ? "List view" : "Grid view"} placement="top">
<Tooltip title={view === "grid" ? "List view (Org Workflows only)" : "Grid view"} placement="top">
<IconButton
style={currTab === 2 ? iconButtonDisabledStyle : iconButtonStyle}
onClick={() => {
const newView = view === "grid" ? "list" : "grid";
localStorage.setItem("workflowView", newView);
setView(newView);
if (view === "grid") {
setCurrTab(0)
}
}}
disabled={currTab === 2}
>
@@ -4699,25 +4770,30 @@ const Workflows2 = (props) => {
paddingBottom: 40
}}>
{currTab === 0 && orgWorkflows.map((data, index) => {
// Shouldn't be a part of this list
if (data.public === true) {
return null
}
{currTab !== 0 ? null :
orgWorkflows.length === 0 ?
<Typography variant="h6" style={{margin: "auto", minWidth: 500, }} >
No workflows found in this org with the Org ID filter. If this is an error, please click the "List View" button at the top to see ALL available workflows ({workflows.length}).
</Typography> :
orgWorkflows.map((data, index) => {
// Shouldn't be a part of this list
if (data.public === true) {
return null
}
// if (firstLoad) {
// workflowDelay += 75
// } else {
// return <WorkflowPaper key={index} data={data} />
// }
// if (firstLoad) {
// workflowDelay += 75
// } else {
// return <WorkflowPaper key={index} data={data} />
// }
return (
<span key={index}>
{/*<Zoom key={index} in={true} style={{ transitionDelay: `${workflowDelay}ms` }}>*/}
<WorkflowPaper data={data} />
{/*</Zoom>*/}
</span>
)
return (
<span key={index}>
{/*<Zoom key={index} in={true} style={{ transitionDelay: `${workflowDelay}ms` }}>*/}
<WorkflowPaper data={data} />
{/*</Zoom>*/}
</span>
)
})}
{currTab === 3 && backupWorkflows.map((data, index) => {
@@ -4742,24 +4818,29 @@ const Workflows2 = (props) => {
})}
{
currTab === 1 && myWorkflows.map((data, index) => {
if (data.public === true) {
return null
}
currTab !== 1 ? null :
myWorkflows.length === 0 ?
<Typography variant="h6" style={{margin: "auto", width: 500, }} >
No workflows found in this org for your user.
</Typography> :
myWorkflows.map((data, index) => {
if (data.public === true) {
return null
}
// if (firstLoad) {
// workflowDelay += 75
// } else {
// return <WorkflowPaper key={index} data={data} />
// }
// if (firstLoad) {
// workflowDelay += 75
// } else {
// return <WorkflowPaper key={index} data={data} />
// }
return (
<span key={index}>
{/*<Zoom key={index} in={true} style={{ transitionDelay: `${workflowDelay}ms` }}>*/}
<WorkflowPaper data={data} />
{/*</Zoom>*/}
</span>
)
return (
<span key={index}>
{/*<Zoom key={index} in={true} style={{ transitionDelay: `${workflowDelay}ms` }}>*/}
<WorkflowPaper data={data} />
{/*</Zoom>*/}
</span>
)
})
}
</div>
@@ -5123,7 +5204,7 @@ const Workflows2 = (props) => {
//isLoaded && isLoggedIn && workflowDone ? (
const loadedCheck =
workflowDone ? (
workflowDone && isLoaded ? (
<div>
{/*
<ShepherdTour steps={newSteps} tourOptions={tourOptions}>