diff --git a/backend/go-app/go.mod b/backend/go-app/go.mod
index d4f96760..aeb3e7e0 100644
--- a/backend/go-app/go.mod
+++ b/backend/go-app/go.mod
@@ -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
diff --git a/backend/go-app/go.sum b/backend/go-app/go.sum
index eabdc33c..098db5c0 100644
--- a/backend/go-app/go.sum
+++ b/backend/go-app/go.sum
@@ -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=
diff --git a/docker-compose.yml b/docker-compose.yml
index adf6b533..0ff2c29c 100755
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -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:
diff --git a/frontend/public/images/singul_green.png b/frontend/public/images/singul_green.png
new file mode 100644
index 00000000..796ad6ef
Binary files /dev/null and b/frontend/public/images/singul_green.png differ
diff --git a/frontend/src/components/AdminNavBar.jsx b/frontend/src/components/AdminNavBar.jsx
index f52e0a79..2a480c95 100644
--- a/frontend/src/components/AdminNavBar.jsx
+++ b/frontend/src/components/AdminNavBar.jsx
@@ -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 }) => (
+
+
+
+);
+
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: , 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: , alt: "Users Icon", text: "Users", component: UserManagmentTab, props: { globalUrl, userdata, serverside, isCloud, selectedOrganization, setSelectedOrganization, handleEditOrg } },
- { iconSrc: , alt: "App Auth Icon", text: "App_auth", component: AppAuthTab, props: { globalUrl, userdata, isCloud, selectedOrganization } },
- { iconSrc: , alt: "Datastore Icon", text: "Datastore", component: CacheView, props: { globalUrl, userdata, selectedOrganization, serverside, isSelectedDataStore, orgId , isCloud} },
- { iconSrc: , alt: "Files Icon", text: "Files", component: Files, props: { isCloud, globalUrl, userdata, serverside, selectedOrganization, isSelectedFiles } },
- { iconSrc: , alt: "Trigger Icon", text: "Triggers", component: SchedulesTab, props: { globalUrl, userdata, isCloud, serverside } },
- { iconSrc: , alt: "Environments Icon", text: "Locations", component: EnvironmentTab, props: { globalUrl, userdata, isCloud, selectedOrganization } },
- { iconSrc: , 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: , 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: , alt: "Users Icon", text: "Users", component: UserManagmentTab, props: { globalUrl, userdata, serverside, isCloud, selectedOrganization, setSelectedOrganization, handleEditOrg } },
+ { iconSrc: , alt: "App Auth Icon", text: "App_auth", component: AppAuthTab, props: { globalUrl, userdata, isCloud, selectedOrganization } },
+ { iconSrc: , alt: "Datastore Icon", text: "Datastore", component: CacheView, props: { globalUrl, userdata, selectedOrganization, serverside, isSelectedDataStore, orgId , isCloud} },
+ { iconSrc: , alt: "Files Icon", text: "Files", component: Files, props: { isCloud, globalUrl, userdata, serverside, selectedOrganization, isSelectedFiles } },
+ { iconSrc: , alt: "Trigger Icon", text: "Triggers", component: SchedulesTab, props: { globalUrl, userdata, isCloud, serverside } },
+ { iconSrc: , alt: "Environments Icon", text: "Locations", component: EnvironmentTab, props: { globalUrl, userdata, isCloud, selectedOrganization } },
+ { iconSrc: , 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 ;
-};
+ return ;
+ };
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 ? : item?.iconSrc}
onClick={() => setConfig(item.text)}
>
{item.text.replace(/_/g, " ")}
@@ -308,7 +339,10 @@ const AdminNavBar = (props) => {
- {renderComponent()}
+
+
+ {renderComponent()}
+
);
};
@@ -424,7 +458,13 @@ const Loader = () => {
const PaddingWrapper2 = memo(({ children }) => {
return (
-
+
{children}
)
diff --git a/frontend/src/components/CacheView.jsx b/frontend/src/components/CacheView.jsx
index 8502c81f..1e94d131 100644
--- a/frontend/src/components/CacheView.jsx
+++ b/frontend/src/components/CacheView.jsx
@@ -1,7 +1,12 @@
import React, { useState, useEffect, useContext, memo } from "react";
+import { makeStyles } from "@mui/styles";
import { getTheme } from "../theme.jsx";
import { toast } from 'react-toastify';
+
import ReactJson from "react-json-view-ssr";
+import { GetIconInfo } from "../views/Workflows2.jsx";
+import { red } from "../views/AngularWorkflow.jsx";
+import CollectIngestModal from "../components/CollectIngestModal.jsx";
import {
Typography,
Tooltip,
@@ -10,7 +15,6 @@ import {
Button,
Tabs,
Tab,
- Grid,
List,
ListItem,
ListItemText,
@@ -25,8 +29,18 @@ import {
DialogContent,
FormControl,
Select,
+ Autocomplete,
+ ButtonGroup,
+ InputLabel,
+ Pagination,
+ PaginationItem,
} from "@mui/material";
+import {
+ DataGrid,
+ GridColDef,
+} from '@mui/x-data-grid';
+
import {
Link as LinkIcon,
AutoFixHigh as AutoFixHighIcon,
@@ -34,6 +48,7 @@ import {
Edit as EditIcon,
FileCopy as FileCopyIcon,
SelectAll as SelectAllIcon,
+ DeleteOutline as DeleteOutlineIcon,
OpenInNew as OpenInNewIcon,
CloudDownload as CloudDownloadIcon,
Description as DescriptionIcon,
@@ -42,7 +57,6 @@ import {
Close as CloseIcon,
Apps as AppsIcon,
Image as ImageIcon,
- Delete as DeleteIcon,
Cached as CachedIcon,
AccessibilityNew as AccessibilityNewIcon,
Lock as LockIcon,
@@ -54,6 +68,14 @@ import {
VisibilityOff as VisibilityOffIcon,
Clear as ClearIcon,
Add as AddIcon,
+ Rocket as RocketIcon,
+ Webhook as WebhookIcon,
+ Air as AirIcon,
+ RocketLaunch as RocketLaunchIcon,
+ Send as SendIcon,
+ SmartToy as SmartToyIcon,
+ Settings as SettingsIcon,
+ FilterAlt as FilterAltIcon,
} from "@mui/icons-material";
import { validateJson, } from "../views/Workflows.jsx";
import { Context } from "../context/ContextApi.jsx";
@@ -74,7 +96,15 @@ const scrollStyle2 = {
overflow: "scroll",
}
+const useStyles = makeStyles({
+ notchedOutline: {
+ borderColor: "#f85a3e !important",
+ },
+});
+//
+
+//const CacheView = (props) => {
const CacheView = memo((props) => {
const { globalUrl, userdata, serverside, orgId, isSelectedDataStore, selectedOrganization } = props;
const [orgCache, setOrgCache] = React.useState("");
@@ -85,7 +115,6 @@ const CacheView = memo((props) => {
const [key, setKey] = React.useState("");
const [value, setValue] = React.useState("");
const [cacheInput, setCacheInput] = React.useState("");
- const [cacheCursor, setCacheCursor] = React.useState("");
const [dataValue, setDataValue] = React.useState({});
const [editCache, setEditCache] = React.useState(false);
const [cachedLoaded, setCachedLoaded] = React.useState(false);
@@ -93,29 +122,142 @@ const CacheView = memo((props) => {
const [showDistributionPopup, setShowDistributionPopup] = useState(false);
const [selectedSubOrg, setSelectedSubOrg] = useState([]);
const [selectedCacheKey, setSelectedCacheKey] = useState("");
+ const [totalAmount, setTotalAmount] = useState(0);
+ const [page, setPage] = useState(0);
+ const [pageSize, setPageSize] = useState(50)
+ const [cursors, setCursors] = useState({
+ 0: "",
+ })
+ const [_, setUpdate] = useState(Math.random())
+ const [selectedRows, setSelectedRows] = useState([]);
// Direct category migration from ../components/Files.jsx
const [selectAllChecked, setSelectAllChecked] = React.useState(false)
const [renderTextBox, setRenderTextBox] = React.useState(false);
- const [fileCategories, setFileCategories] = React.useState(["default"]);
+ const [datastoreCategories, setDatastoreCategories] = React.useState(["default"]);
const [selectedCategory, setSelectedCategory] = React.useState("default");
const [selectedFileId, setSelectedFileId] = React.useState("");
const [updateToThisCategory, setUpdateToThisCategory] = useState("")
- const [showFileCategoryPopup, setShowFileCategoryPopup] = React.useState(false);
+ const [workflows, setWorkflows] = useState([]);
+
const [selectedFiles, setSelectedFiles] = useState([]);
+ const [showAutomationMenu, setShowAutomationMenu] = useState(false);
+ const [showSettingsMenu, setShowSettingsMenu] = useState(false);
+ const [showCollectIngestMenu, setShowCollectIngestMenu] = useState(false);
+
+ const defaultAutomation = [
+ {
+ "name": "Run workflow",
+ "description": "Runs a workflow with the updated value.",
+ "options": [{
+ "key": "workflow_id",
+ "value": "",
+ }],
+ "icon":
,
+ "enabled": false,
+ },
+ {
+ "name": "Send message",
+ "description": "",
+ "type": "singul",
+ "options": [{
+ "key": "app",
+ "value": "",
+ }],
+ "icon":
,
+ "disabled": true,
+ "enabled": false,
+ },
+ {
+ "name": "Enrich",
+ "description": "",
+ "type": "singul",
+ "options": [{
+ "key": "",
+ "value": "",
+ }],
+ "icon": "/images/logos/singul.svg",
+ "enabled": false,
+ "disabled": true,
+ },
+ {
+ "name": "Run AI Agent",
+ "description": "",
+ "options": [{
+ "key": "",
+ "value": "",
+ }],
+ "icon":
,
+ "enabled": false,
+ "disabled": true,
+ },
+ {
+ "name": "Send webhook",
+ "description": "Sends the updated value to a specified webhook URL.",
+ "options": [{
+ "key": "webhook_url",
+ "value": "",
+ }],
+ "icon":
,
+ "enabled": false,
+ },
+ ]
+
+ const [categoryAutomations, setCategoryAutomations] = useState(defaultAutomation)
+ const [categoryConfig, setCategoryConfig] = useState(undefined)
const { themeMode, brandColor } = useContext(Context);
const theme = getTheme(themeMode, brandColor);
+ const classes = useStyles();
+
+
+ const getWorkflows = () => {
+ const url = `${globalUrl}/api/v1/workflows`
+ fetch(url, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ },
+ credentials: "include",
+ })
+ .then((response) => {
+ if (response.status !== 200) {
+ console.log("Status not 200 for workflows :O!");
+ return;
+ }
+
+ return response.json();
+ })
+ .then((responseJson) => {
+ if (responseJson?.success !== true) {
+ setWorkflows(responseJson)
+ } else {
+ toast.warn("Failed to load workflows. Please try again or contact support@shuffler if this persists.")
+ }
+ })
+ .catch((error) => {
+ toast(error.toString());
+ });
+ }
+
+ useEffect(() => {
+ setCursors({
+ 0: "",
+ })
+ setPage(0)
+ setSelectedRows([])
+ }, [selectedCategory])
useEffect(() => {
- if (orgId?.length > 0) {
- listOrgCache(orgId, selectedCategory)
- }
- }, [orgId])
+ getWorkflows()
+ listOrgCache(orgId, selectedCategory, 0, pageSize, page)
+ }, [])
+
const handleKeyDown = (event) => {
if (event.key === 'Enter') {
- fileCategories.push(event.target.value);
+ datastoreCategories.push(event.target.value);
setSelectedCategory(event.target.value);
setRenderTextBox(false);
}
@@ -126,9 +268,41 @@ const CacheView = memo((props) => {
}
}
+ const listOrgCache = (orgId, category, index, amount, page, keyValue) => {
+ setCachedLoaded(false)
+ if (index === undefined || index === null) {
+ index = 0
+ }
+
+ var url = `${globalUrl}/api/v1/orgs/${orgId}/list_cache`
+
+ if (category !== undefined && category !== null && category !== "default" && category !== "") {
+ url += "?category=" + category.replaceAll(" ", "_")
+ } else {
+ url += "?category=default"
+ category = "default"
+ }
+
+ if (amount !== undefined && amount !== null && amount > 0) {
+ url += "&top=" + amount
+ }
+
+ if (page !== undefined && page !== null && page >= 0) {
+ if (cursors[page-1] !== undefined && cursors[page-1] !== null && cursors[page-1] !== "") {
+ url += "&cursor=" + cursors[page-1]
+ }
+ }
+
+ if (keyValue !== undefined && keyValue !== null && keyValue !== "") {
+ url += "&key=" + keyValue
+
+ setCursors({
+ 0: "",
+ })
+ setPage(0)
+ setSelectedRows([])
+ }
- const listOrgCache = (orgId, category) => {
- const url = `${globalUrl}/api/v1/orgs/${orgId}/list_cache${category !== undefined ? `?category=${category.replaceAll(" ", "%20")}` : ""}`
fetch(url, {
method: "GET",
headers: {
@@ -146,29 +320,98 @@ const CacheView = memo((props) => {
return response.json();
})
.then((responseJson) => {
- if (responseJson.success === true) {
- setListCache(responseJson.keys);
- setCachedLoaded(true);
+ setCachedLoaded(true);
+ if (responseJson?.success === true) {
+ setListCache(responseJson.keys)
- if (fileCategories.length === 1 && fileCategories[0] === "default") {
+ if (responseJson.total_amount !== undefined && responseJson.total_amount !== null && responseJson.total_amount > 0) {
+ setTotalAmount(responseJson.total_amount)
+ } else {
+ setTotalAmount(responseJson.keys.length)
+ }
+
+ if (responseJson?.cursor !== undefined && responseJson?.cursor !== null && responseJson?.cursor !== "") {
+ cursors[page] = responseJson.cursor
+ }
+
+ // Especially important during first load
+ if (index < 2 && (category === "default" || category === "" || category === undefined)) {
+ // If it exists and isn't blank/default, load it
+ const urlParams = new URLSearchParams(window.location.search);
+ const categoryParam = urlParams.get("category");
+ if (categoryParam && categoryParam !== undefined && categoryParam !== "default" && categoryParam !== "") {
+ setSelectedCategory(categoryParam);
+ if (index === undefined || index === null) {
+ index = 0
+ }
+
+ listOrgCache(orgId, categoryParam, index+1, amount, page)
+ } else {
+ setSelectedCategory("default");
+ }
+ }
+
+ if ((category === undefined || category === "default" || category === "") && datastoreCategories.length === 1 && datastoreCategories[0] === "default") {
var newcategories = ["default"]
for (var key in responseJson.keys) {
- var category = responseJson.keys[key].category
- if (category !== undefined && category !== null && category !== ""){
- category = category.replaceAll(" ", "_")
+ var foundcategory = responseJson.keys[key].category
+ if (foundcategory !== undefined && foundcategory !== null && foundcategory !== ""){
+ foundcategory = category.replaceAll(" ", "_")
- if (!newcategories.includes(category)) {
- newcategories.push(category)
+ if (!newcategories.includes(foundcategory)) {
+ newcategories.push(foundcategory)
}
}
}
- setFileCategories(newcategories)
- }
- }
+ if (responseJson?.categories !== undefined && responseJson?.categories !== null && responseJson?.categories.length > 0) {
+ for (var i = 0; i < responseJson.categories.length; i++) {
+ const foundcategory = responseJson.categories[i].replaceAll(" ", "_")
+ if (foundcategory !== undefined && foundcategory !== null && foundcategory !== "" && foundcategory !== "default" && !newcategories.includes(foundcategory)) {
+ newcategories.push(responseJson.categories[i]);
+ }
+ }
+ }
- if (responseJson.cursor !== undefined && responseJson.cursor !== null && responseJson.cursor !== "") {
- setCacheCursor(responseJson.cursor);
+ setDatastoreCategories(newcategories)
+ }
+
+
+ if (responseJson?.category_config !== undefined && responseJson?.category_config !== null) {
+
+ if (responseJson?.category_config?.id !== undefined && responseJson?.category_config?.id !== null && responseJson?.category_config?.id !== "") {
+ setCategoryConfig(responseJson.category_config)
+ }
+
+ // Handle other configs here.
+ if (responseJson?.category_config?.automations !== undefined && responseJson?.category_config?.automations !== null && responseJson?.category_config?.automations.length > 0) {
+ // Find icons if they exist
+ for (var key in responseJson.category_config.automations) {
+ //if (responseJson.category_config.automations[key].icon === undefined || responseJson.category_config.automations[key].icon === null || responseJson.category_config.automations[key].icon === "") {
+ const foundItem = defaultAutomation.find((automation) => automation.name === responseJson.category_config.automations[key].name)
+ if (foundItem) {
+ responseJson.category_config.automations[key].disabled = foundItem.disabled
+ responseJson.category_config.automations[key].icon = foundItem.icon
+ responseJson.category_config.automations[key].type = foundItem?.type
+ } else {
+ responseJson.category_config.automations[key].icon =
+ }
+ }
+
+ for (var key in defaultAutomation) {
+ if (!responseJson.category_config.automations.some((automation) => automation.name === defaultAutomation[key].name)) {
+ // If the automation doesn't exist in the response, add it with default values
+ responseJson.category_config.automations.push(defaultAutomation[key])
+ }
+ }
+
+ setCategoryAutomations(responseJson.category_config.automations)
+ } else {
+ setCategoryAutomations(defaultAutomation)
+ }
+ }
+ } else {
+ toast.warn("Failed to load keys. Please try again or contact support@shuffler if this persists.")
}
})
.catch((error) => {
@@ -177,15 +420,19 @@ const CacheView = memo((props) => {
};
- const deleteCache = (orgId, key) => {
+ const deleteEntry = (orgId, key, itemCategory, refreshList) => {
const method = "POST"
const url = `${globalUrl}/api/v1/orgs/${orgId}/delete_cache`
- const parsed = {
+ var parsed = {
"org_id": orgId,
"key": key,
"category": selectedCategory === "" || selectedCategory === "default" ? "" : selectedCategory,
}
+ if (itemCategory !== undefined) {
+ parsed["category"] = itemCategory.replaceAll(" ", "_");
+ }
+
fetch(url, {
method: method,
headers: {
@@ -196,12 +443,15 @@ const CacheView = memo((props) => {
})
.then((response) => {
if (response.status === 200) {
- toast("Successfully deleted Cache");
- setTimeout(() => {
- listOrgCache(orgId, selectedCategory)
- }, 1000);
+ if (refreshList === undefined || refreshList === null || refreshList === true) {
+
+ toast.success("Deleted datastore entry");
+ setTimeout(() => {
+ listOrgCache(orgId, selectedCategory, 0, pageSize, page)
+ }, 1000);
+ }
} else {
- toast("Failed deleting Cache. Does it still exist?");
+ toast.error(`Failed deleting entry ${key} in category ${itemCategory || selectedCategory}. If this persists, please contact support@shuffler.io.`)
}
})
.catch((error) => {
@@ -210,20 +460,25 @@ const CacheView = memo((props) => {
};
const editOrgCache = (orgId) => {
- const cache = {
+ var entry = {
key: dataValue.key,
value: value,
category: selectedCategory,
}
+ if (dataValue?.category !== "" && dataValue?.category !== "default") {
+ entry.category = dataValue.category.replaceAll(" ", "_");
+
+ }
+
if (listCache.length > 0) {
const selectedCache = listCache.find((data) => data.key === dataValue.key);
if (selectedCache?.suborg_distribution?.length > 0) {
- cache.suborg_distribution = selectedCache.suborg_distribution;
+ entry.suborg_distribution = selectedCache.suborg_distribution;
}
}
- setCacheInput([cache]);
+ setCacheInput([entry]);
fetch(globalUrl + `/api/v1/orgs/${orgId}/set_cache`, {
@@ -233,7 +488,7 @@ const CacheView = memo((props) => {
Accept: "application/json",
},
credentials: "include",
- body: JSON.stringify(cache),
+ body: JSON.stringify(entry),
})
.then((response) => {
if (response.status !== 200) {
@@ -245,8 +500,8 @@ const CacheView = memo((props) => {
})
.then((responseJson) => {
setAddCache(responseJson);
- toast("Cache Edited Successfully!");
- listOrgCache(orgId, selectedCategory);
+ toast.success("Edit saved");
+ listOrgCache(orgId, selectedCategory, 0, pageSize, page);
setModalOpen(false);
})
.catch((error) => {
@@ -284,7 +539,7 @@ const CacheView = memo((props) => {
.then((responseJson) => {
setAddCache(responseJson);
toast("New key added Successfully!");
- listOrgCache(orgId, selectedCategory);
+ listOrgCache(orgId, selectedCategory, 0, pageSize, page);
setModalOpen(false);
})
.catch((error) => {
@@ -336,6 +591,19 @@ const CacheView = memo((props) => {
};
+ const timestamp = (timestamp) => {
+ if (timestamp === undefined || timestamp === null || timestamp === "") {
+ return null
+ }
+
+ const date = new Date(timestamp * 1000);
+ if (date.toString() === "Invalid Date" || date.toString() === "Invalid Date NaN") {
+ return null
+ }
+
+ return date.toISOString()?.slice(0, 19)?.replace("T", " ")
+ }
+
const modalView = (
// console.log("key:", dataValue.key),
//console.log("value:",dataValue.value),
@@ -435,6 +703,29 @@ const CacheView = memo((props) => {
value={value}
onChange={(e) => setValue(e.target.value)}
/>
+
+
+ {editCache ?
+
+
+ Created: {timestamp(dataValue?.created)}
+
+
+ Edited: {timestamp(dataValue?.edited)}
+
+ {dataValue?.workflow_id !== "" ?
+
+ Workflow: {dataValue.workflow_id}
+
+ : null}
+ {dataValue?.category !== "" && dataValue?.category !== "default" ?
+
+ Category: {dataValue.category}
+
+ : null}
+
+
+ : null}
{
>
{editCache ? "Edit":"Submit"}
+
);
@@ -528,11 +820,11 @@ const CacheView = memo((props) => {
.then((response) =>
response.json().then((responseJson) => {
if (responseJson["success"] === false) {
- toast("Failed overwriting datastore");
+ toast.error("Failed overwriting datastore");
} else {
- toast("Successfully updated datastore!");
+ toast.success("Successfully updated datastore!");
setTimeout(() => {
- listOrgCache(orgId, selectedCategory);
+ listOrgCache(orgId, selectedCategory, 0, pageSize, page);
setShowDistributionPopup(false);
}, 1000);
}
@@ -637,16 +929,727 @@ const CacheView = memo((props) => {
) : null;
+ const saveAutomation = (allAutomation, settings) => {
+ // Check if icon is a string. Otherwise make it empty.
+ var removedIcons = {}
+ allAutomation.forEach((automation) => {
+ const originalIcon = automation.icon
+ if (typeof automation.icon !== "string") {
+ automation.icon = "";
+ }
+ })
+
+ const url = `${globalUrl}/api/v2/datastore/automate`
+ const data = {
+ "category": selectedCategory === "" || selectedCategory === "default" ? "" : selectedCategory,
+ "automations": allAutomation,
+ }
+
+ if (settings !== undefined && settings !== null && Object.keys(settings).length > 0) {
+ data["settings"] = settings
+ }
+
+ fetch(url, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ },
+ credentials: "include",
+ body: JSON.stringify(data),
+ })
+ .then((response) => {
+ if (response.status !== 200) {
+ console.log("Status not 200 for automations :O!");
+ return;
+ }
+
+ return response.json();
+ })
+ .then((responseJson) => {
+ if (responseJson?.success === true) {
+ toast.success("Saved successfully!")
+ } else {
+ toast.warn("Failed to save automations. Please try again or contact support@shuffler if this persists.")
+ }
+ })
+ .catch((error) => {
+ toast.error(error.toString());
+ })
+ }
+
+
+ const AutomationOptions = ({ automation, index }) => {
+ const [showOptions, setShowOptions] = useState(false);
+ const [hovered, setHovered] = useState(false);
+
+ const [updated, setUpdated] = useState(false);
+ const [updatedAutomation, setUpdatedAutomation] = useState(automation);
+ const [_, setUpdate] = useState(Math.random()) // Force re-render
+
+ if (automation.icon === undefined || automation.icon === null || automation.icon === "") {
+ for (var i = 0; i < defaultAutomation.length; i++) {
+ if (defaultAutomation[i].name === automation.name) {
+ automation.icon = defaultAutomation[i].icon
+ break;
+ }
+ }
+ }
+
+ const runSave = () => {
+ setUpdated(false)
+
+ const newAutomations = [...categoryAutomations]
+ newAutomations[index] = updatedAutomation
+
+ setCategoryAutomations(newAutomations)
+ setUpdate(Math.random()) // Force re-render
+
+ saveAutomation(newAutomations)
+ }
+
+ return (
+ setHovered(true)}
+ onMouseLeave={() => setHovered(false)}
+ >
+
+
+ option.value === "")}
+ onChange={(e) => {
+ e.stopPropagation()
+ e.preventDefault()
+
+ updatedAutomation.enabled = !updatedAutomation.enabled
+ setUpdatedAutomation(updatedAutomation)
+ setUpdated(true)
+
+ setUpdate(Math.random())
+ }}
+ />
+
+
{
+ if (automation?.disabled === true) {
+ return
+ }
+
+ setShowOptions(!showOptions)
+
+ // Auto saves when the options are closed/saved
+ if (updated && showOptions) {
+ runSave()
+ }
+ }}
+ >
+ {typeof automation?.icon === "string" && automation?.icon?.length > 0 ?
+
+ :
+ automation.icon
+ }
+
+
+ {automation.name}
+
+
+
+ {automation?.disabled !== true ?
+
{
+ e.stopPropagation();
+ e.preventDefault();
+
+ runSave()
+ }}
+ >
+ Save
+
+ : null}
+
+
+ {showOptions && (
+ updatedAutomation.options.map((option, optionIndex) => {
+ if (option?.key === "workflow_id") {
+ return (
+
option?.value.includes(w.id)) || []}
+ classes={{ inputRoot: classes.inputRoot }}
+ ListboxProps={{
+ style: {
+ backgroundColor: theme.palette.surfaceColor,
+ color: theme.palette.text.primary,
+ borderRadius: theme.palette.borderRadius,
+ },
+ }}
+ onChange={(event, newValue) => {
+ option.value = ""
+ for (var i = 0; i < newValue.length; i++) {
+ option.value += newValue[i].id + ","
+ }
+
+ if (newValue.length > 0) {
+ updatedAutomation.enabled = true
+ } else {
+ updatedAutomation.enabled = false
+ }
+
+ updatedAutomation.options[optionIndex] = option
+ setUpdatedAutomation(updatedAutomation)
+ setUpdated(true)
+
+ setUpdate(Math.random()) // Force re-render
+ }}
+
+ getOptionLabel={(option) => {
+ if (
+ option === undefined ||
+ option === null ||
+ option?.name === undefined ||
+ option?.name === null
+ ) {
+ return "No Workflows Selected";
+ }
+
+ const newname = (
+ option.name.charAt(0).toUpperCase() + option.name.substring(1)
+ ).replaceAll("_", " ")
+
+ return newname
+ }}
+ options={workflows}
+ fullWidth
+ style={{
+ backgroundColor: theme.palette.textFieldStyle.backgroundColor,
+ borderRadius: theme.palette.textFieldStyle.borderRadius,
+ color: theme.palette.textFieldStyle.color,
+ height: 35,
+ marginBottom: 40,
+ }}
+ renderOption={(props, data, state) => {
+ /*
+ if (data.id === option?.value) {
+ data = workflow;
+ }
+ */
+
+ return (
+
+ {data.image !== undefined && data.image !== null && data.image.length > 0 ?
+
+ : null}
+
+ Choose {data.name}
+
+
+ } >
+
+ {data.name}
+
+
+ )
+ }}
+ renderInput={(params) => {
+ return (
+
+ )
+ }}
+ />
+ )
+ }
+
+ return (
+ {
+ if (e.target.value === "") {
+ updatedAutomation.enabled = false
+ } else {
+ updatedAutomation.enabled = true
+ }
+
+ updatedAutomation.options[optionIndex].value = e.target.value;
+ setUpdatedAutomation(updatedAutomation)
+ setUpdated(true)
+ }}
+ />
+ )
+ })
+ )}
+
+ )
+ }
+
+
+ const setCategorySettingsField = (field, value) => {
+ // Check if categoryConfig.settings is set or not. Otherwise set it.
+ var categoryConfig2 = categoryConfig
+ if (categoryConfig === undefined || categoryConfig === null) {
+ categoryConfig2 = {}
+ }
+
+ if (categoryConfig?.settings === undefined || categoryConfig?.settings === null) {
+ categoryConfig2.settings = {}
+ }
+
+ categoryConfig2.settings[field] = value
+ setCategoryConfig(categoryConfig2)
+
+ saveAutomation(
+ categoryAutomations,
+ categoryConfig2.settings,
+ )
+ }
+
+ const columns: GridColDef<(typeof rows)[number]>[] = [
+ {
+ field: 'key',
+ headerName: 'Key',
+ width: 200,
+ filterable: true,
+ sortable: true,
+ },
+ {
+ width: 600,
+ field: 'value',
+ filterable: true,
+ headerName: 'Value',
+ renderCell: (props) => {
+ const data = props.row
+ const validate = validateJson(data.value)
+
+ return (
+ {
+ e.preventDefault()
+ e.stopPropagation()
+ }}
+ >
+ {validate.valid ?
+ {
+ // handleReactJsonClipboard(copy);
+ }}
+ collapseStringsAfterLength={theme.palette.jsonCollapseStringsAfterLength}
+ iconStyle={theme.palette.jsonIconStyle}
+ displayDataTypes={false}
+ onSelect={(select) => {
+
+ // HandleJsonCopy(showResult, select, data.action.label);
+ console.log("SELECTED!: ", select);
+ }}
+ name={null}
+ />
+ :
+
+ {data.value}
+
+ }
+
+ )
+ }
+ },
+ {
+ field: 'actions',
+ headerName: 'Actions',
+ description: 'Actions for this key.',
+ width: 175,
+ filterable: false,
+ sortable: false,
+
+ renderCell: (props) => {
+ const data = props.row
+
+ return (
+
+ {data?.workflow_id === "" || data?.workflow_id === null || data?.workflow_id === undefined ?
+
+
+
+ : (
+
+
+
+
+
+
+
+
+
+ )}
+
+
+
+ {
+ // Try to make the value JSON indented
+ const valid = validateJson(data.value)
+ var newvalue = data.value
+ if (valid.valid) {
+ // JSON stringify with indentation
+ newvalue = JSON.stringify(valid.result, null, 2)
+ }
+
+
+ setEditCache(true)
+ setDataValue({
+ "key": data.key,
+ "value": newvalue,
+
+ "edited": data.edited,
+ "created": data.created,
+ "workflow_id": data.workflow_id,
+ "category": data.category,
+ })
+ setValue(newvalue)
+ setModalOpen(true)
+ }}
+ >
+
+
+
+
+
+
+
+
+ {
+ window.open(`${globalUrl}/api/v1/orgs/${orgId}/cache/${data.key}?type=text&authorization=${data.public_authorization}`, "_blank");
+ }}
+ >
+
+
+
+
+
+
+ {
+ deleteEntry(orgId, data.key, data.category)
+ }}
+ >
+
+
+
+
+
+
+
+
+ )
+ }
+ },
+ {
+ field: 'distribution',
+ headerName: 'Distribution',
+ description: 'Controls whether this key is distributed to sub-organizations.',
+ width: 100,
+ filterable: false,
+ sortable: false,
+
+ renderCell: (props) => {
+ const data = props.row
+ const isDistributed = data?.suborg_distribution?.length > 0 ? true : false;
+
+ return (
+
+ {selectedOrganization.id !== undefined && data?.org_id !== selectedOrganization.id ?
+
+
+
+ }
+ style={{display: "table-cell", textAlign: 'center', verticalAlign: 'middle', }}
+ />
+ :
+
+ {
+ setShowDistributionPopup(true)
+ if(data?.suborg_distribution?.length > 0){
+ setSelectedSubOrg(data.suborg_distribution)
+ }else{
+ setSelectedSubOrg([])
+ }
+ setSelectedCacheKey(data.key)
+ }}
+ />
+
+ }
+ style={{display: "table-cell", textAlign: 'center', verticalAlign: 'middle', }}
+ />
+ }
+
+ )
+ }
+ },
+ ];
+
+
+ const isAutomating = categoryAutomations?.find((automation) => automation.enabled) !== undefined
return (
-
+
{modalView}
+
+
+
{cacheDistributionModal}
-
+
+
-
Shuffle Datastore {selectedCategory === "" || selectedCategory === "default" ? "" : `- Category '${selectedCategory}'`}
+
+
+
+
+ Shuffle Datastore
+
+
+ {userdata?.support === true ?
+
+
+ {
+ setShowCollectIngestMenu(true)
+ }}
+ >
+
+ Collect (beta)
+
+
+
+ : null}
+
+
- Datastore is a permanent key-value database for storing data that can be used cross-workflow. You can store anything from lists of IPs to complex configurations.
+ Datastore is a permanent key-value database for storing data which can be used for automation.
{
-
{
- setEditCache(false)
- setModalOpen(true)
- setValue("")
- }}
- >
- Add Key
-
-
listOrgCache(orgId,selectedCategory)}
- >
-
-
+
+
+ {
+ setEditCache(false)
+ setModalOpen(true)
- {fileCategories !== undefined &&
- fileCategories !== null &&
- fileCategories.length > 1 ? (
-
-
+ Add Key
+
+ {
+ listOrgCache(orgId, selectedCategory, 0, pageSize, page);
+ }}
+ >
+
+
+
+
+
+ {datastoreCategories !== undefined &&
+ datastoreCategories !== null &&
+ datastoreCategories.length > 1 ? (
+
+
+
+ Category
+
+ {
+ setCategoryConfig(undefined)
+ setCategoryAutomations(defaultAutomation)
+
+ //if (selectAllChecked || listCache.length > 0) {
+ if (selectAllChecked || selectedFiles.length > 0) {
+ setUpdateToThisCategory(event.target.value)
+ return
+ }
+
+
+ setSelectedCategory(event.target.value)
+ if (event.target.value === "all" || event.target.value === "default") {
+ listOrgCache(orgId, "", 0, pageSize, page)
+ } else {
+ listOrgCache(orgId, event.target.value, 0, pageSize, page)
+ }
+
+ // Add it to the url as a query
+ if (window.location.search.includes("category=")) {
+ const newurl = window.location.href.replace(/category=[^&]+/, `category=${event.target.value}`)
+ window.history.pushState({ path: newurl }, "", newurl)
+ } else {
+ window.history.pushState({ path: window.location.href }, "", `${window.location.href}&category=${event.target.value}`)
+ }
+ }}
+ >
+ {datastoreCategories.map((data, index) => {
+ // Should find the icon for things
+ // Fix uppercase at start of words
+ const fixedname = data?.charAt(0)?.toUpperCase() + data?.slice(1)?.replaceAll("_", " ")
+ const iconDetails = GetIconInfo({
+ "app_name": fixedname,
+ "name": fixedname,
+ })
+
+ return (
+
+
+
+ {iconDetails?.originalIcon && (
+ iconDetails?.originalIcon
+ )}
+
+
+ {fixedname}
+
+
+ );
+ })}
+
+
+ ) : null}
+
+
+ {renderTextBox ?
+
+ {
+ setRenderTextBox(false);
+ console.log(" close clicked")
+ }}
+ >
+
+
+
+ :
+
+ 1 ? 50 : 169,
+ height: 35,
+ textTransform: 'none',
+ fontSize: 16,
+
+ borderRadius: "0px 5px 5px 0px",
+ }}
+ variant="outlined"
+ color="secondary"
+ onClick={() => {
+ setRenderTextBox(true);
+ }}
+ >
+
+ {datastoreCategories !== undefined && datastoreCategories !== null && datastoreCategories.length > 1 ? "" : "Category"}
+
+
+ }
+
+ {renderTextBox &&
{
+ handleKeyDown(event);
+ if(event.key === 'Enter' && selectedFileId.length > 0){
+ setUpdateToThisCategory(event.target.value)
+ }
+
+ }}
+ style={{
+ height: 35,
+ width: 200,
+ marginTop: 0,
+ }}
+ InputProps={{
+ style: {
color: theme.palette.textFieldStyle.color,
- minWidth: 122,
- maxWidth: 122,
height: 35,
- float: "right",
- position: 'relative',
- top: 8
- }}
- value={selectedCategory}
- onChange={(event) => {
- //if (selectAllChecked || listCache.length > 0) {
- if (selectAllChecked || selectedFiles.length > 0) {
- setUpdateToThisCategory(event.target.value)
- setShowFileCategoryPopup(true)
- return
- }
+ fontSize: 16,
+ borderRadius: 4,
+ paddingTop: 0,
+ },
+ }}
+ color="primary"
+ placeholder="Category name"
+ required
+ margin="dense"
+ defaultValue={""}
+ autoFocus
+ />}
+
+
- setSelectedCategory(event.target.value)
- if (event.target.value === "all" || event.target.value === "default") {
- listOrgCache(orgId)
- } else {
- listOrgCache(orgId, event.target.value)
- }
-
- // Add it to the url as a query
- if (window.location.search.includes("category=")) {
- const newurl = window.location.href.replace(/category=[^&]+/, `category=${event.target.value}`)
- window.history.pushState({ path: newurl }, "", newurl)
- } else {
- window.history.pushState({ path: window.location.href }, "", `${window.location.href}&category=${event.target.value}`)
- }
- }}
- >
- {fileCategories.map((data, index) => {
- return (
-
- {data.replaceAll("_", " ")}
-
- );
- })}
-
-
{
- setShowFileCategoryPopup(false)
- }}
- >
- File Categories
-
- Please note that your selected files ({selectedFileId?.length}) will be moved to the {updateToThisCategory} category.
-
-
- {
- setShowFileCategoryPopup(false)
- }}
- style={{fontSize: 16, textTransform: 'none'}}
- >
- Close
-
- {
- //handleUpdateFileCategory(updateToThisCategory)
- toast.error("Not implemented.")
- }}
- style={{fontSize: 16, textTransform: 'none', }}
- color="primary"
- variant="contained"
- >
- Update
-
-
-
-
- ) : null}
+
+
+
+
- {renderTextBox ?
-
+ border: isAutomating ? `1px solid ${theme.palette.primary.main}` : null,
+ }}
+ variant="contained"
+ color="secondary"
+ disabled={selectedCategory === undefined || selectedCategory === "" || selectedCategory === "default"}
+ onClick={() => {
+ // Set up:
+ // Workflow triggers for when a key is added/edited/deleted.
+ // Automatic ingest mechanisms:
+ // - IOCs (monitor URLs)
+ // - Singul ingest
+ setShowAutomationMenu(true)
+ }}
+ >
+ {isAutomating ? : }
+ Automate (beta)
+
+
+
+
+
{
- setRenderTextBox(false);
- console.log(" close clicked")
- }}
+ setShowSettingsMenu(true)
+ }}
>
-
-
-
- :
-
- {
- setRenderTextBox(true);
- }}
- >
-
- Category
+
- }
+
- {renderTextBox &&
{
- handleKeyDown(event);
- if(event.key === 'Enter' && selectedFileId.length > 0){
- //setShowFileCategoryPopup(true)
- setUpdateToThisCategory(event.target.value)
- }
+ {showAutomationMenu || showSettingsMenu ?
+ {
+ setShowAutomationMenu(false)
+ setShowSettingsMenu(false)
+ }}
+ 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,
+ },
+ },
+ }}
+ >
+
+
+
+ {showSettingsMenu === true ?
+
+
+
+ Settings for category '{selectedCategory}'
+
+
+
+
+
+ Timeout
+
+
+ You can set a timeout for the category. This will delete all keys in this category after the specified time. Timeout is in seconds and based on last EDITED time.
+
+
+
{
+ // Check if it's a number or not
+ var timeoutValue = 0
+ if (isNaN(e.target.value) || e.target.value === "" || e.target.value === null) {
+ toast.info("Timeout must be a number. Setting to 0.")
+ } else {
+ timeoutValue = parseInt(e.target.value, 10)
+ if (timeoutValue < 60) {
+ toast.info("Timeout must be between 60 seconds or more. Setting to 0.")
+ }
+
+ if (timeoutValue === categoryConfig?.settings?.timeout) {
+ return
+ }
+ }
+
+ setCategorySettingsField("timeout", timeoutValue)
+ }}
+ />
+
+
+
+
+
+ {categoryConfig?.settings?.public === true ? "" : "NOT"} Public
+
+
+ This will make the url for this category public. Metadata will be cleared, except for timestamps. Types: keys,ndjson,csv,values,json,meta
+
+
+
URL (when public):
{globalUrl}/api/v2/datastore/category/{selectedCategory}?top=10000&type=keys&org_id={orgId}
+
+
+
+
+
+ {
+ setCategorySettingsField("public", e.target.checked)
+ setUpdate(Math.random()) // Force update to re-render the component
+ }}
+ style={{marginTop: 10, }}
+ color="secondary"
+ />
+
+
+
+
+
+
+
+ Subscribing
+
+
+ Enabling this feature will allow other organizations to subscribe to this category. This is NOT fully available yet.
+
+
+
+
+ :
+
+
+
+ Automation for category '{selectedCategory}'
+
+
+
+ When
+
+
+ A key is edited
+
+
+
+ Do
+
+ {categoryAutomations.map((automation, index) => {
+
+ return (
+
+ )
+ })}
+
+ }
- }}
- style={{
- height: 35,
- width: 200,
- marginTop: 0,
- }}
- InputProps={{
- style: {
- color: theme.palette.textFieldStyle.color,
- height: 35,
- fontSize: 16,
- borderRadius: 4,
- paddingTop: 0,
- },
- }}
- color="primary"
- placeholder="Category name"
- required
- margin="dense"
- defaultValue={""}
- autoFocus
- />}
+
+
+ : null}
+
+
{isSelectedDataStore? null :
}
-
-
-
- {["Key", "Value", "workflow", "Actions", "Updated", "Distribution"].map((header, index) => (
-
- ))}
-
- {cachedLoaded === false
- ? [...Array(6)].map((_, rowIndex) => (
-
- {Array(6)
- .fill()
- .map((_, colIndex) => (
-
-
-
- ))}
-
- ))
- : listCache?.length === 0 ? (
-
- {Array(5).fill().map((_, index) => (
-
- ))}
-
- ): listCache?.map((data, index) => {
- var category = selectedCategory
- if (selectedCategory === "default") {
- category = ""
+ {
+ setSelectedRows(newSelection)
+ }}
+ keepNonExistentRowsSelected={false}
+ getRowId={(row) => row.key}
+
+ autoHeight={true}
+ sx={{
+ marginTop: 1,
+ height: listCache.length*52+500,
+ width: "100%",
+ '.MuiTablePagination-selectLabel, .MuiTablePagination-select, .MuiTablePagination-selectIcon': {
+ display: 'none',
+ },
+ marginBottom: 20,
+ }}
+
+ loading={cachedLoaded === false}
+ pagination
+ paginationMode="server"
+ page={page}
+ rowCount={totalAmount}
+ onPageChange={(newPage, second) => {
+ listOrgCache(orgId, selectedCategory, 0, pageSize, newPage)
+
+ setPage(newPage)
+ }}
+ onPageSizeChange={(newSize) => {
+ setPageSize(newSize);
+ setPage(0)
+ setSelectedRows([])
+
+ setCursors({
+ 0: "",
+ })
+ }}
+
+
+ filterMode="client"
+ onFilterModelChange={(model) => {
+ // Specific search for the key itself to find it fast across the index
+ if (model?.items?.length === 1) {
+ if (model?.items[0]?.operatorValue === "equals" && model?.items[0]?.columnField === "key") {
+ // Run backend search for a specific key
+ listOrgCache(orgId, selectedCategory, 0, pageSize, page, model?.items[0]?.value)
+ }
+ }
+ }}
+
+ getRowHeight={() => {
+ return "auto"
+ }}
+
+ hideFooterSelectedRowCount={true}
+ hideFooter={true}
+ />
+
+
+
+
+ {page * pageSize + 1} - {Math.min((page + 1) * pageSize, totalAmount)} of {totalAmount}
+
+
+
{
+ var disabled = false
+ if (item?.type === "page") {
+ if (cursors[item.page-1] === undefined) {
+ disabled = true
+ }
+ }
+ if (item?.type === "previous") {
+ disabled = page === 0
+ }
+
+ if (cachedLoaded === false) {
+ disabled = true
+ }
+
+ return (
+
+ )
+
+ }}
+ onChange={(e, value) => {
+ if (value < 1) {
+ return
}
- if (data?.category === undefined && category === "") {
- } else if (data?.category !== category) {
- return null
- }
+ const newPage = value-1
- var bgColor = isSelectedDataStore? themeMode === "dark" ? "#212121" : "#FFFFFF" :"#27292d";
- if (index % 2 === 0) {
- bgColor = isSelectedDataStore? themeMode === "dark" ? "#1A1A1A" : "#EAEAEA" :"#1f2023";
- }
+ listOrgCache(orgId, selectedCategory, 0, pageSize, newPage)
- const validate = validateJson(data.value);
- const isDistributed = data?.suborg_distribution?.length > 0 ? true : false;
- return (
-
-
- {
- // handleReactJsonClipboard(copy);
- }}
- collapseStringsAfterLength={theme.palette.jsonCollapseStringsAfterLength}
- iconStyle={theme.palette.jsonIconStyle}
- displayDataTypes={false}
- onSelect={(select) => {
- // HandleJsonCopy(showResult, select, data.action.label);
- console.log("SELECTED!: ", select);
- }}
- name={"value"}
- />
- :
- data.value
+ setPage(newPage)
+ }}
+ />
+
+ {selectedRows.length > 0 ?
+ {
+ setCachedLoaded(false)
+ for (var key in selectedRows) {
+ // Find the item and its category
+ var foundCategory = ""
+ for (var i = 0; i < listCache.length; i++) {
+ if (listCache[i].key === selectedRows[key]) {
+ foundCategory = listCache[i].category
+ break
+ }
}
- />
-
-
-
- : (
-
-
-
-
-
-
-
-
-
- )
- }
- style={{
- display: "table-cell",
- overflow: "hidden",
- verticalAlign: "middle",
- padding: "8px 8px 8px 15px",
- maxWidth: 200,
- overflowX: "auto",
- }}
- />
-
-
-
- {
- setEditCache(true)
- setDataValue({
- "key": data.key,
- "value":data.value
- })
- setValue(data.value)
- setModalOpen(true)
- }}
- >
-
-
-
-
-
-
-
-
- {
- window.open(`${globalUrl}/api/v1/orgs/${orgId}/cache/${data.key}?type=text&authorization=${data.public_authorization}`, "_blank");
- }}
- >
-
-
-
-
-
-
- {
- deleteCache(orgId, data.key);
- //deleteFile(orgId);
- }}
- >
-
-
-
-
-
-
-
-
- )}
- />
-
- {selectedOrganization.id !== undefined && data?.org_id !== selectedOrganization.id ?
-
-
-
- }
- style={{display: "table-cell", textAlign: 'center', verticalAlign: 'middle', }}
- />
- :
-
- {
- setShowDistributionPopup(true)
- if(data?.suborg_distribution?.length > 0){
- setSelectedSubOrg(data.suborg_distribution)
- }else{
- setSelectedSubOrg([])
- }
- setSelectedCacheKey(data.key)
- }}
- />
-
- }
- style={{display: "table-cell", textAlign: 'center', verticalAlign: 'middle', }}
- />
- }
-
- );
- })}
-
-
+
+ deleteEntry(orgId, selectedRows[key], foundCategory, false)
+ }
+
+ setSelectedRows([])
+ setTimeout(() => {
+ // Refresh the list
+ listOrgCache(orgId, selectedCategory, 0, pageSize, page)
+ toast.success("Deleted " + selectedRows.length + " keys from datastore")
+ }, 2500)
+
+ }}
+ variant={"outlined"}
+ color="secondary"
+ >
+
+ Delete {selectedRows.length} Key{ selectedRows.length > 1 ? "s" : "" }
+
+ : null}
+
+
+
);
-});
+})
+//export default CacheView;
export default memo(CacheView);
diff --git a/frontend/src/components/CollectIngestModal.jsx b/frontend/src/components/CollectIngestModal.jsx
new file mode 100644
index 00000000..afba4afa
--- /dev/null
+++ b/frontend/src/components/CollectIngestModal.jsx
@@ -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 (
+ //
setHovering(true)}
+ onMouseLeave={() => setHovering(false)}
+ >
+
+ {iconDetails?.originalIcon && (
+ iconDetails?.originalIcon
+ )}
+
+
+
+ {appname}
+
+
+
+ {
+ toast.info("Starting ingest for relevant apps")
+ startIngestion(appname, index)
+ }}>
+ Start Ingestion
+
+ {hovering ?
+
+
+ : null}
+
+ {isFinished ?
+
+
+ {ingestedAmount} / X
+
+
+
+ : null}
+
+ )
+ }
+
+ return (
+
{
+ setOpen(false)
+ }}
+ >
+
+
+
+
+
+
+ Collection and Ingestion
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default CollectIngestModal
diff --git a/frontend/src/components/Countries.jsx b/frontend/src/components/Countries.jsx
index 2cb9c853..590e4caf 100644
--- a/frontend/src/components/Countries.jsx
+++ b/frontend/src/components/Countries.jsx
@@ -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' },
{
diff --git a/frontend/src/components/LeftSideBar.jsx b/frontend/src/components/LeftSideBar.jsx
index 27ecf529..eeae20bc 100644
--- a/frontend/src/components/LeftSideBar.jsx
+++ b/frontend/src/components/LeftSideBar.jsx
@@ -681,7 +681,7 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
- Version: 2.1.0-rc1
+ Version: 2.1.0-rc2
@@ -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, }) => {
{
+
+
+ {
+ 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",
+ }}
+ >
+
+ •
+
+
+ Partners
+
+
+
>): null}
diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx
index 4bf7ad17..07be2cb8 100644
--- a/frontend/src/components/Navbar.jsx
+++ b/frontend/src/components/Navbar.jsx
@@ -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
-
{
- if(isCloud) {
- navigate("/partners");
- ReactGA.event({
- category: "navbar",
- action: "partners_click",
- label: "go_to_partners",
- })
- } else {
- window.open("https://shuffler.io/partners", '_blank');
- return;
- }
+
- Partners
-
+
{
+ 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
+
+
+
+ {
+ 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
+
+ {
+ e.preventDefault();
+ e.stopPropagation();
+ navigate("/partners");
+ if(isCloud) {
+ ReactGA.event({
+ category: "navbar",
+ action: "partners_click",
+ label: "discover_partners_click",
+ })
+ }
+ }}
+ >
+ Discover partners
+
+
+
+
);
diff --git a/frontend/src/components/ParsedAction.jsx b/frontend/src/components/ParsedAction.jsx
index 1e090762..fe7b5649 100755
--- a/frontend/src/components/ParsedAction.jsx
+++ b/frontend/src/components/ParsedAction.jsx
@@ -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
diff --git a/frontend/src/components/PartnerApps.jsx b/frontend/src/components/PartnerApps.jsx
new file mode 100644
index 00000000..60e6e1b5
--- /dev/null
+++ b/frontend/src/components/PartnerApps.jsx
@@ -0,0 +1,21 @@
+import { Box, Typography } from '@mui/material'
+import React from 'react'
+
+const PartnerApps = () => {
+ return (
+
+ Partner Apps
+
+ )
+}
+
+export default PartnerApps
diff --git a/frontend/src/components/PartnerDetails.jsx b/frontend/src/components/PartnerDetails.jsx
new file mode 100644
index 00000000..755e187d
--- /dev/null
+++ b/frontend/src/components/PartnerDetails.jsx
@@ -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 (
+
+
+
+
+
+
+
+
+
+
+ Name
+
+
+
+ {/*
+
*/}
+
+
+
+ Region
+
+
+
+
+
+
+
+ Description
+
+
+
+
+
+
+ Website URL
+
+
+
+
+
+ Article URL
+
+
+
+
+
+
+
+
+
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ Name
+
+ {}}
+ 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,
+ },
+ }}
+ />
+
+ {/*
+
+ Expertise
+
+
+ {
+ handleExpertiseChange(event);
+ }}
+ input={ }
+ renderValue={(selected) => selected.join(", ")}
+ MenuProps={MenuProps}
+ >
+ {expertiseOptions?.map((name) => (
+
+ -1}
+ />
+
+
+ ))}
+
+
+
+
+
+ Services
+
+
+ {
+ handleServicesChange(event);
+ }}
+ input={ }
+ renderValue={(selected) => selected.join(", ")}
+ MenuProps={MenuProps}
+ >
+ {servicesOptions?.map((name) => (
+
+ -1}
+ />
+
+
+ ))}
+
+
+
*/}
+
+
+ Solutions
+
+
+ {
+ handleSolutionsChange(event);
+ }}
+ input={ }
+ renderValue={(selected) => selected.join(", ")}
+ MenuProps={MenuProps}
+ >
+ {solutionsOptions?.map((name) => (
+
+ -1}
+ />
+
+
+ ))}
+
+
+
+
+
+ Region
+
+
+
+
+
+ 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) => (
+ img": { mr: 2, flexShrink: 0 } }}
+ {...props}
+ >
+
+ {option.label}
+
+ )}
+ renderInput={(params) => (
+ 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,
+ },
+ }}
+ />
+ )}
+ />
+
+
+
+
+
+ Description
+
+
+ {
+ 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,
+ },
+ }}
+ />
+
+
+
+
+ Website URL
+
+ {}}
+ 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,
+ },
+ }}
+ />
+
+
+
+ Article URL
+
+ {}}
+ 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,
+ },
+ }}
+ />
+
+
+
+
+
+
+
+
+ );
+};
+
+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 (
+
+ {/* Region */}
+ {
+ // 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 (
+
+ {/* show region image through cdn */}
+ {/* */}
+ {region}
+
+ );
+ } else {
+ return (
+
+ {/* */}
+ {region}
+
+ );
+ }
+ })}
+
+
+ );
+ }
+);
diff --git a/frontend/src/components/PartnerHeader.jsx b/frontend/src/components/PartnerHeader.jsx
new file mode 100644
index 00000000..8cb84c0a
--- /dev/null
+++ b/frontend/src/components/PartnerHeader.jsx
@@ -0,0 +1,845 @@
+import React, { memo, useEffect, useState, useContext } from "react";
+import {getTheme} from "../theme.jsx";
+import { makeStyles } from "@mui/styles";
+import { toast } from 'react-toastify';
+import { Context } from "../context/ContextApi.jsx";
+import {
+ Tooltip,
+ TextField,
+ FormControl,
+ InputLabel,
+ OutlinedInput,
+ Checkbox,
+ Select,
+ MenuItem,
+ Button,
+ ListItemText,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ Divider,
+ DialogTitle,
+ Skeleton,
+} from "@mui/material";
+
+import AvatarEditor from "react-avatar-editor";
+
+
+import {
+ AddAPhotoOutlined as AddAPhotoOutlinedIcon,
+ ZoomInOutlined as ZoomInOutlinedIcon,
+ ZoomOutOutlined as ZoomOutOutlinedIcon,
+ Loop as LoopIcon,
+ AddPhotoAlternate as AddPhotoAlternateIcon,
+} from "@mui/icons-material";
+
+import {
+ ExpandLess as ExpandLessIcon,
+ ExpandMore as ExpandMoreIcon,
+ Save as SaveIcon,
+} from "@mui/icons-material";
+
+const useStyles = makeStyles({
+ notchedOutline: {
+ borderColor: "#f85a3e !important",
+ },
+});
+
+const defaultImage =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAK4AAACuCAYAAACvDDbuAAAgAElEQVR4Xu19e9CvV1Xe3r/vnJOcBEhBSgMEBaoUK9POCOVmAuP0HwcUCNYZSUsh9xv3hGl1qNippQRE20IFEhQoBJiaKVpEgQRnhD+0QHSmRkAsxE4doBZQTs71u/zezruv6/Ksvffvkn/qd8bBfN/3XvZe+1nPevbaa+/XuxX/Tbe6C/ece8qOd5dNk3um9+7Jk/OPds49zE3uyPy4ST5zCr/y4f+sfxO4j16vHsqfmV7gpgm8Yzm/lP9+km1B9+W2zn/zzk2sDeR55ffy3enn1Lf5J/1e3bb42kX43/Do1nt9fUdpLrSbbFt8vvls+idlm/qsaJPWuNK/TfvOLU44577pnPuSc9PvT95/9szu3p9c/IH/c2oVKDbeyB8z/Yz7noNd9zzn3U+5hX+6m9wjnXM7GqXCHpbR0+PnjudGxEtBkzRo02vFtRB83rkAXPqPGD4MnmWGGa3CDqp9+pp4Bwd2dCwPzIXur6D1xaGRXaQz8nf4Kfix1/3joDXtDm0jHVbYspirbZdkj4Npcv/XO/8F59xdfv/o7zz0A1/9yxEAd4E7/by74OCke5Fb+Bvd5J7unDvGHmyCKiMzX40ByW+XQCxvyoSafgGMBT0fgTayXmA/kykr8PI1kS0JIOMPRiSJ7ctOWfuI+mcw4Uj7lO25Y0TQoyHGbKsjJbqXR5HoKijK6nuz3bPhokEX+f4956Z7nXP/8cz+w//bxR/4H00GbgJ3ep170sHC/WxgWecvUJ5AemrLA4NF2cCAa4BDSCbjYVR2RYN2ZXlAOgxZqcXUDLh9toyvSkybopAJ+KBbOpHADOEjoO1EkVYU6LB0HbPUDtaN8N7TzrmPOnfwry9631/8mcW+JnD3Xu+eu3DubZNzT1Xhu8eyxaXw47lWBIzcAW2fGSpoM1WPgzYNWqSRwqjsnSYokgMyvW5JnzWZNrQpC9tC+ZVZW1Ek9GbBQK8ZuS99mrrW1sPh7XXsTeDmqHrvYvKve+j77/8MAi9E1v6t7vnOu7c7555gaM4SuiHGOsajcV8xmeEUMSDFf4yJQLiUmpYNTg906iVCl7b6ltu4gi5lTMs6Z4RpwLQsVMP+IZaNTtYngRoNI1uOzwdYhJxtEhqKAFsdvoI0XHy/m3ZeedH7v/ZxCV7Vir1b3HP8wr3PBC1HDsog5IkJxSdDHAyBlRqBgyHwAANuI3sg3j4G+shSHASQaRlbZtDW8AmiT7b3TLJD8qACrWrwkclYg2l70qACWow5IZvQ9lHQUmeZ7p+8v/Lh7/3z36NDw6w7a9rljvvw5NwPWykOOuEono5YT8IvXQNBK5yhGnwFVoCgZYPYZgvSBtnGMMkJnR2ZrIh3Fga3mGoR9YgpL2xAjbGlBC2YTEGWJu01J3mkrwYGuFP25EF1XMFj9y6Wiyse9p+/9hXl2il78G7n3T9radomMVphpMdGUG9wAJRLYJ4Wz2CZ73QmUpkxa9jlA2fmKvNz0414Fm+Abw6b+U8D4Km+xfsb0l5E99Z+S4mApIHhaEz2bCAPSmMkaNMzW4Av94ac3vtPHdu96TG3f32evFXhuH+ru8J5f4dzrmYPVGiKN7Bfsx/syVj+C07AA3WQmlYBa4TR+bpWnhYCgjyLtN+OBrbezPdAwNcQCjq4EAsaCEAS8BF4PNIZoBKhWdo9PMO0DX5vbmGTRMi41SgyxrQlTcmsVQB+2jt/3UXv/9qdxVLTre5RBwv/m25yz+Qspe2NQWt5ZEQ5x79kMgzaoTC4DM9SuoqDvcEWSqKsNlnJHYvvQ5oWvRutiInrijdwALGBLZ003sEcMl7DSEdHoGTHyoQ4eqTxaqymzdmYuq40wrS1D5A80i+985/1u0de/LAPf+Vb4Y7917uXO+dud84fRcClIOKdVwPGQZRuNBuj6DujMHakCUCgadUsubeMS1A/vhRLBxZEgR6g8juLbUzAE1tWe8wslv9hh9HyQPknZNpEAsUQaJWPDgpttyCjsDxuTMQMwAOiUoTknNv1frrmovfd/wE/1x4cTP4jbuF+3AItwFe81ACGXm0SAC/5UTYGoSBAdwCwmZE9GAqhInmvBr8JPMmAo0zLB7G5YmdMALkTy/fOz5+NonO0VaJl0Bmyh3jDakzL2xL6JnLF8dGjk8xKDBWPtc3euY+dWT7kJX73VveMhfe/4Zy7ONOcgDpnP+a+iC1WLpZJ7RPhArLCPD7snZSVpnmGkgMZTdIrA5BIwOoHBkDLHWs1eRBMZ/Wr2FWGTcFmGQSMZYhjAInAB1C2WQIlrBGUi1JtUaJ8DHrtVIJtwyjZ6bxKjA3Q1n590y8Wl/uDW9wt08LfFgpmCCgx81FroU40KqhUvNLPYu+0NBSYiBUckjDHxtX0eCJJeqBN7cfh2WazKa/FK4MK+wGmHV/t40wbmyoBj8erYDQ5VGYC1VzFmBZpjUzEjGIjhREO5NS2A+f9v/T7t7q7nPc/qZPbYuhZeJdGL6lIMgFQ12gsJUBxwObLxP1KHujO+8nQZT15ANiOsrRcooZVXjDEyyovAzypbDKNQRifdSZiFXTI9m2JQCNBZcAcv+x7w9iF/zEACwlLZEd6ZZiFmWI7vJvu8gevd/dNk/+hPFDa02rwtXOZdZij8axlUoldwnilAYaRDKa1c5ixizxMVqfAEcUIo122RW2OCwv8/aPSotolLn5Y+nBkRWxk4aQuqGu7GOMxE5nPtQcN0DLggvGGYySjRR3L1L77/P6t/tvOuUc0gdvSZb0qL+hxtbSw/tkGma49SB3J2sl4R0SOBqNm+AYbFW9Hz0KgqIPIgWsCgHhzvaZ9rwYsjgKyzZiJMVmNVoiNsi3oWxi3BlkUwshMW7JO35mBu+dc3LmgwNvUfZVNasfH5AHVX/kOvHNBDzbXfX3jZkxnThliwSwtSswcLQDng7haFVUEWb99umAm6+5iyzA0yDbcntDmhQSwo2U5EvPzI3pWOzzDC/OaLtMm0Pm9GbhVpkD0hheX9hZwpxdWPYQ6iiqMweCgUMizB7nBYtXOeKeo0optXC1Mg8WFagMzAtV62mpKwCiC4miBdcCNSRg2aOnQNSu4Amn5MAi2LGyzoF2aaDtLflfCS2kDtBPRtBVf+cr4Dr+XgMs7Tn9CA86uNkDBr4k/AY0jQ0XRsoIdUqV9NXjLUXrywAJ8Yj2LBYrXovsrqMwI1Mhlhn6xUWq/I9tzTKsT1hNO15dNkqV5VFHAE7bTzpGep/6Afi8jXXWMBnBB2CeGXVUeaNAaYV5kD3IYLP5mFpTwlFSkx9VraWv1m+UYNqB46AL2Q6yf2lgr0HRoDX1PBTk1EliEgkO8dPhcLs+Ba9ybTRsuXmVFLI6Cwgoihh7gBcFx4JohKoli8nAJqGhczbIZQIWsynXCSA2mHQ6Don0YuHYY5HvEWsArMKs6by2Wthml9pkzeWwVilwI8KQPrdJEU/rI+xtsa/SfQwKQ1RRlKL1OLwpph6rAtUEbH5z+Xkm3LyFoOMtXwwkLBO1g8XgeSADa7COlzWqASHhKS0RU0rAwqJwyLrOGCUrD4XFKrtqusDRMefEVsfyaVR0ya17AK7GLrWXmsoxGioOKYXB4p0SjCA7ZyqpFNh3KTRG4LbZIf4vMKXa66hYKyh2ZJfNl3Poe6hhW9iCxDGkjBF6nAJwHi5EQT1hwrrBYYatOjkDM5I3FCxrJxmt9OaDs2oP2ZCqMd2jo6osL7SiJbcww1okCfu+WzNK5mXUjXjZafmAc4Lb2ZSzF1IOlafnzqIcaOw9qc0jnKhD6LMByu8Tbx3KhYiAt0ALDZ9uRqUJiAhm9UPagioTS15ZDkrqIEu0U0WBNW5+fbwDABflXjhMiXSxihDZK76x/I4+t7U3ARR1AtbRCR5mxR4TC1myarYhx7RaWcNkoU8t3KrXsMGOsqInoEJqCnA2F7x5Lz88m9m9FOMpu5LqxsksepfBuaquuAsiXYG5QT6vG3SCf1nVCMpSgGTqKHYqN/t4tCxN+sAjcvjopDvnSRiJcLOOW9fkm6LAnjw0sdryxe9Hyqnhetmyr5hSDNqGarLqxsxWkYxhEkydtyX6mz7P2aaerTVwNtENRT/Rfw6mzcJJu8BC4Bf55JEjnGsDV9bRGI1TdQZUgVVdZUUCyeSsKGJ7bOvfALD6viwsVEONOysOvBTxZTxtTekWDC5aSsixcR5weArcxEVt1qw1lxnVAW/ycSRhgU3Xhwmng1haE7BCuV2XSO/0AKn4QyMWKWKQaOYlrhArmVEL3mQOrwR0v7YX4PBlNxwT1nm+ExiwS2gfEVdCOti0afgTYluyR0my9ZVwbtNl17D1szPk6EsSF1FmYg00KuP0TCTVo1SY3GOo7RyL1QFFop2afMmzDrTgEF6eqbGSE93CBdphcT8tlUx/wehuR9V5UBL7i3rfEtPhQPQu08ffBqQoZrFZ7wHGWbGKNAxnfdeUB1dsMuCZoFTC0hGiHQnz4XGaMuN1jLOVVo8YIeMigRT/lk6Tig3bZIHcKW3pwdxZyxnTK0doDBHoJFM6erD1W9kFJwnFNC8HHcMLbB6NIa9LO5EP+obavAheE4HIvCvkpTKk/WUaqupY4ee6cAkSKrqjzOuzTkIkA1HRIw3iFaUHRi9K4lkRoRhEN2jjuKzpkM7cenpWVChnOFKuK1h/L08J0ngU+I11GMWUeu8pAi5eY/d7rYlahT/scEklbsf1JJNSmx3lv1dLyk1usMBqZsFpesEoFhhqcov1aEgJKmrktqQi8KT9Im0UYDMAeBC1tuLFVR/SNs/mcMsQBsV/WCEoTK2GAvvdDPHI6MfGuIVMcR4XvtRY/AHDJAyDTijBYrCZZU8oDYGATOCTE03BOHawJjDpoOERZuUyUORgJ0/Wa0qyGU2ikVU1rH/ckB9bapmT1TUeq8Xpa0L9evW/zFHZDcil5YGtuv5sYV64mSX6tPyPWMxqS5AFlzAIkU37USZJkWsYsEBi54ECwUiy1EA2XbRY7F0zH6NScNlkayYPVJ2J27QGaIOWRk7JrTB5Qs9UdvzpLwPCSbIBroBtEwAa4OVGc/O7rdjiENKAKfnRDRhcXJNs2JmJQG5FoaTKZZFlgIBOMaHFhtYlYn2lTaWIa4dijUdDSvnhwdBMBJyCEUqheAIWWcG25xh85tkAQMSijd4dpw4uMsknO8EPAZTWVZWJiMd78brAiVjyydboMaZxmaTLi1pLgWgcqI00LDGy2O4GvKV309nG9WGNXaWVp0ZY9DeCx72yMgraSTZ2M2hKOTpJUerSXPSi2a0QBIeQr47ZCt4yy/cxBsnUFQJywII/LwxK3k0BJkn8PU2ZEFrTAAx2NL7HGd1ttxEzDmRbd3znzoHS4x2RygYeYKnh5I3wXpkWM1ulXfk2LqBioQI2xAJ2yMwRudhfcvgHgjoR5fGJiZs32ipHWZYpZWvKAGAUyknnvINOG53Mwjy24EJAQh1w53RWEZXw/Zz7iJIp0eIiOfwY5WgbK/AOYRCvGtLT06KZSKSEa7TP6FoELdVE1TDUYQH/v8LkVWXAcfJxpYxtRiDdYkBJWKWiR90MtHs6ibUsmDdqsaRX5dEoTY8pq6Ms2iSdQBDJAKyJclWc0PYpAZrM0cy7xvQoWTcPybe78yMJHHcfQzt3XsslZ+F02Mg+DABQDp4CbeUbx0T71ThF+CM7Sfw5MxkxtJSdj4xOxvjQIoC0YaO5h6xS4w2382C4MtDzSjTOt5i+bWa0l9vAMk6ykEwwtMUOHlMCtoMjvR40IBwNyeTAM9hz00nOLkUswTE1oMnWs07WPn7dzmSx3GZkWnOaNBmx04+Wgpm2A1pJWbZbX8sDnhRTm9YwtCyj4OCAZojUnjY7ZYtGksI65MmwZ2xHg4igqgIvSMyDU9j4SEhpmTsRYgTaUBsVrbSas7IA6hu5DedoRphWar+VQWUsSpxypQNM7MizbNRiQrO02t9soWl1B0zJNrO8L6b1YeCIC5IimTZgR8oU+iLI5AS7RRqWBRgPsExPjBKKlbZTIG89lhtCpPl7XCk3cGNUmwtM7oa1ffGQ7yur3jjK7ZjU88dMTn0wWsdWD28eFBFD4r2RFkCvsYjJt2yELHEmUSsAFxmqlnhrbbTqnqLCiCNvQmAnVqYlHLnDuyBG1LV6xeJMlyeF0s4XgZNPKsZKBN5gsGt1c0RMn8zSS7wk4/DWAWFQ7ePRjNRTLAzftnSWk1l8RG4+QCLQj0kBEa8HA+ccM3OAl5Rqkv4ayBw15AItBUOcMlk+TuYrBhTvyYz/jdn7wR51bHojQxH+EYyl1n4oEzUcO/tGwR79BHEyDb5NBWr+mtsf7hdv/8/vcubtuc9PuWTsPLB6igTtQzBNsO764kAu45nSK5Sj+3GuP6P2IaM/U2l+24WyFG2ItAWttVO73O+7Yle91O0990eCwHl4mLbD/p59zp3/5ajedPSl0KdKk4XeC4AQ7lhdIQhqti0DRATu/333tkbkxMWuzxopYjHV9pi29hsciGZMkaml5EsshcDf2xP0vf86d/vfjwGXkC8d9BXkAIxxzmFD2iIOTd4Fxqw6TtkgPEpqWd6ABWlF7AAGuDCAmibV1fIHhELgPAnAx02YJUse9tzwdyAx8/jU1OeQy29utcEqQSJ1zryHAlbqPAHZM2+QHoMUBJLo7TNsqmjkE7vaAe+ZU4hQJ3Phzn2nR2M6/G1uxY4G1FATJd/PMg4/ANSZEDLhi2bFRLRU6S9JW0bmwUbT1a9ledUpw72LWuL92qHE3gG+QCkHjEuDW2S+oBBiYiJX74TIumT8C0jKPstLj78+95iiXERGsheYz0+Zbw8+t0kSRPVCgLS6MnUWfvmIAfmbcqw6BuwFuHQcuZzRddjk+gR5jWj03wsVE5b0J9LGdArh6GZezcaMAPDNqcgOmh1i8seVBdAoiN+SoUIc5BO4mmA33RuBeo7IKxt438T40EWvIA2Ns65gT/crkCcUc07iUcSVwx8O7jiujx4RWT1dgz6ZCM9igcX/N7TztMB22LoI1cKuuZHMn40gpLoDXWFxIDcd12gmwoSGa7Py51xyb9NfHI413GTCSdnw9ERyR0/un05Ql3LywRJ9XRkM/P152JAH3heuO29/4+2bgnvrla5wLeVxRy5HHQuWjENOOTMK0NIi4MWp4W9F3GaQCB67MHsQKLBTes7vUBunMQ3vHadMxSg4GvXsRvhcbGfcQuOt6YAZumJyhCfpaoMVkFtuoQM9rm+E19Z7cnPk3ALiV4eCsnjKh6NhK1f1EBkDAmx6fvPsQuOvitdxXgBvSYRJUePLMJeGaK2I5IjP8kMhagAdIa5mW78696li5fZWUVepA4MVVmTZLi+pBoxVi5NyDxSHjborcANxfmidnBLhoPqGZMr0abbw0AE8Ij0fa/AcqCfkzGL5TzYzPwB2bSeIwMF7lxfXqaoCvVVxx08cipMOOHEqFtfEbgXttzCpkxh2aiM2vHNluwyf3OLKiOYwBXHLSZwKuqMlU2obYhnQMZgGgxybAs9NNbLHOR0IXgIduJeAeaty1cev2v/x5zrhxQPPsgmjStD8s/H297IG9YVYCV8uDQoxkQcyffdV5BKbjB3WsxpYo84Bmp8ZELK3E1SGa17oX7ryrfnWzydlcDjmBU6bXx8L6d9KZB30KgVGB1chbWD4L3LCz4/a/+N/dqf9wQ5QKa+9a6JwHkV4NU16S3Y0a8GAaMUwVuCZT8swAZNnsp3lCxewUwdj8hhjeo8THqbw4gTsw7q9uJBX2P/NBt//Hdzvnd8TIGlkUGG2MyJGf2FhlzMerBozBKDeWzSmvYvATs3H5fL9w04nvuP2v/qFzB7KemYd4u2BmbAkYnoBEGw1wU5JK8NO4ziXgdlbESKfXYloCbLyF3GbajF713i0A99ydP+v2Pv0O5xdHe6dVJjvSyJEmisBJNTMm52WHKKM+06S7dggyixaF3yR8s6+kCPBmgBR2nyOXdNogB8TGxpWLwIk9ObbY5lRAlgWwgWXtiZ4/+6rz4bkKVe3wF+vMA2ScWtKWjISFuRVmyHYaybQZKNsA7kfe4Pbv+RXnFkcLVzFi6hQSxUhisaI16UhDM3Y2rdKbdGDtzxw0xqwbCSTbjh7ZBByts/0+NgVMxKztU+Raf/aV5xtTMdn5uBrGVEFrIrYFeWB++G5+71wddtV7NpIK5wJw35mAK8v3DEZMA58nG2nRT1yMQBsHqSCxOPTIZKfBnErLcuCpw5hDA0bqaRv73xRiBPhauGD3oolYm2kLb2Hgoh2kwjtaTKM+Kd+fiMUBHWDa7DlzrcLVmwN3LwGXj//IwBrn05rhr2r9qi7QbmM0mPF3+S8l6iEQgN/1o4hk2SAXmAhCS/v2xgC52qo0s3g2IQ2oaTURAMaNg1ZDEjr+xy5xK/dZIR7+nuhF677S1awDF9sB7t3vdG5nlgrFl9WuYXnuQf5ZhypsF8p69SMo48Dg77EkSA4F8f9nabaSQ5Ybx9tGEbhZygsxLRkTFuoXTgBXG0WHGkvTJb3aK5hRo03ytOYZXgBUs8a9es4qvEB57+gvzn3oDW7v0xS4IzWnKGwnPrRCaJa1pX8UGIjtKr9C0LJBRPcLSYePouJba8qLRqSLiL6pPbimRWIK4Ccv46LzglURV2wfAS7WtKU/JhOSgbS+hlhoWDaa7Etqnm0LALU14L7LuZ35bAbLIanhJSAIyw0fgTrCZhq4kTmlHTDo9WmSqG9Ivo20zXiWkofSmQ09mw/0s3aRM8+tkTkBV4NWV/J0BrZ4HfZGrYdEOqkl2hGo5rLGq+/YjHE/8ga3d/e7yuRMM7UuYuZ6UYQy9gCuac1zvBh7WkzbYPSq6dipjkUi9E6DDG1GB093okjxWS/OcEPhHWMny5m0uFDmrVpPE8Dmpb2zrzwe7s8J8OLZTLwAMOa/q3pK5Mn8d7HB/GuN8L2g1rd0KkzONgRukAoIuDKKWBMxzD5kBFJN81geFIXakvJCMgR8kagMSwyoA3o9AVd5bT/Er6tpS1daEzHm0DJn7mepcLzkcfGBxRZoeceK90jW0TMY8DmmTigjMqX+544776o73JF/tInG/VdR44Y8bp6Pyn41Io2haQNwm3laBCokQyR4CJuRd8txi5UFDVnB0D1amogJSZlgRtN8fHD4Q4NpG4sLBULhGXihpwAXAg/qWq6rasPHmTY0rCUNwt+VHha3bB243NDJ8NDvOkxWl2/HQaHfg0Ar5Yd1mLU8twAAiICCk+34Mm6RI+UBtM0GaAcWFyo+xLluJAL7s69IUmHVCvho6UQufdCqrSGmR9qGK4M7/8diS8Cd87ghHYZDIyO2sp9JOxVTVi1Q2E6biRpuhwK7B4ydKWPAG8vTctaMmjL+DoIWEh1dc7E+jRvsGRWWsJ0ixvQLf+YVxye9ImaHbl4sI/aWQaaMQ1oOVG4yrWaZ2Bu5fy3uOTvv6tu3IBVmjXuEgSWHXrL4l3CJQZG7FM0vWUKEdzXiI0X0KMqNkIUI18X266W8eqfLNORBdMrBxQW9/y05CsGOP/OKC+qP2GPqoKHywrU0LdI/NijCWEvA+53tAPeeOR12lEoXcEJ5Z+8ca9s4KKJTgu8tIOcmYwPly0j2YF15kDSz/owtGUcdQUsE8Uke6HYjohqzHwCuwbbprYoBqb6BFpWrYsZkpxo+BwMSP9LginMVtsG4u/e8y/mdo8kxrLZhRwunby9RPW/HCQsJH6nzGHXSjwQFYe4MdvV+zszxMrmZjLRt/tMcbZpRsO7EXUEe1JVXrmmzr4L5hJ6ItTIqFbjaY9TDY/+6wCtQDl8gJ4CPf2D3hzkorBBS+5zEe7fCuD/ndu95p/M7x0S9MAkjFpNNS7d4zJPdzt9/bu3T7KOwLh2Twf6f/r47+F/3haL4pKdY/MrprCpFqGSa3M6jv98decpzNDbRGJEFgtAav3DLb3/d7f3R7zq3v8+iKm3EWPUb719AZ1oNaxaoZ2UbXggmYooI63v8mZtnqWCAkcyelUhuMi0qlgGgbzgLazNcgNiGVJiB++6kcQVmgvUbNRkH++7YpVe48696q3MLq5JKPFP8eOZDv+B2P/HuWiuBpJp1ntbBgTv67Be7C69/i3M7qKa2/e75r/tf+oI7+bYbnIM7IKwNBCgSVPwwnGgnVuWusZVGbXMjEvgzN18IAjzXHqsxLZcG+Ulzco+ZEoKWh7bmh/22xrgWcO0JaiCKg3139NIr3PFNgHvnL7jdT94RMiT1H9J98XcsVG8FuJ93J992o3Nye3oam6YmDY0BTDs3NLCttB+aTFbQVvLV/VdFTnOMJsCt+kM1qK17uW8v2qtweQigHqahEL0zh9RlYMnzrn73hlkFi3E7Kbm57csDd/TSl2wZuEqjJq0IjrNaLt2xZ7/YXXD9bRsw7ufdyV+8MTJu2eWbVYsBPBQVEgCqRGg7fdXUekVMbEXRGj3PgCDjqu81IG+RjYvyIDhiU+y3S//aEwWiBefNktfM6bCf6MdE44qzd/6c25ulwlxkk/91qvZL17YOXA3aktaksixT0/JgY+Dufenz7tTMuEQqzJo2t4RzC2VCDcxqlzHQsu/NjdieXBNwVoFbG1YAaIh8rImTrl0RtPFd8pwyoIdpYXM6V+H8DYF77s7EuAW4/bLG8g2xgz139LJZKrxlfY175791u5+4vTqOsJ0KStSpZscJGndzxp13+YZV2qGUGp4PBWnQ+E6IzbJ56UFKBPAeUhdTgFuS7ubhutb+MKJTVi1NHP4ehJvcNH/KkhLjwm0HuDNwZo3ZTmGpAvCDvenopVf441dvCbggBFcCSY5MkbwVxv2CO/WLN6ZjRkcKgYxJPNS0gnxK20HBzFSSOlrrV/FbF9YK47ZOzGvSONq5gNhyYP9aaKBhGPSpoa1JhdtxVoHurxNGDyYJk2tz7OsAAB/vSURBVLOXuO0CN/afARZOYn18/49sxrh7X4rAnaXCqlvIWfYgTci4IpMMamQOGDABdkQFXH6vP3PTQ8J/K+8ugM3/oTUte6dmjPhIAMbxw/FQnWjacBiAO0/ONtW4beDmEKc02ZaAe+4TdzifsgqqOi9EgTpnrsCYgXvgjv7I5RtJhQxcfSCI1NvAoUJjVpMHHKMD2YPUYVjuGYBL8ixjZ4jFnQsFq+ZMEzeueE1TWiQJIq4pTfWzVNgUuG+Mk7NQq1AdlEwyZ7fzcCKxJeDufuIONy129A5qsxY5te5BAy4GLZNpiWFZDUkGsrqwSJDQ8Dj2CBeSbWuUVlp/ttbpxLhwwhXuIIdDhJeinQtISAPaR3uKoDzQK0nqPIfFgwFcvn08GszQfvPkbAtSYWZc7jjJbnq0+KpjAO4sFd68QTrsCyEdNp09neBmT5BUc3A9bVwJJUTIC2YGACtIkL+3gjkBdyTdRQYxOT37Liz1tMZEJ1xmMnRhWRIfa5gqr5j/ujXgzlIhTc5KDUB+U6PgYzlr3J/eWOMOAldvbJyzChsCN0iFtybgijwuIrIwbO0KrwhbMidQErTnkOnvWjZxh/ZnbnooOLbK8gyZ8mrk7EgDV9K0XAixmWSEU92efv6179pQ477R7d1N0lEJryXlxZxRhLitAfc9fOVMTcZQfncG0NIdffbl7sIb1mfcCNybuufjlqE0vufMCEVEqQqD7jaihHFAVEBe+NM3PZT7AAvd6XjJ1Jj2woIV4kAtLayN0BOx5jljs8bdMnCzCIOaVkaRWeNeNmcVbtsgj/smFxk3Lfm2zqbN6IjyLWUVtgxc5TTVc+OhlpKokFPpjY2McMojUZSv72CA52QWoo8ALvKKGi5tabBd0DIBb0mLLQF3lzJuBgViWhHi5lqFY5sC94MUuEb0YrGWSJeQDtsicK1PmI7kaMsYEaxIgDL7YdByojIiTWLfClzzXIFYexAKpUyPLNICa9NWo8XWaAXaYhTR2ZBVeKc78vQXGDWxCn3qF2c/9PNu9573kHQU0LQgdIdfHextEbg0q5GbifOgpRNbA+7N9HxcMn5pKmIc86kKX6BE6KS8yNhq6du+NwIXApJQPllNYxNGqjkJLAZPdIT7+bEexmFq5wee5fwjH08OZ7bCTz2SiKJ3ef8fuYNvfCWFwLHK+zxgoTrs2T/ljr/830SpACcdDedZLNzZD93mzt39PlEdJvpqRYEHB7iswaY8KExGL+8WzOBa7snn3WDkYf19c/70jQ9TJs8aL8CgtQQsdJ+tS1AY5Cmv6Opy/1WjdmC+YVq6sAuBnQkb+m9W/nPy3zHOh0XSR0wapsktHn6xWzz2B5PBUR/xlp/c14Nv/E+3/NZfiAHtMG0miwLcf7d2OmyenJ18682yHjcCqdQd0H4hYjDkgULViDwgTtuM0s5h4M5LrIVarQHBQr28r/wHuF+xSAVFeS0I0ZwOeEhlK4MwgtR2FNk4pUNJupqW31suD1tn5AilvliGp3aZdz6UMuU2KGoT03VbAu6pt97sptOnnMulIDMXhNEfmYjNrZJVgQZexNYkKAnNRRcWzmOqmDJu2GqjcpmlIVX/GFVEbKwa8kOsL/OcdQvwbEeGNlBse1sbccdCwG3v5I0mJO+WABU/g3DWnp3nMYISgbRtTseFydlmjHvqLTe7aS4kX+Ro58O2myI/ZX/LH7g0iBaZK8wkCyBbrfE1SYZLAdyyKmbStAWKKHrCbU3gSS1Iwm+hQQEMaDgRtoutbNCGx4+cLsNHrOSRi0Pn9nQZNQ0kG8d+2aSbyEGA9F5JBFsE7vLM6Tj0wymvyLQQ3K0QX8IikITMYREG+OnvgXEZ05bWIMq3j/WpTGaFCg7arPO4g7ZFeQQfB20FFAItZkedp+2UNBZ/bLN5LcgxnE+xEWpfo7yQ3r8/55G3wbivcMszJ/mh2tLZGEJr7UHdrNifTEWUA8IJg9o/LkrWCvvTN/4tHuFb+tDwJh5+EXA7tQeQxfSg4vNX03UtUORPcaDySKPgw1xybEUVYTtuWIMIGNMMgnYOyftzHvlFG0uFk295RZQKQ5qWfCTRlGW5Q9LJUZTsEEF6lC5wD1IhAreGUmDgTUsTxWDzAe3sps0mhTXDuWf2RGLdc7zyE2Nbx5jW3Gbf1X2tXcIGAAJwZ8Z902ZZhRWBq+RBq28getfLB2RTmtNoTkrAfXBWxDTLRpiBjX9wCZgP2Nhp19rbY6cNYMBdqsbEwRogoemVUw4yGYvO5YeGNJuBe+nl7sIbtw1c5KTcfpHpLEnIJ2M4cqH79fNaW4n8qRuyVLBZi0646MCYX8UJHQOdRYPY1X0MSPnqmK5p3FvCCARu47yEpr4jjhFFespzG4PYqz0IjRxZrROyaX7trHEvvdw9ZKvARRmAdfO087hlIxGjhgpndu4UlinWCefpUf7UDQ8Hw6+32kTT1Y41WZrkgWvTpcbphIrQcMurka6Vnj7OtDGixfvbkz09sLgQSAMtRxvtbBS4Y5ovtHcrwL3XBY0753HpsRcFEegAv4GJWJJ1Cp+43JWhOyaR+1EPALc/O69s1piIJX1DNWLpCA4zbLUrhwndeStMcaeK9wk2E5OnAiZJAErTin6mgWX61wzvQr4U3bfaEnO5LdZGuWl/PtfhhZsx7hcTcOmBIAC0tfed3djFLiPRmzt3G1MKk9MgcEd1aUp5FbShIzQbbFRGx2LbwRAPswdtXcXDToNVBPhVuGrJAwIKrWn7bJuLnIJ5Z+BetiFwv3SvO3kbyirIxQUEWDGOsW98S1fTmTm4cQTHhBHezKTCUPZglGUB5ff27TfrIpA8qMbralriFONMa7ElYfciLwZmySZwkbYUAyvD7IMGXJny6kc4Kn/gbmHm3Qg/0XZas9p2qcCNoCJ6A7GsxZYogzBSMENYJtCI8ZGQrD5Vz7g8MPeHgfsMI8X+lz9iTZt/S+ZncpZtHB00lt1QOVWk97cB3CIV8p4zuRo2NvunCwt1REc20/Lns3qTKteSmTngE3DlZAwclQ71YZoACRT0vU47gK1pG2F7marDMjHmdhjfHGPNbNQf11qKaCwO0NGJF2CWhsbO1VjxXZbEEoXkz7ncPeTmDbbuBOC+Mi1AiOyBOd5AvxLD1v8cm8TJoaPRsHUclz91wyPA19NX1LTp7XqyMhY+zexB03gLt/P3nuUWj3qiqsfVbNrKO1K1icJY7yw0cr/Z3ngNGlTK3mzQtAhOD8mctnRHvv8fuvN+9PK1tw7tAeAW3+8dElO8mcum0uyhVOBIsQ0niowxf+r6R5Bxrg9KGx7SL+SA6lpauL0dpz/YKALQVrliHqocP15y/rXvcEef8eMEuNZo///6+zknip1tpMczcB8gjFvm1D3QBS9EgEUTTD4PiVegKjIRyTAJhM8czM8gwK1hkdODMRlT4UHOAC2D5t9bjcfF19Qxwgx0seOOX/ef3NGnP39kjA6vARYowD09H3qX9G1vAl1ChwRuf4IZNVej9LExh4mvjRGcABeB1gKenIiN3is7ZkzEWjpwbnzu3CFwN3bGCtwzjT2FGJDG5Nb8nkQsfB2v16WdK6qEOJU/df338DlL+cmQB0ysjeZpqdbtZg6KzMqarzY8d2feObBwx69/xyHjbgDfCNxX15WzFuMJQuGXdspRy8fJwcQut589kF+HahYKcMt9kPHQ/rCkVZjhOnvEkr7hE5X0gEaICsAtuZKUZzwE7gaQjbcG4N726phVYJkYnBExT3TshPiYo1XPjCnDwkpC45beYUkZgNsG7fyEWgSeZ3XQak1RbyWZBzRtQTrZanMI3O0CNwIrwUjOVxBJjeZ4O9+DMCN8axK3cP5klgoDTFsEsjRZR5fm7e9aFyUvszw2r6aEIxNFwcchcB8M4Or9dDwnzjdSDkymmh/1E7KTqoaQ4VBEWHPN/uR1WeOi8BAuzJE6PXcke0BmnKnaB8sDOzyE64s2AitOM3Cvm9Nhh1mFdRFcpEKpDuMTsUi/YvKd5V5Dk4ZLzOq+fvbBXowqkX/yJ697JCBClKcFIOukTnKJGvWkYuSBe+MbQWndfO9iBu7bD4G7Lmqpxj192ihr7JcX2vn7+cSLxlctgSwpMlQhkhBX+hsGLqmuMjVtL9/XqqfV0qJoK5pBiAlxWZqYPPYQuBtANt4aGfc1sB63eeAgmTixRoTBG015oWiL7tWgDXdqxm1sIS/UiXK86+ZpReojhRnItLRO9hC4WwDuH8asQpYKhFBUuktpPYyBsXMVNGj1uXQzYOf98hK48b0EuPZO3Mh86V+Lac2Om/fy4vFinIHdrmFydigVNkHv3hclcOVqGAFYayKW/ja+L1Bo6QCwsXWD3F8O3NIAROP9ukxW6Kws2ikCL4YZAG1oXlqAeMbzNhm7v9H3BuC+ec7jzmWNqxe8yPmKwnY4WD/pXJH2yj+GSrgBTcuW/CvjysM6UFmjLQ/Ce9MXCWHKC6bLonOklEVKIIxuZ5lv3UmMewjcdb1vBu6JN7/aOQjcNN4dps27eHNMZYeENOTFKhMxtO3fn7z2UbF2wUpvNEBX1IN5jb24QCdh8TnDoI2zVb9wFwSpcAjczYD7mnIgSJWE/ZRVwGRvi7qBKRu0AgcN6RmBW3K1eMWEG0auIxuyIkposB+j5gXjQf3W2bJop3ES5uHR3h37x1e6nSc9DZyYWFsMI4BigtbQo0hDrjdf0LlPvLI4cvl9inrm81GbV3inX7j9//1Vd/Y33++m3V3yMMS0+rm5UgtiwyLBzGOQ6FT2IJlE6u48OYvAxVvBe3WZ4ECN0JEeS5cjkcaZFgt/ckynFdIMbVXb2dDUJaQYgOjaZ5UyPvAOfUhfncwa381lDjCSsmShdhC0LaYtDcA2w6fao5SX4SxpTBLjVhasHlQYLzelLAdGXYruyUrHSJUkULMshZWnVYRinPAnAbv2hk8UbdBhHbbWz85Qm8RCrmIQpd0YGOV7UPQiX0EKl4/pUnMi1tKzaexKmGfjI9+7yvjb8oAHRm5L/8C1fycMNWvzgKfKWR4AvIBeAnprGRcydWlw9JcsQWqvOCAAw/L+dfZClft72Q3ECLmKDYCuGYmSqXofCmGDJPaIhed3nCq2obC2crBGiI9OKckKOTtxIIIALC2a516QI/bpe1IxeQFuk+IZ2o3UyUCVVxMU7WqjYSMn8Ofrc8ubmqwYuKG3ufuDY4NybYX8poE2ugwmwfTLRjpKvXtkN25+S2FDnTPXJJADcRFJNfUzuoVcO23SoqLbTdCGHsOT0ZPzBMYtT+zWD4A0Wbm5X0wcL0XhV3upmcxuhbOWntUTxTqQPZbtMVFiI9W0DIyATJzHLhFEaVZiE/Jg9n0OOjfp2qUCipFATx6UXSfCASm0KQYEPNdg2kju1KnyL4gNI3DbIZoc1oAFdy97UHWIBC3SZMjAeBCZRiRM25cGIJxB8LbbV8ZuKC3Ebcfw0vrMKGHbsQOpcd/GisDB+LZ2+7YcepoZE+3kbDOtiqyl/7xt/oFrMuMaHpVutLeQG3oujWr3fFrGJo0Dz1i4lNKlhkXNeiOatqdnRdilrNLdxo21cHhEAqxyNPb8GqWKmjOjnLBLKnipcon0A4YHfn/zIG1jPMpjIRl25YFm2hTNBJE7/8A1F4Mu0A5IQU4eMTMNt3rNPLCnji4uoHc1WI9IgzHACjYKN422TTNZ6/zWGu/sKBW/I0b/YYek8qACo00YuHjfdnjZEnQKOLtGyLISfQygWcUy7D50OqchXwFwA0OFlTyac1Ne29XDuUkjmtZyDgO0gnrW0sPBwAi0CaCNMBj+1DrzoTJibmlRS6FH6SMh0NnYSNZJWOWHAflCSkohm7dCvLkiJhwFMKpdq4JLE3larvv8aMv0XgFcoi9N3TaWdomdGGWzkYJlwnjJ8Guddg21rGA9hShp1JEMALGlpDOlaS0mlCmvEdnTOenSiJDFX/KsnaVIUfvqPsfqazjXrKKKkIfx7zrKS7MFvihZBSoVClgNBoThgQ9q1rRmPa0AhQYf8jwNWjOP3FvNCr1HTsWYLDElCPOwQF4OrL4vPBDmaVV/p/q1PLptCjGtHPD4s8nkHabNk2x+f5vh87W4ymtuSv7yH0+X1qBpSB4gYPOhiPMd/oFrHl0vWW3XQnKI2jH+Lsq2yhNDu8eq7PHgFPzVsGzWRZRLWlGAi63ctxrq6945khi326bat+I3xCog4oJBs0CbpKwUoYKQrrfbrH0kUsEAbx+ahAlbpTsZBqwon3CZ+8aBS8IvoujxlNlIyqt2ooLdCpc6TGvQosUP9LyR7MEoy7ZBy9o4mO6i0qrYBQJvhGkHGDqFaO0U6Pn8d7V9KLdv1x4Qh4yRv6TMjHrt1H9JwJVxm2wLBpPuww/3rjbRWSmcEYkC70NhhWkmJA2yyxuhqjCCpemlfBGSKXs/BG26l7e7hFXmzEN9Q9KuHeJrFBqRFlj64AnqGhOxVo2FOPC5sO4sFXDFTnsJN8X69P8MYIwUvMBGE6OX2DdUZKO1aRl4EAkUKAD4WqG2OlSRFJxpUXi0IovczTwAPMFG8Q5iJ9Y/g3zUSeDyvei+egAdj84StMiZAVEwsZoB0d506R+4+jHhNjWGAyFqlcWF+I5sWrAOTVi1yM1STs9BW7A8NBFbLRLQd0cYGA5TjKYHJ7bPp7QXHdoUDpkQBSmv3kocfzfW3C2nJJqRj33oS0yGln/1PzNLmSWmtKsEP9mrY5OovaztXDHKtaJrAO5KoE0fr4olZauBYizMa4/nHYdhVqRTwjX6OPtsuB7T9pwWOFkBfH6HtbgA5AH/1YA2Je/v34s0ewMUzKnKvRGz4X87KS9hG9i+YizM5vYnFeq7/YnEuIzyjeR69bh89Xi9KvQ6jWRwYLAd4hArVO2G5Mto+G2cd9UDbSt7oPoL5AGyidLrnAVjnwf6BhcXkHTRNsfFMgAHUB6OMm0FZjUDllYAuI0kN9NDI9mD+NLciLi1jRReIBC0PHY4hIa2ZV+p+B5e7QM6jMVUNLC19oCRgAJd/Kt50Am7WbeD6gI1uAz0oI2t/rfunQtmwuG2vGetZVyTafPOX4O02gxdHaACN4dHFiq0J8cHj6SV8ApOWEk2NFQ1DDvSPw50oy6Cecfgd3vRztFWlRt/B2A8xbTZkJIJQS2tKrkUoAPSpUaw/kdCYqTkJMJY2nDKONaWdLFTXuxxQ5q2UevLQnW1iwYu8vgm06bOKQ8ywhnybK4j9P63XmV/eaZRFyFOHIyjIcGxWlqIRBE3rZCnpWGgAKNhu+gwDWnAUaIK3MOjW5NYk2mj45knJrIP9RrSQmLJkEH1cEN6Q9t5/YmrHwu+uiPYsgkMrEEboSK1jmuXOKDohHNDwCsmGK+LoObJA6sCjXo+b0cBbm8ZV9iO2iW+UzKm1nTr1tKaac6OHi5jAYGW7Fz+xp2q2pHay1pcWD0K5LEDwK0PC40wQWtV9MuVFOQ5PARV+wCQ9lJe4eYx6aJZdmQSRtpKhXMALP+bFX5nPZt7phzaYrz0ewpaBQqTLfO4rTYWNYokDLDnr1lLC48oWC+K0MDsT1z1WOFXtdHRUKOgiIPIoz7SR5hRIOP1QmgIg6PtE4zZ+kq3ofmyt+McrdCDpEMZuNU2I1VeiI0szTkie9S9Kl1YzK1OJWqDlvWLhbNGtBRjq0jAZPT4TAHcCrT6XCN7QAaXhnkOQAlcBdry0eJ8XzwkZORbEtlCxooYawgIZ0O6DxveXFxQepO3DYfudkQaZOiMnWiU1m7cFkuPTMSgZpbRBxEWQfSwZscROHQ2Mi6XB/E06YZm7IGicYJNNGxRKoMrdmhw12daXR2FmMwA7cBELKJI1tKO1THTnQv9KKSjSO5bm0C0/MEZlb48yPKI+cNgLTeTiL3oKivEMnDDfeXMA6QbLS8aKQLnTJvDAjVu/O9+agfX0iaF0tPDvS8agvBU5QHcQp6YTh4XhTTtWN9wcXznKKs0dmZtc4NlC49AsJGJWBgg4SiWxlcgrHUvhpSs8tp4pjzbzp+46pKI2fIyYxm3AQoVzpBGJBetVYeL9SwEDpqE5ZOyNYvJAeHRh8qg/KHoGvSkM4O6gzwQLUZpRCBcoM2lT8jHkE+PMyZDY8FqU8K9ot5hlGll9snQtC1p0LBLQbOqW/BBKuy5yR+J/VsNtNqx2qmdzLRUv+dQAyoqRE7SOqwjhb2WdjM/oEKBZ0mDOWkhC2aUVmeZjbYu5WE6a1JoS0r3xWjCscKNqO2N+UUB7np1B2U+wirR1gCt6p98hlkhtu9PXPW4b7tpekRT0zKkNcIF8h7GtMjIIzNsJF0QYPXzYzgxjFqkifH3/pFIOfIV4Cq2Y4wn2iekCTNfb3m6PHcctKSxkSda8kA8H0bVAjwLtLy/MdLSVrD5jviDPMaUveM7/q+vvOSPvVs8BbKgYXTaibJo0GC8ONlDE6yG0Zk3GsU8LO6DZ/U+WWSyVXw5XhEjg0HkFQcFmg+g/sdnKUD1loDTuJgLEx0mGzpdxgSu7EefaSuMUKQyMJBsC4nATX/iT1x5ya9PbvFPOHA1C+Iw3xggAWRFxpss4w5M4rqrRg0mDm1dhuR9Z39ZjAR9aQAijZglF/t3J5irTGKN6KgGo69p4fg39ojRgvaIJp8OLGQCGy9RJ80OAR/Gxt3lT1z5fbdObnqzc24nGs8GYx98yKMQ23aYdorZ3FW2tzOSaR7rn9+NBrWeeaAjEGZaFZTW3tHRtn3tHxofdG/pXyX0FGH4OKI8eLxFRgK2JG9Fq1aUUwDSUWggihwsJ/cv/Hdf/vhnOr/8qHPu4hgf5ek0cjWsYaTc01KUk79LSJiLe2m6gwLeAm1nwNLIdpmWHLNJwVls2tuNG2yEKrySXTqSiebM6/uxw4PsCGcuTjRCI0rHRPZr11PX4VxNHkA93LJLrv6DbMGwMV/5zQO38yL/zZf+gwuPH/nrDzvnf0Ks1zIaV1oDNYTpkrZ2yTgrnzRkz1uvYGbsXIDarvzKIJUhYC0WHFlcwJqe+DYZprYmDu3s1SIb1VosxFsSofzekBalpfZWm6ipetucgGRKztfbfl9OsHHut/z+4qdDS7/78u99qXP+Pc65Yxz0sSNDoE254AACMRELv1tb044zLawuY6Gbgza2Nf8z9ogpLSCWcY1CccaWBBiQjdQ7BIBSWM6/tccDkYVle1lPq+/lZx5YoENLzOJawzGi5VEtLopA4Xf7y+Xy2ks+8bvvCz+duO5Jj5x2z/5X5/xl5WEEwbbXElAVFEiwDyx1lo6tybRVV/FwaUx0FPHMxhs6gC5OxBjghx3SIAEGWgC8BFrYZnBvNUBiP4tljfcqp8g4MBh/nJCAY8C5iARt/dk7/wd+37/w4k996i/Lb7/7su/7p5N3t3vnLmBbnLmO0uHNMF69cN08rel1LCjEMArYoLPSZ8sDFOLn5wNduwJo+8BDfYjRqzoKsom+rxBNC7Qj42bZlkYpJg9J9Oo45CDgaQXb6YMDd+3jPnnPh1iPv37dYy64cPfYrzjnXtY3MtZk6r6B1I5Re4DBSIuByASQIVmFbhQCE2u2CmaE5t460ypjaYdpzrAb4Xc8T8ulU462SkJpQJQQr1ROpwA/v6OpaUHfls5/cLFz/vWP+djH5s9gcjF64p8/8UnLxXJG9FOZZGANbzMhCzWtwWnJgx6TldoKCUo7zJSIlw6fy8u4ZmgMI1KlQR2gxrI29SCyvq4Y07Jnjm6TONpARj0hy1jfeCERkU60YCZlqFuTqdBhrJlRJVlvgSk0hETGYXKMfb/3YOFe8rjfuvvPBKdXi//VlY9/rp+m9zq3eEL4bRO03GOLPQeAZzItrgnlH97oz7Dh+n3oymD2II64XFxQg5mAgR1GsfTAwgkFhXKqBuCH5IHB0uVe5gHtmmgOPBSBgYQhwBVREnhqtqn/2r7zV37vb3/qM/Qe6FLffdkTnz/56e1uchG81OMZmEX6I4xU6TDxdjmwjVpaajz53sJIwp8kI4msBgPQQD1tbAKo9OpJnzSaYwdN48kKH1Bh+x7wFI3JzIEGUxxO2pb26TJqeMzic/7MzLgQ8AJT5B33T37nlZd8/JMfl3axYoH7q5c94TneTb/knH8qWk0bK020JhNrFIH3wowxqNUIMXOg9JsaCSYPkr2xpmfGFO9XA9Rhy1iW2JM+OQbWsynCYyGTCdACeaGYlhCDciBgf72ShhyDE5mKIsz+DOz3Huy4Wx73sbt/DzmzCdz54u++/Ik/sJzcG72bLnfOX0B2Log1/E7Kiw1ae7WmNrKChXYWAg+AgodplO4iRi73V2nQ1qVigIDTMFA0QFsYT7Fley5RSQp981fWHuhJWPwNjZhGuE+AVc0bmoQlO1kH62HQnnaT//WlX77pkt/+9FdwBMKFnOzasLK2OPmCaXKvnpz/Ye/cUcbspm6JhRWZIzbZiavDGQqhIPTmy1bMHnBjDUzGoESAQNDnma37OabmMmmZiCX/1cDlQ29p1M7ignbIzBfRhKqQSIwRd/hdN/nPueX0Tnfs+G/k7MHawM03PvDSv/uofeee57z/Se+mp03O/W3n/E63AJywmQIE8wAGxmKAsXwfN0h5ZW8ixt4PFhd65YUEsCoSaIrSS+jDs3YOvPCuXi0t6xvhD1R0bk2m4a6KyqIqOrIBbh8TmoB94Jz/lp/856Zp+V+Wx93vXPLRT3/bAiv9fVMqoAcEBvYP/NByWlzm3OJZzk1Pds5f7Nx0kXPuCF7qNHbiio6q9yWDKr0IBkXei2sPLI9HW25GmXbFLfmFiRIALBsA+VG6rYEGvrWAn6/8ySxNRCwtnmmtpqVFk8i4lZC8c3uTcw+45fQN5xdfnvzyD5bTkc8e3Xf3XfypT50aAWy+5v8BUrIHNHvQF7oAAAAASUVORK5CYII=";
+
+ const defaultLandscapeImage =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIMAAAAcCAYAAABCrQzwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAARPSURBVHgB7VpdUtswEF4zvJOeAHECwgnqzNDnpifAPQHhmc4QZtrnhBNgTlD63HZiTpD0BLgnIL1A3f3Qqiiu/0KcOC76ZnYkSxtLWq1WuxsTOTg4OKThJR+PE1ojvA/fPHJoBXbIwUGwSy8ASZL4XCjUPc8LqQHIHEB7TD+YIp5LnOLpcBEw7TP9Ep5I+vpcoD82bXWj1crAAlJczFk48xLWgOlE6iFtEDLHCYkyWnjHFFt8F1wMUzxvmY6kPpJ3hEwRrQFZyjCncuE2Ajk5p6RPmG+1Y763TDfrOjUrAJuspB4x3TEdkrWhojBDecRabqQe0waRpQwz7/xrj7YMBScMMOY1YL4xK8QZbQ/6UoY8r/clPECP+WbUANrkQNqKcMkEhT0gbUYh5Fj6BqwQ17Q96Ej5swIPNaUIQCt8BtsBZODkD1MsEGDIfCFp36A0nJUrRzF1tvBqWQliRZU8zryK135bLIOy6nd5TLzogIszKTMBJWCClXlgmjJN+PlBHLgs/vtEY5LTP0yeoKTNNw0W60WyCFxpE+G5sN5n456WACIOmSd+NxHC2q7N3IpQyTIkn94ERL9fL7bu3LFvES7yHfOiFgf1zr/n3ZPPBeZxm9fJijCmYkBAXdKOGkiRNtPY1M6W+RuVkYpGsK6Y9LoUaX8KCtpLh7M2qlmGhD33hE+bTf8oRx5fLcDmG1M3kNM6kOtjWSjS1uMVE3wOUGy9u0MrAteOJ7CaQ28ReO4Jz6XFd2DxHFQZj+cM5R7K442840h+jxDWKH2hL9WKa0LuPFgYoxCKdNw9MSacaVRROS5t6yEn5crq71P7YK4ZJKQC20fgOg6SWZ9fpOytSTphUbwQOIpD0nF61+r2hQbiRJ4VOE1hRltjHnxN8KWc5RwIWxaQW5TB064MpJziAHXRcF/okJ4Egn5FOvTMesdWJtSeC5GDOe19KrdsBZbBezxJFjIcDA+alKT+3dzJ9eo3AdnUWyETTiE6eFQScQb/q42vgJjKs5a5/btVvH2JGkJqEAjhSOcYMjcYVoN54DydShOUYhPKsE8NAvKQdDzW+4WfB/RMtMKBlIwinKRRibe/Z9XrVgSV0+5T8zA+z0mWfGA1kdcoeUdrkk5mgQHTVBI2ynTKYkfSD4Q1XhHmOlSJlZiS5BWUVNFmoTLaTGgKOV2nZIO2z9I+pQK0JbRErGzCI0U6Xr63snzIuBnzGNNi3L4qxlZ9KNlKjIcMZkAFCbCaYU6/L3mWx1wLGiSdbtYMBxJ9U2ueJvK6Khpgl7OLS6U8C1FDwiYPuAt5cRA8fIIsjxmWAIsd1+k44o8jHhfKaL4nMN47xsAGRLSZ3AQyo12Zg5K2mTVPKGpMWj5dWgy9I9L5lahogNZ+AylZN6N8cVGadQ1jbmS8gjmQzGGew4M5mpR7XPVwtPZLpyb+6vUa/Ht5mTnI5ke0JNwHsQ5/4ZTBwcHBwaEAfwCepixlSVZHRgAAAABJRU5ErkJggg==";
+
+const PartnerHeader = (props) => {
+ const {
+ userdata,
+ globalUrl,
+ isPublishing,
+ isCloud,
+ partnerData,
+ setPartnerData,
+ loadingPartnerData
+ } = props;
+
+ const classes = useStyles();
+
+ var upload = "";
+ var landScapeUpload = "";
+
+ const { themeMode } = useContext(Context)
+ const {theme} = getTheme(themeMode)
+ const [isDisabled, setIsDisabled] = useState(isCloud ? userdata?.active_org?.is_partner ? false : true : true);
+ const [file, setFile] = React.useState("");
+ const [landscapeFile, setLandscapeFile] = React.useState("");
+ const [fileBase64, setFileBase64] = React.useState(
+ partnerData.image_url
+ );
+ const [landscapeFileBase64, setLandscapeFileBase64] = React.useState(
+ partnerData.landscape_image_url
+ );
+
+ useEffect(() => {
+ setIsDisabled(isCloud ? userdata?.active_org?.is_partner ? false : true : true);
+ if(userdata?.support){
+ setIsDisabled(false);
+ }
+ }, [isCloud, userdata]);
+ useEffect(() => {
+ if (partnerData.image_url !== undefined && partnerData.image_url !== null && partnerData.image_url.length > 0) {
+ setFileBase64(partnerData.image_url);
+ setFile(partnerData.image_url);
+ }
+ if (partnerData.landscape_image_url !== undefined && partnerData.landscape_image_url !== null && partnerData.landscape_image_url.length > 0) {
+ setLandscapeFileBase64(partnerData.landscape_image_url);
+ setLandscapeFile(partnerData.landscape_image_url);
+ }
+ }, [partnerData]);
+
+ useEffect(() => {
+ if(partnerData?.image_url === undefined) {
+ setPartnerData({
+ ...partnerData,
+ image_url: defaultImage,
+ })
+ }
+ if(partnerData?.landscape_image_url === undefined) {
+ setPartnerData({
+ ...partnerData,
+ landscape_image_url: defaultLandscapeImage,
+ })
+ }
+ }, []);
+
+ const removeImage = () => {
+ setFile("");
+ setFileBase64("");
+ setCroppedData(defaultImage);
+ setPartnerData({
+ ...partnerData,
+ image_url: defaultImage,
+ })
+ };
+
+ const removeLandscapeImage = () => {
+ setLandscapeFile("");
+ setLandscapeFileBase64("");
+ setLandscapeCroppedData(defaultLandscapeImage);
+ setPartnerData({
+ ...partnerData,
+ landscape_image_url: defaultLandscapeImage,
+ })
+ }
+
+ const surfaceColor = "#27292D";
+ const inputColor = "#383B40";
+
+ const bodyDivStyle = {
+ margin: "auto",
+ width: "900px",
+ };
+
+ const appIconStyle = {
+ marginLeft: "5px",
+ };
+
+ const dividerStyle = {
+ marginBottom: "10px",
+ marginTop: "10px",
+ height: "1px",
+ width: "100%",
+ backgroundColor: "grey",
+ };
+
+ useEffect(() => {
+ if (file !== "") {
+ const img = document.getElementById("logo");
+ if (img) { // Add check to ensure img exists
+ var canvas = document.createElement("canvas");
+ canvas.width = 174;
+ canvas.height = 174;
+ var ctx = canvas.getContext("2d");
+
+ img.onload = function () {
+ ctx.drawImage(
+ img,
+ 0,
+ 0,
+ img.width,
+ img.height,
+ 0,
+ 0,
+ canvas.width,
+ canvas.height
+ );
+
+ const canvasUrl = canvas.toDataURL();
+ if (canvasUrl !== fileBase64) {
+ setFileBase64(canvasUrl);
+ setPartnerData(prevData => ({
+ ...prevData,
+ image_url: canvasUrl
+ }));
+ }
+ };
+ }
+ }
+ }, [file]); // Add file as dependency
+
+ useEffect(() => {
+ if (landscapeFile !== "") {
+ const landscapeImg = document.getElementById("landscape_logo");
+ if (landscapeImg) { // Add check to ensure landscapeImg exists
+ var landscapeCanvas = document.createElement("canvas");
+ landscapeCanvas.width = 350;
+ landscapeCanvas.height = 120;
+ var landscapeCtx = landscapeCanvas.getContext("2d");
+
+ landscapeImg.onload = function () {
+ landscapeCtx.drawImage(
+ landscapeImg,
+ 0,
+ 0,
+ landscapeImg.width,
+ landscapeImg.height,
+ 0,
+ 0,
+ landscapeCanvas.width,
+ landscapeCanvas.height
+ );
+
+ const landscapeCanvasUrl = landscapeCanvas.toDataURL();
+ if (landscapeCanvasUrl !== landscapeFileBase64) {
+ setLandscapeFileBase64(landscapeCanvasUrl);
+ setPartnerData(prevData => ({
+ ...prevData,
+ landscape_image_url: landscapeCanvasUrl
+ }));
+ }
+ };
+ }
+ }
+ }, [landscapeFile]); // Add landscapeFile as dependency
+
+ var image = "";
+ const editHeaderImage = (event) => {
+ const file = event.target.value;
+ const actualFile = event.target.files[0];
+ const fileObject = URL.createObjectURL(actualFile);
+ setFile(fileObject);
+ };
+
+ //console.log("USER: ", userdata)
+ const orgSaveButton = (
+
+
+ console.log("save")
+ }
+ >
+
+
+
+ );
+
+ const [imageUploadError, setImageUploadError] = React.useState("");
+ const [openImageModal, setOpenImageModal] = React.useState(false);
+ const [openLandscapeImageModal, setOpenLandscapeImageModal] = React.useState(false);
+ const [scale, setScale] = React.useState(1);
+ const [rotate, setRotation] = React.useState(0);
+ const [disableImageUpload, setDisableImageUpload] = React.useState(true);
+ const [croppedData, setCroppedData] = React.useState(
+ partnerData?.image_url || defaultImage
+ );
+ const [landscapeCroppedData, setLandscapeCroppedData ] = React.useState(
+ partnerData?.landscape_image_url || defaultLandscapeImage
+ )
+
+
+ React.useEffect(() => {
+ if (file.length > 0) {
+ setCroppedData(file);
+ } else if (fileBase64 !== undefined && fileBase64 !== null && fileBase64.length > 0) {
+ setCroppedData(fileBase64);
+ } else {
+ setCroppedData(partnerData?.image_url || defaultImage);
+ }
+
+ if (landscapeFile.length > 0) {
+ setLandscapeCroppedData(landscapeFile);
+ } else if (landscapeFileBase64 !== undefined && landscapeFileBase64 !== null && landscapeFileBase64.length > 0) {
+ setLandscapeCroppedData(landscapeFileBase64);
+ } else {
+ setLandscapeCroppedData(partnerData?.landscape_image_url || defaultLandscapeImage);
+ }
+
+ }, [partnerData, file, landscapeFile]);
+
+
+ const alternateImg = (
+
{
+ upload.click();
+ }}
+ />
+ );
+
+ const zoomIn = () => {
+ setScale(scale + 0.1);
+ };
+
+ const zoomOut = () => {
+ setScale(scale - 0.1);
+ };
+
+ const rotation = () => {
+ setRotation(rotate + 10);
+ };
+
+ const onPositionChange = () => {
+ setDisableImageUpload(false);
+ };
+
+ const onCancelSaveAppIcon = () => {
+ setOpenImageModal(false);
+ setOpenLandscapeImageModal(false);
+ setImageUploadError("");
+ };
+
+ let editor;
+ let landscapeEditor;
+ const setEditorRef = (imgEditor) => {
+ editor = imgEditor;
+ };
+ const setLandscapeEditorRef = (imgEditor) => {
+ landscapeEditor = imgEditor;
+ };
+
+
+ const onSaveAppIcon = () => {
+ const canvas = editor.getImageScaledToCanvas();
+ const newImageData = canvas.toDataURL();
+ setCroppedData(newImageData); // Update croppedData with the new image data
+ setOpenImageModal(false);
+ setDisableImageUpload(true);
+ setPartnerData({
+ ...partnerData,
+ image_url: newImageData,
+ })
+ };
+
+ const onSaveLandscapeImage = () => {
+ const canvas = landscapeEditor.getImageScaledToCanvas();
+ const newImageData = canvas.toDataURL();
+ setLandscapeCroppedData(newImageData);
+ setOpenLandscapeImageModal(false);
+ setDisableImageUpload(true);
+ setPartnerData({
+ ...partnerData,
+ landscape_image_url: newImageData,
+ })
+ }
+
+ const imageInfo = (
+
+ );
+
+ const landScapeImageInfo = (
+
+ );
+
+ const errorText = imageUploadError.length > 0 ? (
+ Error: {imageUploadError}
+ ) : null;
+
+
+ const imageUploadModalView = openImageModal && !isDisabled ? (
+
+
+
+ Upload Partner Image
+
+ {errorText}
+
+ setRotation(0)}
+ />
+
+
+
+ {
+ upload.click();
+ }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Cancel
+
+
+ Upload
+
+
+
+
+ ) : null;
+
+ const imageUploadModalViewLandscape = openLandscapeImageModal && !isDisabled ? (
+
+
+
+ Upload Partner Landscape Image
+
+ {errorText}
+
+ setRotation(0)}
+ />
+
+
+
+ {
+ landScapeUpload.click();
+ }}
+ >
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Cancel
+
+
+ Upload
+
+
+
+
+ ) : null;
+
+
+
+ if (loadingPartnerData) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+ {
+ setOpenImageModal(true);
+ }}
+ >
+ (upload = ref)}
+ onChange={(e) => {
+ const reader = new FileReader();
+ reader.onload = (event) => {
+ setCroppedData(event.target.result);
+ };
+ reader.readAsDataURL(e.target.files[0]);
+ }}
+ />
+ {imageInfo}
+
+ {imageUploadModalView}
+
+
+
+ {
+ setOpenImageModal(true);
+ }}
+ >
+ {partnerData?.image_url === null ? "Upload" : "Update"}
+
+
+
+ {
+ removeImage();
+ setOpenImageModal(false);
+ }}
+ >
+ Remove
+
+
+
+
+
+
+ {
+ setOpenLandscapeImageModal(true);
+ }}
+ >
+ (landScapeUpload = ref)}
+ onChange={(e) => {
+ const reader = new FileReader();
+ reader.onload = (event) => {
+ setLandscapeCroppedData(event.target.result);
+ };
+ reader.readAsDataURL(e.target.files[0]);
+ }}
+ />
+ {landScapeImageInfo}
+
+ {imageUploadModalViewLandscape}
+
+
+
+ {
+ setOpenLandscapeImageModal(true);
+ }}
+ >
+ {partnerData?.landscape_image_url === null ? "Upload" : "Update"}
+
+
+
+ {
+ removeLandscapeImage();
+ setOpenLandscapeImageModal(false);
+ }}
+ >
+ Remove
+
+
+
+
+
+ );
+};
+
+export default PartnerHeader;
diff --git a/frontend/src/components/PartnerSettings.jsx b/frontend/src/components/PartnerSettings.jsx
new file mode 100644
index 00000000..a979c7d7
--- /dev/null
+++ b/frontend/src/components/PartnerSettings.jsx
@@ -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 (
+
+
+
+
+
+
+ Configuration
+ {Object?.entries(partnerTypes)?.map(([key, value]) => (
+
+ {key.replace("_", " ").replace(/\b\w/g, char => char.toUpperCase())}
+
+ ))}
+
+
+
+ Set up and manage partner details and information to be displayed on the Partners page.
+
+
+ {isPartner && (
+ <>
+ {/* View Partner Page Button */}
+
+
+
+
+
+
+ {/* Update Details Button */}
+
+ {isPublishing ? "Updating..." : "Update Details"}
+
+ {/* Toggle Publish/Unpublish Button */}
+
+ {isToggling ? (partnerData?.public ? "Unpublishing..." : "Publishing...") : (partnerData?.public ? "Unpublish" : "Publish")}
+
+ >
+ )}
+
+ {!isPartner && (
+ {
+ if(!isCloud){
+ navigate("/become-partner")
+ }else{
+ window.open("https://shuffler.io/become-partner")
+ }
+ }}
+ >
+ Become a partner
+
+ )}
+
+
+
+
+
+
+
+
+
+ )
+}
+
+export default PartnerSettings;
diff --git a/frontend/src/components/PartnerTab.jsx b/frontend/src/components/PartnerTab.jsx
new file mode 100644
index 00000000..10c21f06
--- /dev/null
+++ b/frontend/src/components/PartnerTab.jsx
@@ -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 ;
+ case 'usecases':
+ return ;
+ case `apps`:
+ return ;
+ case 'articles' :
+ return ;
+ case `ai_agents`:
+ return ;
+ case 'branding':
+ return ;
+ default:
+ return ;
+ }
+ };
+
+ 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 (
+
+
+ {tabsOnPartnerTab?.map((tabName, index) => (
+
+ {
+ 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}
+
+
+ ))}
+
+
+ {renderContent()}
+
+
+ );
+};
+
+export default PartnerTab;
diff --git a/frontend/src/components/PartnersArticles.jsx b/frontend/src/components/PartnersArticles.jsx
new file mode 100644
index 00000000..80653231
--- /dev/null
+++ b/frontend/src/components/PartnersArticles.jsx
@@ -0,0 +1,21 @@
+import { Box, Typography } from '@mui/material'
+import React from 'react'
+
+const PartnersArticles = () => {
+ return (
+
+ Partner Articles
+
+ )
+}
+
+export default PartnersArticles
diff --git a/frontend/src/components/PartnersUsecasesTab.jsx b/frontend/src/components/PartnersUsecasesTab.jsx
new file mode 100644
index 00000000..87e34b52
--- /dev/null
+++ b/frontend/src/components/PartnersUsecasesTab.jsx
@@ -0,0 +1,1761 @@
+import {
+ Box,
+ Tooltip,
+ Typography,
+ Switch,
+ Button,
+ Dialog,
+ DialogTitle,
+ DialogContent,
+ DialogActions,
+ TextField,
+ Select,
+ MenuItem,
+ FormControl,
+ InputLabel,
+ IconButton,
+ Menu,
+ ListItemIcon,
+ ListItemText,
+ Checkbox,
+ CircularProgress,
+ Skeleton,
+} from "@mui/material";
+import React, { useContext, useEffect, useState } from "react";
+import { getTheme } from "../theme.jsx";
+import { Context } from "../context/ContextApi.jsx";
+import AddIcon from "@mui/icons-material/Add";
+import CloseIcon from "@mui/icons-material/Close";
+import { toast } from "react-toastify";
+import MoreVertIcon from "@mui/icons-material/MoreVert";
+import EditIcon from "@mui/icons-material/Edit";
+import StarIcon from "@mui/icons-material/Star";
+import DeleteIcon from "@mui/icons-material/Delete";
+import AddCircleOutlineIcon from "@mui/icons-material/AddCircleOutline";
+import DeleteOutlineIcon from "@mui/icons-material/DeleteOutline";
+import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
+import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
+import { Link, useNavigate } from "react-router-dom";
+
+// Helper function to get the correct image path based on app category
+export const getCategoryImagePath = (category) => {
+ if (!category) return "/images/appCategories/other.svg";
+
+ const lowerCategory = category.toLowerCase();
+
+ if (lowerCategory.includes("communication")) {
+ return "/images/appCategories/cases.svg";
+ } else if (lowerCategory.includes("cases")) {
+ return "/images/appCategories/cases.svg";
+ } else if (lowerCategory.includes("network")) {
+ return "/images/appCategories/network.svg";
+ } else if (lowerCategory.includes("siem")) {
+ return "/images/appCategories/siem.svg";
+ } else if (lowerCategory.includes("edr") || lowerCategory.includes("eradication")) {
+ return "/images/appCategories/edr.svg";
+ } else if (lowerCategory.includes("iam")) {
+ return "/images/appCategories/iam.svg";
+ } else if (lowerCategory.includes("assets")) {
+ return "/images/appCategories/assets.svg";
+ } else if (lowerCategory.includes("intel")) {
+ return "/images/appCategories/intel.svg";
+ } else if (lowerCategory.includes("email")) {
+ return "/images/appCategories/email.svg";
+ } else {
+ return "/images/appCategories/other.svg";
+ }
+};
+
+// Skeleton component for loading state
+const UsecaseCardSkeleton = () => {
+ const { themeMode } = useContext(Context);
+ const theme = getTheme(themeMode);
+
+ return (
+
+
+
+ {/* Source App Icon Skeleton */}
+
+
+ {/* Destination App Icon Skeleton */}
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const UsecaseCard = ({ usecase, handleToggle, handleOpenDialog, handleDeleteUsecase }) => {
+ const [anchorEl, setAnchorEl] = useState(null);
+ const open = Boolean(anchorEl);
+ const { themeMode } = useContext(Context);
+ const theme = getTheme(themeMode);
+ const navigate = useNavigate();
+
+ const handleClick = (event) => {
+ event.stopPropagation();
+ event.preventDefault();
+ setAnchorEl(event.currentTarget);
+ };
+
+ const handleClose = () => {
+ setAnchorEl(null);
+ };
+
+ const handleEdit = () => {
+ console.log("Edit usecase:", usecase.id);
+ handleClose();
+ };
+
+ const handleStar = () => {
+ console.log("Star usecase:", usecase.id);
+ handleClose();
+ };
+
+ const handleCardClick = (e) => {
+ // Navigate to usecase detail page
+ navigate(`/usecases/${usecase.id}`);
+ };
+
+ return (
+
+
+
+ {/* Source App Icon */}
+
+
+
+
+ {/* Destination App Icon */}
+
+
+
+
+
+ {
+ e.stopPropagation();
+ }}
+ onChange={(e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ handleToggle(usecase.id);
+ }}
+ size="medium"
+ sx={{
+ "& .MuiSwitch-switchBase.Mui-checked": {
+ color: "#4CAF50",
+ },
+ "& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track": {
+ backgroundColor: "#4CAF50",
+ },
+ }}
+ />
+ {
+ e.stopPropagation();
+ handleClick(e);
+ }}
+ size="small"
+ sx={{
+ color: theme.palette.text.primary,
+ zIndex: 20,
+ "&:hover": {
+ backgroundColor: themeMode === "dark" ? "rgba(255, 255, 255, 0.1)" : "rgba(0, 0, 0, 0.05)",
+ },
+ }}
+ >
+
+
+ e.stopPropagation()}
+ PaperProps={{
+ sx: {
+ backgroundColor: theme.palette.DialogStyle.backgroundColor,
+ border: theme.palette.defaultBorder,
+ borderRadius: "8px",
+ boxShadow: theme.palette.DialogStyle.boxShadow,
+ "& .MuiMenuItem-root": {
+ fontSize: "14px",
+ color: theme.palette.text.primary,
+ "&:hover": {
+ backgroundColor: themeMode === "dark" ? "rgba(255, 255, 255, 0.1)" : "rgba(0, 0, 0, 0.05)",
+ },
+ },
+ },
+ }}
+ transformOrigin={{ horizontal: "right", vertical: "top" }}
+ anchorOrigin={{ horizontal: "right", vertical: "bottom" }}
+ >
+ {
+ e.stopPropagation();
+ handleOpenDialog(usecase);
+ setAnchorEl(null);
+ }}
+ >
+
+
+
+ Edit Usecase
+
+ {
+ e.stopPropagation();
+ handleDeleteUsecase(usecase.id);
+ }}>
+
+
+
+ Delete Usecase
+
+
+
+
+
+ {usecase.name}
+
+
+ );
+};
+
+const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPartnerData }) => {
+ const { themeMode } = useContext(Context);
+ const theme = getTheme(themeMode);
+
+ // Dialog state
+ const [openDialog, setOpenDialog] = useState(false);
+ const [publicWorkflows, setPublicWorkflows] = useState([]);
+ const [usecaseData, setUsecaseData] = useState([]);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [isLoading, setIsLoading] = useState(true);
+ const [isWorkflowLoading, setIsWorkflowLoading] = useState(false);
+ const [formData, setFormData] = useState({
+ id: "",
+ mainContent: {
+ title: "",
+ description: "",
+ categories: [],
+ publicWorkflowId: "",
+ sourceAppType: "",
+ destinationAppType: "",
+ },
+ navigation: {
+ items: [
+ {
+ name: "About Usecase",
+ content: [""],
+ },
+ ],
+ },
+ public: false,
+ });
+
+ // App categories for dropdowns
+ const appCategories = [
+ { value: "communication", label: "Communication" },
+ { value: "cases", label: "Cases" },
+ { value: "network", label: "Network" },
+ { value: "siem", label: "SIEM" },
+ { value: "edr", label: "EDR" },
+ { value: "eradication", label: "Eradication" },
+ { value: "iam", label: "IAM" },
+ { value: "assets", label: "Assets" },
+ { value: "intel", label: "Intel" },
+ { value: "email", label: "Email" },
+ { value: "other", label: "Other" }
+ ];
+
+ // Sample workflow options
+ const workflowOptions = [
+ "Email Analysis Workflow",
+ "Incident Response Workflow",
+ "Alert Triage Workflow",
+ "Threat Hunting Workflow",
+ "Vulnerability Management",
+ ];
+
+ useEffect(() => {
+ // Set loading state to true when fetching starts
+ setIsLoading(true);
+ if(!isCloud || !userdata?.active_org?.is_partner) {
+ // If the user is not a partner or if it's not a cloud environment do not make api call :)
+ return;
+ }
+ // Load usecase data from API
+ fetch(`${globalUrl}/api/v1/partners/${partnerData?.id}/usecases`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ },
+ credentials: "include",
+ })
+ .then((response) => {
+ if (response.status !== 200) {
+ console.log("Status not 200 for PARTNER USECASES :O!");
+ }
+ return response.json();
+ })
+ .then((responseJson) => {
+ if (responseJson.success !== false) {
+ const usecases = responseJson.usecases.map((usecase) => ({
+ id: usecase.id,
+ sourceAppType: usecase.mainContent?.sourceAppType || "",
+ destinationAppType: usecase.mainContent?.destinationAppType || "",
+ srcImg: getCategoryImagePath(usecase.mainContent?.sourceAppType),
+ dstImg: getCategoryImagePath(usecase.mainContent?.destinationAppType),
+ publicWorkflowId: usecase.mainContent?.publicWorkflowId || "",
+ name: usecase.mainContent?.title,
+ description: usecase.mainContent?.description,
+ navigation: usecase.navigation || { items: [] },
+ categories: usecase.mainContent?.categories || [],
+ public: usecase?.public || false,
+ }));
+ if (usecases.length > 0) {
+ setUsecaseData(usecases);
+ } else {
+ setUsecaseData([]);
+ }
+ }
+ })
+ .catch((error) => {
+ console.log(error);
+ })
+ .finally(() => {
+ // Set loading state to false when fetching completes (success or error)
+ setIsLoading(false);
+ });
+ }, [partnerData?.id]);
+
+ // Sample categories
+ const categoryOptions = ["Collect","Enrich", "Detect", "Respond", "Verify"];
+
+ const getUserProfileWorkflows = (orgId) => {
+ setIsWorkflowLoading(true);
+ fetch(`${globalUrl}/api/v1/partners/${orgId}/workflows`, {
+ method: "GET",
+ headers: {
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ },
+ credentials: "include",
+ })
+ .then((response) => {
+ if (response.status !== 200) {
+ console.log("Status not 200 for WORKFLOW EXECUTION :O!");
+ }
+
+ return response.json();
+ })
+ .then((responseJson) => {
+ if (responseJson.success !== false) {
+ setPublicWorkflows(responseJson || []);
+ setIsWorkflowLoading(false);
+ }else {
+ toast.info(responseJson.message || "No public workflows found for this organization.");
+ setPublicWorkflows([]);
+ }
+ })
+ .catch((error) => {
+ console.log(error);
+ setIsWorkflowLoading(false);
+ });
+ }
+
+ useEffect(() => {
+ const orgId = userdata?.active_org?.id;
+ 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;
+ }
+ getUserProfileWorkflows(orgId);
+ }, []);
+
+
+ const handleUpdateUsecase = async (usecaseId, updatedData) => {
+ try {
+ // Transform the frontend data structure to match the backend expected structure
+ const backendUsecaseData = {
+ id: updatedData.id,
+ companyInfo: {
+ id: partnerData?.id.trim(),
+ name: partnerData?.name.trim(),
+ },
+ mainContent: {
+ title: updatedData.name.trim(),
+ description: updatedData.description.trim(),
+ categories: updatedData.categories,
+ publicWorkflowId: updatedData.publicWorkflowId.trim(),
+ sourceAppType: updatedData.sourceAppType.trim(),
+ destinationAppType: updatedData.destinationAppType.trim(),
+ },
+ navigation: updatedData.navigation,
+ public: updatedData.public,
+ edited: Date.now(),
+ };
+
+
+ // return
+ const response = await fetch(`${globalUrl}/api/v1/usecases/${usecaseId}`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ },
+ credentials: "include",
+ body: JSON.stringify(backendUsecaseData),
+ });
+
+ if (response.status !== 200) {
+ console.error("Failed to update usecase");
+ toast.error("Failed to update usecase");
+ return false;
+ }
+
+ const responseData = await response.json();
+ toast.success("Usecase published status updated successfully");
+ return true;
+ } catch (error) {
+ console.error("Error updating usecase:", error);
+ toast.error("Error updating usecase");
+ return false;
+ }
+ };
+
+ const handleDeleteUsecase = async (usecaseId) => {
+ try {
+ const response = await fetch(
+ `${globalUrl}/api/v1/usecases/${usecaseId}`,
+ {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ },
+ credentials: "include",
+ }
+ );
+ if (response.status !== 200) {
+ console.error("Failed to delete usecase");
+ toast.error("Failed to delete usecase");
+ return false;
+ }
+ const responseData = await response.json();
+ toast.success("Usecase deleted successfully");
+ // Remove the deleted usecase from the state
+ setUsecaseData((prevData) =>
+ prevData?.filter((usecase) => usecase.id !== usecaseId)
+ );
+ // Remove the usecase ID from partnerData
+ setPartnerData((prevData) => ({
+ ...prevData,
+ usecases: prevData.usecases?.filter((id) => id !== usecaseId),
+ }));
+ return true;
+ } catch (error) {
+ console.error("Error deleting usecase:", error);
+ toast.error("Error deleting usecase");
+ return false;
+ }
+ };
+
+ const handleToggle = async (currentId) => {
+ // Find the current usecase
+ const currentUsecase = usecaseData.find(usecase => usecase.id === currentId);
+ if (!currentUsecase) return;
+
+ // Optimistically update the UI
+ setUsecaseData((prevData) =>
+ prevData.map((usecase) =>
+ usecase.id === currentId
+ ? { ...usecase, public: !usecase.public }
+ : usecase
+ )
+ );
+
+ // Prepare the updated data
+ const updatedPublishedStatus = !currentUsecase.public;
+
+ // Create a copy of the usecase with updated published status
+ const updatedUsecase = {
+ ...currentUsecase,
+ public: updatedPublishedStatus
+ };
+ toast.info(`Updating usecase ${updatedUsecase.name}...`,{
+ autoClose: 1000,
+ })
+ // Send the update to the server
+ const success = await handleUpdateUsecase(currentId, updatedUsecase);
+
+ // If the update failed, revert the UI change
+ if (!success) {
+ setUsecaseData((prevData) =>
+ prevData.map((usecase) =>
+ usecase.id === currentId
+ ? { ...usecase, public: currentUsecase.public }
+ : usecase
+ )
+ );
+ }
+ };
+
+ const handleOpenDialog = (usecase) => {
+ setFormData({
+ id: usecase?.id || undefined,
+ companyInfo: {
+ id: usecase.companyInfo?.id || "",
+ name: usecase.companyInfo?.name || "",
+ },
+ mainContent: {
+ title: usecase?.name,
+ description: usecase?.description,
+ categories: usecase?.categories || [],
+ publicWorkflowId: usecase?.publicWorkflowId || "",
+ sourceAppType: usecase?.sourceAppType || "",
+ destinationAppType: usecase?.destinationAppType || "",
+ },
+ navigation: usecase?.navigation || {
+ items: [
+ {
+ name: "About Usecase",
+ content: [""],
+ },
+ ],
+ },
+ public: usecase?.public || false,
+ });
+ setOpenDialog(true);
+ };
+
+ const handleCloseDialog = () => {
+ setOpenDialog(false);
+ setIsSubmitting(false); // Reset loading state when closing dialog
+ setFormData({
+ id: "",
+ mainContent: {
+ title: "",
+ description: "",
+ categories: [],
+ publicWorkflowId: "",
+ sourceAppType: "",
+ destinationAppType: "",
+ },
+ navigation: {
+ items: [
+ {
+ name: "About Usecase",
+ content: [""],
+ },
+ ],
+ },
+ public: false,
+ });
+ };
+
+ const handleSubmit = () => {
+ // Validate required fields
+ const validationErrors = [];
+
+ // Check for public workflow selection
+ if (!formData.mainContent.publicWorkflowId || formData.mainContent.publicWorkflowId.trim() === "") {
+ validationErrors.push("Please select a public workflow");
+ }
+
+ if (!formData.mainContent.sourceAppType || formData.mainContent.sourceAppType.trim() === "") {
+ validationErrors.push("Source app type is required");
+ }
+
+ if (!formData.mainContent.destinationAppType || formData.mainContent.destinationAppType.trim() === "") {
+ validationErrors.push("Destination app type is required");
+ }
+
+ // Validate that source and destination are different
+ if (formData.mainContent.sourceAppType && formData.mainContent.destinationAppType &&
+ formData.mainContent.sourceAppType.trim() === formData.mainContent.destinationAppType.trim()) {
+ validationErrors.push("Source and destination app types must be different");
+ }
+
+ // Check main content fields
+ if (!formData.mainContent.title || formData.mainContent.title.trim() === "") {
+ validationErrors.push("Title is required");
+ } else if (formData.mainContent.title.length < 5) {
+ validationErrors.push("Title must be not be less than 5 characters");
+ }
+
+ if (!formData.mainContent.description || formData.mainContent.description.trim() === "") {
+ validationErrors.push("Description is required");
+ }
+
+ // Check categories
+ if (!formData.mainContent.categories || !Array.isArray(formData.mainContent.categories) || formData.mainContent.categories.length === 0) {
+ validationErrors.push("At least one category must be selected");
+ }
+
+ // Check navigation items
+ if (!formData.navigation.items || !Array.isArray(formData.navigation.items) || formData.navigation.items.length === 0) {
+ validationErrors.push("Please add at least one section");
+ } else {
+ // Validate each section
+ for (let i = 0; i < formData.navigation.items.length; i++) {
+ const item = formData.navigation.items[i];
+
+ if (!item.name || item.name.trim() === "") {
+ validationErrors.push(`Section ${i+1} must have a name`);
+ }
+
+ if (!item.content || !Array.isArray(item.content) || item.content.length === 0 ||
+ !item.content.some(content => content && content.trim() !== "")) {
+ validationErrors.push(`Section "${item.name || i+1}" must have content`);
+ }
+ }
+ }
+
+ // If there are validation errors, show the first one and return
+ if (validationErrors.length > 0) {
+ toast.error(validationErrors[0]);
+ return;
+ }
+
+ // Set loading state to true
+ setIsSubmitting(true);
+
+ // Get the image paths for source and destination app types
+ const srcImg = getCategoryImagePath(formData.mainContent.sourceAppType);
+ const dstImg = getCategoryImagePath(formData.mainContent.destinationAppType);
+
+ const response = {
+ ...formData,
+ companyInfo: {
+ id: partnerData?.id,
+ name: partnerData?.name,
+ },
+ }
+
+ fetch(`${globalUrl}/api/v1/usecases/${formData.id}`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ Accept: "application/json",
+ },
+ credentials: "include",
+ body: JSON.stringify(response),
+ })
+ .then((res) => {
+ if (res.status !== 200) {
+ console.error("Failed to submit usecase");
+ toast.error("Failed to submit usecase");
+ setIsSubmitting(false);
+ return null;
+ }
+ return res.json();
+ })
+ .then((responseData) => {
+ console.log("Response data:", responseData);
+ // Reset loading state
+ setIsSubmitting(false);
+
+ if (responseData) {
+ // Only close the dialog if the request was successful
+ const usecaseData = {
+ id: responseData.usecaseId.trim(),
+ sourceAppType: formData.mainContent?.sourceAppType.trim() || "",
+ destinationAppType: formData.mainContent?.destinationAppType.trim() || "",
+ srcImg: getCategoryImagePath(formData.mainContent?.sourceAppType.trim()),
+ dstImg: getCategoryImagePath(formData.mainContent?.destinationAppType.trim()),
+ publicWorkflowId: formData.mainContent?.publicWorkflowId.trim() || "",
+ name: formData.mainContent?.title.trim(),
+ description: formData.mainContent?.description.trim(),
+ categories: formData.mainContent?.categories || [],
+ navigation: formData.navigation || {},
+ public: formData?.public || false,
+ }
+
+ setUsecaseData((prevData) => {
+ // Handle case when prevData is null or undefined
+ if (!prevData) {
+ return [usecaseData];
+ }
+
+ // Check if the usecase already exists
+ const existingIndex = prevData.findIndex(
+ (usecase) => usecase.id === responseData.usecaseId
+ );
+ if (existingIndex !== -1) {
+ // Update existing usecase
+ const updatedData = [...prevData];
+ updatedData[existingIndex] = usecaseData;
+ return updatedData;
+ } else {
+ // Add new usecase
+ return [...prevData, usecaseData];
+ }
+ });
+
+ setPartnerData((prevData) => {
+ // Handle case when prevData is null or undefined
+ if (!prevData) {
+ return {
+ usecases: [responseData.usecaseId.trim()]
+ };
+ }
+
+ return {
+ ...prevData,
+ usecases: [
+ ...(prevData.usecases || []),
+ responseData.usecaseId.trim(),
+ ],
+ };
+ });
+
+ // Show success message
+ if(formData?.id){
+ toast.success("Usecase updated successfully");
+ }else{
+ toast.success("Usecase created successfully, publish it with toggle button");
+ }
+ handleCloseDialog();
+ }
+ })
+ .catch((error) => {
+ console.error("Error submitting usecase:", error);
+ toast.error("Error submitting usecase");
+ // Reset loading state on error
+ setIsSubmitting(false);
+ });
+ };
+
+ return (
+
+ {/* Add Usecase Button */}
+
+
+ Usecases
+
+ }
+ disabled={isWorkflowLoading}
+ onClick={handleOpenDialog}
+ sx={{
+ px: 3,
+ textTransform: "none",
+ fontSize: "16px",
+ fontWeight: 600,
+ }}
+ >
+ Add Usecase
+
+
+
+ {/* Usecases Grid */}
+ {isLoading ? (
+ // Skeleton loading state
+
+ {/* Display 4 skeleton cards while loading */}
+ {[...Array(4)].map((_, index) => (
+
+ ))}
+
+ ) : usecaseData.length > 0 ? (
+ // Actual data display
+
+ {usecaseData.map((usecase) => (
+
+ ))}
+
+ ) : (
+ // Empty state
+
+
+ No usecases found
+
+
+ )}
+
+ {/* Add Usecase Dialog */}
+
+
+
+ {formData?.id ? "Update" : "Add New"} Usecase : {formData?.public ? "Published" : "Draft"}
+
+
+
+
+
+
+
+
+
+
+ Public Workflow
+
+
+ workflow.objectID ===
+ formData.mainContent.publicWorkflowId
+ )?.name || ""
+ }
+ onChange={(e) => {
+ setFormData({
+ ...formData,
+ mainContent: {
+ ...formData.mainContent,
+ publicWorkflowId: e.target.value,
+ },
+ });
+ }}
+ renderValue={(selected) => {
+ if (!selected) {
+ return (
+
+ Select Public Workflow
+
+ );
+ }
+ // Find the workflow with the matching ID and display its name
+ if (
+ Array.isArray(publicWorkflows) &&
+ publicWorkflows.length > 0
+ ) {
+ const selectedWorkflow = publicWorkflows.find(
+ (w) => w.id === selected
+ );
+ return selectedWorkflow ? selectedWorkflow.name : selected;
+ }
+ return selected;
+ }}
+ sx={{
+ backgroundColor: theme.palette.usecaseDialogFieldColor,
+ height: 40,
+ color: theme.palette.text.primary,
+ "& .MuiOutlinedInput-notchedOutline": {
+ border: theme.palette.defaultBorder,
+ },
+ "& .MuiSelect-icon": {
+ color: theme.palette.text.secondary,
+ },
+ "& .MuiSelect-select": {
+ "&.MuiInputBase-input": {
+ color: theme.palette.text.primary,
+ },
+ "&.Mui-focused": {
+ backgroundColor: "transparent",
+ },
+ },
+ }}
+ >
+ {Array.isArray(publicWorkflows) &&
+ publicWorkflows.length > 0 ? (
+ publicWorkflows.map((workflow) => (
+
+ {workflow.name}
+
+ ))
+ ) : (
+
+ No workflows available
+
+ )}
+
+
+
+ {/* App Type Selection */}
+
+
+
+ Source App Type
+
+ {
+ setFormData({
+ ...formData,
+ mainContent: {
+ ...formData.mainContent,
+ sourceAppType: e.target.value,
+ },
+ });
+ }}
+ renderValue={(selected) => {
+ if (!selected) {
+ return (
+
+ Select Source App Type
+
+ );
+ }
+ const selectedCategory = appCategories.find(
+ (cat) => cat.value === selected
+ );
+ return selectedCategory ? selectedCategory.label : selected;
+ }}
+ sx={{
+ backgroundColor: theme.palette.usecaseDialogFieldColor,
+ height: 40,
+ color: theme.palette.text.primary,
+ "& .MuiSelect-icon": {
+ color: "#ff8544",
+ },
+ "& .MuiSelect-select": {
+ "&.MuiInputBase-input": {
+ color: theme.palette.text.primary,
+ },
+ "&.Mui-focused": {
+ backgroundColor: "transparent",
+ },
+ },
+ }}
+ >
+
+ None
+
+ {appCategories.map((category) => (
+
+ {category.label}
+
+ ))}
+
+
+
+
+
+ Destination App Type
+
+ {
+ setFormData({
+ ...formData,
+ mainContent: {
+ ...formData.mainContent,
+ destinationAppType: e.target.value,
+ },
+ });
+ }}
+ renderValue={(selected) => {
+ if (!selected) {
+ return (
+
+ Select Destination App Type
+
+ );
+ }
+ const selectedCategory = appCategories.find(
+ (cat) => cat.value === selected
+ );
+ return selectedCategory ? selectedCategory.label : selected;
+ }}
+ sx={{
+ backgroundColor: theme.palette.usecaseDialogFieldColor,
+ height: 40,
+ color: theme.palette.text.primary,
+ "& .MuiSelect-icon": {
+ color: "#ff8544",
+ },
+ "& .MuiSelect-select": {
+ "&.MuiInputBase-input": {
+ color: theme.palette.text.primary,
+ },
+ "&.Mui-focused": {
+ backgroundColor: "transparent",
+ },
+ },
+ }}
+ >
+
+ None
+
+ {appCategories.map((category) => (
+
+ {category.label}
+
+ ))}
+
+
+
+
+
+
+ Main Content
+
+
+
+
+ Title
+
+
+ setFormData({
+ ...formData,
+ mainContent: {
+ ...formData.mainContent,
+ title: e.target.value,
+ },
+ })
+ }
+ sx={{
+ "& .MuiOutlinedInput-root": {
+ height: 40,
+ backgroundColor: theme.palette.usecaseDialogFieldColor,
+ color: theme.palette.text.primary,
+ "& fieldset": { border: theme.palette.defaultBorder },
+ },
+ }}
+ />
+
+
+
+ Description
+
+
+ setFormData({
+ ...formData,
+ mainContent: {
+ ...formData.mainContent,
+ description: e.target.value,
+ },
+ })
+ }
+ sx={{
+ "& .MuiOutlinedInput-root": {
+ backgroundColor: theme.palette.usecaseDialogFieldColor,
+ p: 1.5,
+ color: theme.palette.text.primary,
+ "& fieldset": { border: theme.palette.defaultBorder },
+ },
+ }}
+ />
+
+
+
+ Categories
+
+
+ setFormData({
+ ...formData,
+ mainContent: {
+ ...formData.mainContent,
+ categories: e.target.value,
+ },
+ })
+ }
+ renderValue={(selected) => {
+ if (
+ !selected ||
+ (Array.isArray(selected) && selected.length === 0)
+ ) {
+ return (
+
+ Select Categories
+
+ );
+ }
+ return selected.join(", ");
+ }}
+ sx={{
+ backgroundColor: theme.palette.usecaseDialogFieldColor,
+ height: 40,
+ color: theme.palette.text.primary,
+ "& .MuiSelect-icon": {
+ color: "#ff8544",
+ },
+ "& .MuiChip-root": {
+ color: theme.palette.text.primary,
+ backgroundColor: theme.palette.chipStyle.backgroundColor,
+ margin: "2px",
+ },
+ "& .MuiSelect-select": {
+ "&.MuiInputBase-input": {
+ color: theme.palette.text.primary,
+ },
+ "&.Mui-focused": {
+ backgroundColor: "transparent",
+ },
+ },
+ }}
+ >
+ {categoryOptions.map((category) => (
+
+
+ {category}
+
+ ))}
+
+
+
+
+
+ {/* Navigation Items Section */}
+
+
+ Navigation Items
+
+ {formData.navigation.items.map((item, itemIndex) => (
+
+
+ {
+ const newItems = [...formData.navigation.items];
+ newItems[itemIndex].name = e.target.value;
+ setFormData({
+ ...formData,
+ navigation: { items: newItems },
+ });
+ }}
+ placeholder="Section Name"
+ sx={{
+ flex: 1,
+ "& .MuiOutlinedInput-root": {
+ height: 40,
+ backgroundColor: theme.palette.usecaseDialogFieldColor,
+ color: theme.palette.text.primary,
+ "& fieldset": { border: theme.palette.defaultBorder },
+ },
+ }}
+ />
+
+ {
+ if (itemIndex > 0) {
+ const newItems = [...formData.navigation.items];
+ [newItems[itemIndex - 1], newItems[itemIndex]] = [
+ newItems[itemIndex],
+ newItems[itemIndex - 1],
+ ];
+ setFormData({
+ ...formData,
+ navigation: { items: newItems },
+ });
+ }
+ }}
+ disabled={itemIndex === 0}
+ sx={{
+ color: theme.palette.text.secondary,
+ "&:not(:disabled):hover": {
+ backgroundColor: themeMode === "dark" ? "rgba(255, 255, 255, 0.08)" : "rgba(0, 0, 0, 0.05)",
+ },
+ }}
+ >
+
+
+ {
+ if (
+ itemIndex <
+ formData.navigation.items.length - 1
+ ) {
+ const newItems = [...formData.navigation.items];
+ [newItems[itemIndex], newItems[itemIndex + 1]] = [
+ newItems[itemIndex + 1],
+ newItems[itemIndex],
+ ];
+ setFormData({
+ ...formData,
+ navigation: { items: newItems },
+ });
+ }
+ }}
+ disabled={
+ itemIndex === formData.navigation.items.length - 1
+ }
+ sx={{
+ color: theme.palette.text.secondary,
+ "&:not(:disabled):hover": {
+ backgroundColor: themeMode === "dark" ? "rgba(255, 255, 255, 0.08)" : "rgba(0, 0, 0, 0.05)",
+ },
+ }}
+ >
+
+
+ {
+ const newItems = formData.navigation.items?.filter(
+ (_, i) => i !== itemIndex
+ );
+ setFormData({
+ ...formData,
+ navigation: { items: newItems },
+ });
+ }}
+ sx={{
+ color: "#ff4444",
+ "&:hover": {
+ backgroundColor: themeMode === "dark" ? "rgba(255, 68, 68, 0.08)" : "rgba(255, 68, 68, 0.15)",
+ },
+ }}
+ >
+
+
+
+
+
+ {item.content.map((content, contentIndex) => (
+
+ {
+ const newItems = [...formData.navigation.items];
+ newItems[itemIndex].content[contentIndex] =
+ e.target.value;
+ setFormData({
+ ...formData,
+ navigation: { items: newItems },
+ });
+ }}
+ placeholder="Content"
+ fullWidth
+ sx={{
+ "& .MuiOutlinedInput-root": {
+ backgroundColor: theme.palette.usecaseDialogFieldColor,
+ color: theme.palette.text.primary,
+ "& fieldset": { border: theme.palette.defaultBorder },
+ },
+ }}
+ />
+ {item.content.length > 1 && (
+ {
+ const newItems = [...formData.navigation.items];
+ newItems[itemIndex].content = newItems[
+ itemIndex
+ ].content?.filter((_, i) => i !== contentIndex);
+ setFormData({
+ ...formData,
+ navigation: { items: newItems },
+ });
+ }}
+ sx={{
+ color: "#ff4444",
+ "&:hover": {
+ textDecoration: "underline",
+ textUnderlineOffset: "4px",
+ backgroundColor: "transparent",
+ },
+ }}
+ >
+ Remove Paragraph
+
+ )}
+
+ ))}
+
+ }
+ onClick={() => {
+ const newItems = [...formData.navigation.items];
+ newItems[itemIndex].content.push("");
+ setFormData({
+ ...formData,
+ navigation: { items: newItems },
+ });
+ }}
+ sx={{
+ color: "#4CAF50",
+ "&:hover": { backgroundColor: themeMode === "dark" ? "rgba(76, 175, 80, 0.08)" : "rgba(76, 175, 80, 0.15)" },
+ }}
+ >
+ Add Paragraph
+
+
+ ))}
+
+ }
+ onClick={() => {
+ setFormData({
+ ...formData,
+ navigation: {
+ items: [
+ ...formData.navigation.items,
+ { name: "", content: [""] },
+ ],
+ },
+ });
+ }}
+ sx={{
+ color: theme.palette.accentColor,
+ "&:hover": { backgroundColor: themeMode === "dark" ? "rgba(255, 133, 68, 0.08)" : "rgba(255, 133, 68, 0.15)" },
+ }}
+ >
+ Add New Section
+
+
+
+
+
+
+
+ Cancel
+
+ : }
+ onClick={handleSubmit}
+ disabled={isSubmitting}
+ sx={{
+ px: 3,
+ textTransform: "none",
+ fontSize: "14px",
+ fontWeight: 600,
+ opacity: isSubmitting ? 0.7 : 1,
+ }}
+ >
+ {isSubmitting
+ ? (formData?.id ? "Updating..." : "Adding...")
+ : (formData?.id ? "Update Usecase" : "Add Usecase")
+ }
+
+
+
+
+ );
+};
+
+export default PartnersUsecasesTab;
diff --git a/frontend/src/components/Priorities.jsx b/frontend/src/components/Priorities.jsx
index cd9d5e0d..639c6263 100644
--- a/frontend/src/components/Priorities.jsx
+++ b/frontend/src/components/Priorities.jsx
@@ -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 + '"')
- {`Configure ${selectedAppDetails.name} workflow`}
+ {`Configure ${selectedAppDetails?.name?.replaceAll("_", " ")} workflow`}
{console.log("len Selected app details: ", selectedAppDetails)}
+
{(selectedAppDetails.authentication_data || (selectedAppDetails.auth_config && selectedAppDetails.auth_config.required == false) || (selectedAppDetails.authentication && selectedAppDetails.authentication.required == false)) ?
<>
@@ -1152,13 +984,15 @@ print('"' + encoded + '"')
const renderChips = useCallback(() => {
const appList = Object.values(notificationAppsDetails);
+
return (
+
{appList.map((app) => (
{
@@ -1176,6 +1010,7 @@ print('"' + encoded + '"')
avatar={ }
/>
))}
+
Set up app authentication
- to unlock additional workflow options.
+ to show additional workflow options.
@@ -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",
diff --git a/frontend/src/components/ShuffleCodeEditor1.jsx b/frontend/src/components/ShuffleCodeEditor1.jsx
index 3132034f..043c5de2 100644
--- a/frontend/src/components/ShuffleCodeEditor1.jsx
+++ b/frontend/src/components/ShuffleCodeEditor1.jsx
@@ -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
*/}
- {isFileEditor ? null :
+ {isFileEditor || isWorkflowEditor ? null :
{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 ? (
{
- {isFileEditor ? null :
+ {isFileEditor || isWorkflowEditor ? null :
{
-
+
{
{
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("")
diff --git a/frontend/src/components/TenantsTab.jsx b/frontend/src/components/TenantsTab.jsx
index 998bce83..ffd15912 100644
--- a/frontend/src/components/TenantsTab.jsx
+++ b/frontend/src/components/TenantsTab.jsx
@@ -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 (
+
+
+
{
+ setAccountDeleteButtonClicked(false);
+ setSelectedSuborg(null);
+ }}
+ >
+
+
+
Sub-Organization
+ {/*
*/}
+
+
If you delete your suborg then:
+
+
+
+ Your suborg information will be removed from our Database
+ permanently.
+
+
+
+ This action cannot be reversed.
+
+
+
+
+
+ I have read the above information and I agree to it completely
+
+
+
+ Enter password to delete suborg {" " + selectedSuborg?.name}
+
+ {showPassword ? : }
+
+ ),
+ }}
+ />
+
+
+ Delete Suborg
+
+
+
+
+ );
+ };
+
const modalView = (
{
{modalView}
{cloudSyncModal}
+ {accountDeleteButtonClicked &&
}
@@ -1340,7 +1562,8 @@ const TenantsTab = memo((props) => {
+ <>
+
{
>
Change Active Org
+
+ {selectedOrganization?.creator_org?.length > 0 ? null :
+ {
+ setAccountDeleteButtonClicked(true);
+ setSelectedSuborg(data);
+ }}
+ >
+ Delete Org
+ }
+
+ >
}
style={{ display: "table-cell", verticalAlign: "middle" }}
/>
diff --git a/frontend/src/components/UserManagmentTab.jsx b/frontend/src/components/UserManagmentTab.jsx
index 7671bbbb..81b65b94 100644
--- a/frontend/src/components/UserManagmentTab.jsx
+++ b/frontend/src/components/UserManagmentTab.jsx
@@ -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 (
diff --git a/frontend/src/theme.jsx b/frontend/src/theme.jsx
index d7bb0e1e..4835e4f1 100644
--- a/frontend/src/theme.jsx
+++ b/frontend/src/theme.jsx
@@ -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,
diff --git a/frontend/src/views/Admin2.jsx b/frontend/src/views/Admin2.jsx
index de3143df..f94c3441 100644
--- a/frontend/src/views/Admin2.jsx
+++ b/frontend/src/views/Admin2.jsx
@@ -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,
diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx
index 7526c9d7..0aca44b5 100755
--- a/frontend/src/views/AngularWorkflow.jsx
+++ b/frontend/src/views/AngularWorkflow.jsx
@@ -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) => {
)}
- {!distributedFromParent ?
- isCorrectOrg ? null :
+ {!distributedFromParent || userdata?.support === true ?
+ isCorrectOrg ? null :
Warning : {
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) => {
{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
+
{
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
+ */
: 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 ?
{
>
{
.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) => {
+
+
+ {userdata?.support === true ?
+
+
+ {
+ setupWorkflowYaml(workflow)
+ }}
+ >
+
+
+
+
+ : null}
@@ -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}
diff --git a/frontend/src/views/AppCreator.jsx b/frontend/src/views/AppCreator.jsx
index 1a4c5104..1eaf5fcb 100755
--- a/frontend/src/views/AppCreator.jsx
+++ b/frontend/src/views/AppCreator.jsx
@@ -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)
+ }
}
}
diff --git a/frontend/src/views/AppExplorer.jsx b/frontend/src/views/AppExplorer.jsx
index 63c81d63..f0069e4f 100644
--- a/frontend/src/views/AppExplorer.jsx
+++ b/frontend/src/views/AppExplorer.jsx
@@ -4066,7 +4066,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
}
- {isMobile ? null : (
+ {isMobile || app?.reference_org === userdata?.active_org?.id ? null : (
{
- navigate(`/apps/new?id=${appId}`)
+ navigate(`/apps/new?id=${appId}&fork=true`)
}}
style={{ height: 40, marginTop: 5, marginLeft: 5, }}
>
diff --git a/frontend/src/views/Apps2.jsx b/frontend/src/views/Apps2.jsx
index da7dd5c8..cdfebe78 100644
--- a/frontend/src/views/Apps2.jsx
+++ b/frontend/src/views/Apps2.jsx
@@ -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())}
@@ -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"}
- {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) => (
{tag}
{tagIndex < data.tags.length - 1 ? ", " : ""}
diff --git a/frontend/src/views/LoginPage.jsx b/frontend/src/views/LoginPage.jsx
index b2cb9426..b717b9f6 100755
--- a/frontend/src/views/LoginPage.jsx
+++ b/frontend/src/views/LoginPage.jsx
@@ -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 => {
diff --git a/frontend/src/views/Workflows2.jsx b/frontend/src/views/Workflows2.jsx
index 9753bfb0..d702f24f 100644
--- a/frontend/src/views/Workflows2.jsx
+++ b/frontend/src/views/Workflows2.jsx
@@ -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: ,
},
- };
+ eradication: {
+ icon: "",
+ iconColor: "white",
+ iconBackgroundColor: "green",
+ originalIcon: ,
+ },
+ iam: {
+ icon: "",
+ iconColor: "white",
+ iconBackgroundColor: "green",
+ originalIcon: ,
+ },
+ intel: {
+ icon: "",
+ iconColor: "white",
+ iconBackgroundColor: "green",
+ originalIcon: ,
+ },
+ network: {
+ icon: "",
+ iconColor: "white",
+ iconBackgroundColor: "green",
+ originalIcon: ,
+ },
+ assets: {
+ icon: "",
+ iconColor: "white",
+ iconBackgroundColor: "green",
+ originalIcon: ,
+ },
+ }
+
+ /*
+ { 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 = (
{
onClick={() => {
localStorage.setItem("view", "list");
setView("list");
+
+ setCurrTab(0)
}}
>
@@ -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) => {
-
+
{
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 ?
+
+ 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}).
+ :
+ orgWorkflows.map((data, index) => {
+ // Shouldn't be a part of this list
+ if (data.public === true) {
+ return null
+ }
- // if (firstLoad) {
- // workflowDelay += 75
- // } else {
- // return
- // }
+ // if (firstLoad) {
+ // workflowDelay += 75
+ // } else {
+ // return
+ // }
- return (
-
- {/**/}
-
- {/* */}
-
- )
+ return (
+
+ {/**/}
+
+ {/* */}
+
+ )
})}
{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 ?
+
+ No workflows found in this org for your user.
+ :
+ myWorkflows.map((data, index) => {
+ if (data.public === true) {
+ return null
+ }
- // if (firstLoad) {
- // workflowDelay += 75
- // } else {
- // return
- // }
+ // if (firstLoad) {
+ // workflowDelay += 75
+ // } else {
+ // return
+ // }
- return (
-
- {/**/}
-
- {/* */}
-
- )
+ return (
+
+ {/**/}
+
+ {/* */}
+
+ )
})
}
@@ -5123,7 +5204,7 @@ const Workflows2 = (props) => {
//isLoaded && isLoggedIn && workflowDone ? (
const loadedCheck =
- workflowDone ? (
+ workflowDone && isLoaded ? (
{/*