diff --git a/frontend/public/images/icons/aws_logo.svg b/frontend/public/images/icons/aws_logo.svg deleted file mode 100644 index f023cba5..00000000 --- a/frontend/public/images/icons/aws_logo.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/frontend/src/components/AdminNavBar.jsx b/frontend/src/components/AdminNavBar.jsx index 38aff384..6a771afd 100644 --- a/frontend/src/components/AdminNavBar.jsx +++ b/frontend/src/components/AdminNavBar.jsx @@ -142,10 +142,6 @@ const AdminNavBar = (props) => { // navigate(`?tab=organization`, { replace: true }); // } // } - const adminTab = queryParams.get('admin_tab'); - if (adminTab) { - setSelectedItem("Organization"); - } if (partnerTab) { setSelectedItem("Partner"); } diff --git a/frontend/src/components/ApiExplorer.jsx b/frontend/src/components/ApiExplorer.jsx index 2e5009fa..3bf6785b 100644 --- a/frontend/src/components/ApiExplorer.jsx +++ b/frontend/src/components/ApiExplorer.jsx @@ -1977,7 +1977,7 @@ const Action = memo(( const actionId = action.name.replace(/ /g, "-").replace(/_/g, "-"); window.history.pushState(null, "", `#${actionId}`); setExampleBody(action?.example_response); - document.getElementById(`action-list-${nextSelectedActionIndex}`)?.scrollIntoView({ behavior: "smooth", block: "center" }); + document.getElementById(`action-list-${nextSelectedActionIndex}`).scrollIntoView({ behavior: "smooth", block: "center" }); } }, 300); } diff --git a/frontend/src/components/AppAuthTab.jsx b/frontend/src/components/AppAuthTab.jsx index af7fbef2..7c404e1c 100644 --- a/frontend/src/components/AppAuthTab.jsx +++ b/frontend/src/components/AppAuthTab.jsx @@ -20,7 +20,6 @@ import { isMobile } from "react-device-detect" import PaperComponent from "../components/PaperComponent.jsx"; import { CodeHandler, Img, OuterLink, } from '../views/Docs.jsx' import { v4 as uuidv4} from "uuid"; -import DeleteConfirmDialog from "./DeleteConfirmDialog.jsx"; import { Divider, @@ -64,11 +63,10 @@ import { } from "react-instantsearch-dom"; import aa from "search-insights"; import { Context } from '../context/ContextApi.jsx'; -import SubOrgDistributionDialog from './SubOrgDistributionDialog.jsx'; const searchClient = algoliasearch( "JNSS5CFDZZ", - "33e4e3564f4f060e96e0531957bed552" + "c8f882473ff42d41158430be09ec2b4e" ) const AppAuthTab = memo((props) => { @@ -91,13 +89,9 @@ const AppAuthTab = memo((props) => { const [searchQuery, setSearchQuery] = React.useState(""); const [showAppModal, setShowAppModal] = useState(false) const [selectedAuthId, setSelectedAuthId] = useState(""); - const [selectedAuthName, setSelectedAuthName] = useState(""); const [showDistributionPopup, setShowDistributionPopup] = useState(false); const [showAuthenticationLoader, setShowAuthenticationLoader] = useState(true) const [showAppAuthGroupLoader, setShowAppAuthGroupLoader] = useState(true) - const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false); - const [deleteConfirmTarget, setDeleteConfirmTarget] = useState(null); - const [distribOrgOrder, setDistribOrgOrder] = useState([]); const { themeMode, supportEmail, brandColor } = useContext(Context) const theme = getTheme(themeMode, brandColor) @@ -189,7 +183,7 @@ const AppAuthTab = memo((props) => { }; const deleteAuthentication = (data) => { - toast("Deleting auth " + data?.label); + toast("Deleting auth " + data.label); // Just use this one? const url = globalUrl + "/api/v1/apps/authentication/" + data.id; @@ -219,6 +213,33 @@ const AppAuthTab = memo((props) => { }); }; + const handleSelectSubOrg = (id, action) => { + if (action === "all") { + const childOrgs = userdata.orgs.filter( + (data) => data.creator_org === userdata.active_org.id + ); + setSelectedSubOrg((prev) => { + if (prev.length === childOrgs.length) { + // If all child orgs are already selected, clear the selection + return []; + } else { + // Otherwise, select all child org IDs + return childOrgs.map((data) => data.id); + } + }); + } else if (action === "none") { + setSelectedSubOrg([]); + } else { + setSelectedSubOrg((prev) => { + if (prev.includes(id)) { + return prev.filter((data) => data !== id); + } else { + return [...prev, id]; + } + }); + } + }; + const editAuthenticationConfig = (id, parentAction, selectedSuborgs) => { const data = { id: id, @@ -263,22 +284,100 @@ const AppAuthTab = memo((props) => { editAuthenticationConfig(id, "suborg_distribute", [...new Set(selectedSubOrg)]) } + + + const cacheDistributionModal = showDistributionPopup ? ( + {setShowDistributionPopup(false);setSelectedAuthId("")}} + PaperProps={{ + sx: { + borderRadius: theme?.palette?.DialogStyle?.borderRadius, + border: theme?.palette?.DialogStyle?.border, + fontFamily: theme?.typography?.fontFamily, + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + zIndex: 1000, + minWidth: "600px", + minHeight: "320px", + overflow: "auto", + '& .MuiDialogContent-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogTitle-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogActions-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + }, + }} + > + + + Select sub-org to distribute Datastore key + + + + {handleSelectSubOrg(null, "none")}}>None + {handleSelectSubOrg(null, "all")}}>All + {userdata.orgs.map((data, index) => { + if (data.creator_org !== userdata.active_org.id) { + return null; + } + + const imagesize = 22; + const imageStyle = { + width: imagesize, + height: imagesize, + pointerEvents: "none", + marginRight: 10, + marginLeft: data.id === userdata.active_org.id ? 0 : 20, + }; + + const image = data.image === "" ? ( + {data.name} + ) : ( + {data.name} + ); - const deleteConfirmDialog = ( - { setDeleteConfirmOpen(false); setDeleteConfirmTarget(null); }} - onConfirm={() => { - deleteAuthentication(deleteConfirmTarget); - setDeleteConfirmOpen(false); - setDeleteConfirmTarget(null); - }} - title="Delete Authentication?" - description={<>Are you sure you want to delete {deleteConfirmTarget?.app?.name} authentication?} - warningText="This cannot be undone. Any workflows using this authentication will lose access." - /> - ); - + return ( + handleSelectSubOrg(data.id)} + style={{ display: "flex", alignItems: "center" }} + > + + {image} + {data.name} + + ); + })} + +
+ + +
+
+
+ ) : null; const editAuthenticationModal = selectedAuthenticationModalOpen ? ( { return (
{appModal} - { setShowDistributionPopup(false); setSelectedAuthId(""); setSelectedAuthName(""); }} - title="Distribute App Auth to Sub-Organizations" - extraInfo={selectedAuthName ? `Selected Auth: ${selectedAuthName}` : null} - orgs={distribOrgOrder.map(id => (userdata?.orgs || []).find(o => o.id === id)).filter(Boolean)} - selectedOrgIds={selectedSubOrg} - onSelectionChange={setSelectedSubOrg} - onSave={(ids) => { changeDistribution(selectedAuthId, ids); }} - /> - {deleteConfirmDialog} + {cacheDistributionModal}
@@ -1273,10 +1362,10 @@ const AppAuthTab = memo((props) => { { - setDeleteConfirmTarget(data); - setDeleteConfirmOpen(true); + deleteAuthentication(data); }} > delete icon @@ -1311,27 +1400,21 @@ const AppAuthTab = memo((props) => { color="secondary" onClick={() => { setShowDistributionPopup(true) - let initialSelected = []; - if (data?.suborg_distributed) { - const allSuborg = userdata?.orgs?.map((d) => { - if (d.creator_org !== userdata.active_org.id) return null; - return d.id; - }); - initialSelected = allSuborg.filter((d) => d !== null); - } else if (data?.suborg_distribution?.length > 0) { - initialSelected = data.suborg_distribution; + if(data?.suborg_distribution?.length > 0){ + setSelectedSubOrg(data.suborg_distribution) + }else{ + setSelectedSubOrg([]) + } + setSelectedAuthId(data.id) + if (data?.suborg_distributed) { + const allSuborg = userdata?.orgs?.map((data, index) => { + if (data.creator_org !== userdata.active_org.id) { + return null; + } + return data.id; + }) + setSelectedSubOrg(allSuborg.filter((data) => data !== null)) } - setSelectedSubOrg(initialSelected); - setSelectedAuthId(data.id); - setSelectedAuthName(data?.app?.name); - const suborgs = (userdata?.orgs || []).filter(o => o.creator_org === userdata?.active_org?.id); - const sorted = [...suborgs].sort((a, b) => { - const aS = initialSelected.includes(a.id); - const bS = initialSelected.includes(b.id); - if (aS !== bS) return bS - aS; - return a.name.localeCompare(b.name); - }); - setDistribOrgOrder(sorted.map(o => o.id)); }} /> diff --git a/frontend/src/components/AppCreationModal.jsx b/frontend/src/components/AppCreationModal.jsx index e1e62e28..6a66f0e0 100644 --- a/frontend/src/components/AppCreationModal.jsx +++ b/frontend/src/components/AppCreationModal.jsx @@ -708,7 +708,7 @@ const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud, startOpenA fontWeight: 500, fontFamily: theme?.typography?.fontFamily, }}> - Generate an app based on documentation + Generate an app based on documentation (beta) { diff --git a/frontend/src/components/AppGrid.jsx b/frontend/src/components/AppGrid.jsx index 68b4f7db..cd37fb23 100644 --- a/frontend/src/components/AppGrid.jsx +++ b/frontend/src/components/AppGrid.jsx @@ -1,9 +1,10 @@ -import React, { useEffect, useState, useRef, useMemo } from "react"; +import React, { useEffect, useState, useRef } from "react"; import theme from "../theme.jsx"; import ReactGA from "react-ga4"; import { Link } from "react-router-dom"; import { removeQuery } from "../components/ScrollToTop.jsx"; +import { useMemo } from "react"; import { Tabs, Tab, Collapse } from "@mui/material"; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; @@ -26,7 +27,7 @@ import { InstantSearch, Configure, connectSearchBox, - connectInfiniteHits, + connectHits, connectHitInsights, RefinementList, ClearRefinements, @@ -38,7 +39,6 @@ import aa from "search-insights"; import { useLocation } from 'react-router-dom'; import "./FilterCSS.css"; -import SearchContactForm from "../components/SearchContactForm.jsx"; import { Zoom, @@ -54,7 +54,7 @@ import { const searchClient = algoliasearch( "JNSS5CFDZZ", - "eb5fd80aa6ed5ab4730d836cff3ea283" + "c8f882473ff42d41158430be09ec2b4e" ); //const searchClient = algoliasearch("L55H18ZINA", "a19be455e7e75ee8f20a93d26b9fc6d6") @@ -77,13 +77,62 @@ const AppGrid = (props) => { const xs = parsedXs === undefined || parsedXs === null ? (isMobile ? 6 : 3) : parsedXs; + const [formMail, setFormMail] = React.useState(""); + const [message, setMessage] = React.useState(""); + const [formMessage, setFormMessage] = React.useState(""); const [deactivatedIndexes, setDeactivatedIndexes] = React.useState([]); + const buttonStyle = { + borderRadius: 30, + height: 50, + width: 220, + margin: isMobile ? "15px auto 15px auto" : 20, + fontSize: 18, + }; const innerColor = "rgba(255,255,255,0.65)"; const borderRadius = 3; window.title = "Shuffle | Apps | Find and integrate any app"; const noImage = "/public/no_image.png"; + const submitContact = (email, message) => { + const data = { + firstname: "", + lastname: "", + title: "", + companyname: "", + email: email, + phone: "", + message: message, + }; + + const errorMessage = + "Something went wrong. Please contact frikky@shuffler.io directly."; + + fetch(globalUrl + "/api/v1/contact", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }) + .then((response) => response.json()) + .then((response) => { + if (response?.success === true) { + setFormMessage(response.reason); + //toast("Thanks for submitting!") + } else { + setFormMessage(errorMessage); + } + + setFormMail(""); + setMessage(""); + }) + .catch((error) => { + setFormMessage(errorMessage); + console.log(error); + }); + }; + const SearchBox = ({ currentRefinement, refine, isSearchStalled, searchQuery, setSearchQuery }) => { var defaultSearch = ""; @@ -99,11 +148,10 @@ const AppGrid = (props) => { const params = Object.fromEntries(urlSearchParams.entries()); const foundQuery = params["q"]; if (foundQuery !== null && foundQuery !== undefined) { + console.log("Got query: ", foundQuery); refine(foundQuery); defaultSearch = foundQuery; - if (searchQuery !== foundQuery) { - setSearchQuery(foundQuery); - } + searchQuery = foundQuery } } //}, []) @@ -186,13 +234,7 @@ const AppGrid = (props) => { onChange={(event) => { const value = event.currentTarget.value; setSearchQuery(value); - const urlSearchParams = new URLSearchParams(window.location.search); - if (value) { - urlSearchParams.set("q", value); - } else { - urlSearchParams.delete("q"); - } - window.history.replaceState({}, '', `${window.location.pathname}?${urlSearchParams.toString()}`); + removeQuery("q"); debouncedRefine(value); }} onKeyDown={(event) => { @@ -210,21 +252,6 @@ const AppGrid = (props) => { const [currTab, setCurrTab] = useState(0); const location = useLocation(); - const conditionalSearchClient = useMemo(() => ({ - ...searchClient, - search(requests) { - if (currTab !== 0) { - return Promise.resolve({ - results: requests.map(() => ({ - hits: [], nbHits: 0, page: 0, nbPages: 0, hitsPerPage: 0, - processingTimeMS: 0, exhaustiveNbHits: true, query: "", params: "", - })), - }); - } - return searchClient.search(requests); - }, - }), [currTab]); - useEffect(() => { const queryParams = new URLSearchParams(location.search); const tabParam = queryParams.get('tab'); @@ -242,6 +269,7 @@ const AppGrid = (props) => { const newQueryParam = newTab === 0 ? 'all_apps' : newTab === 1 ? 'org_apps' : 'my_apps'; const queryParams = new URLSearchParams(location.search); queryParams.set('tab', newQueryParam); + queryParams.delete('q'); window.history.replaceState({}, '', `${location.pathname}?${queryParams.toString()}`); }; @@ -253,8 +281,6 @@ const AppGrid = (props) => { // Component to fetch all public app from the algolia. const Hits = ({ hits, - hasMore, - refineNext, insights, setIsAnyAppActivated, searchQuery @@ -262,32 +288,6 @@ const AppGrid = (props) => { const [mouseHoverIndex, setMouseHoverIndex] = useState(-1); var counted = 0; const [hoverEffect, setHoverEffect] = useState(-1); - const [isLoadingMore, setIsLoadingMore] = useState(false); - const loadMoreRef = useRef(null); - const scrollContainerRef = useRef(null); - const isFetchingMore = useRef(false); - - useEffect(() => { - isFetchingMore.current = false; - setIsLoadingMore(false); - }, [hits.length]); - - useEffect(() => { - return; // infinite scroll disabled - if (!loadMoreRef.current || !scrollContainerRef.current) return; - const observer = new IntersectionObserver( - (entries) => { - if (entries[0].isIntersecting && hasMore && !isFetchingMore.current) { - isFetchingMore.current = true; - setIsLoadingMore(true); - refineNext(); - } - }, - { root: scrollContainerRef.current, rootMargin: "200px" } - ); - observer.observe(loadMoreRef.current); - return () => observer.disconnect(); - }, [hasMore, refineNext]); const normalizedString = (name) => { if (typeof name === 'string') { @@ -359,11 +359,11 @@ const AppGrid = (props) => { } else { //toast.success(`App ${type}d Successfully!`); if (type === 'activate') { - setAllActivatedAppIds(prev => [...(prev || []), data.objectID]); + setAllActivatedAppIds(prev => [...prev, data.objectID]); setIsAnyAppActivated(true); } if (type === 'deactivate') { - const updatedIds = (allActivatedAppIds || []).filter(id => id !== data.objectID); + const updatedIds = allActivatedAppIds.filter(id => id !== data.objectID); setAllActivatedAppIds(updatedIds); } } @@ -373,16 +373,6 @@ const AppGrid = (props) => { }); } - const sortedHits = useMemo(() => { - const list = [...(hits || [])]; - if (!allActivatedAppIds?.length) return list; - return list.sort((a, b) => { - const aActive = allActivatedAppIds.includes(a.objectID) ? 1 : 0; - const bActive = allActivatedAppIds.includes(b.objectID) ? 1 : 0; - return bActive - aActive; - }); - }, [hits, hits?.length, allActivatedAppIds]); - let workflowDelay = 0; const isHeader = true; const paperStyle = { @@ -414,7 +404,6 @@ const AppGrid = (props) => { ) : (
{ scrollbarColor: "#494949 #2f2f2f", }} > - {sortedHits.map((data, index) => { + {hits?.map((data, index) => { const appUrl = isCloud === true ? `/apps/${data.objectID}` @@ -640,12 +629,6 @@ const AppGrid = (props) => { ); }) } -
- {isLoadingMore && ( -
- -
- )}
)} @@ -943,7 +926,7 @@ const AppGrid = (props) => { //Component to display all apps. const AllApps = ({ setIsAnyAppActivated }) => { - var [searchQuery, setSearchQuery] = useState(() => new URLSearchParams(window.location.search).get('q') || ""); + var [searchQuery, setSearchQuery] = useState(""); return (
{ }} onClick={() => { setSearchQuery(''); - removeQuery("q"); }} /> )} @@ -1033,15 +1015,7 @@ const AppGrid = (props) => { placeholder="Search your Activated or self-built apps" id="shuffle_search_field" onChange={(event) => { - const value = event.currentTarget.value; - setSearchQuery(value); - const urlSearchParams = new URLSearchParams(window.location.search); - if (value) { - urlSearchParams.set("q", value); - } else { - urlSearchParams.delete("q"); - } - window.history.replaceState({}, '', `${window.location.pathname}?${urlSearchParams.toString()}`); + setSearchQuery(event.currentTarget.value); }} onKeyDown={(event) => { if(event.key === "Enter") { @@ -1618,7 +1592,7 @@ const AppGrid = (props) => { //Component to fetch all apps created by user and Org const UserAndOrgApps = ({ selectedCategoryForUsersAndOgsApps, selectedTagsForUserAndOrgApps, selectedOptionOfCreatedWith, setselectedCategoryForUsersAndOgsApps, setSelectedTagsForUserAndOrgApps, setSelectedOptionOfCreatedWith }) => { - const [searchQuery, setSearchQuery] = useState(() => new URLSearchParams(window.location.search).get('q') || ""); + const [searchQuery, setSearchQuery] = useState(""); const [appsToShow, setAppsToShow] = useState([]); useEffect(() => { if (currTab === 1) { @@ -2029,7 +2003,7 @@ const AppGrid = (props) => { }; const CustomSearchBox = connectSearchBox(SearchBox); - const CustomHits = connectInfiniteHits(Hits); + const CustomHits = connectHits(Hits); const DisplayAllAppsTab = () => { const [selectedCategoryForUsersAndOgsApps, setselectedCategoryForUsersAndOgsApps] = useState([]); @@ -2038,7 +2012,7 @@ const AppGrid = (props) => { return (
- +
{currTab === 0 ? ( @@ -2063,7 +2037,7 @@ const AppGrid = (props) => { />
- {currTab === 0 && } +
); @@ -2086,7 +2060,80 @@ const AppGrid = (props) => { > {showSuggestion === true ? ( - +
+ + Can't find what you're looking for? + +
+ setFormMail(e.target.value)} + /> + setMessage(e.target.value)} + /> +
+ + + {formMessage} + +
) : null}
diff --git a/frontend/src/components/AppModal.jsx b/frontend/src/components/AppModal.jsx index b0f8728f..38c8581f 100644 --- a/frontend/src/components/AppModal.jsx +++ b/frontend/src/components/AppModal.jsx @@ -35,7 +35,7 @@ import { Context } from '../context/ContextApi.jsx'; const searchClient = algoliasearch( "JNSS5CFDZZ", - "33e4e3564f4f060e96e0531957bed552" + "c8f882473ff42d41158430be09ec2b4e" );; const AppModal = ({ open, onClose, app, globalUrl, getApps}) => { diff --git a/frontend/src/components/AppSearch1.jsx b/frontend/src/components/AppSearch1.jsx index d551841d..449ad925 100644 --- a/frontend/src/components/AppSearch1.jsx +++ b/frontend/src/components/AppSearch1.jsx @@ -13,7 +13,7 @@ import { InputAdornment, Typography, } from '@mui/material'; -const searchClient = algoliasearch("JNSS5CFDZZ", "33e4e3564f4f060e96e0531957bed552") +const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e") const Appsearch = props => { const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, newSelectedApp, setNewSelectedApp, defaultSearch, showSearch, ConfiguredHits, userdata, cy, isCreatorPage, actionImageList, setActionImageList, setUserSpecialzedApp, placeholder, diff --git a/frontend/src/components/Appsearch.jsx b/frontend/src/components/Appsearch.jsx index 5b529d9d..904cf7c2 100644 --- a/frontend/src/components/Appsearch.jsx +++ b/frontend/src/components/Appsearch.jsx @@ -20,7 +20,7 @@ import { } from '@mui/material'; import aa from 'search-insights' -const searchClient = algoliasearch("JNSS5CFDZZ", "33e4e3564f4f060e96e0531957bed552") +const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e") const Appsearch = props => { const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, newSelectedApp, setNewSelectedApp, defaultSearch, showSearch, ConfiguredHits, userdata, cy, isCreatorPage, actionImageList, setActionImageList, setUserSpecialzedApp, inputHeight, apps, } = props const { themeMode } = useContext(Context) diff --git a/frontend/src/components/BillingStats.jsx b/frontend/src/components/BillingStats.jsx index 05796cbd..d2d2e304 100644 --- a/frontend/src/components/BillingStats.jsx +++ b/frontend/src/components/BillingStats.jsx @@ -92,11 +92,6 @@ const AppStats = (defaultprops) => { const statKey = syncStats === true ? "onprem_stats" : "daily_statistics" const dailyStats = inputdata[statKey] if (dailyStats === undefined || dailyStats === null) { - setAppruns(undefined) - setWorkflowRuns(undefined) - setSubflowRuns(undefined) - setChildOrgsAppRuns(undefined) - setApprunCosts(undefined) return } @@ -383,17 +378,6 @@ const AppStats = (defaultprops) => { return } - if (syncStats && (statistics[statKey] === undefined || statistics[statKey] === null)) { - setOnpremAppRuns(0) - setFilteredStatistics(statistics) - setAppruns(undefined) - setWorkflowRuns(undefined) - setSubflowRuns(undefined) - setChildOrgsAppRuns(undefined) - setApprunCosts(undefined) - return - } - // Calculate month to date cost var mtd_cost = 0 for (let key in statistics[statKey]) { diff --git a/frontend/src/components/CacheView.jsx b/frontend/src/components/CacheView.jsx index 4c9fd46c..ef5719e7 100644 --- a/frontend/src/components/CacheView.jsx +++ b/frontend/src/components/CacheView.jsx @@ -1,8 +1,6 @@ import React, { useState, useEffect, useContext, memo } from "react"; import { makeStyles } from "@mui/styles"; import { getTheme } from "../theme.jsx"; -import SubOrgDistributionDialog from "./SubOrgDistributionDialog.jsx"; -import DeleteConfirmDialog from "./DeleteConfirmDialog.jsx"; import { toast } from 'react-toastify'; import ReactJson from "react-json-view-ssr"; @@ -19,6 +17,8 @@ import { Button, Tabs, Tab, + List, + ListItem, ListItemText, IconButton, Dialog, @@ -82,7 +82,6 @@ import { Hub as HubIcon, Key as KeyIcon, FlashOn as FlashOnIcon, - Search as SearchIcon, } from "@mui/icons-material"; import { Context } from "../context/ContextApi.jsx"; @@ -128,9 +127,6 @@ const CacheView = memo((props) => { const [showDistributionPopup, setShowDistributionPopup] = useState(false); const [selectedSubOrg, setSelectedSubOrg] = useState([]); const [selectedCacheKey, setSelectedCacheKey] = useState(""); - const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false); - const [deleteConfirmTarget, setDeleteConfirmTarget] = useState(null); - const [distribOrgOrder, setDistribOrgOrder] = useState([]); const [totalAmount, setTotalAmount] = useState(0); const [page, setPage] = useState(0); const [pageSize, setPageSize] = useState(50) @@ -237,7 +233,7 @@ const CacheView = memo((props) => { { "name": "Enrich", - "description": "Enriches the data. Uses regex keys and runs a workflow in the background. Added to the 'enrichments' key.", + "description": "Enriches the data. Only runs on valid JSON data AND if the 'enrichment' field does not exist.", "type": "singul", "options": [{ "key": "", @@ -335,14 +331,6 @@ const CacheView = memo((props) => { // In order to make linking weird urls from workflow page work. if (urlParams.get("src") == "workflow") { - if (categoryParam === "OCSF") { - const newParam = "shuffle-security incidents" - - urlParams.set("category", newParam) - window.history.replaceState({}, '', `${window.location.pathname}?${urlParams.toString()}`) - categoryParam = newParam - } - if (categoryParam?.toLowerCase().startsWith("list")) { const newParam = categoryParam.substring(5).replaceAll("%20", "_") @@ -582,26 +570,17 @@ const CacheView = memo((props) => { .then((response) => { if (response.status === 200) { if (refreshList === undefined || refreshList === null || refreshList === true) { + toast.success("Deleted datastore entry"); setTimeout(() => { listOrgCache(orgId, selectedCategory, 0, pageSize, page) }, 1000); } } else { - if (refreshList === undefined || refreshList === null || refreshList === true) { - setTimeout(() => { - listOrgCache(orgId, selectedCategory, 0, pageSize, page) - }, 1000); - } toast.error(`Failed deleting entry ${key} in category ${itemCategory || selectedCategory}. If this persists, please contact support@shuffler.io.`) } }) .catch((error) => { - if (refreshList === undefined || refreshList === null || refreshList === true) { - setTimeout(() => { - listOrgCache(orgId, selectedCategory, 0, pageSize, page) - }, 1000); - } toast(error.toString()); }); }; @@ -852,11 +831,6 @@ const CacheView = memo((props) => { Category: {dataValue.category} : null} - {dataValue?.enrichments !== undefined && dataValue?.enrichments !== null && dataValue.enrichments.length > 0 ? - - Enrichments: {dataValue.enrichments.length} - - : null} {dataValue?.tags !== undefined && dataValue?.tags !== null && dataValue?.tags?.length > 0 ?
@@ -926,6 +900,33 @@ const CacheView = memo((props) => {
); + const handleSelectSubOrg = (id, action) => { + if (action === "all") { + const childOrgs = userdata.orgs.filter( + (data) => data.creator_org === userdata.active_org.id + ); + setSelectedSubOrg((prev) => { + if (prev.length === childOrgs.length) { + // If all child orgs are already selected, clear the selection + return []; + } else { + // Otherwise, select all child org IDs + return childOrgs.map((data) => data.id); + } + }); + } else if (action === "none") { + setSelectedSubOrg([]); + } else { + setSelectedSubOrg((prev) => { + if (prev.includes(id)) { + return prev.filter((data) => data !== id); + } else { + return [...prev, id]; + } + }); + } + }; + const changeDistribution = (id, selectedSubOrg) => { editFileConfig(id, [...new Set(selectedSubOrg)], selectedCategory) @@ -939,6 +940,8 @@ const CacheView = memo((props) => { selected_suborgs: selectedSubOrg, category: category === undefined || category === "" || category === "default" ? "" : category, } + + console.log("data: ", data); const url = `${globalUrl}/api/v1/orgs/${orgId}/cache/config`; @@ -972,41 +975,98 @@ const CacheView = memo((props) => { }; + const cacheDistributionModal = showDistributionPopup ? ( + setShowDistributionPopup(false)} + PaperProps={{ + sx: { + borderRadius: theme?.palette?.DialogStyle?.borderRadius, + border: theme?.palette?.DialogStyle?.border, + fontFamily: theme?.typography?.fontFamily, + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + zIndex: 1000, + minWidth: "600px", + minHeight: "320px", + overflow: "auto", + '& .MuiDialogContent-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogTitle-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogActions-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + }, + }} + > + + + Select sub-org to distribute Datastore key + + + + {handleSelectSubOrg(null, "none")}}>None + {handleSelectSubOrg(null, "all")}}>All + {userdata.orgs.map((data, index) => { + if (data.creator_org !== userdata.active_org.id) { + return null; + } + const imagesize = 22; + const imageStyle = { + width: imagesize, + height: imagesize, + pointerEvents: "none", + marginRight: 10, + marginLeft: data.id === userdata.active_org.id ? 0 : 20, + }; - const deleteConfirmDialog = ( - { setDeleteConfirmOpen(false); setDeleteConfirmTarget(null); }} - onConfirm={() => { - if (deleteConfirmTarget?.bulk) { - const itemsToDelete = selectedRows.map(rowId => - listCache.find(item => `${item.key}_${item.category || ""}` === rowId) - ).filter(Boolean); + const image = data.image === "" ? ( + {data.name} + ) : ( + {data.name} + ); - const count = itemsToDelete.length; - setSelectedRows([]); - itemsToDelete.forEach(item => deleteEntry(orgId, item.key, item.category, false)); + return ( + handleSelectSubOrg(data.id)} + style={{ display: "flex", alignItems: "center" }} + > + + {image} + {data.name} + + ); + })} - setTimeout(() => { - listOrgCache(orgId, selectedCategory, 0, pageSize, page); - toast.success("Deleted " + count + " keys from datastore"); - }, 3000); - } else { - deleteEntry(orgId, deleteConfirmTarget.key, deleteConfirmTarget.category); - } - setDeleteConfirmOpen(false); - setDeleteConfirmTarget(null); - }} - title={deleteConfirmTarget?.bulk ? `Delete ${selectedRows?.length} Key${selectedRows?.length > 1 ? "s" : ""}?` : "Delete Key?"} - description={ - deleteConfirmTarget?.bulk - ? <>Are you sure you want to delete {selectedRows?.length} key{selectedRows?.length > 1 ? "s" : ""}? - : <>Are you sure you want to delete {deleteConfirmTarget?.key}? - } - warningText="This cannot be undone. Any workflows using these keys will lose access." - /> - ); +
+ + +
+
+
+ ) : null; const saveAutomation = (allAutomation, settings) => { // Check if icon is a string. Otherwise make it empty. @@ -1699,10 +1759,6 @@ const CacheView = memo((props) => { enableClipboard={(copy) => { handleReactJsonClipboard(copy) }} - onSelect={(select) => { - //currentParams.set("category", selectedCategory); - //HandleJsonCopy(validate.result, select, "exec"); - }} collapseStringsAfterLength={theme.palette.jsonCollapseStringsAfterLength} iconStyle={theme.palette.jsonIconStyle} displayDataTypes={false} @@ -1879,7 +1935,6 @@ const CacheView = memo((props) => { "workflow_id": data.workflow_id, "category": data.category, "tags": data.tags, - "enrichments": data.enrichments, }) setValue(newvalue) setModalOpen(true) @@ -1964,8 +2019,7 @@ const CacheView = memo((props) => { onClick={(e) => { e.preventDefault() e.stopPropagation() - setDeleteConfirmTarget({ key: data.key, category: data.category }) - setDeleteConfirmOpen(true) + deleteEntry(orgId, data.key, data.category) }} > { style={{ margin: "auto" }} color="secondary" onClick={() => { - setShowDistributionPopup(true); - let initialSelected = []; + setShowDistributionPopup(true) if(data?.suborg_distribution?.length > 0){ - initialSelected = data.suborg_distribution; + setSelectedSubOrg(data.suborg_distribution) + }else{ + setSelectedSubOrg([]) } - setSelectedSubOrg(initialSelected); - setSelectedCacheKey(data.key); - const suborgs = (userdata?.orgs || []).filter(o => o.creator_org === userdata?.active_org?.id); - const sorted = [...suborgs].sort((a, b) => { - const aS = initialSelected.includes(a.id); - const bS = initialSelected.includes(b.id); - if (aS !== bS) return bS - aS; - return a.name.localeCompare(b.name); - }); - setDistribOrgOrder(sorted.map(o => o.id)); + setSelectedCacheKey(data.key) }} /> @@ -2070,7 +2116,6 @@ const CacheView = memo((props) => { var previousgroup = "" const isAutomating = categoryAutomations?.find((automation) => automation.enabled) !== undefined - const isAutomatingAccess = categoryConfig?.settings?.timeout >= 60 || categoryConfig?.settings?.public === true ? true : false return (
{ apps={apps} /> - { setShowDistributionPopup(false); setSelectedCacheKey(""); }} - title="Distribute Datastore Key to Sub-Organizations" - extraInfo={selectedCacheKey ? `Selected Key: ${selectedCacheKey}` : null} - orgs={distribOrgOrder.map(id => (userdata?.orgs || []).find(o => o.id === id)).filter(Boolean)} - selectedOrgIds={selectedSubOrg} - onSelectionChange={setSelectedSubOrg} - onSave={(ids) => { changeDistribution(selectedCacheKey, ids); }} - /> - {deleteConfirmDialog} + {cacheDistributionModal}
@@ -2169,6 +2204,7 @@ const CacheView = memo((props) => { height: 35, textTransform: 'none', + //border: isAutomating ? `1px solid ${theme.palette.primary.main}` : null, }} variant="outlined" color="secondary" @@ -2236,12 +2272,12 @@ const CacheView = memo((props) => { datastoreCategories !== null && datastoreCategories.length > 1 ? ( - + { marginLeft: 3, }} variant="outlined" - color={isAutomatingAccess ? "primary" : "secondary"} + color="secondary" disabled={selectedCategory === undefined || selectedCategory === "" || selectedCategory === "default"} onClick={() => { setShowSettingsMenu(true) }} > - + @@ -2841,8 +2877,27 @@ const CacheView = memo((props) => { + {formMessage} +
+ : null }
) diff --git a/frontend/src/components/DeleteConfirmDialog.jsx b/frontend/src/components/DeleteConfirmDialog.jsx deleted file mode 100644 index 459f077b..00000000 --- a/frontend/src/components/DeleteConfirmDialog.jsx +++ /dev/null @@ -1,61 +0,0 @@ -import React, { memo, useContext } from 'react'; -import { - Button, - Dialog, - DialogTitle, - DialogContent, - DialogActions, - Typography, -} from '@mui/material'; -import { getTheme } from '../theme.jsx'; -import { Context } from '../context/ContextApi.jsx'; - -const DeleteConfirmDialog = memo(({ open, onClose, onConfirm, title, description, warningText }) => { - const { themeMode, brandColor } = useContext(Context); - const theme = getTheme(themeMode, brandColor); - - return ( - - - {title} - - - {description} - {warningText && ( - - {warningText} - - )} - - - - - - - ); -}); - -export default DeleteConfirmDialog; diff --git a/frontend/src/components/DiscordChat.jsx b/frontend/src/components/DiscordChat.jsx index c220d36a..2bbc3b2c 100644 --- a/frontend/src/components/DiscordChat.jsx +++ b/frontend/src/components/DiscordChat.jsx @@ -3,8 +3,11 @@ import algoliasearch from 'algoliasearch'; import theme from '../theme.jsx'; import { InstantSearch, connectSearchBox, connectHits } from 'react-instantsearch-dom'; import { + Grid, + Paper, TextField, Typography, + Button, InputAdornment, Avatar, List, @@ -13,7 +16,6 @@ import { ListItemText, } from '@mui/material'; import { Search as SearchIcon } from '@mui/icons-material'; -import SearchContactForm from '../components/SearchContactForm.jsx'; import useDebouncedCallback from '../utils/useDebouncedCallback.jsx'; @@ -21,9 +23,52 @@ const searchClient = algoliasearch("JNSS5CFDZZ", "1e5f29b1550939855de5915eac3bf5 const DiscordChat = props => { const { isMobile, globalUrl } = props + const [value, setValue] = useState(""); + const [formMail, setFormMail] = React.useState(""); + const [message, setMessage] = React.useState(""); + const [formMessage, setFormMessage] = React.useState(""); + const buttonStyle = {borderRadius: 30, height: 50, width: 220, margin: isMobile ? "15px auto 15px auto" : 20, fontSize: 18,} const borderRadius = 3 + const submitContact = (email, message) => { + const data = { + "firstname": "", + "lastname": "", + "title": "", + "companyname": "", + "email": email, + "phone": "", + "message": message, + } + + const errorMessage = "Something went wrong. Please contact frikky@shuffler.io directly." + + fetch(globalUrl+"/api/v1/contact", { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }) + .then(response => response.json()) + .then(response => { + if (response.success === true) { + setFormMessage(response.reason) + //toast("Thanks for submitting!") + } else { + setFormMessage(errorMessage) + } + + setFormMail("") + setMessage("") + }) + .catch(error => { + setFormMessage(errorMessage) + console.log(error) + }); + } + const SearchBox = ({ currentRefinement, refine }) => { const [inputValue, setInputValue] = useState(""); const debouncedRefine = useDebouncedCallback((value) => refine(value), 300); @@ -123,7 +168,61 @@ const DiscordChat = props => {
- +
+ + Can't find what you're looking for? + +
+ setFormMail(e.target.value)} + /> + setMessage(e.target.value)} + /> +
+ + {formMessage} +
{/* Search by diff --git a/frontend/src/components/DocsGrid.jsx b/frontend/src/components/DocsGrid.jsx index 64cbd8c4..57616bd2 100644 --- a/frontend/src/components/DocsGrid.jsx +++ b/frontend/src/components/DocsGrid.jsx @@ -4,7 +4,6 @@ import theme from '../theme.jsx'; import ReactGA from 'react-ga4'; import {Link} from 'react-router-dom'; import { removeQuery } from '../components/ScrollToTop.jsx'; -import SearchContactForm from '../components/SearchContactForm.jsx'; import { Search as SearchIcon, CloudQueue as CloudQueueIcon, Code as CodeIcon, Close as CloseIcon, Folder as FolderIcon, LibraryBooks as LibraryBooksIcon } from '@mui/icons-material'; import aa from 'search-insights' @@ -31,19 +30,61 @@ import { useDebouncedCallback } from "../utils/useDebouncedCallback.jsx"; -const searchClient = algoliasearch("JNSS5CFDZZ", "eb5fd80aa6ed5ab4730d836cff3ea283") +const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e") const DocsGrid = props => { const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, userdata, } = props const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows const xs = parsedXs === undefined || parsedXs === null ? isMobile ? 6 : 2 : parsedXs //const [apps, setApps] = React.useState([]); //const [filteredApps, setFilteredApps] = React.useState([]); + const [formMail, setFormMail] = React.useState(""); + const [message, setMessage] = React.useState(""); + const [formMessage, setFormMessage] = React.useState(""); + const buttonStyle = {borderRadius: 30, height: 50, width: 220, margin: isMobile ? "15px auto 15px auto" : 20, fontSize: 18,} const innerColor = "rgba(255,255,255,0.65)" const borderRadius = 3 window.title = "Shuffle | Apps | Find and integrate any app" + const submitContact = (email, message) => { + const data = { + "firstname": "", + "lastname": "", + "title": "", + "companyname": "", + "email": email, + "phone": "", + "message": message, + } + + const errorMessage = "Something went wrong. Please contact frikky@shuffler.io directly." + + fetch(globalUrl+"/api/v1/contact", { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }) + .then(response => response.json()) + .then(response => { + if (response.success === true) { + setFormMessage(response.reason) + //toast("Thanks for submitting!") + } else { + setFormMessage(errorMessage) + } + + setFormMail("") + setMessage("") + }) + .catch(error => { + setFormMessage(errorMessage) + console.log(error) + }); + } + const SearchBox = ({currentRefinement, refine, isSearchStalled} ) => { var defaultSearch = "" if (window !== undefined && window.location !== undefined && window.location.search !== undefined && window.location.search !== null) { @@ -83,15 +124,8 @@ const DocsGrid = props => { placeholder="Search our Documentation..." id="shuffle_search_field" onChange={(event) => { - const value = event.currentTarget.value - debouncedRefine(value) - const urlSearchParams = new URLSearchParams(window.location.search) - if (value) { - urlSearchParams.set("q", value) - } else { - urlSearchParams.delete("q") - } - window.history.replaceState(null, "", value ? `?${urlSearchParams.toString()}` : window.location.pathname) + removeQuery("q") + debouncedRefine(event.currentTarget.value) }} onKeyDown={(event) => { if(event.key === "Enter") { @@ -257,8 +291,62 @@ const DocsGrid = props => { {showSuggestion === true ? - - : null +
+ + Can't find what you're looking for? + +
+ setFormMail(e.target.value)} + /> + setMessage(e.target.value)} + /> +
+ + {formMessage} +
+ : null } {/* diff --git a/frontend/src/components/EnvironmentTab.jsx b/frontend/src/components/EnvironmentTab.jsx index 6d17f383..58dd47d5 100644 --- a/frontend/src/components/EnvironmentTab.jsx +++ b/frontend/src/components/EnvironmentTab.jsx @@ -36,7 +36,6 @@ import { ExpandLess as ExpandLessIcon, ExpandMore as ExpandMoreIcon, Delete as DeleteIcon, - Computer as ComputerIcon, } from "@mui/icons-material"; import { toast } from 'react-toastify'; import { Context } from '../context/ContextApi.jsx'; @@ -50,7 +49,6 @@ const EnvironmentTab = memo((props) => { const [modalUser, setModalUser] = React.useState({}); const [loginInfo, setLoginInfo] = React.useState(""); const [modalOpen, setModalOpen] = React.useState(false); - const [sensorGroup, setSensorGroup] = React.useState(false); const [showLoader, setShowLoader] = useState(true) const [commandController, setCommandController] = React.useState({ pipelines: false, @@ -537,36 +535,13 @@ const EnvironmentTab = memo((props) => { }, }, }} - style={{ - }} > - - Add Location - + + Add Location - - {sensorGroup ? - 'With "Sensor Groups" enabled, Runtime Locations allows you to run a lightweight log-collector and response agent onprem.' - : - 'Runtime Locations are a way to run automation in Shuffle. By default, it runs in Docker/Kubernetes and allows you to run AI Agents and Workflows in your designated datacenter.' - } - -
- - {sensorGroup ? - "Sensor Group name" - : - "Location Name" - } - +
+ Location Name { }} required fullWidth={true} - placeholder="automation location 3" + placeholder="datacenter froglantern" id="environment_name" margin="normal" variant="outlined" - onChange={(event) => { + onChange={(event) => changeModalData("environment", event.target.value) - setUpdate(Math.random()) - }} + } />
{loginInfo} @@ -602,13 +576,12 @@ const EnvironmentTab = memo((props) => { @@ -983,7 +956,7 @@ const EnvironmentTab = memo((props) => { { > - - - : environment.run_type === "cloud" || environment.name === "Cloud" ? ( @@ -1307,9 +1275,6 @@ const EnvironmentTab = memo((props) => { - : - environment?.sensor_group === true ? - "N/A" : environment?.data_lake?.enabled && environment?.archived !== true ? ( { /> {
- {environment?.sensor_group === true ? -
- - Sensor Group - Host controls available in Shuffle Security - - - Total registered hosts: {environment?.sensor_hosts?.length || 0}.
Host management and response actions is done in Shuffle Security. Click the link above to manage. -
-
- :
Self-Hosted Orborus instance @@ -1748,7 +1703,6 @@ const EnvironmentTab = memo((props) => { }
- }
{currentEnvQueue.length === 0 ? null : diff --git a/frontend/src/components/Files.jsx b/frontend/src/components/Files.jsx index 5428a881..f98e8e22 100644 --- a/frontend/src/components/Files.jsx +++ b/frontend/src/components/Files.jsx @@ -29,8 +29,6 @@ import { Menu, Pagination, PaginationItem, - Box, - InputAdornment, } from "@mui/material"; import { DataGrid } from "@mui/x-data-grid"; @@ -47,12 +45,9 @@ import { Clear as ClearIcon, Add as AddIcon, SelectAll, - Search as SearchIcon, } from "@mui/icons-material"; import Dropzone from "../components/Dropzone.jsx"; -import SubOrgDistributionDialog from "./SubOrgDistributionDialog.jsx"; -import DeleteConfirmDialog from "./DeleteConfirmDialog.jsx"; import ShuffleCodeEditor from "../components/ShuffleCodeEditor1.jsx"; import {getTheme} from "../theme.jsx"; import { Context } from "../context/ContextApi.jsx"; @@ -87,10 +82,6 @@ const Files = memo((props) => { const [showDistributionPopup, setShowDistributionPopup] = useState(false) const [selectedSubOrg, setSelectedSubOrg] = useState([]) const [fileIdSelectedForDistribution, setFileIdSelectedForDistribution] = useState("") - const [fileNameSelectedForDistribution, setFileNameSelectedForDistribution] = useState("") - const [distribOrgOrder, setDistribOrgOrder] = useState([]) - const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false) - const [deleteConfirmTarget, setDeleteConfirmTarget] = useState(null) const [totalAmount, setTotalAmount] = useState(0); const [page, setPage] = useState(0); const [pageSize, setPageSize] = useState(50) @@ -335,7 +326,7 @@ const [filesLoaded, setFilesLoaded] = useState(false); navigator.clipboard.writeText(file.id); document.execCommand("copy"); - toast.info(file.id + " copied to clipboard"); + toast(file.id + " copied to clipboard"); }} > { e.stopPropagation(); e.preventDefault(); - setDeleteConfirmTarget({ id: file.id, filename: file.filename }); - setDeleteConfirmOpen(true); + deleteFile(file.id, true); }} > o.creator_org === userdata.active_org.id) - .map(o => o.id) - ) }} />
@@ -550,7 +534,7 @@ const [filesLoaded, setFilesLoaded] = useState(false); }) .then((responseJson) => { if (responseJson.success === true) { - toast.success("Successfully updated file"); + toast("Successfully updated file"); } }) .catch((error) => { @@ -864,39 +848,125 @@ const [filesLoaded, setFilesLoaded] = useState(false); : null - const deleteConfirmDialog = ( - { setDeleteConfirmOpen(false); setDeleteConfirmTarget(null); }} - onConfirm={() => { - if (deleteConfirmTarget?.bulk) { - const toDelete = [...selectedRows]; - const count = toDelete.length; - setSelectedRows([]); - toDelete.forEach(fileId => deleteFile(fileId, false)); - setTimeout(() => { - getFiles(selectedCategory); - toast.success('Deleted ' + count + ' file' + (count > 1 ? 's' : '')); - }, 3000); - } else { - deleteFile(deleteConfirmTarget.id, true); - } - setDeleteConfirmOpen(false); - setDeleteConfirmTarget(null); - }} - title={ - deleteConfirmTarget?.bulk - ? 'Delete ' + selectedRows?.length + ' File' + (selectedRows?.length > 1 ? 's' : '') + '?' - : 'Delete File?' - } - description={ - deleteConfirmTarget?.bulk - ? <>Are you sure you want to delete {selectedRows?.length} file{selectedRows?.length > 1 ? 's' : ''}? - : <>Are you sure you want to delete {deleteConfirmTarget?.filename}? - } - warningText="This cannot be undone." - /> - ); + const handleSelectSubOrg = (id, action) => { + if (action === "all") { + const childOrgs = userdata.orgs.filter( + (data) => data.creator_org === userdata.active_org.id + ); + setSelectedSubOrg((prev) => { + if (prev.length === childOrgs.length) { + // If all child orgs are already selected, clear the selection + return []; + } else { + // Otherwise, select all child org IDs + return childOrgs.map((data) => data.id); + } + }); + } else if (action === "none") { + setSelectedSubOrg([]); + } else { + setSelectedSubOrg((prev) => { + if (prev.includes(id)) { + return prev.filter((data) => data !== id); + } else { + return [...prev, id]; + } + }); + } + }; + + const fileDistributionModal = showDistributionPopup ? ( + setShowDistributionPopup(false)} + PaperProps={{ + sx: { + borderRadius: theme?.palette?.DialogStyle?.borderRadius, + border: theme?.palette?.DialogStyle?.border, + fontFamily: theme?.typography?.fontFamily, + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + zIndex: 1000, + minWidth: "600px", + minHeight: "320px", + overflow: "auto", + '& .MuiDialogContent-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogTitle-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + '& .MuiDialogActions-root': { + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + }, + }, + }} + > + + + Select sub-org to distribute files + + + + {handleSelectSubOrg(null, "none")}}>None + {handleSelectSubOrg(null, "all")}}>All + {userdata.orgs.map((data, index) => { + if (data.creator_org !== userdata.active_org.id) { + return null; + } + + const imagesize = 22; + const imageStyle = { + width: imagesize, + height: imagesize, + pointerEvents: "none", + marginRight: 10, + marginLeft: data.id === userdata.active_org.id ? 0 : 20, + }; + + const image = data.image === "" ? ( + {data.name} + ) : ( + {data.name} + ); + + return ( + handleSelectSubOrg(data.id)} + style={{ display: "flex", alignItems: "center" }} + > + + {image} + {data.name} + + ); + })} + +
+ + +
+
+
+ + ): null const deleteFile = (fileId, showSinglDeleteToast) => { @@ -1225,17 +1295,7 @@ const [filesLoaded, setFilesLoaded] = useState(false); }} onDrop={uploadFile} > - { setShowDistributionPopup(false); }} - title="Distribute File to Sub-Organizations" - extraInfo={fileNameSelectedForDistribution ? `Selected File: ${fileNameSelectedForDistribution}` : null} - orgs={distribOrgOrder.map(id => (userdata?.orgs || []).find(o => o.id === id)).filter(Boolean)} - selectedOrgIds={selectedSubOrg} - onSelectionChange={setSelectedSubOrg} - onSave={(ids) => { changeDistribution(fileIdSelectedForDistribution, ids); setShowDistributionPopup(false); }} - /> - {deleteConfirmDialog} + {fileDistributionModal}
@@ -1499,20 +1559,17 @@ const [filesLoaded, setFilesLoaded] = useState(false); autoFocus />} - {openEditor === true && fileContent !== undefined && fileContent !== null && fileContent.length > 0 ? - - : null} - + {isSelectedFiles?null: { + setSelectedRows([]); + getFiles(selectedCategory); + toast.success( + `Deleted ${selectedRows.length} file${selectedRows.length === 1 ? "" : "s"}` + ); + }, 2500); + } + } + }} variant={"outlined"} color="secondary" startIcon={ diff --git a/frontend/src/components/HealthPage.jsx b/frontend/src/components/HealthPage.jsx index 8018237f..b60a46be 100644 --- a/frontend/src/components/HealthPage.jsx +++ b/frontend/src/components/HealthPage.jsx @@ -1,17 +1,7 @@ -import React, { useEffect, useState, useCallback, useMemo, useContext } from 'react'; -import { Context } from '../context/ContextApi'; -import { useNavigate } from 'react-router-dom'; +import React, { useEffect, useState, useCallback } from 'react'; import { toast } from "react-toastify"; import { - CheckCircle as CheckCircleIcon, - Error as ErrorIcon, - Warning as WarningIcon, - AccountTree as WorkflowIcon, - Apps as AppsIcon, - Storage as StorageIcon, - FolderOpen as FileIcon, - FindInPage as SearchIcon, - ContentCopy as CopyIcon, + CheckOutlined as CheckOutlinedIcon, } from '@mui/icons-material'; import { @@ -19,274 +9,52 @@ import { ButtonGroup, Typography, LinearProgress, - Chip, - Tooltip, - Select, - MenuItem, - FormControl, - TextField, - Popover, } from "@mui/material"; -import { CalendarMonth as CalendarIcon } from '@mui/icons-material'; - import HealthBarChart from '../components/HealthBarChart.jsx'; -import LiveExecutionsChart from './LiveExecutionsGraph.jsx'; - - -const STATUS_STYLE = { - operational: { color: '#00F670', label: 'Operational', bg: 'rgba(0, 246, 112, 0.07)' }, - degraded: { color: '#FFD700', label: 'Degraded', bg: 'rgba(255, 215, 0, 0.07)' }, - outage: { color: '#FF354C', label: 'Outage', bg: 'rgba(255, 53, 76, 0.07)' }, -}; - -const REGION_DOMAIN = { - 'london': 'https://shuffler.io', - 'california': 'https://california.shuffler.io', - 'EU': 'https://frankfurt.shuffler.io', - 'canada': 'https://ca.shuffler.io', - 'australia': 'https://au.shuffler.io', -}; - -const RANGE_MILLIS = { - '24hr': 24 * 60 * 60 * 1000, - '7day': 7 * 24 * 60 * 60 * 1000, - '30d': 30 * 24 * 60 * 60 * 1000, - '90d': 90 * 24 * 60 * 60 * 1000, - '180d': 180 * 24 * 60 * 60 * 1000, - '365d': 365 * 24 * 60 * 60 * 1000, -}; - -const SERVICE_CONFIG = [ - { - key: 'workflows', - label: 'Workflows', - Icon: WorkflowIcon, - sloTarget: 99.95, - isHealthy: (item) => item.workflows?.run_finished === true, - getOperations: (item) => [ - { key: 'create', label: 'Create', value: item.workflows?.create }, - { key: 'run', label: 'Execute', value: item.workflows?.run }, - { key: 'run_finished', label: 'Completed', value: item.workflows?.run_finished }, - { key: 'delete', label: 'Delete', value: item.workflows?.delete }, - ], - getExtra: (item) => { - const took = item.workflows?.execution_took; - return (took != null && took > 0) ? `Exec time: ${Number(took).toFixed(2)}s` : null; - }, - getIds: (item) => [ - { label: 'Execution ID', value: item.workflows?.execution_id }, - { label: 'Workflow ID', value: item.workflows?.workflow_id }, - ].filter(id => !!id.value), - getErrors: (item) => { - const e = item.workflows?.error; - if (!e) return []; - return [ - { key: 'create', label: 'Create', msg: e.create }, - { key: 'run', label: 'Execute', msg: e.run }, - { key: 'run_finished', label: 'Completed', msg: e.run_finished }, - { key: 'workflow_validation', label: 'Validation', msg: e.workflow_validation }, - { key: 'delete', label: 'Delete', msg: e.delete }, - ].filter(err => !!err.msg); - }, - }, - { - key: 'apps', - label: 'Apps', - Icon: AppsIcon, - sloTarget: 99.95, - isHealthy: (item) => { const a = item.apps; return !!(a && a.create && a.run && a.delete); }, - getOperations: (item) => [ - { key: 'create', label: 'Create', value: item.apps?.create }, - { key: 'validate', label: 'Validate', value: item.apps?.validate }, - { key: 'run', label: 'Execute', value: item.apps?.run }, - { key: 'read', label: 'Read', value: item.apps?.read }, - { key: 'delete', label: 'Delete', value: item.apps?.delete }, - ], - getExtra: () => null, - getIds: (item) => [ - { label: 'App ID', value: item.apps?.app_id }, - { label: 'Execution ID', value: item.apps?.execution_id }, - ].filter(id => !!id.value), - getErrors: (item) => { - const e = item.apps?.error; - if (!e) return []; - return [ - { key: 'create', label: 'Create', msg: e.create }, - { key: 'validate', label: 'Validate', msg: e.validate }, - { key: 'run', label: 'Execute', msg: e.run }, - { key: 'read', label: 'Read', msg: e.read }, - { key: 'delete', label: 'Delete', msg: e.delete }, - ].filter(err => !!err.msg); - }, - }, - { - key: 'datastore', - label: 'Datastore', - Icon: StorageIcon, - sloTarget: 99.95, - isHealthy: (item) => { const d = item.datastore; return !!(d && d.create && d.read && d.delete); }, - getOperations: (item) => [ - { key: 'create', label: 'Create', value: item.datastore?.create }, - { key: 'read', label: 'Read', value: item.datastore?.read }, - { key: 'delete', label: 'Delete', value: item.datastore?.delete }, - ], - getExtra: () => null, - getIds: () => [], - getErrors: (item) => { - const e = item.datastore?.error; - if (!e) return []; - return [ - { key: 'create', label: 'Create', msg: e.create }, - { key: 'read', label: 'Read', msg: e.read }, - { key: 'delete', label: 'Delete', msg: e.delete }, - ].filter(err => !!err.msg); - }, - }, - { - key: 'fileops', - label: 'File Storage', - Icon: FileIcon, - sloTarget: 99.95, - isHealthy: (item) => { const f = item.fileops; return !!(f && f.create && f.get_file && f.delete); }, - getOperations: (item) => [ - { key: 'create', label: 'Create', value: item.fileops?.create }, - { key: 'get_file', label: 'Upload/Fetch', value: item.fileops?.get_file }, - { key: 'delete', label: 'Delete', value: item.fileops?.delete }, - ], - getExtra: () => null, - getIds: (item) => [ - { label: 'File ID', value: item.fileops?.fileId }, - ].filter(id => !!id.value), - getErrors: (item) => { - const e = item.fileops?.error; - if (!e) return []; - return [ - { key: 'create', label: 'Create', msg: e.create }, - { key: 'upload', label: 'Upload/Fetch', msg: e.upload }, - { key: 'delete', label: 'Delete', msg: e.delete }, - ].filter(err => !!err.msg); - }, - }, -]; - -const OPENSEARCH_CONFIG = { - key: 'opensearch', - label: 'OpenSearch', - Icon: SearchIcon, - sloTarget: 99.95, - isHealthy: (item) => item.opnsearch?.status === 'green', - getOperations: (item) => { - const s = item.opnsearch?.status; - return [{ key: 'cluster', label: 'Cluster', value: s === 'green' ? true : s === 'yellow' ? 'warn' : false }]; - }, - getExtra: (item) => item.opnsearch?.status ? `Cluster: ${item.opnsearch.status}` : null, - getIds: () => [], - getErrors: () => [], -}; - -const computeAvgUptime = (chartData) => { - if (!chartData || chartData.length === 0) return 100; - const withData = chartData.filter(d => d.avgRunFinished !== null); - if (withData.length === 0) return 100; - return parseFloat((withData.reduce((acc, d) => acc + d.avgRunFinished, 0) / withData.length).toFixed(2)); -}; - -const getStatusKey = (uptime, sloTarget) => { - if (uptime >= sloTarget) return 'operational'; - if (uptime >= 95) return 'degraded'; - return 'outage'; -}; - -// --- +import LiveExecutionsChart from '../components/LiveExecutionsGraph.jsx'; const HealthPage = (props) => { - const { userdata, isLoaded } = props; - const navigate = useNavigate(); - const { leftSideBarOpenByClick } = useContext(Context); + const { userdata, globalUrl } = props; const [healthData, setHealthData] = useState(null); - const [selectedRange, setSelectedRange] = useState('24hr'); - const [selectedRegion, setSelectedRegion] = useState('london'); + const [selectedRange, setSelectedRange] = useState('30d'); const [liveExecutionsData, setLiveExecutionsData] = useState([]); - const [liveExecutionsRange, setLiveExecutionsRange] = useState('1h'); - const [isHealthLoading, setIsHealthLoading] = useState(false); - const [isLiveExecutionsLoading, setIsLiveExecutionsLoading] = useState(false); + const [filteredData, setFilteredData] = useState([]); + const [averageUptime, setAverageUptime] = useState(0); + const [liveExecutionsRange, setLiveExecutionsRange] = useState('1h'); // Default to 1h + const [isHealthLoading, setIsHealthLoading] = useState(false); // Loading state for HealthBarChart + const [isLiveExecutionsLoading, setIsLiveExecutionsLoading] = useState(false); // Loading state for LiveExecutionsChart const [isFixingOpensearchPrefix, setIsFixingOpensearchPrefix] = useState(false); - const [selectedFailureDetails, setSelectedFailureDetails] = useState(null); - const [calendarAnchor, setCalendarAnchor] = useState(null); - const [customStart, setCustomStart] = useState(''); - const [customEnd, setCustomEnd] = useState(''); - const [customRange, setCustomRange] = useState(null); // { after: unix, before: unix } or null - const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; + const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (import.meta.env.VITE_IS_SSR === "true"); const fetchHealthStats = useCallback(async () => { - setIsHealthLoading(true); + setIsHealthLoading(true); // Start loading for HealthBarChart try { - const activeDomain = isCloud ? REGION_DOMAIN[selectedRegion] : window.location.origin; - const nowMs = Date.now(); - const CHUNK_MS = 90 * 24 * 60 * 60 * 1000; // 90-day chunks - - let rangeStartSec, rangeEndSec; - if (customRange) { - rangeStartSec = customRange.after; - rangeEndSec = customRange.before; - } else { - const rangeMs = RANGE_MILLIS[selectedRange] || RANGE_MILLIS['30d']; - rangeStartSec = Math.floor((nowMs - rangeMs) / 1000); - rangeEndSec = Math.floor(nowMs / 1000); + const response = await fetch(`${globalUrl}/api/v1/health/stats`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }); + if (!response.ok) { + throw new Error("Failed to fetch health stats"); } - - // Split large ranges into 90-day chunks to avoid oversized responses - const chunks = []; - let chunkEndSec = rangeEndSec; - const CHUNK_SEC = Math.floor(CHUNK_MS / 1000); - while (chunkEndSec > rangeStartSec) { - const chunkStartSec = Math.max(chunkEndSec - CHUNK_SEC, rangeStartSec); - chunks.push({ after: chunkStartSec, before: chunkEndSec }); - chunkEndSec = chunkStartSec; - } - - const fetchChunk = async (after, before) => { - const resp = await fetch(`${activeDomain}/api/v1/health/stats?after=${after}&before=${before}`, { - method: "GET", - headers: { - "Content-Type": "application/json", - Accept: "application/json", - }, - credentials: "include", - }); - if (!resp.ok) throw new Error("Failed to fetch health stats"); - return resp.json(); - }; - - let allData = []; - // Fetch chunks sequentially to avoid overwhelming the server - for (const chunk of chunks) { - const chunkData = await fetchChunk(chunk.after, chunk.before); - if (Array.isArray(chunkData)) { - // Filter to only items within this chunk's window to avoid duplicates - const filtered = chunkData.filter( - item => item.updated >= chunk.after && item.updated < chunk.before - ); - allData = allData.concat(filtered); - } - } - - setHealthData(allData); + const data = await response.json(); + setHealthData(data); } catch (error) { console.error("Error fetching health stats:", error); toast.error("Failed loading health stats"); } finally { - setIsHealthLoading(false); + setIsHealthLoading(false); // Stop loading for HealthBarChart } - }, [selectedRegion, selectedRange, customRange]); + }, [globalUrl]); const fetchLiveExecutions = useCallback(async (range = '1h') => { - if (!userdata.support_access) return; - setIsLiveExecutionsLoading(true); + setIsLiveExecutionsLoading(true); // Start loading for LiveExecutionsChart try { - const activeDomain = isCloud ? REGION_DOMAIN[selectedRegion] : window.location.origin; const fetchOptions = { method: "GET", credentials: "include", @@ -299,217 +67,326 @@ const HealthPage = (props) => { }; } + const now = Math.floor(Date.now() / 1000); + // let after = now - 3600; // Default to 1h + let mode = "" + + switch (range) { + case '1h': + mode = "1h" + break; + case '7h': + mode = "7h" + break; + case '1d': + mode = "1d" + break; + case '7d': + mode = "7d" + break; + case 'month': + mode = "month" + break; + default: + mode = "1h" + } + + if (!userdata.support_access) { + return + } + const response = await fetch( - `${activeDomain}/api/v1/health/executions/live?mode=${range}`, + `${globalUrl}/api/v1/health/executions/live?mode=${mode}`, fetchOptions ); - if (!response.ok) throw new Error("Failed to fetch live executions"); + if (!response.ok) { + throw new Error("Failed to fetch live executions"); + } const data = await response.json(); + console.log("Raw live executions data:", data); + if (Array.isArray(data)) { const formattedData = data .map(item => ({ ...item, + // failed: Number(item.failed) || 0, executing: Number(item.executing) || 0, finished: Number(item.finished) || 0, aborted: Number(item.aborted) || 0, - created_at: Number(item.created_at) || 0, + created_at: Number(item.created_at) || 0 })) .sort((a, b) => a.created_at - b.created_at); + + console.log("Formatted live executions data:", formattedData); setLiveExecutionsData(formattedData); } else { + console.error("Received invalid data format:", data); setLiveExecutionsData([]); } } catch (error) { console.error("Error fetching live executions:", error); toast.error("Failed loading live executions data"); } finally { - setIsLiveExecutionsLoading(false); + setIsLiveExecutionsLoading(false); // Stop loading for LiveExecutionsChart } - }, [userdata.support_access, selectedRegion]); + }, [globalUrl]); useEffect(() => { fetchHealthStats(); fetchLiveExecutions(liveExecutionsRange); + const interval = setInterval(() => fetchLiveExecutions(liveExecutionsRange), 60000); return () => clearInterval(interval); }, [fetchHealthStats, fetchLiveExecutions, liveExecutionsRange]); - // --- Derived data (memoized) --- - - const filteredHealthData = useMemo(() => { - if (!healthData || !Array.isArray(healthData)) return []; - if (customRange) { - return healthData.filter(item => item.updated >= customRange.after && item.updated <= customRange.before); - } - const now = Date.now(); - const ms = RANGE_MILLIS[selectedRange] || RANGE_MILLIS['30d']; - return healthData.filter(item => now - item.updated * 1000 <= ms); - }, [healthData, selectedRange, customRange]); - - const hasOpensearch = useMemo(() => - Array.isArray(healthData) && healthData.some(item => item.opnsearch && item.opnsearch.status), - [healthData]); - - const latestEntry = useMemo(() => { - if (!filteredHealthData.length) return null; - return filteredHealthData.reduce((best, item) => item.updated > (best?.updated || 0) ? item : best, null); - }, [filteredHealthData]); - - const extractServiceChartData = useCallback((config, data, range) => { + const extractRunFinished = (data, range) => { if (!data || !Array.isArray(data)) return []; - const agg = new Map(); - // Anchor pre-fill to the most recent data point so the newest bar always - // reflects actual data instead of an empty "today/current-hour" slot. - const mostRecentMs = data.length > 0 - ? Math.max(...data.map(item => item.updated)) * 1000 - : Date.now(); - // Custom range uses UTC keys; all preset ranges use local timezone keys - const isCustom = range === 'custom'; - const getKey = (timestamp) => { - const d = new Date(timestamp); - if (isCustom) { - // UTC bucketing for custom date range (user picks UTC dates) - if (range === '24hr') { - return `${d.toISOString().split('T')[0]} ${String(d.getUTCHours()).padStart(2, '0')}:00`; - } - return d.toISOString().split('T')[0]; - } - // Local timezone bucketing for preset ranges - if (range === '24hr') { - return `${d.toLocaleDateString()} ${String(d.getHours()).padStart(2, '0')}:00`; - } - return d.toLocaleDateString(); + const currentDate = new Date().getTime(); + const rangeInMillis = { + '24hr': 24 * 60 * 60 * 1000, + '7day': 7 * 24 * 60 * 60 * 1000, + '30d': 30 * 24 * 60 * 60 * 1000, + '90d': 90 * 24 * 60 * 60 * 1000 }; + const filteredData = data.filter(item => currentDate - item.updated * 1000 <= rangeInMillis[range]); - // Pre-fill all slots anchored to the most recent data point (newest -> oldest) - if (range === '24hr') { - for (let h = 0; h <= 23; h++) { - const d = new Date(mostRecentMs - h * 60 * 60 * 1000); - d.setMinutes(0, 0, 0); - agg.set(getKey(d.getTime()), { total: 0, healthyCount: 0, failures: [] }); - } - } else if (range === '7day') { - for (let day = 0; day <= 6; day++) { - const d = new Date(mostRecentMs - day * 86400000); - agg.set(d.toLocaleDateString(), { total: 0, healthyCount: 0, failures: [] }); - } - } else if (range === '30d') { - for (let day = 0; day <= 29; day++) { - const d = new Date(mostRecentMs - day * 86400000); - agg.set(d.toLocaleDateString(), { total: 0, healthyCount: 0, failures: [] }); - } - } else if (range === '90d') { - for (let day = 0; day <= 89; day++) { - const d = new Date(mostRecentMs - day * 86400000); - agg.set(d.toLocaleDateString(), { total: 0, healthyCount: 0, failures: [] }); - } - } else if (range === '180d') { - for (let day = 0; day <= 179; day++) { - const d = new Date(mostRecentMs - day * 86400000); - agg.set(d.toLocaleDateString(), { total: 0, healthyCount: 0, failures: [] }); - } - } else if (range === '365d') { - for (let day = 0; day <= 364; day++) { - const d = new Date(mostRecentMs - day * 86400000); - agg.set(d.toLocaleDateString(), { total: 0, healthyCount: 0, failures: [] }); - } - } else if (range === 'custom' && customRange) { - // Pre-fill using original UTC date strings to avoid local TZ rollover - const startDay = new Date(customRange.startLabel + 'T00:00:00Z'); - const endDay = new Date(customRange.endLabel + 'T00:00:00Z'); - const totalDays = Math.round((endDay - startDay) / 86400000) + 1; - for (let day = 0; day < totalDays; day++) { - agg.set(new Date(startDay.getTime() + day * 86400000).toISOString().split('T')[0], { total: 0, healthyCount: 0, failures: [] }); - } - } + const aggregatedData = new Map(); - data.forEach(item => { - const key = getKey(item.updated * 1000); - const healthy = config.isHealthy(item); - if (agg.has(key)) { - const ex = agg.get(key); - ex.total++; - if (healthy) ex.healthyCount++; - else ex.failures.push(item); + filteredData.forEach(item => { + const timestamp = item.updated * 1000; // Convert Unix timestamp to milliseconds + let key; + + switch (range) { + case '24hr': + const date = new Date(timestamp); + const hour = date.getHours(); + const formattedDate = `${date.toLocaleDateString()} ${hour}:00`; + key = formattedDate; + break; + case '7day': + const date1 = new Date(timestamp); + const hour1 = date1.getHours(); + const formattedDate1 = `${date1.toLocaleDateString()} ${hour1}:00`; + key = formattedDate1; + break; + default: + key = new Date(timestamp).toLocaleDateString(); + } + + // Check if date already exists in the map + if (aggregatedData.has(key)) { + // Update aggregated values + const existingData = aggregatedData.get(key); + existingData.totalEntries++; + existingData.totalRunFinished += item.workflows.run_finished ? 1 : 0; + existingData.executionIds.push(item.workflows.execution_id); + } else { + // Add new entry to the map + aggregatedData.set(key, { + totalEntries: 1, + totalRunFinished: item.workflows.run_finished ? 1 : 0, + executionIds: [item.workflows.execution_id] + }); } - // skip items that fall outside the pre-filled window }); - return Array.from(agg.entries()) - .map(([date, { total, healthyCount, failures }]) => { - const pct = total === 0 ? null : (healthyCount / total) * 100; - const color = pct === null ? '#2a2a2a' : pct >= 99.5 ? '#00F670' : pct >= 95 ? '#FFD700' : '#FF354C'; + // Calculate averages and assign colors + const result = Array.from(aggregatedData.entries()).map(([key, { totalEntries, totalRunFinished, executionIds }]) => { + const avg = totalEntries > 0 ? totalRunFinished / totalEntries : 0; + const FinalAvg = avg * 100; + let color; + + if (FinalAvg >= 100) { + color = '#00F670'; + } else if (FinalAvg >= 98.50 && FinalAvg <= 99.99) { + color = '#FFD700'; + } else if (FinalAvg <= 98.49) { + color = '#FF354C'; + } + return { - date, - avgRunFinished: pct === null ? null : parseFloat(pct.toFixed(2)), - total, + date: range === '24hr' ? `${key}:00` : key, + avgRunFinished: FinalAvg, color, - executionIds: failures.map(f => f.workflows?.execution_id || f.id), - failures, + executionIds }; }); - }, [customRange]); - const serviceCharts = useMemo(() => { - const configs = hasOpensearch ? [...SERVICE_CONFIG, OPENSEARCH_CONFIG] : SERVICE_CONFIG; - const result = {}; - const activeRange = customRange ? 'custom' : selectedRange; - for (const cfg of configs) { - result[cfg.key] = extractServiceChartData(cfg, filteredHealthData, activeRange); - } return result; - }, [filteredHealthData, hasOpensearch, extractServiceChartData, selectedRange, customRange]); - - const systemStatus = useMemo(() => { - const configs = hasOpensearch ? [...SERVICE_CONFIG, OPENSEARCH_CONFIG] : SERVICE_CONFIG; - const statuses = configs.map(cfg => getStatusKey(computeAvgUptime(serviceCharts[cfg.key]), cfg.sloTarget)); - if (statuses.every(s => s === 'operational')) return 'operational'; - if (statuses.some(s => s === 'outage')) return 'outage'; - return 'degraded'; - }, [serviceCharts, hasOpensearch]); - - // Access guard — only redirect after auth state is confirmed loaded + }; useEffect(() => { - if (!isLoaded) return; - if (!userdata || !userdata.id) { - navigate('/login?view=health&message=You must be logged in to view this page', { replace: true }); - } else if (userdata.support_access === false) { - navigate('/', { replace: true }); + if (healthData) { + const newData = extractRunFinished(healthData, selectedRange); + setFilteredData(newData); + + const totalUptime = newData.reduce((acc, curr) => acc + curr.avgRunFinished, 0); + const avgUptime = totalUptime / newData.length; + setAverageUptime(avgUptime); } - }, [userdata, isLoaded, navigate]); + }, [selectedRange, healthData]); - // Render nothing only when definitively not authenticated/authorized - if (!isLoaded || !userdata?.id || userdata?.support_access === false) return null; + const filterDataByRange = (range) => { + setSelectedRange(range); + }; - const activeServices = hasOpensearch ? [...SERVICE_CONFIG, OPENSEARCH_CONFIG] : SERVICE_CONFIG; - const lastUpdated = latestEntry ? new Date(latestEntry.updated * 1000).toLocaleString() : null; - const backendVersion = latestEntry?.workflows?.backend_version || null; + const updateChartData = () => { + if (!filteredData) { + return { + labels: [], + datasets: [{ + label: "", + data: [], + backgroundColor: [], + borderWidth: 1, + barThickness: 7, // Default bar thickness + }], + }; + } + let barThickness = 7; - // --- Handlers --- + const labels = filteredData.map((value, i) => { + if (selectedRange === '24hr') { + const [datePart, hourPart] = value.date.split(' '); + const [month, day, year] = datePart.split('/'); + const monthIndex = parseInt(month, 10) - 1; + const date = new Date(year, monthIndex, day); + let formattedDate = `${date.toLocaleString('en-US', { month: 'short', day: '2-digit' })}`; + // Add hour part if available + if (hourPart) { + formattedDate += `, ${hourPart.split(':').slice(0, 2).join(':')}`; + } - const copyToClipboard = (text) => { - navigator.clipboard.writeText(text) - .then(() => toast.success('Copied to clipboard')) - .catch(() => toast.error('Failed to copy')); + return `${formattedDate} \nUptime: ${value.avgRunFinished.toFixed(2)}%`; + } else if (selectedRange === '7day') { + const [datePart, hourPart] = value.date.split(' '); + const [month, day, year] = datePart.split('/'); + const monthIndex = parseInt(month, 10) - 1; + const date = new Date(year, monthIndex, day); + let formattedDate = `${date.toLocaleString('en-US', { month: 'short', day: '2-digit' })}`; + + // Add hour part if available + if (hourPart) { + formattedDate += `, ${hourPart.split(':').slice(0, 2).join(':')}`; + } + return `${formattedDate} \nUptime: ${value.avgRunFinished.toFixed(2)}%`; + } + else { + const dateParts = value.date.split('/'); // Assuming the date format is "DD/MM/YYYY" + const date = new Date(`${dateParts[2]}-${dateParts[0]}-${dateParts[1]}`); // Reformat the date string to "YYYY-MM-DD" + + return `${date.toLocaleDateString('en-US', { month: 'short', day: '2-digit', year: 'numeric' })} \nUptime: ${value.avgRunFinished.toFixed(2)}%`; + } + }); + + if (selectedRange === '24hr') { + barThickness = 35; + } + else if (selectedRange === '7day') { + barThickness = 5; + } + else if (selectedRange === '30d') { + barThickness = 25; + } + + const datasets = [{ + label: "", + data: filteredData.map(item => 1), + backgroundColor: filteredData.map(item => item.color), + borderWidth: 1, + barThickness: barThickness, + }]; + + return { labels, datasets }; + }; + + const options = { + legend: { + display: false + }, + layout: { + padding: { + top: 0, // Adjust the top padding as needed + bottom: 20, // Adjust the bottom padding as needed + left: 20, // Adjust the left padding as needed + right: 20 // Adjust the right padding as needed + } + }, + scales: { + yAxes: [{ + ticks: { + display: false + } + }], + xAxes: [{ + ticks: { + display: false + } + }] + }, + tooltips: { + callbacks: { + label: function (tooltipItem, data) { + const label = data.labels[tooltipItem.index]; + return label.split('\n')[0]; // Return only the date part + }, + afterLabel: function (tooltipItem, data) { + const label = data.labels[tooltipItem.index]; + const uptime = label.match(/Uptime:\s*(\d+(?:\.\d+)?)/)[1]; // Extract uptime value using regex + return `Test-Workflow Health: ${uptime}%`; // Customize the uptime display + }, + title: function () { + return 'Fully Operational'; // Hide the tooltip title + } + } + } + }; + + const handleBarClick = (event, elements) => { + if (event && event.length > 0) { + const clickedIndex = event[0]._index + const clickedData = filteredData[clickedIndex] + const executionIds = clickedData.executionIds + .filter(executionId => { + const item = healthData.find(dataItem => dataItem.workflows.execution_id === executionId); + return item && item.workflows.run_finished === false; + }); + + // console.log("Filtered Execution IDs:", executionIds); + if (executionIds.length > 0) { + const url = `${globalUrl}/api/v1/health/stats?execution_id=${executionIds.join(',')}`; + window.open(url, '_blank'); + } else { + toast.success("All executions in selected period succeeded"); + } + } }; const handleFixOpensearchPrefix = async () => { - if (isFixingOpensearchPrefix) return; + if (isFixingOpensearchPrefix) { + return; + } + setIsFixingOpensearchPrefix(true); try { - const response = await fetch(`${window.location.origin}/api/v1/health/opensearch-prefix`, { + const response = await fetch(`${globalUrl}/api/v1/health/opensearch-prefix`, { method: "POST", - headers: { "Content-Type": "application/json", Accept: "application/json" }, + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, credentials: "include", }); + const data = await response.json(); if (!response.ok || !data.success) { - throw new Error(data?.reason || "Failed to fix opensearch prefix"); + const reason = data && data.reason ? data.reason : "Failed to fix opensearch prefix"; + throw new Error(reason); } + const reindexed = data.reindexed ? data.reindexed.length : 0; const aliasUpdates = data.alias_updates ? data.alias_updates.length : 0; const deleted = data.deleted_indices ? data.deleted_indices.length : 0; @@ -522,513 +399,130 @@ const HealthPage = (props) => { } }; - const handleBarClick = (serviceKey, barData) => { - if (!barData.failures || barData.failures.length === 0) { - toast.success('All checks passed in this period'); - setSelectedFailureDetails(null); - return; - } - setSelectedFailureDetails({ serviceKey, barData }); - }; + const healthBarData = updateChartData() - // --- Render helpers --- - - const renderOpBadge = (op) => { - const ok = op.value === true; - const warn = op.value === 'warn'; - const dotColor = ok ? '#00F670' : warn ? '#FFD700' : '#FF354C'; - const bg = ok ? 'rgba(0,246,112,0.07)' : warn ? 'rgba(255,215,0,0.07)' : 'rgba(255,53,76,0.07)'; - const border = ok ? 'rgba(0,246,112,0.18)' : warn ? 'rgba(255,215,0,0.18)' : 'rgba(255,53,76,0.18)'; - return ( -
-
- {op.label} -
- ); - }; - - const renderServiceCard = (cfg) => { - const chartData = serviceCharts[cfg.key] || []; - const avgUptime = computeAvgUptime(chartData); - const statusKey = getStatusKey(avgUptime, cfg.sloTarget); - const status = STATUS_STYLE[statusKey]; - const sloMet = statusKey === 'operational'; - const latestOps = latestEntry ? cfg.getOperations(latestEntry) : []; - const extra = latestEntry ? cfg.getExtra(latestEntry) : null; - const totalFails = chartData.reduce((acc, d) => acc + (d.failures?.length || 0), 0); - const IconComp = cfg.Icon; - - return ( -
- {/* LEFT — identity + uptime number */} -
-
-
- -
- {cfg.label} - -
- -
- - {avgUptime.toFixed(2)} - - % - uptime -
- -
- SLO {cfg.sloTarget}% - - {totalFails > 0 && ( - {totalFails} incident{totalFails !== 1 ? 's' : ''} - )} -
-
- - {/* CENTRE — full-width bar chart + SLO rail */} -
- {/* SLO progress rail */} -
-
- -
- -
- - {/* History spark bars */} - handleBarClick(cfg.key, barData)} - /> - - - ← newest · oldest → · click bar for details - -
- - {/* RIGHT — last check operations */} -
- Last Check -
- {latestOps.map(renderOpBadge)} -
- {extra && ( - {extra} - )} -
-
- ); - }; - - const renderFailureDetails = () => { - if (!selectedFailureDetails) return null; - const { serviceKey, barData } = selectedFailureDetails; - const cfg = activeServices.find(s => s.key === serviceKey); - if (!cfg) return null; - - console.log("bar is: ", barData) - - return ( -
- {/* Panel header */} -
-
-
- - {cfg.label} — {barData.date} - - - {barData.value != null ? barData.value.toFixed(2) : '—'}% uptime in this period -
- -
- - {/* Failure rows */} -
- {barData.failures.slice(0, 20).map((item, idx) => { - const ops = cfg.getOperations(item); - const extra = cfg.getExtra(item); - const ids = cfg.getIds ? cfg.getIds(item) : []; - const errors = cfg.getErrors ? cfg.getErrors(item) : []; - return ( -
- - {/* Row header: index + timestamp + op-status badges */} -
- #{idx + 1} - - {new Date(item.updated * 1000).toLocaleString()} - -
- {ops.map(op => { - const ok = op.value === true; - const warn = op.value === 'warn'; - return ( - - ); - })} -
- {extra && {extra}} -
- - {/* Error reasons */} - {errors.length > 0 && ( -
- Details - {errors.map(err => ( -
- {err.label}: - {err.msg} -
- ))} -
- )} - - {/* IDs with copy buttons */} - {ids.length > 0 && ( -
- {ids.map(id => ( -
- {id.label} - - {id.value.length > 20 ? id.value.substring(0, 20) + '…' : id.value} - - - - -
- ))} -
- )} - -
- ); - })} - {barData.failures.length > 20 && ( -
- - +{barData.failures.length - 20} more records not shown - -
- )} -
-
- ); - }; - - // --- Render --- - - const rangeLabel = { '24hr': 'last 24h', '7day': 'last 7 days', '30d': 'last 30 days', '90d': 'last 90 days', '180d': 'last 180 days', '365d': 'last 365 days' }; - const activRangeLabel = customRange - ? `${customRange.startLabel} – ${customRange.endLabel}` - : (rangeLabel[selectedRange] || selectedRange); return ( -
-
- - {/* === Page header === */} -
-
-
- Platform Health - {isCloud && ( - - - - )} - {!isCloud && ( - - )} -
- -
- {backendVersion && Backend v{backendVersion}} - {lastUpdated && Last check: {lastUpdated}} - {isCloud ? 'Cloud' : 'On-Premises'} -
- - {/* === Legend (Moved to top) === */} -
- {[{ color: '#00F670', label: '≥ SLO Healthy' }, { color: '#FFD700', label: '95–SLO Degraded' }, { color: '#FF354C', label: '< 95% Outage' }].map(({ color, label }) => ( -
-
- {label} -
- ))} - Click any bar to view failure details · White marker = SLO target -
-
- -
-
- - {[['24hr', '24h'], ['7day', '7d'], ['30d', '30d'], ['90d', '90d'], ['180d', '180d'], ['365d', '365d']].map(([key, label]) => { - const isDisabled = isHealthLoading || ['90d', '180d', '365d'].includes(key); - const isActive = !customRange && selectedRange === key; - return ( - - ); - })} - - - {/* Calendar range picker button */} - - - -
- - {/* Show active custom range label */} - {customRange && ( -
- - {customRange.startLabel} – {customRange.endLabel} - - -
- )} -
- - {/* Calendar popover */} - setCalendarAnchor(null)} - anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} - transformOrigin={{ vertical: 'top', horizontal: 'right' }} - PaperProps={{ style: { backgroundColor: '#111', border: '1px solid #252525', borderRadius: 12, minWidth: 310, overflow: 'hidden', boxShadow: '0 12px 40px rgba(0,0,0,0.7)', display: 'flex', flexDirection: 'column' } }} +
+ {/* Health Bar Chart Section */} +
+ + + + + + + + - -
- + {isFixingOpensearchPrefix ? 'Fixing Opensearch Prefix...' : 'Fix Opensearch Prefix'} +
- {/* Loading */} - {isHealthLoading && } + {/* Loading Bar for HealthBarChart */} + {isHealthLoading && ( + + )} - {/* === System status banner === */} - {!isHealthLoading && filteredHealthData.length > 0 && ( -
- {systemStatus === 'operational' - ? - : systemStatus === 'degraded' - ? - : } +
+
+
+ +
+ Workflow Health + Operational +
+
- - {systemStatus === 'operational' ? 'All Systems Operational' : systemStatus === 'degraded' ? 'Degraded Performance Detected' : 'Some Services Affected'} - - - {filteredHealthData.length} health check{filteredHealthData.length !== 1 ? 's' : ''} · {activRangeLabel} - + {averageUptime.toFixed(2)}% + Success Rate
+
+ +
- {/* SLO summary dots */} -
- {activeServices.map(cfg => { - const uptime = computeAvgUptime(serviceCharts[cfg.key]); - const dotColor = uptime >= cfg.sloTarget ? '#00F670' : uptime >= 95 ? '#FFD700' : '#FF354C'; - return ( - -
-
- {cfg.label} - {uptime.toFixed(2)}% -
- - ); - })} + {userdata.support_access && ( +
+
+ Live Executions + + + + + + {/* */} +
+ {/* Loading Bar for LiveExecutionsChart */} + {isLiveExecutionsLoading && ( + + )} +
)} - {/* === Service health rows (vertical) === */} -
- {activeServices.map(cfg => renderServiceCard(cfg))} -
- {/* === Failure details panel === */} - {renderFailureDetails()} - - {/* Legend removed and moved to the top */} - - {/* === Live Executions === */} - {userdata.support_access && ( - <> -
-
-
- Live Executions - - {[['1h', '1h'], ['7h', '7h'], ['1d', '1d'], ['7d', '7d']].map(([key, label]) => ( - - ))} - -
- {isLiveExecutionsLoading && } - -
- - )} -
); }; -export default HealthPage; \ No newline at end of file +export default HealthPage; diff --git a/frontend/src/components/LeftSideBar.jsx b/frontend/src/components/LeftSideBar.jsx index 6050f092..056867ae 100644 --- a/frontend/src/components/LeftSideBar.jsx +++ b/frontend/src/components/LeftSideBar.jsx @@ -52,12 +52,6 @@ const ShuffleLogo = "/images/Shuffle_logo.png"; const detectionIcon = "/icons/detection.svg"; const documentationIcon = "/icons/documentation.svg"; const ExpandMoreAndLessIcon = "/icons/expandMoreIcon.svg"; -const shuffleSecurityLogo = ( - - - -); - const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, SHUFFLE_VERSION }) => { @@ -93,17 +87,6 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, SHUFFLE_V ); const [activeOrgData, setActiveOrgData] = useState(null); const [isProdStatusOn, setIsProdStatusOn] = useState(false); - const [productAnchorEl, setProductAnchorEl] = useState(null); - - const handleProductClick = (event) => { - event.preventDefault(); - setProductAnchorEl((prev) => (prev ? null : event.currentTarget)); - }; - - const handleProductClose = () => { - setProductAnchorEl(null); - }; - const userOrgs = React.useMemo(() => { return orgOptions.find((option) => option.name === selectedOrg); }, [selectedOrg, orgOptions]); @@ -137,7 +120,7 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, SHUFFLE_V setCurrentSelectedTheme(userdata?.theme); } }, [userdata]); - + const CustomPopper = (props) => { @@ -856,8 +839,6 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, SHUFFLE_V regiontag = "EU-2"; } else if (regiontag === "ca"){ regiontag = "CA"; - } else if (regiontag === "uk"){ - regiontag = "UK"; } } } @@ -1041,7 +1022,7 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, SHUFFLE_V }} > - - { - !showPartnerLogo && e.preventDefault(); - }} - > - Shuffle Logo - {!showPartnerLogo && expandLeftNav && ( - - )} - - - - - - { - if (isCloud) { - //ReactGA.event({ - // category: "sidebar", - // action: "click_shuffle_security", - // label: "", - //}) - - window.location.href = "https://security.shuffler.io/incidents?utm_source=shuffler_sidebar"; - } else { - const { protocol, hostname } = window.location; - - var newPort = 3002; - if (protocol === "https") { - newPort = 3444 - } - - const newUrl = `${protocol}//${hostname}:${newPort}/incidents`; - window.location.href = newUrl; - } - }} - sx={{ - borderRadius: "8px", - padding: "10px 12px", - border: "1px solid transparent", - "&:hover": { - backgroundColor: themeMode === "dark" ? "#2C2C2C" : "#F5F5F5", - }, - display: "flex", - gap: "12px", - alignItems: "center", - }} - > - - {shuffleSecurityLogo} - - - Shuffle{" "} - Security - - - { - handleProductClose(); - window.location.href = - isCloud && !showPartnerLogo ? "/" : "/workflows"; - }} - sx={{ - borderRadius: "8px", - padding: "10px 12px", - border: - themeMode === "dark" - ? "1px solid rgba(242, 100, 2, 0.3)" - : "1px solid rgba(242, 100, 2, 0.2)", - backgroundColor: - themeMode === "dark" - ? "rgba(242, 100, 2, 0.05)" - : "rgba(242, 100, 2, 0.02)", - "&:hover": { - backgroundColor: - themeMode === "dark" - ? "rgba(242, 100, 2, 0.1)" - : "rgba(242, 100, 2, 0.06)", - }, - display: "flex", - gap: "12px", - alignItems: "center", - }} - > - - Shuffle - - - Shuffle{" "} - Core - - - + + Shuffle Logo + +
{ !isCloud && expandLeftNav && ( @@ -1944,8 +1747,7 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, SHUFFLE_V
- + {activeMainTab === "setup" && ( + <> + {(isIntegration || isAgent) && selectedAction && Object.getOwnPropertyNames(selectedAction).length > 0 && selectedActionParameters?.length > 0 ? + apps !== undefined && apps !== null && apps.length > 0 && wrapperapp !== undefined && newimage !== undefined ? +
+
{ + selectedAction.example = "noapp" + selectedAction.large_image = newimage + if (cy !== undefined && cy !== null) { + const foundnode = cy.getElementById(selectedAction.id) + if (foundnode !== undefined && foundnode !== null) { + foundnode.data("large_image", newimage) + } + } + + /* + const iconInfo = GetIconInfo(selectedAction) + if (iconInfo !== undefined && iconInfo !== null) { + selectedAction.fillGradient = iconInfo.fillGradient + + selectedAction.iconBackground = iconInfo.iconBackgroundColor + selectedAction.fillstyle = "linear-gradient" + } + */ + + const paramIndex = selectedAction.parameters !== undefined && selectedAction.parameters !== null ? selectedAction.parameters.findIndex((param) => param.name === "app_name") : -1 + if (paramIndex === -1) { + console.log("Couldn't find app_name parameter") + selectedAction.parameters.push({ + name: "app_name", + value: wrapperapp.name, + autocompleted: false, + }) + } else { + selectedAction.parameters[paramIndex].value = wrapperapp.name + } + + setSelectedAction(selectedAction) + setUpdate(Math.random()) + + }}> + +
+ +
+
+
+ + {apps.map((app, appIndex) => { + // Forces it into every category (for now) + // This is to make it possible to "use" shuffle for Singul natively + if (app.name === "Shuffle Tools") { + if (actionname == "Intel" || actionname == "Intel") { + app.categories = [actionname] + } + } + + if (app.categories === undefined || app.categories === null || app.categories.length === 0) { + return null + } + + var newactionname = actionname.toLowerCase() + if (isAgent === true) { + newactionname = "ai" + } + + var found = false + for (var key in app.categories) { + + var localnewactionname = newactionname + if (newactionname == "comms") { + localnewactionname = "communication" + } + + if (app.categories[key].toLowerCase() !== localnewactionname) { + continue + } + + found = true + break + } + + if (!found) { + return null + } + + var isAppSelected = false + const paramIndex = selectedAction.parameters.findIndex((param) => param.name === "app_name") + if (paramIndex > -1) { + // Check the actual value and if it's the same + if (selectedAction.parameters[paramIndex].value === app.name) { + isAppSelected = true + } + } + + return ( +
{ + selectedAction.example = "" + selectedAction.large_image = app.large_image + if (cy !== undefined && cy !== null) { + const foundnode = cy.getElementById(selectedAction.id) + if (foundnode !== undefined && foundnode !== null) { + foundnode.data("large_image", app.large_image) + } + } + + if (paramIndex === -1) { + console.log("Couldn't find app_name parameter") + selectedAction.parameters.push({ + name: "app_name", + value: app.name, + autocompleted: false, + }) + } else { + selectedAction.parameters[paramIndex].value = app.name + } + + setSelectedAction(selectedAction) + setUpdate(Math.random()) + + + var requiresAuth = app?.authentication?.required + if (requiresAuth && appAuthentication?.length > 0) { + for (var key in appAuthentication) { + if (appAuthentication[key]?.app?.name === app?.name) { + requiresAuth = false + break + } + } + } + + setRequiresAuthentication(requiresAuth); + }}> + + + +
+ ) + })} +
+ : null + : + null + } - {activeMainTab === "setup" && ( - <>
Name @@ -2652,21 +2657,7 @@ const ParsedAction = (props) => { placeholder={selectedAction.execution_delay} value={delay} onChange={(event) => { - // Check if positive number - if (isNaN(event.target.value) || Number(event.target.value) < 0) { - toast.error("Please enter a valid positive number for delay.") - return - } - - // Check if first number is 0 - if (event.target.value.length > 1 && event.target.value.charAt(0) === "0") { - event.target.value = event.target.value.substring(1) - } - setDelay(event.target.value) - selectedAction.execution_delay = event.target.value - setSelectedAction(selectedAction) - setUpdate(Math.random()) }} /> @@ -2711,13 +2702,9 @@ const ParsedAction = (props) => { fullWidth variant="contained" onClick={() => { - if (isCloud) { - ReactGA.event({ - category: "Integration", - action: "Authenticate", - label: `${selectedApp?.name} - Open 1`, - }) - } + //if (authenticationType.type === "oauth2" && authenticationType.redirect_uri !== undefined && authenticationType.redirect_uri !== null) { + // return null + //} setAuthenticationModalOpen(true); }} @@ -2731,7 +2718,19 @@ const ParsedAction = (props) => { ) : null} {/* Change made in new release when we added Tabs system in it */} - {appMayNeedAuth ? ( + {( + (selectedAction.authentication !== undefined && + selectedAction.authentication !== null && + selectedAction.authentication.length > 0) || + (selectedApp.name !== undefined && + (((selectedAction.authentication === undefined || + selectedAction.authentication === null || + selectedAction.authentication.length === 0)) || + isAgent || + isIntegration) && + requiresAuthentication) + ) ? ( +
{ 0 && selectedAction?.selectedAuthentication && typeof selectedAction.selectedAuthentication === 'object' && Object.getOwnPropertyNames(selectedAction.selectedAuthentication).length !== 0 ? ( + workflow?.suborg_distribution?.length > 0 && selectedAction?.selectedAuthentication && Object.getOwnPropertyNames(selectedAction.selectedAuthentication).length !== 0 ? (
{ labelId="select-app-auth" value={ selectedAction?.authentication_id === "authgroups" ? "authgroups" : - (selectedAction?.selectedAuthentication === null || !selectedAction?.selectedAuthentication || typeof selectedAction.selectedAuthentication !== 'object' || Object.getOwnPropertyNames(selectedAction.selectedAuthentication).length === 0) + !selectedAction?.selectedAuthentication || Object.getOwnPropertyNames(selectedAction.selectedAuthentication).length === 0 ? "No selection" : selectedAction?.selectedAuthentication } @@ -2998,14 +2997,6 @@ const ParsedAction = (props) => { variant="outlined" style={{}} onClick={() => { - if (isCloud) { - ReactGA.event({ - category: "Integration", - action: "Authenticate", - label: `${selectedApp?.name} - Open 2`, - }) - } - setAuthenticationModalOpen(true); }} > @@ -3014,18 +3005,68 @@ const ParsedAction = (props) => {
- + {requiresAuthentication && (!selectedAction.authentication_id || selectedAction.authentication_id === "") ? ( +
+ + Authentication needed. + + + Some steps in this workflow won’t run until you connect your account. + + +
+ ) : null}
) : null} - {selectedAction.authentication_id === "authgroups" && (authGroups === undefined || authGroups === null || authGroups.length === 0) ? - - - Create your first Authentication group - - - : null} + {selectedAction.authentication_id === "authgroups" && (authGroups === undefined || authGroups === null || authGroups.length === 0) ? + + + Create your first Authentication group + + + : null} {/*showEnvironment !== undefined && showEnvironment && environments.length > 1 && !isIntegration ? ( @@ -3118,8 +3159,6 @@ const ParsedAction = (props) => {
) : null*/} - - {workflow.execution_variables !== undefined && workflow.execution_variables !== null && workflow.execution_variables.length > 0 ? (
Runtime variable (optional) @@ -3431,80 +3470,11 @@ const ParsedAction = (props) => { ) : null}
- {appMayNeedAuth && !hasAuth && !isAgent && !isIntegration ? ( -
- - Authentication needed {isIntegration || isAgent ? `` : "."} - - - This step may not work until you authenticate it. - - -
- ) : null} - {activeMainTab === "setup" && ( - - - - setAnchorEl(null)} - - anchorOrigin={{ - vertical: 'bottom', - horizontal: 'left', - }} - transformOrigin={{ - vertical: 'top', - horizontal: 'left', - }} - - style={{ - zIndex: 20000, - marginTop: 2, - border: "1px solid rgba(255,255,255,0.3)", - }} - - PaperProps={{ - style: { - maxHeight: 600, - maxWidth: 450, - } - }} - > - { - e.preventDefault() - e.stopPropagation() - - const paramIndex = selectedAction.parameters.findIndex((param) => param.name === "app_name") - if (paramIndex === -1) { - selectedAction.parameters.push({ - "description": "The name of the app to run the LLM query against", - "id": "", - "name": "app_name", - "example": "", - "value": "", - "multiline": false, - "multiselect": false, - "options": null, - "action_field": "", - "variant": "STATIC_VALUE", - "required": true, - "configuration": false, - "tags": null, - "schema": { - "type": "" - }, - "skip_multicheck": false, - "value_replace": null, - "unique_toggled": false, - "error": "", - "hidden": false, - - "custom_value": true, - }) - } else { - selectedAction.parameters[paramIndex].custom_value = true - } - - // Overwrite params for custom value handling - const newSelectedActionParameters = JSON.parse(JSON.stringify(selectedAction?.parameters)) - setSelectedActionParameters(newSelectedActionParameters) - setSelectedAction(selectedAction) - setAnchorEl(null) - }} - selected={false} - style={{ - margin: 7, - display: "flex", - minWidth: 400, - maxWidth: 400, - cursor: "pointer", - }} - > - - Custom Value - - - - - {apps.map((item, index) => { - const parsedName = (item.name?.charAt(0).toUpperCase() + item.name?.substring(1)).replace(/_/g, " ") - return ( - { - //handleSelect(item) - setSelectedActionLocal(selectedAction, item) - setAnchorEl(null) - }} - selected={false} - style={{ - margin: 7, - display: "flex", - minWidth: 400, - maxWidth: 400, - cursor: "pointer", - }} - > - - - {parsedName} - - - ) - })} - - - - {apps.map((app, appIndex) => { - // Forces it into every category (for now) - // This is to make it possible to "use" shuffle for Singul natively - if (app.name === "Shuffle Tools") { - if (actionname == "Intel" || actionname == "Intel") { - app.categories = [actionname] - } - } - - var isAppSelected = false - const paramIndex = selectedAction.parameters.findIndex((param) => param.name === "app_name") - if (paramIndex > -1) { - // Check the actual value and if it's the same - //if (selectedAction.parameters[paramIndex].value === app.name) { - if (selectedAction.parameters[paramIndex].value.includes(app.name)) { - isAppSelected = true - } - } - - if (app.categories === undefined || app.categories === null || app.categories.length === 0) { - if (!isAppSelected) { - return null - } - } - - var newactionname = actionname.toLowerCase() - if (isAgent === true) { - //newactionname = "ai" - } else { - var found = false - for (var key in app.categories) { - - var localnewactionname = newactionname - if (newactionname == "comms") { - localnewactionname = "communication" - } - - if (app.categories[key].toLowerCase() !== localnewactionname) { - continue - } - - found = true - break - } - - if (!found && !isAppSelected) { - return null - } - } - - - return ( -
{ - setSelectedActionLocal(selectedAction, app) - }}> - - - -
- ) - })} - -
-
- : null - : null - } +
@@ -3824,7 +3524,7 @@ const ParsedAction = (props) => { marginBottom: hideExtraTypes ? 50 : 200, }} > { - selectedActionParameters !== undefined && selectedActionParameters !== null && selectedAction && selectedAction !== null && typeof selectedAction === 'object' && Object.getOwnPropertyNames(selectedAction).length > 0 && selectedActionParameters.length > 0 ? + selectedActionParameters !== undefined && selectedActionParameters !== null && selectedAction && Object.getOwnPropertyNames(selectedAction).length > 0 && selectedActionParameters.length > 0 ?
{/* { } if ((isIntegration || isAgent) && data.name === "app_name") { - if (data?.custom_value === true) { - //data.label = "Allowed MCPs" - } else { - return null - } - } + return null + } /* // Somehow autogenerate from the app itself @@ -4131,9 +3827,13 @@ const ParsedAction = (props) => { } if (selectedAction.name === "custom_action" && data.name === "body") { - const methodParam = selectedActionParameters.find(p => p.name === "method") || selectedAction.parameters?.find(p => p.name === "method"); - if (methodParam?.value?.toUpperCase() === "GET") { - return null + for (var key in selectedActionParameters) { + const param = selectedActionParameters[key] + if (param.name === "method") { + if (param.value === "GET") { + return null + } + } } } @@ -4197,7 +3897,6 @@ const ParsedAction = (props) => { backgroundColor: themeMode === "dark" ? "#161616" : "#CCCCCC", color: theme.palette.text.primary, fontWeight: 600, - borderRadius: "6px !important", "&:hover": { backgroundColor: themeMode === "dark" ? "rgba(0,0,0,0.3)" : "rgba(0,0,0,0.1)", }, @@ -4623,7 +4322,7 @@ const ParsedAction = (props) => { } - if ((multiline === undefined || multiline === false) && (data.name.startsWith("${") && data.name.endsWith("}"))) { + if ((multiline === undefined || multiline === false) && ((data?.autocompleted === true || data?.field_active === true) || data.name.startsWith("${") && data.name.endsWith("}"))) { multiline = true } @@ -5128,16 +4827,6 @@ const ParsedAction = (props) => { fullWidth id={"rightside_field_" + count} onChange={(e) => { - if (e.target.value.includes("custom_shuffle_action")) { - data.options = [] - selectedActionParameters[count].options = [] - setSelectedActionParameters(selectedActionParameters) - selectedAction.parameters = selectedActionParameters - setSelectedAction(selectedAction) - setUpdate(Math.random()) - return - } - changeActionParameter(e, count, data); setUpdate(Math.random()); }} @@ -5182,22 +4871,6 @@ const ParsedAction = (props) => { ); } )} - - - {isAgent || isIntegration ? - - Custom Value - - : null} ); } else if (data.variant === "STATIC_VALUE") { @@ -5590,14 +5263,12 @@ const ParsedAction = (props) => { const buttonTitle = `Authenticate the ${selectedApp?.name?.replaceAll("_", " ")} API` const hasAutocomplete = data?.autocompleted === true - const isPathField = selectedAction?.name === "custom_action" && data?.name === "path" - if (data.variant === undefined || data.variant === null) { data.variant = "STATIC_VALUE" } - var isFirstOptional = optionalFound === false && data.configuration === false && data.required === false && !isPathField ? true : false - if (optionalFound === false && data.configuration === false && data.required === false && !isPathField) { + var isFirstOptional = optionalFound === false && data.configuration === false && data.required === false ? true : false + if (optionalFound === false && data.configuration === false && data.required === false) { optionalFound = true } @@ -5624,7 +5295,7 @@ const ParsedAction = (props) => { } } - const isOptional = (data.configuration === false && data.required === false) && !isPathField + const isOptional = data.configuration === false && data.required === false return (
@@ -5681,13 +5352,6 @@ const ParsedAction = (props) => { color: theme.palette.textPrimary, }} onClick={() => { - if (isCloud) { - ReactGA.event({ - category: "Integration", - action: "Authenticate", - label: `${selectedApp?.name} - Open 4`, - }) - } setAuthenticationModalOpen(true); }} /> diff --git a/frontend/src/components/PartnersUsecasesTab.jsx b/frontend/src/components/PartnersUsecasesTab.jsx index 85c40d56..9cbc31a1 100644 --- a/frontend/src/components/PartnersUsecasesTab.jsx +++ b/frontend/src/components/PartnersUsecasesTab.jsx @@ -846,7 +846,6 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar setIsLoading(true); if(!isCloud || !userdata?.active_org?.is_partner) { // If the user is not a partner or if it's not a cloud environment do not make api call :) - setIsLoading(false); return; } // Load usecase data from API diff --git a/frontend/src/components/RuntimeDebugger.jsx b/frontend/src/components/RuntimeDebugger.jsx index 51a3aaaf..ba531c5b 100644 --- a/frontend/src/components/RuntimeDebugger.jsx +++ b/frontend/src/components/RuntimeDebugger.jsx @@ -1087,9 +1087,6 @@ const RuntimeDebugger = (props) => { options={[{ "name": "Agent Runs", "id": "AGENT", - },{ - "name": "Sensor Actions", - "id": "SENSOR_ACTION", }].concat(workflows)} fullWidth style={{ diff --git a/frontend/src/components/SearchContactForm.jsx b/frontend/src/components/SearchContactForm.jsx deleted file mode 100644 index ff5747a5..00000000 --- a/frontend/src/components/SearchContactForm.jsx +++ /dev/null @@ -1,121 +0,0 @@ -import React, { useState } from "react"; -import theme from "../theme.jsx"; -import { TextField, Typography, Button } from "@mui/material"; - -const SearchContactForm = ({ globalUrl, isMobile, tabName }) => { - const [formMail, setFormMail] = useState(""); - const [message, setMessage] = useState(""); - const [formMessage, setFormMessage] = useState(""); - - const submitContact = (email, message) => { - const data = { - firstname: "", - lastname: "", - title: "", - companyname: "", - email: email, - phone: "", - message: message, - }; - - const errorMessage = - "Something went wrong. Please contact frikky@shuffler.io directly."; - - fetch(globalUrl + "/api/v1/contact", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(data), - }) - .then((response) => response.json()) - .then((response) => { - setFormMessage( - response?.success === true ? response.reason : errorMessage - ); - setFormMail(""); - setMessage(""); - }) - .catch(() => { - setFormMessage(errorMessage); - }); - }; - - return ( -
- - Can't find what you're looking for? - -
- setFormMail(e.target.value)} - /> - setMessage(e.target.value)} - /> -
- - - {formMessage} - -
- ); -}; - -export default SearchContactForm; diff --git a/frontend/src/components/SearchData.jsx b/frontend/src/components/SearchData.jsx index f7a7dd33..de209b4b 100644 --- a/frontend/src/components/SearchData.jsx +++ b/frontend/src/components/SearchData.jsx @@ -47,7 +47,7 @@ const chipStyle = { backgroundColor: "#3d3f43", height: 30, marginRight: 5, paddingLeft: 5, paddingRight: 5, height: 28, cursor: "pointer", borderColor: "#3d3f43", color: "white", } -const searchClient = algoliasearch("JNSS5CFDZZ", "33e4e3564f4f060e96e0531957bed552") +const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e") const SearchData = props => { const { serverside, globalUrl, userdata } = props let navigate = useNavigate(); diff --git a/frontend/src/components/Searchfield.jsx b/frontend/src/components/Searchfield.jsx index 57558241..4447c73b 100644 --- a/frontend/src/components/Searchfield.jsx +++ b/frontend/src/components/Searchfield.jsx @@ -38,6 +38,7 @@ import aa from 'search-insights' import { InstantSearch, Configure, connectSearchBox, connectHits, Index } from 'react-instantsearch-dom'; //import { InstantSearch, SearchBox, Hits, connectSearchBox, connectHits, Index } from 'react-instantsearch-dom'; +import { HotKeys } from 'react-hotkeys'; // https://www.algolia.com/doc/api-reference/widgets/search-box/react/ const chipStyle = { backgroundColor: "#3d3f43", height: 30, marginRight: 5, paddingLeft: 5, paddingRight: 5, height: 28, cursor: "pointer", borderColor: "#3d3f43", color: "white", @@ -167,4 +168,4 @@ const SearchField = props => { ) } -export default SearchField; \ No newline at end of file +export default SearchField; diff --git a/frontend/src/components/ShuffleCodeEditor1.jsx b/frontend/src/components/ShuffleCodeEditor1.jsx index c2438b97..3059d936 100644 --- a/frontend/src/components/ShuffleCodeEditor1.jsx +++ b/frontend/src/components/ShuffleCodeEditor1.jsx @@ -61,6 +61,8 @@ import PaperComponent from "../components/PaperComponent.jsx"; import { padding, textAlign } from '@mui/system'; import data from '../frameworkStyle.jsx'; import { useNavigate, Link, useParams, useSearchParams } from "react-router-dom"; +import { tags as t } from '@lezer/highlight'; + import AceEditor from "react-ace"; import ace from "ace-builds"; @@ -147,10 +149,7 @@ const CodeEditor = (props) => { // Auto-indent JSON-like content (with safety hehe) const autoIndentContent = React.useCallback((content) => { - if (!isFileEditor) { - console.log("Autoindent disabled") - return content - } + return content // Safety checks :) if (!content || typeof content !== 'string' || content.trim().length === 0) { @@ -239,24 +238,6 @@ const CodeEditor = (props) => { expectedOutput(localcodedata) }, [localcodedata]) - useEffect(() => { - if (!isFileEditor) { - return - } - - if (codedata === undefined || codedata === null || typeof codedata !== 'string') { - return - } - - const indentedContent = autoIndentContent(codedata); - if (indentedContent !== undefined && indentedContent !== null) { - console.log("SETTING: ", indentedContent) - setlocalcodedata(indentedContent); - } else { - console.log("INDENT FAILED") - } - }, []) - // Auto-indent when codedata prop changes useEffect(() => { if (codedata && codedata !== localcodedata && typeof codedata === 'string') { @@ -1616,7 +1597,7 @@ const CodeEditor = (props) => { if (e.srcElement.className === "ace_content") { console.log("DRAG STOP IN CONTENT!", e.srcElement.className) - const usedposition = e.offsetY + let usedposition = e.offsetY if (usedposition === undefined || usedposition === null) { toast.info(`Error: LayerY is undefined or null. Please contact ${supportEmail}`) return @@ -1803,8 +1784,8 @@ const CodeEditor = (props) => { // zIndex: 12501, pointerEvents: "auto", color: theme.palette.DialogStyle.color, - minWidth: isMobile || isWorkflowEditor || fullScreenModeEnabled ? "100%" : isFileEditor ? 800 : "80%", - maxWidth: isMobile || isWorkflowEditor || fullScreenModeEnabled ? "100%" : isFileEditor ? 800 : "1100px", + minWidth: isMobile || isWorkflowEditor || fullScreenModeEnabled ? "100%" : isFileEditor ? "650px" : "80%", + maxWidth: isMobile || isWorkflowEditor || fullScreenModeEnabled ? "100%" : isFileEditor ? "650px" : "1100px", minHeight: isMobile || isWorkflowEditor || fullScreenModeEnabled ? "100%" : "auto", maxHeight: isMobile || isWorkflowEditor || fullScreenModeEnabled ? "100%" : "700px", border: "3px solid rgba(255,255,255,0.3)", @@ -2000,8 +1981,7 @@ const CodeEditor = (props) => { paddingLeft: 10, }} > - {/* cba positioning */} - File Editor ({localcodedata.length})                                                                             {validation === true ? Valid JSON : Invalid JSON} + File Editor ({localcodedata.length})
@@ -2531,7 +2511,7 @@ const CodeEditor = (props) => { }
- {/*(actionId || triggerId || conditionId) && !isWorkflowEditor && !isFileEditor ? + {(actionId || triggerId || conditionId) && !isWorkflowEditor && !isFileEditor ? <> { : null - */} + }
} @@ -2603,7 +2583,7 @@ const CodeEditor = (props) => { mode={isWorkflowEditor ? "yaml" : selectedAction === undefined ? "json" : selectedAction.name === "execute_python" ? "python" : selectedAction.name === "execute_bash" ? "bash" : "json"} theme="gruvbox" height={fullScreenModeEnabled ? "84vh" : isFileEditor ? 450 : isWorkflowEditor ? "90vh" : 550} - width={isFileEditor ? 800 : fullScreenModeEnabled ? isFileEditor ? "100%" : "50vw" : isWorkflowEditor ? "90vw" : "100%"} + width={isFileEditor ? 650 : fullScreenModeEnabled ? "50vw" : isWorkflowEditor ? "90vw" : "100%"} markers={markers} highlightActiveLine={false} @@ -2737,7 +2717,7 @@ const CodeEditor = (props) => {
: - {selectedAction?.name === "execute_python" || selectedAction?.name === "execute_bash" ? + {selectedAction.name === "execute_python" || selectedAction.name === "execute_bash" ? "Code to run" : triggerId ? `Output: ${triggerName?.replaceAll("_", " ").slice(0, 1).toUpperCase() + triggerName?.replaceAll("_", " ").slice(1)} (${triggerField})` : diff --git a/frontend/src/components/SubOrgDistributionDialog.jsx b/frontend/src/components/SubOrgDistributionDialog.jsx deleted file mode 100644 index 31740c80..00000000 --- a/frontend/src/components/SubOrgDistributionDialog.jsx +++ /dev/null @@ -1,265 +0,0 @@ -import React, { useState, useContext } from "react"; -import { - Dialog, - DialogTitle, - DialogContent, - Box, - TextField, - Button, - Typography, - List, - ListItem, - ListItemText, - Checkbox, - InputAdornment, -} from "@mui/material"; -import { Search as SearchIcon } from "@mui/icons-material"; -import { Context } from "../context/ContextApi.jsx"; -import { getTheme } from "../theme.jsx"; - -/** - * A reusable dialog for selecting/distributing sub-organizations. - * - * Props: - * open {boolean} - controls dialog visibility - * onClose {function} - called on Cancel or backdrop click (no args) - * title {string} - dialog title text - * extraInfo {string} - secondary line below the title (e.g. "Selected Key: xxx") - * orgs {Array} - ordered array of { id, name, image? } objects to display - * selectedOrgIds {string[]} - currently selected org IDs (controlled) - * onSelectionChange {function} - called with a updater fn (prev => next) when selection changes - * onSave {function} - called with the final selectedOrgIds array when Save is clicked - * disabled {boolean} - disables checkboxes and Save button (default: false) - */ -const SubOrgDistributionDialog = ({ - open, - onClose, - title, - extraInfo = null, - orgs = [], - selectedOrgIds = [], - onSelectionChange, - onSave, - disabled = false, -}) => { - const [searchQuery, setSearchQuery] = useState(""); - const { themeMode, brandColor } = useContext(Context); - const theme = getTheme(themeMode, brandColor); - - const safeOrgs = orgs || []; - - const filteredOrgs = safeOrgs.filter( - o => o && o.name.toLowerCase().includes(searchQuery.toLowerCase()) - ); - - const handleSelectAll = () => { - if (searchQuery) { - const filteredIds = filteredOrgs.map(o => o.id); - onSelectionChange(prev => [...new Set([...prev, ...filteredIds])]); - } else { - const allIds = safeOrgs.map(o => o.id); - onSelectionChange(prev => [...new Set([...prev, ...allIds])]); - } - }; - - const handleDeselectAll = () => { - if (searchQuery) { - const filteredIds = filteredOrgs.map(o => o.id); - onSelectionChange(prev => prev.filter(id => !filteredIds.includes(id))); - } else { - onSelectionChange([]); - } - }; - - const handleToggle = (id) => { - if (disabled) return; - onSelectionChange(prev => - prev.includes(id) ? prev.filter(i => i !== id) : [...prev, id] - ); - }; - - const handleClose = () => { - setSearchQuery(""); - onClose(); - }; - - const filteredSelected = filteredOrgs.filter(o => selectedOrgIds.includes(o.id)).length; - const countText = searchQuery - ? `${filteredSelected} of ${filteredOrgs.length} filtered selected` - : `${selectedOrgIds.length} of ${safeOrgs.length} selected`; - - const imageSize = 22; - const imageStyle = { width: imageSize, height: imageSize, pointerEvents: "none", marginRight: 10 }; - - return ( - - - - {title} - - {extraInfo && ( - - {extraInfo} - - )} - - - - setSearchQuery(e.target.value)} - InputProps={{ - startAdornment: ( - - - - ), - style: { color: theme.palette.textFieldStyle?.color }, - }} - style={{ backgroundColor: theme.palette.textFieldStyle?.backgroundColor, flexShrink: 0 }} - /> - -
- - - - {countText} - -
- -
- - {filteredOrgs.map((org, index) => { - const isSelected = selectedOrgIds.includes(org.id); - const hasImage = org.image !== undefined; - return ( - handleToggle(org.id)} - sx={{ - cursor: disabled ? "default" : "pointer", - backgroundColor: index % 2 === 0 - ? "transparent" - : themeMode === "dark" ? "rgba(255,255,255,0.02)" : "rgba(0,0,0,0.02)", - "&:hover": { - backgroundColor: themeMode === "dark" ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.05)", - }, - }} - > - - {hasImage && ( - org.image === "" ? ( - {org.name} - ) : ( - {org.name} - ) - )} - - - ); - })} - {filteredOrgs.length === 0 && ( - - - - )} - -
-
- - -
- - -
-
-
- ); -}; - -export default SubOrgDistributionDialog; diff --git a/frontend/src/components/TenantsTab.jsx b/frontend/src/components/TenantsTab.jsx index 779cc910..16cd5f11 100644 --- a/frontend/src/components/TenantsTab.jsx +++ b/frontend/src/components/TenantsTab.jsx @@ -1,6 +1,5 @@ import React, { memo, useContext, useEffect, useState } from 'react'; -import { DataGrid } from '@mui/x-data-grid'; -import { getTheme } from "../theme.jsx"; +import {getTheme} from "../theme.jsx"; import { Context } from '../context/ContextApi.jsx'; import { FormControl, @@ -25,11 +24,9 @@ import { IconButton, Modal, Checkbox, - Select, - MenuItem, -} from "@mui/material"; - -import { + } from "@mui/material"; + + import { Edit as EditIcon, Polyline as PolylineIcon, CheckCircle as CheckCircleIcon, @@ -37,13 +34,11 @@ import { Apps as AppsIcon, Business as BusinessIcon, Flag, - ArrowDropDown as ArrowDropDownIcon, + ArrowDropDown as ArrowDropDownIcon, VisibilityOff, Visibility, - KeyboardArrowLeft, - KeyboardArrowRight, -} from "@mui/icons-material"; + } from "@mui/icons-material"; import { toast } from 'react-toastify'; @@ -78,15 +73,8 @@ const TenantsTab = memo((props) => { const theme = getTheme(themeMode, brandColor); const [accountDeleteButtonClicked, setAccountDeleteButtonClicked] = useState(false); const [selectedSuborg, setSelectedSuborg] = useState(null); - const [rowsPerPage, setRowsPerPage] = useState(10); - const [nextCursor, setNextCursor] = useState(""); - const [currentCursor, setCurrentCursor] = useState(""); - const [cursorStack, setCursorStack] = useState([]); - const [localPage, setLocalPage] = useState(0); - const [loadingSubOrgs, setLoadingSubOrgs] = useState(false); - const [isChangingOrg, setIsChangingOrg] = useState(false); useEffect(() => { - if (parentOrg !== null && parentOrgFlag === null) { + if(parentOrg !== null && parentOrgFlag === null) { let regiontag = "UK"; let regionCode = "gb"; @@ -97,25 +85,25 @@ const TenantsTab = memo((props) => { regiontag = namesplit[namesplit.length - 1]; if (regiontag === "california") { - regiontag = "US"; - regionCode = "us"; + regiontag = "US"; + regionCode = "us"; } else if (regiontag === "frankfurt") { - regiontag = "EU-2"; - regionCode = "eu"; + regiontag = "EU-2"; + regionCode = "eu"; } else if (regiontag === "ca") { - regiontag = "CA"; - regionCode = "ca"; - } else if (regiontag === "au") { + regiontag = "CA"; + regionCode = "ca"; + }else if (regiontag === "au") { regiontag = "AUS"; regionCode = "au" } } setParentOrgFlag(regionCode); setParentOrgRegionName(regiontag); - } } + } }, [parentOrg, parentOrgFlag]); - + var syncList = [ { primary: "Workflows", @@ -139,26 +127,19 @@ const TenantsTab = memo((props) => { useEffect(() => { if (userdata.orgs !== undefined && userdata.orgs !== null && userdata.orgs.length > 0) { - handleGetSubOrgs(userdata.active_org.id, "", 100); + handleGetSubOrgs(userdata.active_org.id); } else console.log("error in user data") }, [userdata]); - const handleGetSubOrgs = (orgId, cursor = "", limit = 100, direction = "next") => { - const effectiveLimit = limit !== null ? limit : 100; + const handleGetSubOrgs = (orgId) => { if (orgId.length === 0) { toast("Organization ID not defined. Please contact us on https://shuffler.io if this persists logout."); return; } - setLoadingSubOrgs(true); - let url = `${globalUrl}/api/v1/orgs/${orgId}/suborgs?limit=${effectiveLimit}`; - if (cursor) { - url += `&cursor=${encodeURIComponent(cursor)}`; - } - - fetch(url, { + fetch(`${globalUrl}/api/v1/orgs/${orgId}/suborgs`, { method: "GET", credentials: "include", headers: { @@ -173,87 +154,49 @@ const TenantsTab = memo((props) => { }) .then((responseJson) => { if (responseJson.success === false) { - setLoadOrgs(false); - setLoadingSubOrgs(false); + setLoadOrgs(false) //toast("Failed getting your org. If this persists, please contact support."); } else { - const { subOrgs, parentOrg, cursor: responseCursor } = responseJson; - setLoadOrgs(false); - setLoadingSubOrgs(false); - setSubOrgs(subOrgs || []); + const { subOrgs, parentOrg } = responseJson; + setLoadOrgs(false) + setSubOrgs(subOrgs); setParentOrg(parentOrg); - setNextCursor(responseCursor || ""); - - if (direction === "prev") { - const len = (subOrgs || []).length; - setLocalPage(len > 0 ? Math.ceil(len / rowsPerPage) - 1 : 0); - } else { - setLocalPage(0); - } let regiontag = "UK"; let regionCode = "gb"; if (parentOrg?.region_url?.length > 0) { - const regionsplit = parentOrg?.region_url.split("."); - if (regionsplit.length > 2 && !regionsplit[0].includes("shuffler")) { - const namesplit = regionsplit[0].split("/"); - regiontag = namesplit[namesplit.length - 1]; + const regionsplit = parentOrg?.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" - } + 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" } + } setParentOrgFlag(regionCode); setParentOrgRegionName(regiontag); - } + } } }) .catch((error) => { console.log("Error getting sub orgs: ", error); //toast("Error getting sub organizations"); - setLoadOrgs(false); - setLoadingSubOrgs(false); + setLoadOrgs(false) }); }; - const handleNextPage = () => { - const maxLocalPage = Math.ceil(subOrgs.length / rowsPerPage) - 1; - if (localPage < maxLocalPage) { - setLocalPage(prev => prev + 1); - } else if (nextCursor && nextCursor !== currentCursor) { - setCursorStack(prev => [...prev, currentCursor]); - setCurrentCursor(nextCursor); - handleGetSubOrgs(userdata.active_org.id, nextCursor, 100, "next"); - } - }; - - const handlePrevPage = () => { - if (localPage > 0) { - setLocalPage(prev => prev - 1); - } else if (cursorStack.length > 0) { - const prevCursor = cursorStack[cursorStack.length - 1]; - setCursorStack(prev => prev.slice(0, -1)); - setCurrentCursor(prevCursor); - handleGetSubOrgs(userdata.active_org.id, prevCursor, 100, "prev"); - } - }; - - const handleChangeRowsPerPage = (newSize) => { - setRowsPerPage(Number(newSize)); - setLocalPage(0); - }; - const GridItem = (props) => { const [expanded, setExpanded] = React.useState(false); const [showEdit, setShowEdit] = React.useState(false); @@ -559,7 +502,7 @@ const TenantsTab = memo((props) => { const createSubOrg = (currentOrgId, name) => { const data = { name: name, org_id: currentOrgId }; const url = globalUrl + `/api/v1/orgs/${currentOrgId}/create_sub_org`; - setSuborglistOpen(true) + setSuborglistOpen(true) fetch(url, { mode: "cors", @@ -577,8 +520,8 @@ const TenantsTab = memo((props) => { if (responseJson["success"] === false) { if (responseJson.reason !== undefined) { toast.error(responseJson.reason, { - autoClose: 5000, - }) + autoClose: 5000, + }) } else { toast("Failed creating suborg. Please try again"); } @@ -611,7 +554,6 @@ const TenantsTab = memo((props) => { localStorage.setItem("globalUrl", ""); localStorage.setItem("getting_started_sidebar", "open"); - setIsChangingOrg(true); fetch(`${globalUrl}/api/v1/orgs/${orgId}/change`, { mode: "cors", credentials: "include", @@ -627,8 +569,8 @@ const TenantsTab = memo((props) => { if (response.status !== 200) { console.log("Error in response"); } else { - localStorage.setItem("apps", []) - } + localStorage.setItem("apps", []) + } return response.json(); }) @@ -665,215 +607,212 @@ const TenantsTab = memo((props) => { const [disabled, setDisabled] = useState(true); const [open, setOpen] = useState(true); const boxStyling = { - position: "relative", - top: "50%", - left: "50%", - transform: "translate(-50%, -50%)", - zIndex: "9999", - backgroundColor: theme.palette.backgroundColor, - color: theme.palette.text.primary, - padding: 20, - borderRadius: 5, - boxShadow: "0 0 10px rgba(0, 0, 0, 0.3)", - width: 430, - height: 430, + position: "relative", + top: "50%", + left: "50%", + transform: "translate(-50%, -50%)", + zIndex: "9999", + backgroundColor: theme.palette.backgroundColor, + color: theme.palette.text.primary, + padding: 20, + borderRadius: 5, + boxShadow: "0 0 10px rgba(0, 0, 0, 0.3)", + width: 430, + height: 430, }; - + const closeIconButtonStyling = { - color: theme.palette.text.primary, - border: "none", - backgroundColor: "transparent", - position: 'relative', - width: 20, - height: 20, - cursor: "pointer", - left: "calc(100% - 30px)", + color: theme.palette.text.primary, + border: "none", + backgroundColor: "transparent", + position: 'relative', + width: 20, + height: 20, + cursor: "pointer", + left: "calc(100% - 30px)", }; - + const handlePasswordVisibility = () => { - setShowPassword(!showPassword); + setShowPassword(!showPassword); }; - + const buttonStyle = { - marginTop: 20, - height: 50, - border: "none", - width: "100%", - fontSize: 16, - backgroundColor: disabled ? "gray" : "red", - color: theme.palette.text.primary, - cursor: disabled === false && "pointer", - }; - + marginTop: 20, + height: 50, + border: "none", + width: "100%", + fontSize: 16, + backgroundColor: disabled ? "gray" : "red", + color: theme.palette.text.primary, + cursor: disabled === false && "pointer", + }; + const handlePasswordChange = (e) => { - setPassword(e.target.value); + setPassword(e.target.value); }; - + const handleCheckBoxEvent = () => { - setUserDeleteAccepted((prev) => !prev); + setUserDeleteAccepted((prev) => !prev); }; - + useEffect(() => { - if (password.length > 8 && userDeleteAccepted) { - setDisabled(false); - } else { - setDisabled(true); - } + if (password.length > 8 && userDeleteAccepted) { + setDisabled(false); + } else { + setDisabled(true); + } }, [password, userDeleteAccepted]); - + const handleDeleteAccount = () => { - const baseURL = globalUrl; + const baseURL = globalUrl; + + const url = `${baseURL}/api/v1/orgs/${selectedSuborg?.id}`; - const url = `${baseURL}/api/v1/orgs/${selectedSuborg?.id}`; + const data = { + password: password, + }; - const data = { - password: password, - }; + fetch(url, { + mode: "cors", + method: "DELETE", + body: JSON.stringify(data), + credentials: "include", + crossDomain: true, + withCredentials: true, + headers: { + "Content-Type": "application/json", + }, + }) + .then((response) => response.json()) + .then((data) => { + if (data.success) { + toast.success( + "Suborg deleted" + ); + handleGetSubOrgs(userdata.active_org.id); + setAccountDeleteButtonClicked(false); - fetch(url, { - mode: "cors", - method: "DELETE", - body: JSON.stringify(data), - credentials: "include", - crossDomain: true, - withCredentials: true, - headers: { - "Content-Type": "application/json", - }, + } else { + if (data.reason) { + toast.error(data.reason); + }else { + toast.error("Failed to delete suborg. Please try again or contact support@shuffler.io for help."); + } + } }) - .then((response) => response.json()) - .then((data) => { - if (data.success) { - toast.success( - "Suborg deleted" - ); - setCursorStack([]); - setCurrentCursor(""); - setNextCursor(""); - handleGetSubOrgs(userdata.active_org.id); - setAccountDeleteButtonClicked(false); - - } else { - if (data.reason) { - toast.error(data.reason); - } else { - toast.error("Failed to delete suborg. Please try again or contact support@shuffler.io for help."); - } - } - }) - .catch((error) => { - console.error( - "There was a problem with your fetch operation:", - error - ); - }); + .catch((error) => { + console.error( + "There was a problem with your fetch operation:", + error + ); + }); }; - + return ( - -
- { - setAccountDeleteButtonClicked(false); - setSelectedSuborg(null); - }} - > - - -

Sub-Organization

- {/*
*/} -
- -
    -
  • - -
  • -
  • - -
  • -
-
+
+ { + setAccountDeleteButtonClicked(false); + setSelectedSuborg(null); + }} + > + + +

Sub-Organization

+ {/*
*/} +
+ +
    +
  • + +
  • +
  • + +
  • +
+
+ + +
+
+ + - - -
-
- - - {showPassword ? : } - - ), - }} - /> -
- -
-
- + {showPassword ? : } + + ), + }} + /> +
+ +
+
+ ); - }; + }; const modalView = ( { backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, zIndex: 1000, '& .MuiDialogContent-root': { - backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, }, '& .MuiDialogTitle-root': { - backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, }, '& .MuiDialogActions-root': { - backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, + backgroundColor: theme?.palette?.DialogStyle?.backgroundColor, }, }, }} @@ -941,7 +880,7 @@ const TenantsTab = memo((props) => { - - - {!selectedOrganization?.creator_org?.length && ( - - )} -
- ), - }, - ]; - return ( -
+
{modalView} {cloudSyncModal} - {accountDeleteButtonClicked && } -
-
-
- Tenants - - Create, manage and change to sub-organizations (tenants)! {" "} - {isCloud - ? `You can only make a sub organization if you are a customer of shuffle or running a POC of the platform. Please contact ${supportEmail} to try it out.` - : ''}  - - Learn more - - -
- - - - - -
- } +
+
+
+ Tenants + + Create, manage and change to sub-organizations (tenants)! {" "} + {isCloud + ? `You can only make a sub organization if you are a customer of shuffle or running a POC of the platform. Please contact ${supportEmail} to try it out.` + : ''}  + - Your Parent Organization - -
- + + + + +
+ + Your Parent Organization + +
+
+ {/* { /> */} -
- - - - - {isCloud && ( - - )} - - - - - {loadOrgs ? ( - [...Array(3)].map((_, rowIndex) => ( - - {[ - { width: 100, minWidth: 100, maxWidth: 100 }, - { width: 250, minWidth: 50, maxWidth: 250 }, - { width: 400, minWidth: 400, maxWidth: 400 }, - { width: "28%", minWidth: "28%" }, - { width: 400, minWidth: 400, maxWidth: 400 }, - ].map((style, colIndex) => ( - - - - ))} - - )) - ) : parentOrg?.id?.length > 0 ? ( - - - } - style={{ - width: 100, - minWidth: 100, - maxWidth: 100, - display: "table-cell", - padding: "8px 8px 8px 20px", - textAlign: "center", - }} - /> - - {isCloud && ( - - {parentOrgFlag} - -
- } - style={{ display: "table-cell", padding: 8, verticalAlign: "middle" }} - /> - )} - - - - - - - } - style={{ display: "table-cell", verticalAlign: "middle" }} - /> - - ) : ( - - {Array(5).fill().map((_, index) => ( - - ))} - - )} - -
-
- - {(subOrgs.length > 0 || cursorStack.length > 0 || loadingSubOrgs) && ( -
- - -
- - Sub Organizations of the Current Organization ({subOrgs.length}) - -
- -
- {!suborglistOpen ? ( - - ) : ( - row.id} - sx={{ - border: "none", - color: theme.palette.text.primary, - '& .MuiDataGrid-columnHeaders': { - borderBottom: theme.palette.defaultBorder, - backgroundColor: theme.palette.platformColor, - }, - '& .MuiDataGrid-cell': { - borderBottom: theme.palette.defaultBorder, - display: "flex", - alignItems: "center", - }, - '& .MuiDataGrid-row': { - backgroundColor: theme.palette.platformColor, - }, - '& .MuiDataGrid-overlayWrapper': { - minHeight: subOrgs.length > 0 ? 0 : 100, - }, - }} - /> - )} -
- {suborglistOpen && ( -
- - Rows per page: - - - - - - = Math.ceil(subOrgs.length / rowsPerPage) - 1 && (!nextCursor || nextCursor === currentCursor))} - size="small" - sx={{ color: (loadingSubOrgs || (localPage >= Math.ceil(subOrgs.length / rowsPerPage) - 1 && (!nextCursor || nextCursor === currentCursor))) ? theme.palette.text.disabled : theme.palette.text.primary }} - > - - -
- )} -
- )} - - + - -
- + - All Tenants - + + + {isCloud && ( + + )} + + + + + {loadOrgs ? ( + [...Array(3)].map((_, rowIndex) => ( + + {[ + { width: 100, minWidth: 100, maxWidth: 100 }, + { width: 250, minWidth: 50, maxWidth: 250 }, + { width: 400, minWidth: 400, maxWidth: 400 }, + { width: "28%", minWidth: "28%" }, + { width: 400, minWidth: 400, maxWidth: 400 }, + ].map((style, colIndex) => ( + + + + ))} + + )) + ) : parentOrg?.id?.length > 0 ? ( + + + } + style={{ + width: 100, + minWidth: 100, + maxWidth: 100, + display: "table-cell", + padding: "8px 8px 8px 20px", + textAlign: "center", + }} + /> + + {isCloud && ( + + {parentOrgFlag} + +
+ } + style={{ display: "table-cell", padding: 8, verticalAlign: "middle" }} + /> + )} + + + + + + + } + style={{ display: "table-cell", verticalAlign: "middle" }} + /> + + ): ( + + {Array(5).fill().map((_, index) => ( + + ))} + + )} +
+
- {/* 0 && ( +
+ + +
+ + Sub Organizations of the Current Organization ({subOrgs.length}) + +
+ + {/* */} + +
+ + {!suborglistOpen ? + + setSuborglistOpen(true)} + > + Show Sub-Organizations + + } + style={{ + width: 100, + minWidth: 100, + maxWidth: 100, + paddingLeft: 20, + display: "table-cell", + padding: "0px 8px 8px 8px", + textAlign: "center", + borderBottom: theme.palette.defaultBorder, + verticalAlign: "middle", + }} + /> + + : + + + + + {isCloud && ( + + )} + + + + {subOrgs.map((data, index) => { + let regiontag = "UK"; + let regionCode = "gb"; + + if (data.region_url?.length > 0) { + const regionsplit = data.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"; + } + } + } + var bgColor = themeMode === "dark" ? "#212121" : "#FFFFFF"; + if (index % 2 === 0) { + bgColor = themeMode === "dark" ? "#1A1A1A" : "#EAEAEA"; + } + + return ( + + } style={{ width: 100, + minWidth: 100, + maxWidth: 100, + display: "table-cell", + padding: "8px 8px 8px 20px", + textAlign: "center", }} /> + + + {isCloud && ( + + {regiontag} + +
+ } + style={{ display: "table-cell", padding: 8, verticalAlign: "middle" }} + /> + )} + + + + + + + + {selectedOrganization?.creator_org?.length > 0 ? null : + } + + + } + style={{ display: "table-cell", verticalAlign: "middle" }} + /> + + )})} + + } + + +
+
+ )} + + + +
+ + All Tenants + +
+ + {/* */} -
- - {!allTenantsOpen ? - + + {!allTenantsOpen ? + - setAllTenantsOpen(true)} - > - Show ALL your tenants - - } - style={{ - width: 100, - minWidth: 100, - maxWidth: 100, - paddingLeft: 20, - display: "table-cell", - padding: "0px 8px 8px 8px", - textAlign: "center", - borderBottom: theme.palette.defaultBorder, - verticalAlign: "middle", - }} - /> - - : - - - - - {isCloud && ( - - )} - - - + }} + > + setAllTenantsOpen(true)} + > + Show ALL your tenants + + } + style={{ + width: 100, + minWidth: 100, + maxWidth: 100, + paddingLeft: 20, + display: "table-cell", + padding: "0px 8px 8px 8px", + textAlign: "center", + borderBottom: theme.palette.defaultBorder, + verticalAlign: "middle", + }} + /> + + : + + + + + {isCloud && ( + + )} + + + - {userdata?.orgs?.length <= 0 ? ( - [...Array(6)].map((_, rowIndex) => ( - - {Array(7) - .fill() - .map((_, colIndex) => ( - - - - ))} - - )) - ) : ( - userdata?.orgs?.length > 0 && - userdata.orgs.map((data, index) => { - let regiontag = "UK"; - let regionCode = "gb"; + {userdata?.orgs?.length <= 0 ? ( + [...Array(6)].map((_, rowIndex) => ( + + {Array(7) + .fill() + .map((_, colIndex) => ( + + + + ))} + + )) + ) : ( + userdata?.orgs?.length > 0 && + userdata.orgs.map((data, index) => { + let regiontag = "UK"; + let regionCode = "gb"; - if (data.region_url?.length > 0) { - const regionsplit = data.region_url.split("."); - if (regionsplit.length > 2 && !regionsplit[0].includes("shuffler")) { - const namesplit = regionsplit[0].split("/"); - regiontag = namesplit[namesplit.length - 1]; + if (data.region_url?.length > 0) { + const regionsplit = data.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"; - } - } - } + if (regiontag === "california") { + regiontag = "US"; + regionCode = "us"; + } else if (regiontag === "frankfurt") { + regiontag = "EU-2"; + regionCode = "eu"; + } else if (regiontag === "ca") { + regiontag = "CA"; + regionCode = "ca"; + } + } + } - var bgColor = themeMode === "dark" ? "#212121" : "#FFFFFF"; - if (index % 2 === 0) { - bgColor = themeMode === "dark" ? "#1A1A1A" : "#EAEAEA"; - } + var bgColor = themeMode === "dark" ? "#212121" : "#FFFFFF"; + if (index % 2 === 0) { + bgColor = themeMode === "dark" ? "#1A1A1A" : "#EAEAEA"; + } - return ( - - - } - style={{ - width: 100, - minWidth: 100, - maxWidth: 100, - display: "table-cell", - padding: "8px 8px 8px 20px", - textAlign: "center", - }} - /> - - {isCloud ? ( - - {regiontag} + return ( + + + } + style={{ + width: 100, + minWidth: 100, + maxWidth: 100, + display: "table-cell", + padding: "8px 8px 8px 20px", + textAlign: "center", + }} + /> + + {isCloud ? ( + + {regiontag} - -
- } - style={{ - display: "table-cell", - padding: 8, - verticalAlign: "middle", - }} - > - ) : null} - - { - handleClickChangeOrg(data?.id); - }} - > - Change Active Org - - } - style={{ - display: "table-cell", - padding: 8, - verticalAlign: "middle", - }} - > - - ); - }) - )} - } - -
+ +
+ } + style={{ + display: "table-cell", + padding: 8, + verticalAlign: "middle", + }} + > + ) : null} + + { + handleClickChangeOrg(data?.id); + }} + > + Change Active Org + + } + style={{ + display: "table-cell", + padding: 8, + verticalAlign: "middle", + }} + > + + ); + }) + )} + } + +
diff --git a/frontend/src/components/UserManagmentTab.jsx b/frontend/src/components/UserManagmentTab.jsx index a27fdedf..41ab96f0 100644 --- a/frontend/src/components/UserManagmentTab.jsx +++ b/frontend/src/components/UserManagmentTab.jsx @@ -1,10 +1,11 @@ import React, { useState, useEffect, useContext, memo } from "react"; import { toast } from 'react-toastify'; import { Context } from "../context/ContextApi.jsx"; -import { Link } from "react-router-dom"; import { FormControl, InputLabel, + OutlinedInput, + Checkbox, Tooltip, Typography, Select, @@ -30,12 +31,23 @@ import { import { Cached as CachedIcon, Edit as EditIcon, + Style, } from "@mui/icons-material"; import ModeEditOutlineOutlinedIcon from '@mui/icons-material/ModeEditOutlineOutlined'; import ContentCopyOutlinedIcon from '@mui/icons-material/ContentCopyOutlined'; import {getTheme} from "../theme.jsx"; -import SubOrgDistributionDialog from "./SubOrgDistributionDialog.jsx"; +const ITEM_HEIGHT = 48; +const ITEM_PADDING_TOP = 8; +const MenuProps = { + PaperProps: { + style: { + maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP, + width: 500, + }, + }, + getContentAnchorEl: () => null, +}; const logsViewModal = false; const userdata = ""; @@ -64,11 +76,10 @@ const UserManagmentTab = memo((props) => { const [logsViewModal, setLogsViewModal] = React.useState(false); const [ipSelected, setIpSelected] = React.useState(""); const [userLogViewing, setUserLogViewing] = React.useState({}); - const [subOrgModalOpen, setSubOrgModalOpen] = React.useState(false); - const [pendingSubOrgs, setPendingSubOrgs] = React.useState([]); const { themeMode, supportEmail, brandColor } = useContext(Context); const theme = getTheme(themeMode, brandColor); + useEffect(() => { if (selectedOrganization?.mfa_required !== MFARequired) { setMFARequired(selectedOrganization?.mfa_required); @@ -236,6 +247,30 @@ const UserManagmentTab = memo((props) => { }); }; + const handleOrgEditChange = (event) => { + if (userdata.id === selectedUser.id) { + toast("Can't remove orgs from yourself"); + return; + } + + if (event.target.value.includes("ALL")) { + toast.info("Adding to available all sub-organizations. This may take a minute.") + event.target.value = selectedOrganization.child_orgs.map((org) => org.id) + } else if (event.target.value.includes("None")) { + toast.info("Removing from all sub-organizations. This may take a minute") + event.target.value = [] + } + + setMatchingOrganizations(event.target.value); + // Workaround for empty orgs + if (event.target.value.length === 0) { + event.target.value.push("REMOVE"); + } + + setUser(selectedUser.id, "suborgs", event.target.value); + //setUser(selectedUser.id, "suborgs", matchingOrganizations) + }; + const userOrgEdit = selectedUser.id !== undefined && selectedUser?.orgs !== undefined && @@ -243,44 +278,44 @@ const UserManagmentTab = memo((props) => { selectedOrganization?.child_orgs !== undefined && selectedOrganization?.child_orgs !== null && selectedOrganization?.child_orgs?.length > 0 ? ( - + + + Accessible Sub-Organizations ( + {selectedUser?.orgs ? selectedUser?.orgs?.length - 1 : 0}) + + + ) : null; - const subOrgManagementDialog = ( - setSubOrgModalOpen(false)} - title={`Manage Sub-Organizations for ${selectedUser?.username || ''}`} - orgs={selectedOrganization?.child_orgs || []} - selectedOrgIds={pendingSubOrgs} - onSelectionChange={setPendingSubOrgs} - onSave={(ids) => { - if (userdata.id === selectedUser.id) { - toast("Can't modify orgs for yourself"); - return; - } - const newValue = ids.length === 0 ? ["REMOVE"] : [...ids]; - setMatchingOrganizations([...ids]); - setUser(selectedUser.id, "suborgs", newValue); - setSubOrgModalOpen(false); - setSelectedUserModalOpen(false); - }} - disabled={selectedUser?.id === userdata?.id} - /> - ); - const getUsers = () => { fetch(globalUrl + "/api/v1/getusers", { method: "GET", @@ -1082,8 +1117,6 @@ const UserManagmentTab = memo((props) => { }); }; - var previousreferrer = "" - var nextreferrer = "" const logview = logsViewModal ? ( { onChange={(event) => { setIpSelected(event.target.value); getLogs(event.target.value, userLogViewing.id); + + }} > {(() => { const uniqueIPs = new Set(); - console.log("Login info: ", userLogViewing.login_info) return userLogViewing.login_info.map((data, index) => { - console.log("Data: ", data) - if (data.ip.includes("127.0.0.1") || uniqueIPs.has(data.ip)) { - return null + if ( + data.ip.includes("127.0.0.1") || + uniqueIPs.has(data.ip) + ) { + return null; } - uniqueIPs.add(data.ip) + uniqueIPs.add(data.ip); return ( - {data?.timestamp ? new Date(data.timestamp * 1000).toLocaleString() : "N/A"} - {data?.ip} + {data.ip} ); }); @@ -1193,13 +1229,12 @@ const UserManagmentTab = memo((props) => { minWidth: 700, maxWidth: 700, overflow: "hidden", - marginLeft: 50, + marginLeft: 10, }} /> {logs.map((data, index) => { - previousreferrer = nextreferrer - nextreferrer = data.referer + //console.log("LOG: ", data) return ( // redirect user to logs @@ -1208,8 +1243,6 @@ const UserManagmentTab = memo((props) => { key={index} style={{ backgroundColor: index % 2 === 0 ? "#1f2023" : "#27292d", - paddingTop: data.referer !== previousreferrer ? 50 : 0, - borderTop: data.referer !== previousreferrer ? `1px solid rgba(255,255,255,0.3)` : "none", }} > { }} /> - - - + )})} @@ -1260,7 +1290,6 @@ const UserManagmentTab = memo((props) => {
{modalView} {editUserModal} - {subOrgManagementDialog} {logview}
diff --git a/frontend/src/components/WorkflowGrid.jsx b/frontend/src/components/WorkflowGrid.jsx index 9e843206..e4a34134 100644 --- a/frontend/src/components/WorkflowGrid.jsx +++ b/frontend/src/components/WorkflowGrid.jsx @@ -3,7 +3,6 @@ import React, { useEffect, useState } from 'react'; import {Link} from 'react-router-dom'; import theme from '../theme.jsx'; import { removeQuery } from '../components/ScrollToTop.jsx'; -import SearchContactForm from '../components/SearchContactForm.jsx'; import { Search as SearchIcon, CloudQueue as CloudQueueIcon, Code as CodeIcon } from '@mui/icons-material'; @@ -26,23 +25,66 @@ import { useDebouncedCallback } from "../utils/useDebouncedCallback.jsx"; import WorkflowPaper from "../components/WorkflowPaper.jsx" import WorkflowPaperNew from "../components/WorkflowPaperNew.jsx" -const searchClient = algoliasearch("JNSS5CFDZZ", "eb5fd80aa6ed5ab4730d836cff3ea283") +const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e") const AppGrid = props => { const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, alternativeView, onlyResults, inputsearch } = props - const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; + const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows const xs = parsedXs === undefined || parsedXs === null ? isMobile ? 6 : 4 : parsedXs //const [apps, setApps] = React.useState([]); //const [filteredApps, setFilteredApps] = React.useState([]); + const [formMail, setFormMail] = React.useState(""); + const [message, setMessage] = React.useState(""); + const [formMessage, setFormMessage] = React.useState(""); const [usecases, setUsecases] = React.useState([]); const [localMessage, setLocalMessage] = React.useState(""); + const buttonStyle = {borderRadius: 30, height: 50, width: 220, margin: isMobile ? "15px auto 15px auto" : 20, fontSize: 18,} + const innerColor = "rgba(255,255,255,0.65)" const borderRadius = 3 window.title = "Shuffle | Workflows | Discover your use-case" + const submitContact = (email, message) => { + const data = { + "firstname": "", + "lastname": "", + "title": "", + "companyname": "", + "email": email, + "phone": "", + "message": message, + } + + const errorMessage = "Something went wrong. Please contact frikky@shuffler.io directly." + + fetch(globalUrl+"/api/v1/contact", { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(data), + }) + .then(response => response.json()) + .then(response => { + if (response.success === true) { + setFormMessage(response.reason) + //toast("Thanks for submitting!") + } else { + setFormMessage(errorMessage) + } + + setFormMail("") + setMessage("") + }) + .catch(error => { + setFormMessage(errorMessage) + console.log(error) + }); + } + const handleKeysetting = (categorydata, workflows) => { console.log("Workflows: ", workflows) //workflows[0].category = ["detect"] @@ -187,16 +229,10 @@ const AppGrid = props => { placeholder="Find Workflows..." id="shuffle_search_field" onChange={(event) => { + removeQuery("q") const value = event.currentTarget.value setInputValue(value) debouncedRefine(value) - const urlSearchParams = new URLSearchParams(window.location.search) - if (value) { - urlSearchParams.set("q", value) - } else { - urlSearchParams.delete("q") - } - window.history.replaceState(null, "", value ? `?${urlSearchParams.toString()}` : window.location.pathname) }} onKeyDown={(event) => { if(event.key === "Enter") { @@ -297,10 +333,64 @@ const AppGrid = props => { {showSuggestion === true ? - - : null +
+ + Can't find what you're looking for? + +
+ setFormMail(e.target.value)} + /> + setMessage(e.target.value)} + /> +
+ + {formMessage} +
+ : null } - {/* {onlyResults === true ? null : + {onlyResults === true ? null : Search by @@ -309,7 +399,7 @@ const AppGrid = props => { Algolia logo - } */} + }
) } diff --git a/frontend/src/components/Workflowsearch.jsx b/frontend/src/components/Workflowsearch.jsx index 44f77b98..1b342645 100644 --- a/frontend/src/components/Workflowsearch.jsx +++ b/frontend/src/components/Workflowsearch.jsx @@ -10,7 +10,7 @@ import algoliasearch from 'algoliasearch'; import { InstantSearch, connectSearchBox, connectHits } from 'react-instantsearch-dom'; import { Grid, Paper, TextField, ButtonBase, InputAdornment, Typography, Button, Tooltip} from '@mui/material'; -const searchClient = algoliasearch("JNSS5CFDZZ", "33e4e3564f4f060e96e0531957bed552") +const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e") const WorkflowSearch = props => { const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, newSelectedApp, setNewSelectedApp, defaultSearch, showSearch, ConfiguredHits, selectAble, } = props const rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows diff --git a/frontend/src/components/ssoTab.jsx b/frontend/src/components/ssoTab.jsx index 1d019800..c361e1b1 100644 --- a/frontend/src/components/ssoTab.jsx +++ b/frontend/src/components/ssoTab.jsx @@ -1,16 +1,16 @@ import { useEffect, useContext } from "react"; import React from "react"; -import { - Typography, - Switch, - Button, - Tooltip, - TextField, - Grid, +import { + Typography, + Switch, + Button, + Tooltip, + TextField, + Grid, Checkbox } from "@mui/material"; import { makeStyles } from "@mui/styles"; -import { Link, useSearchParams } from "react-router-dom"; +import { Link } from "react-router-dom"; import theme from "../theme.jsx"; import { toast } from "react-toastify"; import { Context } from "../context/ContextApi.jsx"; @@ -28,13 +28,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle // Check if user is admin const isAdmin = userdata?.active_org?.role === "admin" || userdata?.support === true; - - // Read region_url override from URL params (only allow shuffler.io domains) - const [searchParams] = useSearchParams(); - const rawRegionUrl = searchParams.get("region_url"); - const regionUrlOverride = rawRegionUrl && rawRegionUrl.includes("shuffler.io") ? rawRegionUrl : null; - const effectiveGlobalUrl = regionUrlOverride || globalUrl; - + // State for tracking user SSO connection status const [users, setUsers] = React.useState([]); const [userSSOConnected, setUserSSOConnected] = React.useState(false); @@ -115,7 +109,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle // Function to fetch users and check current user's SSO status const checkUserSSOStatus = () => { setCheckingSSOStatus(true); - fetch(effectiveGlobalUrl + "/api/v1/getusers", { + fetch(globalUrl + "/api/v1/getusers", { method: "GET", headers: { "Content-Type": "application/json", @@ -315,7 +309,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle }; const HandleTestSSO = () => { - const url = `${effectiveGlobalUrl}/api/v1/orgs/${selectedOrganization?.id}/change`; + const url = `${globalUrl}/api/v1/orgs/${selectedOrganization?.id}/change`; const data = { org_id: selectedOrganization?.id, sso: true, @@ -372,7 +366,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle }; const HandleDisconnectSSO = () => { - const url = `${effectiveGlobalUrl}/api/v1/disconnect_sso`; + const url = `${globalUrl}/api/v1/disconnect_sso`; const data = { org_id: selectedOrganization?.id, }; @@ -450,11 +444,6 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle : "Connect your account with this org's SSO!" } - {regionUrlOverride && ( - - Using region override: {regionUrlOverride} - - )} - createTheme({ +export const getTheme = (themeMode, brandColor) => { + // Handle "system" mode by checking user's system preference + let resolvedMode = themeMode; + if (themeMode === "system" || !themeMode) { + resolvedMode = window?.matchMedia?.("(prefers-color-scheme: dark)")?.matches ? "dark" : "light"; + } + // Ensure mode is only "dark" or "light" + if (resolvedMode !== "dark" && resolvedMode !== "light") { + resolvedMode = "dark"; + } + + return createTheme({ palette: { - mode: themeMode, + mode: resolvedMode, main: brandColor || "#FF8544", primary: { main: brandColor || "#FF8544", @@ -167,36 +177,35 @@ export const getTheme = (themeMode, brandColor) => contrastText:"#000000", }, text: { - primary: themeMode === "dark" ? "#ffffff" : "#1A1A1A", - secondary: themeMode === "dark" ? "#9E9E9E" : "#616161", + primary: resolvedMode === "dark" ? "#ffffff" : "#1A1A1A", + secondary: resolvedMode === "dark" ? "#9E9E9E" : "#616161", }, - type: themeMode, - inputColor: themeMode === "dark" ? "rgba(39,41,45,1)" : "rgba(245, 245, 245, 1)", - textColor: themeMode === "dark" ? "#F1F1F1" : "#1A1A1A", - textPrimary: themeMode === "dark" ? "rgba(255, 255, 255, 0.8)" : "rgba(26, 26, 26, 0.8)", - surfaceColor: themeMode === "dark" ? "#27292d" : "#EFEFEF", - platformColor: themeMode === "dark" ? "#212121" : "#ffffff", - backgroundColor: themeMode === "dark" ? "#1a1a1a" : "#f1f1f1", - cytoscapeBackgroundColor: themeMode === "dark" ? "#161616" : "#f5f5f5", - distributionColor: themeMode === "dark" ? "#40E0D0" : "#008080", - 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', + type: resolvedMode, + inputColor: resolvedMode === "dark" ? "rgba(39,41,45,1)" : "rgba(245, 245, 245, 1)", + textColor: resolvedMode === "dark" ? "#F1F1F1" : "#1A1A1A", + textPrimary: resolvedMode === "dark" ? "rgba(255, 255, 255, 0.8)" : "rgba(26, 26, 26, 0.8)", + surfaceColor: resolvedMode === "dark" ? "#27292d" : "#EFEFEF", + platformColor: resolvedMode === "dark" ? "#212121" : "#ffffff", + backgroundColor: resolvedMode === "dark" ? "#1a1a1a" : "#f1f1f1", + cytoscapeBackgroundColor: resolvedMode === "dark" ? "#161616" : "#f5f5f5", + distributionColor: resolvedMode === "dark" ? "#40E0D0" : "#008080", + cardBackgroundColor: resolvedMode === "dark" ? "#1e1e1e" : "#eaeaea", + cardHoverColor: resolvedMode === "dark" ? "#323232" : "#F0F0F0", + hoverColor: resolvedMode === "dark" ? "#323232" : "#D6D6D6", + usecaseCardColor: resolvedMode === "dark" ? "#2f2f2f" : "rgba(245, 245, 245, 1)", + usecaseCardHoverColor: resolvedMode === "dark" ? "#2F2F2F" : "rgba(245, 245, 245, 1)", + usecaseDialogFieldColor: resolvedMode === "dark" ? "#2B2B2B" : "#F5F5F5", + accentColor: resolvedMode === "dark" ? "#ff8544" : "#ff8544", + green: resolvedMode === "dark" ? "#5cc879" : "#008000", + defaultBorder: resolvedMode === "dark" ? '1px solid #494949' : '1px solid #CCCCCC', linkColor: brandColor === "#ff8544" ? "#f86a3e" : brandColor, - slateGrayColor: themeMode === "dark" ? "#494949" : "#CCCCCC", - parsedAppPaperColor: themeMode === "dark" ? "#2f2f2f" : "#CCCCCC", - welcomeCardSubtextColor: themeMode === "dark" ? "#C8C8C8" : "#2f2f2f", - deleteColor: themeMode === "dark" ? "#FD4C62" : "#d32f2f", + slateGrayColor: resolvedMode === "dark" ? "#494949" : "#CCCCCC", + parsedAppPaperColor: resolvedMode === "dark" ? "#2f2f2f" : "#CCCCCC", + borderRadius: 10, - loaderColor: themeMode === "dark" ? "#1a1a1a" : "#E0E0E0", + loaderColor: resolvedMode === "dark" ? "#1a1a1a" : "#E0E0E0", jsonIconStyle: "round", - jsonTheme: themeMode === "dark" ? "summerfruit" : { + jsonTheme: resolvedMode === "dark" ? "summerfruit" : { base00: "#ffffff", // background base01: "#f0f0f0", // very light grey base02: "#f5f5f5", // light grey @@ -216,11 +225,11 @@ export const getTheme = (themeMode, brandColor) => }, jsonCollapseStringsAfterLength: 100, drawer: { - backgroundColor: themeMode === "dark" ? "#262626" : "#f9f9f9" + backgroundColor: resolvedMode === "dark" ? "#262626" : "#f9f9f9" }, actionSidebarField: { - backgroundColor: themeMode === "dark" ? "#2F2F2F" : "#F1F1F1", - color: themeMode === "dark" ? "#ffffff" : "#000000", + backgroundColor: resolvedMode === "dark" ? "#2F2F2F" : "#F1F1F1", + color: resolvedMode === "dark" ? "#ffffff" : "#000000", borderRadius: 8, height: 40, border: "none", @@ -229,53 +238,53 @@ export const getTheme = (themeMode, brandColor) => padding: 5, width: "98%", borderRadius: 5, - border: themeMode === "dark" ? "1px solid rgba(255,255,255,0.7)" : "1px solid rgba(0,0,0,0.3)", - backgroundColor: themeMode === "dark" + border: resolvedMode === "dark" ? "1px solid rgba(255,255,255,0.7)" : "1px solid rgba(0,0,0,0.3)", + backgroundColor: resolvedMode === "dark" ? "#1A1A1A" : "#f1f1f1", - color: themeMode === "dark" + color: resolvedMode === "dark" ? "#F1F1F1" : "#1A1A1A", overflowX: "auto", }, textFieldStyle: { - backgroundColor: themeMode === "dark" ? "#212121" : "#FFFFFF", - color: themeMode === "dark" ? "#ffffff" : "#000000", + backgroundColor: resolvedMode === "dark" ? "#212121" : "#FFFFFF", + color: resolvedMode === "dark" ? "#ffffff" : "#000000", borderRadius: "5px", height: 40, - border: themeMode === "dark" ? "1px solid #4D4D4D" : "1px solid #E0E0E0", + border: resolvedMode === "dark" ? "1px solid #4D4D4D" : "1px solid #E0E0E0", }, DialogStyle: { - backgroundColor: themeMode === "dark" ? "#212121" : "#ffffff", + backgroundColor: resolvedMode === "dark" ? "#212121" : "#ffffff", borderRadius: 2, - boxShadow: themeMode === "dark" ? "0px 0px 10px 0px rgba(0,0,0,0.75)" : "0px 0px 10px 0px rgba(0,0,0,0.2)", - border: themeMode === "dark" ? "1px solid #494949" : "1px solid #cccccc", + boxShadow: resolvedMode === "dark" ? "0px 0px 10px 0px rgba(0,0,0,0.75)" : "0px 0px 10px 0px rgba(0,0,0,0.2)", + border: resolvedMode === "dark" ? "1px solid #494949" : "1px solid #cccccc", }, innerTextfieldStyle: { height: 40, fontSize: 16, - backgroundColor: themeMode === "dark" ? "#212121" : "#f5f5f5", + backgroundColor: resolvedMode === "dark" ? "#212121" : "#f5f5f5", }, tooltip: { - backgroundColor: themeMode === "dark" ? "#212121" : "#ffffff", - color: themeMode === "dark" ? "#ffffff" : "#000000", - border: themeMode === "dark" ? "1px solid #494949" : "1px solid #cccccc", + backgroundColor: resolvedMode === "dark" ? "#212121" : "#ffffff", + color: resolvedMode === "dark" ? "#ffffff" : "#000000", + border: resolvedMode === "dark" ? "1px solid #494949" : "1px solid #cccccc", }, chipStyle: { - backgroundColor: themeMode === "dark" ? "#333333" : "#F5F5F5", - borderColor: themeMode === "dark" ? "#444444" : "#E0E0E0", - color: themeMode === "dark" ? "#FFFFFF" : "#333333", + backgroundColor: resolvedMode === "dark" ? "#333333" : "#F5F5F5", + borderColor: resolvedMode === "dark" ? "#444444" : "#E0E0E0", + color: resolvedMode === "dark" ? "#FFFFFF" : "#333333", }, defaultImage: "/images/no_image.png", singulOrange: "/images/singul_orange.png", singulGreen: "/images/singul_green.png", singulBlackWhite: "/icons/workflow-page/shuffle_agent.png", - scrollbarColor: themeMode === "dark" ? "#494949 #2f2f2f": "#c1c1c1 #f1f1f1", - scrollbarColorTransparent: themeMode === "dark" ? '#494949 transparent': "#c1c1c1 transparent", + scrollbarColor: resolvedMode === "dark" ? "#494949 #2f2f2f": "#c1c1c1 #f1f1f1", + scrollbarColorTransparent: resolvedMode === "dark" ? '#494949 transparent': "#c1c1c1 transparent", }, typography: { fontFamily: `"inter", "Roboto", "Helvetica", "Arial", sans-serif`, - color: themeMode === "dark" ? "#ffffff" : "#000000", + color: resolvedMode === "dark" ? "#ffffff" : "#000000", useNextVariants: true, fontWeightLight: 300, fontWeightRegular: 400, @@ -283,36 +292,36 @@ export const getTheme = (themeMode, brandColor) => fontWeightSemiBold: 600, fontWeightBold: 700, allVariants: { - color: themeMode === "dark" ? "#ffffff" : "#1A1A1A", + color: resolvedMode === "dark" ? "#ffffff" : "#1A1A1A", }, h1: { fontSize: 40, - color: themeMode === "dark" ? "#ffffff" : "#1A1A1A" + color: resolvedMode === "dark" ? "#ffffff" : "#1A1A1A" }, h2: { fontSize: 36, - color: themeMode === "dark" ? "#ffffff" : "#1A1A1A" + color: resolvedMode === "dark" ? "#ffffff" : "#1A1A1A" }, h3: { fontSize: 32, - color: themeMode === "dark" ? "#ffffff" : "#1A1A1A" + color: resolvedMode === "dark" ? "#ffffff" : "#1A1A1A" }, h4: { fontSize: 30, fontWeight: 500, - color: themeMode === "dark" ? "#ffffff" : "#1A1A1A" + color: resolvedMode === "dark" ? "#ffffff" : "#1A1A1A" }, h6: { fontSize: 22, - color: themeMode === "dark" ? "#ffffff" : "#1A1A1A" + color: resolvedMode === "dark" ? "#ffffff" : "#1A1A1A" }, body1: { fontSize: 16, - color: themeMode === "dark" ? "#ffffff" : "#1A1A1A" + color: resolvedMode === "dark" ? "#ffffff" : "#1A1A1A" }, body2: { fontSize: 14, - color: themeMode === "dark" ? "#ffffff" : "#1A1A1A" + color: resolvedMode === "dark" ? "#ffffff" : "#1A1A1A" }, }, components: { @@ -327,7 +336,7 @@ export const getTheme = (themeMode, brandColor) => { props: { variant: 'text', color: 'primary' }, style: { - color: themeMode === "dark" ? "#ffffff" : "#1A1A1A", + color: resolvedMode === "dark" ? "#ffffff" : "#1A1A1A", whiteSpace: "nowrap", textWrap: "normal", }, @@ -335,7 +344,7 @@ export const getTheme = (themeMode, brandColor) => { props: { variant: 'text', color: 'secondary' }, style: { - color: themeMode === "dark" ? "#9E9E9E" : "#616161", + color: resolvedMode === "dark" ? "#9E9E9E" : "#616161", whiteSpace: "nowrap", textWrap: "normal", }, @@ -343,24 +352,24 @@ export const getTheme = (themeMode, brandColor) => { props: { variant: 'contained', color: 'primary' }, style: { - backgroundColor: themeMode === "dark" ? brandColor || '#ff8544' : brandColor || '#FF7C35', - color: themeMode === "dark" ? '#1a1a1a': '#FFFFFF', + backgroundColor: resolvedMode === "dark" ? brandColor || '#ff8544' : brandColor || '#FF7C35', + color: resolvedMode === "dark" ? '#1a1a1a': '#FFFFFF', borderRadius: '4px', whiteSpace: "nowrap", textWrap: "normal", transition: 'background-color 0.2s ease-in-out', '&:hover': { fontWeight: 600, - backgroundColor: themeMode === 'dark' ? brandColor || "#ff955c" : brandColor || '#FF8D4F', - color: themeMode === "dark" ? '#1a1a1a': '#FFFFFF', + backgroundColor: resolvedMode === 'dark' ? brandColor || "#ff955c" : brandColor || '#FF8D4F', + color: resolvedMode === "dark" ? '#1a1a1a': '#FFFFFF', }, }, }, { props: { variant: 'contained', color: 'secondary' }, style: { - backgroundColor: themeMode === "dark" ? '#494949' : '#C9C9C9', - color: themeMode === "dark" ? '#ffffff' : '#4C4C4C', + backgroundColor: resolvedMode === "dark" ? '#494949' : '#C9C9C9', + color: resolvedMode === "dark" ? '#ffffff' : '#4C4C4C', borderRadius: '4px', boxShadow: 'none', whiteSpace: "nowrap", @@ -368,23 +377,23 @@ export const getTheme = (themeMode, brandColor) => textWrap: "normal", '&:hover': { fontWeight: 600, - border: themeMode === "dark" ? '1px solid #f1f1f1' : 'none', - backgroundColor: themeMode === "dark" ? '#494949' : '#C9C9C9', - color: themeMode === "dark" ? '#ffffff' : '#4C4C4C', + border: resolvedMode === "dark" ? '1px solid #f1f1f1' : 'none', + backgroundColor: resolvedMode === "dark" ? '#494949' : '#C9C9C9', + color: resolvedMode === "dark" ? '#ffffff' : '#4C4C4C', }, }, }, { props: { variant: 'outlined', color: 'primary' }, style: { - borderColor: themeMode === "dark" ? brandColor || "#ff8544" : brandColor || "#cc5f1f", - color: themeMode === "dark" ? brandColor || "#ff8544" : brandColor || "#cc5f1f", + borderColor: resolvedMode === "dark" ? brandColor || "#ff8544" : brandColor || "#cc5f1f", + color: resolvedMode === "dark" ? brandColor || "#ff8544" : brandColor || "#cc5f1f", whiteSpace: "nowrap", fontWeight: 'normal', textWrap: "normal", '&:hover': { - backgroundColor: themeMode === "dark" ? brandColor || "#ff8544" : "#ffe8dc", - color: themeMode === "dark" ? "#1a1a1a" : "#8a3d00", + backgroundColor: resolvedMode === "dark" ? brandColor || "#ff8544" : "#ffe8dc", + color: resolvedMode === "dark" ? "#1a1a1a" : "#8a3d00", fontWeight: 600, }, }, @@ -393,14 +402,14 @@ export const getTheme = (themeMode, brandColor) => props: { variant: 'outlined', color: 'secondary' }, style: { border: '1px solid #C5C5C5', - color: themeMode === "dark" ? '#C5C5C5' : '#2D2D2D', + color: resolvedMode === "dark" ? '#C5C5C5' : '#2D2D2D', whiteSpace: "nowrap", textWrap: "normal", '&:hover': { - backgroundColor: themeMode === "dark" ? '#C5C5C5' : '#EFEFEF', - borderColor: themeMode === "dark" ? '#C5C5C5' : '#2D2D2D', + backgroundColor: resolvedMode === "dark" ? '#C5C5C5' : '#EFEFEF', + borderColor: resolvedMode === "dark" ? '#C5C5C5' : '#2D2D2D', fontWeight: 600, - color: themeMode === "dark" ? '#1a1a1a' : '#1A1A1A', + color: resolvedMode === "dark" ? '#1a1a1a' : '#1A1A1A', }, }, }, @@ -423,8 +432,8 @@ export const getTheme = (themeMode, brandColor) => background: 'linear-gradient(90deg, #e6743a 0%, #d4456e 50%, #8a4de8 100%)', }, '&:disabled': { - background: themeMode === "dark" ? '#494949' : '#C9C9C9', - color: themeMode === "dark" ? '#9E9E9E' : '#616161', + background: resolvedMode === "dark" ? '#494949' : '#C9C9C9', + color: resolvedMode === "dark" ? '#9E9E9E' : '#616161', }, }, }, @@ -469,9 +478,9 @@ export const getTheme = (themeMode, brandColor) => }, '&:disabled': { background: 'transparent', - color: themeMode === "dark" ? '#9E9E9E' : '#616161', + color: resolvedMode === "dark" ? '#9E9E9E' : '#616161', '&::before': { - background: themeMode === "dark" ? '#494949' : '#C9C9C9', + background: resolvedMode === "dark" ? '#494949' : '#C9C9C9', }, }, }, @@ -481,7 +490,7 @@ export const getTheme = (themeMode, brandColor) => MuiTab: { styleOverrides: { root: { - color: themeMode === "dark" ? "#C5C5C5" : "#1A1A1A", + color: resolvedMode === "dark" ? "#C5C5C5" : "#1A1A1A", }, }, }, @@ -490,7 +499,7 @@ export const getTheme = (themeMode, brandColor) => overrides: { MuiMenu: { list: { - backgroundColor: themeMode === "dark" ? "#27292d" : "#ffffff", + backgroundColor: resolvedMode === "dark" ? "#27292d" : "#ffffff", }, }, MuiCssBaseline: { @@ -536,4 +545,5 @@ export const getTheme = (themeMode, brandColor) => }, }, }); +} diff --git a/frontend/src/views/AgentUI.jsx b/frontend/src/views/AgentUI.jsx index 7e3ccc3f..6b2fbbd9 100644 --- a/frontend/src/views/AgentUI.jsx +++ b/frontend/src/views/AgentUI.jsx @@ -44,7 +44,6 @@ import { Add as AddIcon, Warning as WarningIcon, Pause as PauseIcon, - Chat as ChatIcon, } from '@mui/icons-material' import { @@ -72,7 +71,6 @@ const AgentUI = (props) => { const [newSelectedApp, setNewSelectedApp] = React.useState({}) const [appPickerAnchor, setAppPickerAnchor] = React.useState(null) const [chosenApps, setChosenApps] = useState([]) - const [planningEnabled, setPlanningEnabled] = useState([]) const activateApp = (appId) => { if (appId === undefined || appId === null || appId === "") { @@ -190,11 +188,11 @@ const AgentUI = (props) => { const agentWrapperStyle = { width: "100%", - minHeight: "100vh", + maxHeight: "100vh", margin: "auto", backgroundColor: theme.palette.backgroundColor, - paddingBottom: showAgentStarter ? 0 : 50, + paddingBottom: showAgentStarter ? 0 : 1500, } @@ -250,25 +248,21 @@ const AgentUI = (props) => { return } - // If no node_id provided, look for the AI Agent node if (node_id === undefined || node_id === null || node_id === "") { + // Look for AI agent + /* for (var key in execution_data.results) { const item = execution_data.results[key] - if (item?.action?.app_name === "AI Agent") { - node_id = item?.action?.id - break + if (item?.action?.app_name !== "AI Agent") { + continue } + + node_id = item?.action?.id + break } + */ if (node_id === undefined || node_id === null || node_id === "") { - // Fallback: if only one result, use it - if (execution_data?.results?.length === 1) { - setAgentActionResult(execution_data.results[0]) - const validatedData = validateJson(execution_data.results[0].result) - if (validatedData.valid) { - setData(validatedData.result) - } - } return } } @@ -276,11 +270,6 @@ const AgentUI = (props) => { var found = false for (var key in execution_data.results) { const item = execution_data.results[key] - - if (item?.action?.app_name !== "AI Agent") { - continue - } - if (item?.action?.id !== node_id) { continue } @@ -300,6 +289,16 @@ const AgentUI = (props) => { if (found === false) { toast.warn("Failed to find the relevant AI Agent result") + + if (execution_data?.results?.length === 1) { + setAgentActionResult(execution_data.results[0]) + const validatedData = validateJson(execution_data.results[0].result) + if (validatedData.valid) { + setData(validatedData.result) + } else { + toast.warn("Action output result is not valid JSON!") + } + } } } @@ -476,7 +475,7 @@ const AgentUI = (props) => { getAppAuth() }, []) - const maxTimelineWidth = 275 + const maxTimelineWidth = 375 const submitQuestions = (decisionId, questionAnswers, isContinuation) => { console.log("Submitting questions: ", decisionId, questionAnswers) @@ -522,12 +521,8 @@ const AgentUI = (props) => { const executionId = params.get("execution_id") const nodeId = params.get("node_id") const authorization = params.get("authorization") - const workflowIdParam = params.get("workflow_id") - // workflow_id param > execution.workflow.id > executionId - const foundWorkflowId = workflowIdParam !== undefined && workflowIdParam !== null && workflowIdParam !== "" ? workflowIdParam : execution?.workflow?.id !== undefined && execution?.workflow?.id !== null && execution?.workflow?.id !== "" ? execution.workflow.id : executionId - - const url = `${globalUrl}/api/v1/workflows/${foundWorkflowId}/run?reference_execution=${executionId}&authorization=${authorization}&answer=true¬e=${encodeURIComponent(JSON.stringify(newArgument))}&agentic=true&decision_id=${decisionId}&node_id=${nodeId}` + const url = `${globalUrl}/api/v1/workflows/${executionId}/run?reference_execution=${executionId}&authorization=${authorization}&answer=true¬e=${encodeURIComponent(JSON.stringify(newArgument))}&agentic=true&decision_id=${decisionId}` fetch(url, { method: "GET", credentials: "include", @@ -898,32 +893,15 @@ const AgentUI = (props) => { />
- - {itemLabel} - + {itemLabel}
{
: null} - {item.category !== "agent" && questions?.length > 0 && (item?.status === "RUNNING" || item?.status === "WAITING") ? + {questions?.length > 0 && item?.status === "RUNNING" || item?.status === "WAITING" ?
{questions.map((q, questionIndex) => { return ( @@ -1211,25 +1189,7 @@ const AgentUI = (props) => { const [continuationText, setContinuationText] = useState("") - // Find the AI Agent result specifically, not just results[0] - var actionResult = null - if (execution?.results?.length > 0) { - for (var key in execution.results) { - const item = execution.results[key] - if (item?.action?.app_name === "AI Agent") { - actionResult = item - break - } - } - - // Fallback to first result if no AI Agent found - if (actionResult === null) { - actionResult = execution.results[0] - } - } else { - actionResult = execution - } - + var actionResult = execution?.results?.length > 0 ? execution.results[0] : execution const validate = validateJson(actionResult?.result) if (validate.valid === true) { actionResult.result = validate.result @@ -1286,11 +1246,6 @@ const AgentUI = (props) => { for (var key in agent_data?.decisions) { const item = agent_data.decisions[key] - if (item.run_details === undefined) { - console.log("Skipping item without run_details:", item) - continue - } - if (item.run_details.started_at === undefined || item.run_details.started_at === null) { item.run_details.started_at = originalStartTime } @@ -1540,7 +1495,6 @@ const AgentUI = (props) => { parsedAction = parsedAction.slice(0, -1) // Remove last comma } - /* const data = { "id": uuid, "name":"agent", @@ -1563,23 +1517,9 @@ const AgentUI = (props) => { "name":"action", "value": parsedAction, } - ], - "planning_mode": planningEnabled, - } + ]} + const url = `${globalUrl}/api/v1/apps/agent_starter/run` - */ - - const data = { - "jsonrpc": "2.0", - "method": "tools/call", - "params": { - "tool_name": parsedAction, - "input": { - "text": inputText, - }, - } - } - const url = `${globalUrl}/api/v1/agent` fetch(url, { method: "POST", body: JSON.stringify(data), @@ -1664,8 +1604,8 @@ const AgentUI = (props) => { return ( -
-
+
+
{ agentRequestLoading ? : - - + + @@ -1742,29 +1678,12 @@ const AgentUI = (props) => { }} /> -
+
- {/* - - - } label="Planning Mode" - style={chipStyle} - variant={planningEnabled ? "contained" : "outlined"} - onClick={() => { - setPlanningEnabled(!planningEnabled) - }} - disabled={true} - /> - - - */} } label="Select Apps / MCPs" style={chipStyle} - variant={"outlined"} onClick={() => { setAppPickerAnchor(document.getElementById("add_app_chip")) }} @@ -1875,21 +1794,18 @@ const AgentUI = (props) => { {chosenApps?.map((app, index) => { return( - + { - if (app?.id !== undefined) { - window.open(`/apps/${app.id}`, '_blank', 'noopener,noreferrer'); - } + window.open(`/apps/${app.id}`, '_blank', 'noopener,noreferrer'); }} style={{ cursor: "pointer", width: 30, height: 30, - backgroundColor: theme.palette.backgroundColor, }} /> diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index f995697f..db69e886 100755 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -231,7 +231,7 @@ export const triggers = [ status: "uninitialized", trigger_type: "SCHEDULE", errors: null, - large_image: "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OCA0OCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4IiByeD0iOCIgZmlsbD0iI0UzQTQxQiIvPgo8cmVjdCB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDEyIDEyKSIgZmlsbD0iI0UzQTQxQiIvPgo8Y2lyY2xlIGN4PSIyNCIgY3k9IjI0IiByPSI4Ljc1IiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjEuNSIvPgo8cGF0aCBkPSJNMjguNSAyNEgyNC4yNUMyNC4xMTE5IDI0IDI0IDIzLjg4ODEgMjQgMjMuNzVWMjAuNSIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4=", + large_image: "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDgiIGhlaWdodD0iNDgiIHZpZXdCb3g9IjAgMCA0OCA0OCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjQ4IiBoZWlnaHQ9IjQ4IiByeD0iOCIgZmlsbD0iIzIxQTBCRCIvPgo8Y2lyY2xlIGN4PSIyNCIgY3k9IjI0IiByPSI4Ljc1IiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjEuNSIvPgo8cGF0aCBkPSJNMjguNSAyNEgyNC4yNUMyNC4xMTE5IDI0IDI0IDIzLjg4ODEgMjQgMjMuNzVWMjAuNSIgc3Ryb2tlPSJ3aGl0ZSIgc3Ryb2tlLXdpZHRoPSIxLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIvPgo8L3N2Zz4K", label: "Schedule", is_valid: true, environment: "onprem", @@ -300,11 +300,6 @@ export const triggers = [ "name": "subflow", "example": "", "value": "", - }, - { - "name": "subflow_failure", - "example": "", - "value": "", } ], status: "running", @@ -503,31 +498,6 @@ export function setActionState(actionId, updates, workflowId = null) { } } -// Will use this function to remove the action data when the node will get removed from the cytoscape. -export function removeActionState(actionId, workflowId = null) { - if (!actionId) return; - - try { - const stored = localStorage.getItem(ACTION_STATES_STORAGE_KEY); - if (!stored) return; - - const allStates = JSON.parse(stored); - - if (workflowId && allStates[workflowId]) { - delete allStates[workflowId][actionId]; - - // Clean up empty workflow objects - if (Object.keys(allStates[workflowId]).length === 0) { - delete allStates[workflowId]; - } - } - - localStorage.setItem(ACTION_STATES_STORAGE_KEY, JSON.stringify(allStates)); - } catch (e) { - console.error("Failed to remove action state:", e); - } -} - const splitter = "|~|"; const svgSize = 24; const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); @@ -535,7 +505,7 @@ const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); //const referenceUrl = "https://shuffler.io/functions/webhooks/" //const referenceUrl = window.location.origin+"/api/v1/hooks/" -const searchClient = algoliasearch("JNSS5CFDZZ", "33e4e3564f4f060e96e0531957bed552") +const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e") const AngularWorkflow = (defaultprops) => { const { globalUrl, setCookie, isLoggedIn, isLoaded, userdata, data_id, ReactGA, } = defaultprops; const {themeMode, supportEmail, brandColor} = useContext(Context) @@ -596,8 +566,6 @@ const AngularWorkflow = (defaultprops) => { const [originalWorkflow, setOriginalWorkflow] = React.useState({}); const [originalSelectedEnvironment, setOriginalSelectedEnvironment] = React.useState({}); const [subworkflow, setSubworkflow] = React.useState({}); - const [subworkflowFailure, setSubworkflowFailure] = React.useState({}); - const [subworkflowFailureStartnode, setSubworkflowFailureStartnode] = React.useState(""); const [subworkflowStartnode, setSubworkflowStartnode] = React.useState(""); const [leftViewOpen, setLeftViewOpen] = React.useState(isMobile ? false : true); const [leftBarSize, setLeftBarSize] = React.useState(isMobile ? 0 : 235) @@ -1162,14 +1130,7 @@ const AngularWorkflow = (defaultprops) => { "description": "Translates your JSON data into a standard formats, then stores it in the Shuffle Datastore", "label": "Translate standard", "example": "{\"source_data\": \"{\\\"event\\\": \\\"login\\\", \\\"user\\\": \\\"john_doe\\\", \\\"timestamp\\\": \\\"2023-10-01T12:00:00Z\\\"}\", \"standard\": \"OCSF\"}", - "parameters": [ - { - "name": "app_name", - "value": "", - "required": true, - "multiline": false, - }, - { + "parameters": [{ "name": "source_data", "value": "", "required": true, @@ -1189,14 +1150,7 @@ const AngularWorkflow = (defaultprops) => { "name": "Cases", "description": "Available actions for case management", "label": "Cases", - "parameters": [ - { - "name": "app_name", - "value": "", - "required": true, - "multiline": false, - }, - { + "parameters": [{ "name": "action", "value": "list_tickets", "options": [ @@ -1221,14 +1175,7 @@ const AngularWorkflow = (defaultprops) => { "name": "Communication", "description": "Available actions for communication", "label": "Communication", - "parameters": [ - { - "name": "app_name", - "value": "", - "required": true, - "multiline": false, - }, - { + "parameters": [{ "name": "action", "value": "list_messages", "options": [ @@ -1263,8 +1210,7 @@ const AngularWorkflow = (defaultprops) => { "disable_user", "get_identity", "get_asset", - "search_identity", - "list_users", + "search_identity" ], "required": true, }, @@ -2253,26 +2199,6 @@ const AngularWorkflow = (defaultprops) => { } } - if (param.name === "subflow_failure" && param.value !== undefined && param.value !== null && param.value.length > 0) { - if (param.value === workflow?.id) { - setSubworkflowFailure(workflow); - } else { - const sub = responseJson.find((data) => data?.id === param.value); - if (sub !== undefined) { - setSubworkflowFailure(sub); - - // Populate startnode if set - const startnodeParam = trigger.parameters.find((p) => p.name === "subflow_failure_startnode"); - if (startnodeParam && startnodeParam.value && sub.actions) { - const foundAction = sub.actions.find((a) => a?.id === startnodeParam.value); - if (foundAction) { - setSubworkflowFailureStartnode(foundAction); - } - } - } - } - } - if (param.name === "startnode" && param.value !== undefined && param.value !== null) { if (Object.getOwnPropertyNames(baseSubflow).length > 0) { @@ -2418,7 +2344,7 @@ const AngularWorkflow = (defaultprops) => { return } - setExecutionsLoading(true); + setExecutionsLoading(true); var url = `${globalUrl}/api/v2/workflows/${id}/executions` var method = "GET" @@ -2899,7 +2825,6 @@ const AngularWorkflow = (defaultprops) => { stop() return } - //console.log(responseJson) // Loop nodes and find results // Update on every interval? idk @@ -4046,8 +3971,6 @@ const AngularWorkflow = (defaultprops) => { if (actionAppname === appname) { workflow.actions[actionkey].selectedAuthentication = item; workflow.actions[actionkey].authentication_id = item.id; - selectedAction.selectedAuthentication = item; - selectedAction.authentication_id = item.id; appUpdates = true; } } @@ -5272,8 +5195,7 @@ const AngularWorkflow = (defaultprops) => { if (responseJson.public) { - // Delay setting appAuthentication to prevent race condition with graph setup - setTimeout(() => setAppAuthentication([]), 100) + setAppAuthentication([]) setLeftBarSize(300) if (Object.getOwnPropertyNames(creatorProfile).length === 0) { @@ -5664,7 +5586,7 @@ const AngularWorkflow = (defaultprops) => { } ReactDOM.unstable_batchedUpdates(() => { - // setRightSideBarOpen(true); + setRightSideBarOpen(true); setLastSaved(false); /* @@ -6850,7 +6772,7 @@ const AngularWorkflow = (defaultprops) => { } //event.target.unselect(); - // setRightSideBarOpen(true); + setRightSideBarOpen(true); return } else if (data.buttonType === "copy") { @@ -6942,7 +6864,7 @@ const AngularWorkflow = (defaultprops) => { if (sourcenode !== null && sourcenode !== undefined) { const sourcedata = sourcenode.data() - if (sourcedata?.trigger_type !== "SUBFLOW" && sourcedata?.trigger_type !== "USERINPUT") { + if (sourcedata.trigger_type !== "SUBFLOW" && sourcedata.trigger_type !== "USERINPUT") { continue } @@ -7207,12 +7129,12 @@ const AngularWorkflow = (defaultprops) => { const tmpAuth = JSON.parse(JSON.stringify(newAppAuth)); - const curappName = curapp.name.toLowerCase().replaceAll(" ", "_") + const curappName = curapp.name.toLowerCase() for (let tmpAuthKey in tmpAuth) { var item = tmpAuth[tmpAuthKey]; const newfields = {}; - if (item.app.name.toLowerCase().replaceAll(" ", "_") !== curappName) { + if (item.app.name.toLowerCase() !== curappName) { continue } @@ -7594,6 +7516,7 @@ const AngularWorkflow = (defaultprops) => { setSelectedTriggerIndex(trigger_index) setSelectedTrigger(data) + //setSelectedActionEnvironment(data.env) }, 25) } else if (data.type === "COMMENT") { if (selectedNodes?.length > 1) { @@ -7906,7 +7829,7 @@ const AngularWorkflow = (defaultprops) => { continue } - const paramname = param.name?.toLowerCase()?.trim()?.replaceAll("_", " "); + const paramname = param.name.toLowerCase().trim().replaceAll("_", " "); const foundresult = GetParamMatch(paramname, exampledata, ""); if (foundresult.length > 0) { @@ -7943,7 +7866,10 @@ const AngularWorkflow = (defaultprops) => { continue } - const paramname = param.name?.toLowerCase()?.trim()?.replaceAll("_", " "); + const paramname = param.name + .toLowerCase() + .trim() + .replaceAll("_", " "); const foundresult = GetParamMatch(paramname, exampledata, ""); if (foundresult.length > 0) { @@ -8243,11 +8169,11 @@ const AngularWorkflow = (defaultprops) => { if (workflow.branches[branchkey].destination_id === edge.target && workflow.branches[branchkey].source_id === edge.source) { console.log("That branch already exists: ", workflow.branches[branchkey]) - //const foundbranch = cy.getElementById(workflow.branches[branchkey].id) - const foundbranch = cy.getElementById(edge.id) + const foundbranch = cy.getElementById(workflow.branches[branchkey].id) if (foundbranch !== undefined && foundbranch !== null && foundbranch.data() !== undefined && foundbranch.data() !== null) { console.log("Removing branch: ", foundbranch.data()) - //event.target.remove() + + event.target.remove() found = true break @@ -8619,10 +8545,6 @@ const AngularWorkflow = (defaultprops) => { workflow.actions = workflow.actions.filter((a) => a.id !== data.id); workflow.triggers = workflow.triggers.filter((a) => a.id !== data.id); - - // Clean up action state from localStorage - removeActionState(data.id, workflow.id); - if (workflow.start === data.id && workflow.actions.length > 0) { // FIXME - should check branches connected to startnode, as picking random // is just confusing @@ -8732,7 +8654,7 @@ const AngularWorkflow = (defaultprops) => { if ((event.ctrlKey || event.metaKey) && !event.shiftKey) { // If any modal/sidebar is open, let browser handle normal copy - if (isAnyModalOrSidebarOpen || event.target?.closest('.MuiDialog-root, .MuiModal-root, [role="dialog"]')) { + if (isAnyModalOrSidebarOpen) { return } @@ -10183,8 +10105,8 @@ const AngularWorkflow = (defaultprops) => { // Calculates how a branch should curve (it's still weird~) // https://codepen.io/guillaumethomas/pen/xxbbBKO const calculateEdgeCurve = (sourcenodePosition, destinationnodePosition) => { - const xParsed = destinationnodePosition?.x - sourcenodePosition?.x - const yParsed = destinationnodePosition?.y - sourcenodePosition?.y + const xParsed = destinationnodePosition.x - sourcenodePosition.x + const yParsed = destinationnodePosition.y - sourcenodePosition.y const z = Math.sqrt(xParsed * xParsed + yParsed * yParsed) const costheta = xParsed / z @@ -10351,7 +10273,7 @@ const AngularWorkflow = (defaultprops) => { action.iconBackground = iconInfo.iconBackgroundColor action.fillstyle = "linear-gradient" } - } else if(!action.isStartNode) { + }else if(!action.isStartNode) { // This is to round the corners of the image // If action has no large_image (e.g. imported/synced workflow where it was stripped), // inject it from the available apps in the sidebar @@ -10361,7 +10283,6 @@ const AngularWorkflow = (defaultprops) => { apps.find((a) => a.name === action.app_name) imageSource = (foundApp && foundApp.large_image) ? foundApp.large_image : "" } - const originalBase64 = imageSource !== "" ? imageSource : theme.palette.defaultImage const roundedImage = await roundBase64Image(originalBase64, 16); action = {...action, large_image: roundedImage} @@ -11503,10 +11424,10 @@ const AngularWorkflow = (defaultprops) => { // No matter what, it's being stopped. if (!responseJson.success) { if (responseJson.reason !== undefined) { - toast.warn("Failed to stop schedule: " + responseJson.reason); + toast("Failed to stop schedule: " + responseJson.reason); } } else { - toast.success("Successfully stopped schedule"); + toast("Successfully stopped schedule"); } if (triggerindex !== undefined && triggerindex !== null && triggerindex >= 0) { @@ -13240,7 +13161,7 @@ const AngularWorkflow = (defaultprops) => { if (queryID !== undefined && queryID !== null) { aa('init', { appId: "JNSS5CFDZZ", - apiKey: "33e4e3564f4f060e96e0531957bed552", + apiKey: "c8f882473ff42d41158430be09ec2b4e", }) const timestamp = new Date().getTime() @@ -16111,7 +16032,7 @@ const AngularWorkflow = (defaultprops) => { onClick={() => { // Change Direction of the branch target/source const foundBranch = cy.getElementById(selectedEdge.id) - if (foundBranch !== undefined && foundBranch !== null && foundBranch?.length > 0) { + if (foundBranch !== undefined && foundBranch !== null) { const source = foundBranch.data("source") const target = foundBranch.data("target") @@ -19371,236 +19292,6 @@ const AngularWorkflow = (defaultprops) => { /> ) : null} - {workflow?.triggers && - workflow?.triggers[selectedTriggerIndex] && - workflow?.triggers[selectedTriggerIndex].parameters - ? ( -
- On Decline - - Optionally trigger a workflow when the user declines - - {workflows === undefined || - workflows === null || - workflows.length === 0 ? null : ( - option.id === value.id} - getOptionLabel={(option) => { - if (option === undefined || option === null || option.name === undefined || option.name === null) { - return "No Workflow Selected"; - } - const newname = (option.name.charAt(0).toUpperCase() + option.name.substring(1)).replaceAll("_", " "); - return newname; - }} - options={ - [{ - "id": "", - "name": "No Workflow Selected", - }].concat(workflows) - } - fullWidth - onChange={(event, newValue) => { - if (newValue === null || newValue === undefined || newValue.id === undefined) { - return - } - - var failureParamIndex = workflow.triggers[selectedTriggerIndex].parameters.findIndex((param) => param.name === "subflow_failure") - if (failureParamIndex === -1) { - workflow.triggers[selectedTriggerIndex].parameters.push({ - "name": "subflow_failure", - "value": "", - }) - failureParamIndex = workflow.triggers[selectedTriggerIndex].parameters.length - 1 - } - - workflow.triggers[selectedTriggerIndex].parameters[failureParamIndex].value = newValue.id - setSubworkflowFailureStartnode("") - - // Fetch workflow to get actions for startnode selection - if (newValue.id.length > 0 && (newValue.actions === undefined || newValue.actions === null || newValue.actions.length === 0)) { - fetch(`${globalUrl}/api/v1/workflows/${newValue.id}`, { - method: "GET", - headers: { "Content-Type": "application/json" }, - credentials: "include", - }) - .then((resp) => resp.json()) - .then((responseJson) => { - if (responseJson.id !== undefined) { - setSubworkflowFailure(responseJson) - - // Default startnode - const startAction = responseJson.actions?.find((a) => a.id === responseJson.start) - if (startAction) { - setSubworkflowFailureStartnode(startAction) - } - } - }) - .catch((error) => { - console.log("Failed fetching decline workflow: ", error) - }) - } else { - setSubworkflowFailure(newValue) - const startAction = newValue.actions?.find((a) => a.id === newValue.start) - if (startAction) { - setSubworkflowFailureStartnode(startAction) - } - } - - setWorkflow(workflow) - setUpdate(Math.random()) - setLastSaved(false) - event.target.blur() - }} - renderOption={(props, data, state) => { - return ( - - - {data.name} - - ) - }} - renderInput={(params) => { - return ( -
- - {subworkflowFailure === null || subworkflowFailure === undefined || subworkflowFailure?.id === undefined || subworkflowFailure?.id === null || subworkflowFailure?.id.length === 0 ? null : - - - - - - } -
- ); - }} - /> - )} - - {subworkflowFailure?.actions !== undefined && subworkflowFailure?.actions !== null && subworkflowFailure?.actions?.length > 0 ? ( - option.id === value.id} - getOptionLabel={(option) => { - if (option === undefined || option === null || option.label === undefined || option.label === null) { - return "Default"; - } - const newname = (option.label.charAt(0).toUpperCase() + option.label.substring(1)).replaceAll("_", " "); - return newname; - }} - options={subworkflowFailure.actions} - fullWidth - onChange={(event, newValue) => { - setSubworkflowFailureStartnode(newValue) - - var startnodeParamIndex = workflow.triggers[selectedTriggerIndex].parameters.findIndex((param) => param.name === "subflow_failure_startnode") - if (startnodeParamIndex === -1) { - workflow.triggers[selectedTriggerIndex].parameters.push({ - "name": "subflow_failure_startnode", - "value": "", - }) - startnodeParamIndex = workflow.triggers[selectedTriggerIndex].parameters.length - 1 - } - - workflow.triggers[selectedTriggerIndex].parameters[startnodeParamIndex].value = newValue?.id || "" - setWorkflow(workflow) - setUpdate(Math.random()) - setLastSaved(false) - }} - renderOption={(props, action, state) => { - return ( - - {action.label} - - ) - }} - renderInput={(params) => { - return ( - - ); - }} - /> - ) : null} -
- ) : null} -
Required Input-Questions @@ -20287,7 +19978,6 @@ const AngularWorkflow = (defaultprops) => { "&.Mui-selected": { backgroundColor: themeMode === "dark" ? "#1e1e1e" : "#CCCCCC", color: theme.palette.text.primary, - borderRadius: "6px !important", fontWeight: 600, "&:hover": { backgroundColor: themeMode === "dark" ? "rgba(0,0,0,0.3)" : "rgba(0,0,0,0.1)", @@ -20330,6 +20020,7 @@ const AngularWorkflow = (defaultprops) => { justifyContent: "space-between", width: "100%", position: "relative", + minHeight: 80, }}> {/* Left: Workflow Name Container */}
{ }} > {workflow?.name !== undefined && workflow?.name !== null && workflow?.name?.length > 0 ? - + : null } {workflow.name} - {/* Warning Messages */} - {!distributedFromParent || userdata?.support === true ? - isCorrectOrg ? null : - - Warning: { - toast.info("Changing to correct organisation. Please wait a few seconds.") - changeOrg() - }} - >Change Active Organization to edit this Workflow. - - : - - suborgWorkflows?.length === 0 ? - - Warning: This workflow is controlled by your parent org and may not be editable. - - : - null - } - {parentWorkflows === undefined || parentWorkflows === null || parentWorkflows.length === 0 ? null : - - }
{/* Center: Build/Debug Toggle */} @@ -20602,7 +20237,6 @@ const AngularWorkflow = (defaultprops) => { saveWorkflow(workflow, undefined, undefined, e.target.value) /* Standard re-loads */ - setAllTriggers(undefined) setSelectedTriggerIndex(-1) getEnvironments(e.target.value) @@ -20927,7 +20561,7 @@ const AngularWorkflow = (defaultprops) => { id="execution_location" style={{ color: theme.palette.text.primary }} > - Runtime Location ({selectedActionEnvironment?.Name}) + Runtime Location