From c1c6a9f12857daf8f22d4a781b619e599c367641 Mon Sep 17 00:00:00 2001 From: Frikky Date: Tue, 17 Jun 2025 19:23:15 +0200 Subject: [PATCH] Cloud sync for all edits in May->June --- backend/go-app/go.mod | 2 +- backend/go-app/go.sum | 4 +- docker-compose.yml | 10 +- frontend/public/images/singul_green.png | Bin 0 -> 9430 bytes frontend/src/components/AdminNavBar.jsx | 92 +- frontend/src/components/CacheView.jsx | 2199 ++++++++++++----- .../src/components/CollectIngestModal.jsx | 205 ++ frontend/src/components/Countries.jsx | 8 - frontend/src/components/LeftSideBar.jsx | 51 +- frontend/src/components/Navbar.jsx | 169 +- frontend/src/components/ParsedAction.jsx | 49 +- frontend/src/components/PartnerApps.jsx | 21 + frontend/src/components/PartnerDetails.jsx | 987 ++++++++ frontend/src/components/PartnerHeader.jsx | 845 +++++++ frontend/src/components/PartnerSettings.jsx | 391 +++ frontend/src/components/PartnerTab.jsx | 245 ++ frontend/src/components/PartnersArticles.jsx | 21 + .../src/components/PartnersUsecasesTab.jsx | 1761 +++++++++++++ frontend/src/components/Priorities.jsx | 227 +- .../src/components/ShuffleCodeEditor1.jsx | 44 +- frontend/src/components/TenantsTab.jsx | 254 +- frontend/src/components/UserManagmentTab.jsx | 27 +- frontend/src/theme.jsx | 4 + frontend/src/views/Admin2.jsx | 4 +- frontend/src/views/AngularWorkflow.jsx | 453 +++- frontend/src/views/AppCreator.jsx | 9 +- frontend/src/views/AppExplorer.jsx | 4 +- frontend/src/views/Apps2.jsx | 6 +- frontend/src/views/LoginPage.jsx | 138 +- frontend/src/views/Workflows2.jsx | 213 +- 30 files changed, 7290 insertions(+), 1153 deletions(-) create mode 100644 frontend/public/images/singul_green.png create mode 100644 frontend/src/components/CollectIngestModal.jsx create mode 100644 frontend/src/components/PartnerApps.jsx create mode 100644 frontend/src/components/PartnerDetails.jsx create mode 100644 frontend/src/components/PartnerHeader.jsx create mode 100644 frontend/src/components/PartnerSettings.jsx create mode 100644 frontend/src/components/PartnerTab.jsx create mode 100644 frontend/src/components/PartnersArticles.jsx create mode 100644 frontend/src/components/PartnersUsecasesTab.jsx 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 0000000000000000000000000000000000000000..796ad6eff6ed3ac4a3fd87670d9341e59bf2543a GIT binary patch literal 9430 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 z ( + + + +); + 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}
+ ); @@ -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.name} + : + automation.icon + } + + + {automation.name} + +
+ + {automation?.disabled !== true ? + + : 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 ? + {data.name} + : 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 ? + + + + + + : 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.   {
- - +
+ + + + : + + + + } + + {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. - - - - - - - - ) : null} + + + + + + + + - - : - - - } + - {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 ? +
+ + 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} + +
+ + + {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, }) => { { + + + + ): 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 - + + + + + + + + ); 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 + + +
+ {/*
+
+ Expertise +
+ +
+
+
+ Services +
+ +
*/} +
+
+ Solutions +
+ +
+
+ + Region + + +
+
+
+ Country +
+ +
+
+
+ + 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 +
+ + + +
+
+
+ Services +
+ + + +
*/} +
+
+ Solutions +
+ + + +
+
+ + 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 */} + + + ); + } +); 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,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"; + + 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 = ( + + + + ); + + 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)} + /> + +
+ + + + + + + + + + + + +
+ +
+ + + + +
+
+ ) : null; + + const imageUploadModalViewLandscape = openLandscapeImageModal && !isDisabled ? ( + + + +
Upload Partner Landscape Image
+
+ {errorText} + + setRotation(0)} + /> + +
+ + + + + + + + + + + + +
+ +
+ + + + +
+
+ ) : 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} +
+
+
+ +
+
+ +
+
+
+
+ +
{ + 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} +
+
+
+ +
+
+ +
+
+
+
+ ); +}; + +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 */} + + {/* Toggle Publish/Unpublish Button */} + + + )} + + {!isPartner && ( + + )} + + +
+
+
+ + +
+
+ ) +} + +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) => ( +
+ +
+ ))} +
+
+ {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 */} + + Source + + + {/* Destination App Icon */} + + Destination + +
+
+ { + 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 + + + + + {/* 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 + + + + + {/* App Type Selection */} + + + + Source App Type + + + + + + + Destination App Type + + + + + + + + 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 + + + + + + + {/* 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 && ( + + )} + + ))} + + + + ))} + + + + + + + + + + + + + ); +}; + +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={{app.name}} /> ))} + 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 :
{
-
+
+
+
+ + ); + }; + const modalView = ( {
{modalView} {cloudSyncModal} + {accountDeleteButtonClicked && }
@@ -1340,7 +1562,8 @@ const TenantsTab = memo((props) => { + <> + + + {selectedOrganization?.creator_org?.length > 0 ? null : + } + + } 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 + + */ : 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 ? + + + {userdata?.support === true ? + + + + + + : 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 : (
@@ -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 ? (
{/*