diff --git a/frontend/src/components/AdminNavBar.jsx b/frontend/src/components/AdminNavBar.jsx index f32a284f..6a771afd 100644 --- a/frontend/src/components/AdminNavBar.jsx +++ b/frontend/src/components/AdminNavBar.jsx @@ -211,15 +211,7 @@ const AdminNavBar = (props) => { } else if (userdata && isOrgLoaded && isUserDataLoaded && userdata?.active_org?.role !== "admin" && !userdata?.support) { const queryParams = new URLSearchParams(location.search); const tabName = queryParams?.get('admin_tab')?.toLowerCase(); - if (tabName === "sso") { - toast.info("You are not allowed to access this tab. Please contact your admin for more information. Redirecting to Organization Configuration tab."); - setTimeout(() => { - setSelectedItem("Organization"); - navigate(`?admin_tab=org_config`, { replace: true }); - window.location.reload(); - } - , 3000); - } + // SSO tab is now accessible to non-admins (removed restriction) const params = new URLSearchParams(location.search); const tab = params?.get('tab')?.toLowerCase(); diff --git a/frontend/src/components/AppFramework.jsx b/frontend/src/components/AppFramework.jsx index d6fc1f42..c0cd74d9 100644 --- a/frontend/src/components/AppFramework.jsx +++ b/frontend/src/components/AppFramework.jsx @@ -1129,7 +1129,7 @@ const AppFramework = (props) => { }, [newSelectedApp]) - const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true"); + const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; const imgSize = 50; var parsedFrameworkData = frameworkData === undefined ? {} : frameworkData diff --git a/frontend/src/components/AppSelection.jsx b/frontend/src/components/AppSelection.jsx index 6d83f2b0..07edb99c 100644 --- a/frontend/src/components/AppSelection.jsx +++ b/frontend/src/components/AppSelection.jsx @@ -64,7 +64,7 @@ const AppSelection = props => { document.title = "Choose your apps" const ref = useRef() let navigate = useNavigate(); - const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true"); + const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; useEffect(() => { if (newSelectedApp === undefined || newSelectedApp.objectID === undefined || newSelectedApp.objectID === undefined || newSelectedApp.objectID.length === 0) { diff --git a/frontend/src/components/Appsearch.jsx b/frontend/src/components/Appsearch.jsx index 6b032bc7..f55a59de 100644 --- a/frontend/src/components/Appsearch.jsx +++ b/frontend/src/components/Appsearch.jsx @@ -25,7 +25,7 @@ const Appsearch = props => { const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, newSelectedApp, setNewSelectedApp, defaultSearch, showSearch, ConfiguredHits, userdata, cy, isCreatorPage, actionImageList, setActionImageList, setUserSpecialzedApp, inputHeight, } = props const { themeMode } = useContext(Context) const theme = getTheme(themeMode) - const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true"); + 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 ? 12 : parsedXs const [formMail, setFormMail] = React.useState(""); diff --git a/frontend/src/components/BillingStats.jsx b/frontend/src/components/BillingStats.jsx index 19c0c176..c952dc42 100644 --- a/frontend/src/components/BillingStats.jsx +++ b/frontend/src/components/BillingStats.jsx @@ -3,7 +3,7 @@ import React, { useState, useEffect, useContext, useCallback } from 'react'; import {getTheme} from '../theme.jsx'; import classNames from "classnames"; import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs' -import { DataGrid, GridColDef, GridValueGetterParams } from '@mui/x-data-grid' +import { DataGrid, GridValueGetterParams } from '@mui/x-data-grid' import { toast } from "react-toastify" import { @@ -602,7 +602,7 @@ const AppStats = (defaultprops) => { }, } - const columns: GridColDef[] = [ + const columns = [ { field: 'workflow.name', headerName: 'Workflow Name', diff --git a/frontend/src/components/CacheView.jsx b/frontend/src/components/CacheView.jsx index 342331ca..5b308ead 100644 --- a/frontend/src/components/CacheView.jsx +++ b/frontend/src/components/CacheView.jsx @@ -37,11 +37,11 @@ import { Pagination, PaginationItem, Avatar, + ListSubheader, } from "@mui/material"; import { DataGrid, - GridColDef, } from '@mui/x-data-grid'; import { @@ -138,6 +138,7 @@ const CacheView = memo((props) => { const [selectAllChecked, setSelectAllChecked] = React.useState(false) const [renderTextBox, setRenderTextBox] = React.useState(false); const [datastoreCategories, setDatastoreCategories] = React.useState(["default", "protected"]); + const [datastoreCategoryGroups, setDatastoreCategoryGroups] = React.useState([]); const [selectedCategory, setSelectedCategory] = React.useState("default"); const [selectedFileId, setSelectedFileId] = React.useState(""); const [updateToThisCategory, setUpdateToThisCategory] = useState("") @@ -206,13 +207,13 @@ const CacheView = memo((props) => { { "name": "Run AI Agent", "description": "Runs an AI Agent to process the updated value. Uses built-in ShuffleAI configs. Learn more: https://shuffler.io/docs/AI", + "type": "singul", "options": [{ "key": "", "value": "", }], "icon": , - "enabled": false, - "disabled": true, + "disabled": false, }, { @@ -452,7 +453,31 @@ const CacheView = memo((props) => { } } + var foundstartwords = {} + var categorygroups = [] + for (var key in newcategories) { + const category = newcategories[key] + if (!category.includes("_")) { + continue + } + + const startword = category.split("_")[0] + if (startword.length <= 2) { + continue + } + + if (!foundstartwords.hasOwnProperty(startword)) { + foundstartwords[startword] = 1 + } else { + foundstartwords[startword] += 1 + if (foundstartwords[startword] === 2) { + categorygroups.push(startword) + } + } + } + setDatastoreCategories(newcategories) + setDatastoreCategoryGroups(categorygroups) } if (responseJson?.category_config !== undefined && responseJson?.category_config !== null) { @@ -1070,7 +1095,6 @@ const CacheView = memo((props) => { saveAutomation(newAutomations) } - console.log("AUTO: ", automation) return ( @@ -1094,7 +1118,7 @@ const CacheView = memo((props) => { style={{ marginRight: 10, marginTop: -10, }} checked={automation.enabled} // Check if automation options have a value - disabled={automation.name !== "Enrich" && (automation?.disabled === true || automation.options.length === 0 || automation.options.some((option) => option.value === ""))} + disabled={automation.name !== "Enrich" && automation.name != "Run AI Agent" && (automation?.disabled === true || automation.options.length === 0 || automation.options.some((option) => option.value === ""))} onChange={(e) => { e.stopPropagation() e.preventDefault() @@ -1511,13 +1535,33 @@ const CacheView = memo((props) => { ) } - const columns: GridColDef<(typeof rows)[number]>[] = [ + const urlParams = new URLSearchParams(window?.location?.search) + const highlightedKey = urlParams.get("key") || "" + const columns = [ { field: 'key', headerName: 'Key', width: 200, filterable: true, sortable: true, + renderCell: (props) => { + return ( + + {props.row.key} + + ) + } }, { width: 540, @@ -1919,7 +1963,7 @@ const CacheView = memo((props) => { }, ]; - + var previousgroup = "" const isAutomating = categoryAutomations?.find((automation) => automation.enabled) !== undefined return (
{ - - {datastoreCategories !== undefined && + + {datastoreCategories !== undefined && datastoreCategories !== null && datastoreCategories.length > 1 ? ( - - - Category - - + */} ) : null} @@ -2159,7 +2287,7 @@ const CacheView = memo((props) => { : - +
diff --git a/frontend/src/components/ChatBot.jsx b/frontend/src/components/ChatBot.jsx index 85d7135d..feb76253 100644 --- a/frontend/src/components/ChatBot.jsx +++ b/frontend/src/components/ChatBot.jsx @@ -43,6 +43,8 @@ const ChatBot = (props) => { const [appname, setAppname] = useState(""); const [threadId, setThreadId] = useState(""); const [runId, setRunId] = useState(""); + const [responseId, setResponseId] = useState(""); + const [conversationId, setConversationId] = useState(""); // New state for thread management const [isLoadingThread, setIsLoadingThread] = useState(false); @@ -54,7 +56,7 @@ const ChatBot = (props) => { const [showAppSearch, setShowAppSearch] = useState(false); // Get thread ID from URL params - const { threadId: urlThreadId } = useParams(); + const { conversationId: urlConversationId } = useParams(); let navigate = useNavigate(); const waitingMsg = "Processing..." @@ -93,24 +95,24 @@ const ChatBot = (props) => { } }, [appname]) - // Load existing thread if threadId is in URL + // Load existing conversation if conversationId is in URL useEffect(() => { - if (urlThreadId && urlThreadId !== threadId) { - loadExistingThread(urlThreadId); + if (urlConversationId && urlConversationId !== conversationId) { + loadExistingThread(urlConversationId); } - }, [urlThreadId]); + }, [urlConversationId]); - const loadExistingThread = (threadIdToLoad) => { + const loadExistingThread = (conversationIdToLoad) => { setIsLoadingThread(true); setThreadError(""); - fetch(`${globalUrl}/api/v1/conversation/thread`, { + fetch(`${globalUrl}/api/v1/conversation/history`, { method: "POST", headers: { "Content-Type": "application/json", }, credentials: "include", - body: JSON.stringify({ thread_id: threadIdToLoad }), + body: JSON.stringify({ conversation_id: conversationIdToLoad }), }) .then((response) => { if (response.status === 403) { @@ -138,22 +140,22 @@ const ChatBot = (props) => { return; } - // Set thread data - this is crucial for continuing the conversation - setThreadId(data.thread_id); - console.log("Loaded existing thread:", data.thread_id); + // Set conversation data - this is crucial for continuing the conversation + setConversationId(data.conversation_id); // Transform API message format to UI format const transformedMessages = (data.messages || []).map((msg, index) => ({ - id: `${data.thread_id}_${index}`, + id: `${data.conversation_id}_${index}`, status: msg.role === "user" ? "sent" : "received", message: msg.content, timestamp: msg.timestamp })); setMessages(transformedMessages); - setThreadOrgId(data.thread_org_id); + setThreadOrgId(data.org_id); setIsActiveOrg(data.is_active_org); + // Disable chat if not in active org if (!data.is_active_org) { setChatDisabled(true); } else { @@ -260,12 +262,9 @@ const ChatBot = (props) => { const sentId = uuidv4(); var parsedData = { "query": inputmsg, - "thread_id": threadId, - "run_id": runId, + "conversation_id": conversationId, } - console.log("Sending message with thread_id:", threadId); - if (appname !== undefined && appname !== null && appname !== "") { parsedData["app_name"] = appname } @@ -315,169 +314,125 @@ const ChatBot = (props) => { setMessages(newmessages); - //fetch(`http://localhost:8080/api/v1/conversation`, { - fetch(`${globalUrl}/api/v1/conversation`, { + // Use streaming endpoint + fetch(`${globalUrl}/api/v1/conversation/stream`, { method: "POST", headers: { "Content-Type": "application/json", + "Accept": "text/event-stream", }, credentials: "include", body: JSON.stringify(parsedData), }) - .then((res) => res.text()) - .then((resText) => { - setLoading(false) - var data = {} - - // JSON parse - try { - data = JSON.parse(resText); - } catch (e) { - console.log("Error parsing response as JSON: ", e); - - newmessages = newmessages.filter((msg) => msg.message !== waitingMsg); - newmessages.push({ - "status": "received", - "message": resText, - "id": uuidv4(), - }); - - setMessages(newmessages); - - return; + .then(async (response) => { + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`Server returned status ${response.status}: ${errorText}`); } + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ""; + let streamedText = ""; + let newConversationId = ""; - if (data.run_id !== undefined && data.run_id !== null && data.run_id !== "") { - setRunId(data.run_id) - } + // Keep waiting message until first chunk arrives + const streamMessageId = uuidv4(); + let firstChunkReceived = false; - if (data.thread_id !== undefined && data.thread_id !== null && data.thread_id !== "") { - setThreadId(data.thread_id) + while (true) { + const { done, value } = await reader.read(); - // Update URL if this is a new thread (not already in URL) - if (!urlThreadId && data.thread_id !== threadId) { - console.log("Navigating to new thread URL:", data.thread_id); - navigate(`/chat/${data.thread_id}`, { replace: true }); + if (done) { + setLoading(false); + break; } - } - if (data.success === undefined) { - newmessages = newmessages.filter((msg) => msg.message !== waitingMsg); - newmessages.push({ - "status": "received", - "message": resText, - "id": uuidv4(), - }); + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split('\n'); + buffer = lines.pop() || ""; // Keep incomplete line in buffer - setMessages(newmessages); + for (const line of lines) { + if (line.startsWith('data: ')) { + const jsonStr = line.replace('data: ', ''); + try { + const data = JSON.parse(jsonStr); + const eventType = data.type; - return; - } + if (eventType === "chunk") { + // On first chunk, remove waiting message and add streaming message + if (!firstChunkReceived) { + newmessages = newmessages.filter((msg) => msg.message !== waitingMsg); + newmessages.push({ + "id": streamMessageId, + "status": "received", + "message": "", + }); + firstChunkReceived = true; + } - // authentication for app - // app validation (choose one) - const defaultMessage = `Default output. The feature you're interacting with may not have been implemented yet. Contact ${supportEmail} with a screenshot of this and your input please.` - - var outputmessage = defaultMessage; - var status = "received"; - var action = "" - if (data.success === false) { - if (data.reason !== undefined) { - outputmessage = data.reason - } + // Append chunk to streamed text + streamedText += data.chunk || ""; + + // Update the message with accumulated text + newmessages = newmessages.map(msg => + msg.id === streamMessageId + ? { ...msg, message: streamedText } + : msg + ); + setMessages([...newmessages]); - status = "error" - } else { - if (data.reason !== undefined) { - outputmessage = data.reason - } - } + } else if (eventType === "done") { + // Capture conversation ID for next turn + newConversationId = data.data; + if (newConversationId) { + setConversationId(newConversationId); + + // Update URL if this is a new conversation + if (!urlConversationId && newConversationId !== conversationId) { + navigate(`/chat/${newConversationId}`, { replace: true }); + } + } - if (data.action !== undefined) { - //console.log("Action is defined: ", data.action); - action = data.action - - if (data.action === "app_authentication") { - // If success & app auth -> say auth success and show available labels - // If !success & app auth -> do authentication - if (data.success === true) { - newmessages = newmessages.filter((msg) => msg.message !== waitingMsg); - // No action for this. - // "action": action, - var appname = "" - if (data.apps !== undefined && data.apps !== null && data.apps.length > 0) { - appname = data.apps[0].name.replaceAll("_", " ") - } - - var outputmessage = `**Please specify which ${appname} action you want to use**: \n` - if (data.available_labels !== undefined && data.available_labels !== null && data.available_labels.length > 0) { - for (var i = 0; i < data.available_labels.length; i++) { - outputmessage += "* " + data.available_labels[i] + "\n" + } else if (eventType === "error") { + // Handle error event + const errorMsg = data.data || "An error occurred"; + console.error("[Stream Error]:", errorMsg); + + newmessages = newmessages.map(msg => + msg.id === streamMessageId + ? { ...msg, status: "error", message: errorMsg } + : msg + ); + setMessages([...newmessages]); + setLoading(false); + return; } - outputmessage += "* Reauthenticate ([see auth](/admin?tab=app_auth))" - } - //Some opavailable actions: " + data.apps.map((app) => app.name).join(", ") - const parsedmessage = { - "status": status, - "message": outputmessage, - "id": uuidv4(), - "category": data.category, - "thread_id": data.thread_id, - "run_id": data.run_id, - } - newmessages.push(parsedmessage); - setMessages(newmessages); - - return - - } else { - if (data.apps !== undefined) { - setInputAuth(data.apps) - - setMessage(inputmsg); - - setForceReauthentication(true) + } catch (e) { + console.error("[ERROR] Failed to parse SSE data:", e, jsonStr); } } - } else if (data.action === "select_category" || data.action === "select_app") { - console.log("[DEBUG] APP SELECTION! Should help them choose an app to use") - // Show a search field - setShowAppSearch(true) } } - newmessages = newmessages.filter((msg) => msg.message !== waitingMsg); - const parsedmessage = { - "status": status, - "message": outputmessage, - "id": uuidv4(), - "action": action, - "category": data.category, - - "thread_id": data.thread_id, - "run_id": data.run_id, - } - newmessages.push(parsedmessage); - setMessages(newmessages); - console.log("New message: ", parsedmessage) + setLoading(false); }) .catch((err) => { setLoading(false) console.log("Problem: ", err); - setMessage(message); + setMessage(inputmsg); newmessages = newmessages.filter((msg) => msg.message !== waitingMsg); - // Find the message with the sentId and change the status to error + // Add error message newmessages.push({ "status": "error", - "message": message, - "error_message": "Failed to send: "+err, + "message": inputmsg, + "error_message": "Failed to send: "+err.message, "id": sentId, }); - setMessages(newmessages); + setMessages([...newmessages]); }); }; @@ -654,21 +609,21 @@ const ChatBot = (props) => { - How many incidents did we get last week? + What is the difference between a trigger and an action? - Answer the last email from Jim about the new project, and say we're on it + How do I create a new workflow? - Is the IP 1.2.3.4 blocked? If not, block it. + Is there a way to mass abort executions of a specific workflow? @@ -1007,9 +962,9 @@ const ChatBot = (props) => { padding: "0 20px" }}> {[ - "How many incidents did we get last week?", - "Answer the last email from Jim about the new project", - "Is the IP 1.2.3.4 blocked? If not, block it." + "What is the difference between a trigger and an action?", + "How do I create a new workflow?", + "Is there a way to mass abort executions of a specific workflow?" ].map((sample, index) => ( { const [organizationFeatures, setOrganizationFeatures] = React.useState({}); const [users, setUsers] = React.useState([]); const [orgRequest, setOrgRequest] = React.useState(true); - const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true"); + const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; useEffect(() => { if(users.length === 0) { getUsers(); diff --git a/frontend/src/components/EditWorkflow.jsx b/frontend/src/components/EditWorkflow.jsx index 34727a1f..6c61a650 100644 --- a/frontend/src/components/EditWorkflow.jsx +++ b/frontend/src/components/EditWorkflow.jsx @@ -633,7 +633,7 @@ const EditWorkflow = (props) => { )} - AI Generate + AI Generate (beta)
diff --git a/frontend/src/components/LeftSideBar.jsx b/frontend/src/components/LeftSideBar.jsx index 5245c9bc..73a37a37 100644 --- a/frontend/src/components/LeftSideBar.jsx +++ b/frontend/src/components/LeftSideBar.jsx @@ -127,7 +127,7 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => { setCurrentSelectedTheme(userdata?.theme); } }, [userdata]); - + const CustomPopper = (props) => { @@ -206,7 +206,11 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => { setOpenAutocomplete(false) }} > - Add suborg + diff --git a/frontend/src/components/LicencePopup.jsx b/frontend/src/components/LicencePopup.jsx index 322d0840..54611352 100644 --- a/frontend/src/components/LicencePopup.jsx +++ b/frontend/src/components/LicencePopup.jsx @@ -1319,8 +1319,14 @@ const LicencePopup = (props) => { > {`${ isPaidPlan ? "Next billing: " : "App runs resets on " - }${new Date( - (localSub.enddate || localSub.Enddate) * 1000 + }${( + isPaidPlan + ? new Date((localSub.enddate || localSub.Enddate) * 1000) + : new Date( + new Date().getFullYear(), + new Date().getMonth() + 1, + 1 + ) ).toLocaleDateString(undefined, { day: "2-digit", month: "short", diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx index 63efe7a9..18dc748c 100644 --- a/frontend/src/components/Navbar.jsx +++ b/frontend/src/components/Navbar.jsx @@ -183,7 +183,7 @@ const menuData = { title: "Articles", icon: "/images/icons/articles.svg", hoverIcon: "/images/icons/articles_hover.svg", - link: "/articles/2.0_release", + link: "/articles", gaData: { category: "navbar", action: "resources_click", diff --git a/frontend/src/components/Oauth2Auth.jsx b/frontend/src/components/Oauth2Auth.jsx index f9db63b3..8e82ae62 100755 --- a/frontend/src/components/Oauth2Auth.jsx +++ b/frontend/src/components/Oauth2Auth.jsx @@ -535,7 +535,7 @@ const AuthenticationOauth2 = (props) => { } } - toast("Authentication successful!") + toast.info("Authentication window closed") // This is more a guess than anything // Should be handled in getAppAuthentication() diff --git a/frontend/src/components/OrgHeaderexpanded.jsx b/frontend/src/components/OrgHeaderexpanded.jsx index 059ad8ac..3b1c8808 100644 --- a/frontend/src/components/OrgHeaderexpanded.jsx +++ b/frontend/src/components/OrgHeaderexpanded.jsx @@ -343,7 +343,7 @@ console.log("defatult in handleEditOrg", defaults) const url = `${globalUrl}/api/v1/orgs/${selectedOrganization?.id}/change`; const data = { org_id: selectedOrganization?.id, - sso_test: true, + sso: true, }; fetch(url, { diff --git a/frontend/src/components/OrganizationTab.jsx b/frontend/src/components/OrganizationTab.jsx index be65b078..f66bcd32 100644 --- a/frontend/src/components/OrganizationTab.jsx +++ b/frontend/src/components/OrganizationTab.jsx @@ -62,11 +62,8 @@ const OrganizationTab = (props) => { if (isIntegrationPartner && isChildOrg && !isGlobalUser) { setVisibleTabs(items.filter((item) => item !== 'Branding' && item !== 'SSO')); } else { - if (userdata && userdata.active_org && userdata.active_org.role === 'admin') { - setVisibleTabs(items); - } else { - setVisibleTabs(items.filter((item) => item !== 'SSO')); - } + // Show SSO tab for all users now + setVisibleTabs(items); } //if (isCloud) { diff --git a/frontend/src/components/ParsedAction.jsx b/frontend/src/components/ParsedAction.jsx index 6af8691e..9a9c47f8 100755 --- a/frontend/src/components/ParsedAction.jsx +++ b/frontend/src/components/ParsedAction.jsx @@ -1911,7 +1911,7 @@ const ParsedAction = (props) => { - Rerun this action with results from previous executions. Built for testing individual actions in the middle of workflows. + Rerun this action with results from your previously selected workflow run. Allows testing individual action changes without rerunning the full workflow. } placement="top" @@ -1923,8 +1923,8 @@ const ParsedAction = (props) => { marginTop: "auto", marginBottom: "auto", height: 30, - marginLeft: 98, textTransform: "none", + marginLeft: 115, }} disabled={autoCompleting} onClick={() => { @@ -1935,7 +1935,7 @@ const ParsedAction = (props) => { } }} > - + Rerun @@ -2814,6 +2814,8 @@ const ParsedAction = (props) => { style: { backgroundColor: theme.palette.platformColor, color: theme.palette.textColor, + border: "1px solid rgba(255,255,255,0.3)", + paddingRight: 20, }, }} filterOptions={(options, { inputValue }) => { @@ -3092,6 +3094,14 @@ const ParsedAction = (props) => { {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 } @@ -3177,7 +3187,7 @@ const ParsedAction = (props) => { marginRight: 5, borderRadius: 5, cursor: "pointer", - border: isAppSelected ? "3px solid #86c142" : "2px solid rgba(255,255,255,0.6)", + border: isAppSelected ? "5px solid #86c142" : "2px solid rgba(255,255,255,0.6)", }} /> diff --git a/frontend/src/components/PartnerApps.jsx b/frontend/src/components/PartnerApps.jsx index 60e6e1b5..124067c3 100644 --- a/frontend/src/components/PartnerApps.jsx +++ b/frontend/src/components/PartnerApps.jsx @@ -13,9 +13,9 @@ const PartnerApps = () => { minHeight: "500px", }} > - Partner Apps + Partner's Apps ) } -export default PartnerApps +export default PartnerApps \ No newline at end of file diff --git a/frontend/src/components/PartnerHeader.jsx b/frontend/src/components/PartnerHeader.jsx index 3a6a910b..51f14791 100644 --- a/frontend/src/components/PartnerHeader.jsx +++ b/frontend/src/components/PartnerHeader.jsx @@ -46,10 +46,10 @@ const useStyles = makeStyles({ }); const defaultImage = - "data:image/png;base64,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"; + "data:image/png;base64,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" const defaultLandscapeImage = - "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIMAAAAcCAYAAABCrQzwAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAARPSURBVHgB7VpdUtswEF4zvJOeAHECwgnqzNDnpifAPQHhmc4QZtrnhBNgTlD63HZiTpD0BLgnIL1A3f3Qqiiu/0KcOC76ZnYkSxtLWq1WuxsTOTg4OKThJR+PE1ojvA/fPHJoBXbIwUGwSy8ASZL4XCjUPc8LqQHIHEB7TD+YIp5LnOLpcBEw7TP9Ep5I+vpcoD82bXWj1crAAlJczFk48xLWgOlE6iFtEDLHCYkyWnjHFFt8F1wMUzxvmY6kPpJ3hEwRrQFZyjCncuE2Ajk5p6RPmG+1Y763TDfrOjUrAJuspB4x3TEdkrWhojBDecRabqQe0waRpQwz7/xrj7YMBScMMOY1YL4xK8QZbQ/6UoY8r/clPECP+WbUANrkQNqKcMkEhT0gbUYh5Fj6BqwQ17Q96Ej5swIPNaUIQCt8BtsBZODkD1MsEGDIfCFp36A0nJUrRzF1tvBqWQliRZU8zryK135bLIOy6nd5TLzogIszKTMBJWCClXlgmjJN+PlBHLgs/vtEY5LTP0yeoKTNNw0W60WyCFxpE+G5sN5n456WACIOmSd+NxHC2q7N3IpQyTIkn94ERL9fL7bu3LFvES7yHfOiFgf1zr/n3ZPPBeZxm9fJijCmYkBAXdKOGkiRNtPY1M6W+RuVkYpGsK6Y9LoUaX8KCtpLh7M2qlmGhD33hE+bTf8oRx5fLcDmG1M3kNM6kOtjWSjS1uMVE3wOUGy9u0MrAteOJ7CaQ28ReO4Jz6XFd2DxHFQZj+cM5R7K442840h+jxDWKH2hL9WKa0LuPFgYoxCKdNw9MSacaVRROS5t6yEn5crq71P7YK4ZJKQC20fgOg6SWZ9fpOytSTphUbwQOIpD0nF61+r2hQbiRJ4VOE1hRltjHnxN8KWc5RwIWxaQW5TB064MpJziAHXRcF/okJ4Egn5FOvTMesdWJtSeC5GDOe19KrdsBZbBezxJFjIcDA+alKT+3dzJ9eo3AdnUWyETTiE6eFQScQb/q42vgJjKs5a5/btVvH2JGkJqEAjhSOcYMjcYVoN54DydShOUYhPKsE8NAvKQdDzW+4WfB/RMtMKBlIwinKRRibe/Z9XrVgSV0+5T8zA+z0mWfGA1kdcoeUdrkk5mgQHTVBI2ynTKYkfSD4Q1XhHmOlSJlZiS5BWUVNFmoTLaTGgKOV2nZIO2z9I+pQK0JbRErGzCI0U6Xr63snzIuBnzGNNi3L4qxlZ9KNlKjIcMZkAFCbCaYU6/L3mWx1wLGiSdbtYMBxJ9U2ueJvK6Khpgl7OLS6U8C1FDwiYPuAt5cRA8fIIsjxmWAIsd1+k44o8jHhfKaL4nMN47xsAGRLSZ3AQyo12Zg5K2mTVPKGpMWj5dWgy9I9L5lahogNZ+AylZN6N8cVGadQ1jbmS8gjmQzGGew4M5mpR7XPVwtPZLpyb+6vUa/Ht5mTnI5ke0JNwHsQ5/4ZTBwcHBwaEAfwCepixlSVZHRgAAAABJRU5ErkJggg=="; + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAV4AAAB4CAIAAAA1yw3SAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAEvmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSfvu78nIGlkPSdXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQnPz4KPHg6eG1wbWV0YSB4bWxuczp4PSdhZG9iZTpuczptZXRhLyc+CjxyZGY6UkRGIHhtbG5zOnJkZj0naHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyc+CgogPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9JycKICB4bWxuczpBdHRyaWI9J2h0dHA6Ly9ucy5hdHRyaWJ1dGlvbi5jb20vYWRzLzEuMC8nPgogIDxBdHRyaWI6QWRzPgogICA8cmRmOlNlcT4KICAgIDxyZGY6bGkgcmRmOnBhcnNlVHlwZT0nUmVzb3VyY2UnPgogICAgIDxBdHRyaWI6Q3JlYXRlZD4yMDI1LTExLTE4PC9BdHRyaWI6Q3JlYXRlZD4KICAgICA8QXR0cmliOkV4dElkPmRhNGQ3YjEyLWI5M2ItNGY0NS1iNTA3LTAzODliZmQ4N2EyZDwvQXR0cmliOkV4dElkPgogICAgIDxBdHRyaWI6RmJJZD41MjUyNjU5MTQxNzk1ODA8L0F0dHJpYjpGYklkPgogICAgIDxBdHRyaWI6VG91Y2hUeXBlPjI8L0F0dHJpYjpUb3VjaFR5cGU+CiAgICA8L3JkZjpsaT4KICAgPC9yZGY6U2VxPgogIDwvQXR0cmliOkFkcz4KIDwvcmRmOkRlc2NyaXB0aW9uPgoKIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PScnCiAgeG1sbnM6ZGM9J2h0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvJz4KICA8ZGM6dGl0bGU+CiAgIDxyZGY6QWx0PgogICAgPHJkZjpsaSB4bWw6bGFuZz0neC1kZWZhdWx0Jz5VbnRpdGxlZCBkZXNpZ24gLSAxPC9yZGY6bGk+CiAgIDwvcmRmOkFsdD4KICA8L2RjOnRpdGxlPgogPC9yZGY6RGVzY3JpcHRpb24+CgogPHJkZjpEZXNjcmlwdGlv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"; const PartnerHeader = (props) => { const { diff --git a/frontend/src/components/PartnerTab.jsx b/frontend/src/components/PartnerTab.jsx index c32bed0e..5a992c0e 100644 --- a/frontend/src/components/PartnerTab.jsx +++ b/frontend/src/components/PartnerTab.jsx @@ -126,9 +126,9 @@ const PartnerTab = (props) => { case `apps`: return ; case 'articles' : - return ; - case `ai_agents`: - return ; + return ; + case `aiagents`: + return ; case 'branding': return { !isCloud && (tabName === "Usecases" || tabName === "Apps" || - tabName === "AI Agents" || - tabName === "Articles") + tabName === "AI Agents") ) { return true; } @@ -166,7 +165,6 @@ const PartnerTab = (props) => { // Disable Apps, Articles, and AI Agents for all non-support users if ( tabName === "Apps" || - tabName === "Articles" || tabName === "AI Agents" ) { return true; @@ -196,7 +194,7 @@ const PartnerTab = (props) => { } const isSupportOnlyTab = (tabName) => { - return tabName === "Apps" || tabName === "Articles" || tabName === "AI Agents"; + return tabName === "Apps" || tabName === "AI Agents"; } return ( diff --git a/frontend/src/components/PartnersArticles.jsx b/frontend/src/components/PartnersArticles.jsx index 80653231..104446ee 100644 --- a/frontend/src/components/PartnersArticles.jsx +++ b/frontend/src/components/PartnersArticles.jsx @@ -1,7 +1,8 @@ import { Box, Typography } from '@mui/material' import React from 'react' -const PartnersArticles = () => { +const PartnersArticles = (props) => { + const {tabName = "Partner's Articles"} = props; return ( { minHeight: "500px", }} > - Partner Articles + {tabName} ) } -export default PartnersArticles +export default PartnersArticles \ No newline at end of file diff --git a/frontend/src/components/PartnersUsecasesTab.jsx b/frontend/src/components/PartnersUsecasesTab.jsx index 39944ff5..9cbc31a1 100644 --- a/frontend/src/components/PartnersUsecasesTab.jsx +++ b/frontend/src/components/PartnersUsecasesTab.jsx @@ -24,6 +24,7 @@ import { import React, { useContext, useEffect, useState } from "react"; import { getTheme } from "../theme.jsx"; import { Context } from "../context/ContextApi.jsx"; +import { triggers } from "../views/AngularWorkflow.jsx"; import AddIcon from "@mui/icons-material/Add"; import CloseIcon from "@mui/icons-material/Close"; import { toast } from "react-toastify"; @@ -37,6 +38,35 @@ import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; import InfoIcon from "@mui/icons-material/Info"; import { Link, useNavigate } from "react-router-dom"; +import { grey } from "../views/AngularWorkflow.jsx" + +const IMAGE_UPLOAD_PLACEHOLDER = "Uploading image..."; + +// Helper to extract the first markdown image URL from navigation content +export const getFirstImageFromNavigation = (navigation) => { + if (!navigation || !Array.isArray(navigation.items)) { + return ""; + } + + for (const item of navigation.items) { + if (!item || !Array.isArray(item.content)) { + continue; + } + + for (const paragraph of item.content) { + if (typeof paragraph !== "string") { + continue; + } + + const match = paragraph.match(/!\[[^\]]*]\(([^)]+)\)/); + if (match && match[1]) { + return match[1]; + } + } + } + + return ""; +}; // Helper function to get the correct image path based on app category export const getCategoryImagePath = (category) => { @@ -62,16 +92,132 @@ export const getCategoryImagePath = (category) => { return "/images/appCategories/intel.svg"; } else if (lowerCategory.includes("email")) { return "/images/appCategories/email.svg"; + } else if (lowerCategory.includes("webhook")) { + return triggers.find((trigger) => trigger?.name.toLowerCase() === "webhook")?.large_image; + } else if (lowerCategory.includes("schedule")) { + return triggers.find((trigger) => trigger?.name.toLowerCase() === "schedule")?.large_image; + } else if (lowerCategory.includes("pipelines")) { + return triggers.find((trigger) => trigger?.name.toLowerCase() === "pipelines")?.large_image; + } else if (lowerCategory.includes("shuffle workflow")) { + return triggers.find((trigger) => trigger?.name.toLowerCase() === "shuffle workflow")?.large_image; + } else if (lowerCategory.includes("user input")) { + return triggers.find((trigger) => trigger?.name.toLowerCase() === "user input")?.large_image; } else { return "/images/appCategories/other.svg"; } }; // Skeleton component for loading state -const UsecaseCardSkeleton = () => { +const UsecaseCardSkeleton = ({ isArticlesTab }) => { const { themeMode } = useContext(Context); const theme = getTheme(themeMode); - + + // Skeleton for Articles Tab + if (isArticlesTab) { + return ( + + {/* Header image skeleton */} + + + {/* Body skeleton */} + + + + + + + + + + + ); + } + + // Skeleton for Usecases Tab return ( { ); }; -const UsecaseCard = ({ usecase, handleToggle, handleOpenDialog, handleDeleteUsecase }) => { +// Card renderer for both usecases and articles +const UsecaseCard = ({ usecase, handleToggle, handleOpenDialog, handleDeleteUsecase, isArticlesTab, partnerData }) => { const [anchorEl, setAnchorEl] = useState(null); const open = Boolean(anchorEl); const { themeMode } = useContext(Context); @@ -209,9 +356,215 @@ const UsecaseCard = ({ usecase, handleToggle, handleOpenDialog, handleDeleteUsec navigate(`/usecases/${usecase.id}`); }; + // Article Card Layout + if (isArticlesTab) { + return ( + + + + + + {usecase.name} + + + {usecase.created && ( + + {new Date(usecase.created * 1000).toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + })} + + )} + + e.stopPropagation()} + > + { + e.stopPropagation(); + window.open(`${window.location.origin}/partners/${partnerData?.name.toLowerCase().replaceAll(" ", "_")}/articles/${usecase.name.toLowerCase().replaceAll(" ", "_")}`, "_blank"); + }} + sx={{ + color: theme.palette.primary.main, + cursor: "pointer", + }} + /> + { + e.stopPropagation(); + }} + onChange={(e) => { + e.preventDefault(); + e.stopPropagation(); + handleToggle(usecase.id); + }} + size="medium" + sx={{ + "& .MuiSwitch-switchBase.Mui-checked": { + color: "#4CAF50", + }, + "& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track": { + backgroundColor: "#4CAF50", + }, + }} + /> + { + e.stopPropagation(); + handleClick(e); + }} + size="small" + sx={{ + color: theme.palette.text.primary, + "&:hover": { + backgroundColor: + themeMode === "dark" + ? "rgba(255, 255, 255, 0.1)" + : "rgba(0, 0, 0, 0.05)", + }, + }} + > + + + + + e.stopPropagation()} + PaperProps={{ + sx: { + backgroundColor: theme.palette.DialogStyle.backgroundColor, + border: theme.palette.defaultBorder, + borderRadius: "8px", + boxShadow: theme.palette.DialogStyle.boxShadow, + "& .MuiMenuItem-root": { + fontSize: "14px", + color: theme.palette.text.primary, + "&:hover": { + backgroundColor: + themeMode === "dark" + ? "rgba(255, 255, 255, 0.1)" + : "rgba(0, 0, 0, 0.05)", + }, + }, + }, + }} + transformOrigin={{ horizontal: "right", vertical: "top" }} + anchorOrigin={{ horizontal: "right", vertical: "bottom" }} + > + { + e.stopPropagation(); + handleOpenDialog(usecase); + setAnchorEl(null); + }} + > + + + + Edit Article + + { + e.stopPropagation(); + handleDeleteUsecase(usecase.id); + }} + > + + + + Delete Article + + + + + ); + } + + // Usecase Card Layout return (
+ { + e.stopPropagation(); + window.open(`${window.location.origin}/usecases/${usecase.name.toLowerCase().replaceAll(" ", "_")}`, "_blank"); + }} + sx={{ + color: theme.palette.primary.main, + cursor: "pointer", + }} + /> { @@ -414,9 +777,11 @@ const UsecaseCard = ({ usecase, handleToggle, handleOpenDialog, handleDeleteUsec ); }; -const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPartnerData }) => { +const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPartnerData, selectedTab }) => { const { themeMode } = useContext(Context); const theme = getTheme(themeMode); + const isArticlesTab = selectedTab?.toLowerCase() === "articles"; + const entityLabel = isArticlesTab ? "Article" : "Usecase"; // Dialog state const [openDialog, setOpenDialog] = useState(false); @@ -438,12 +803,13 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar navigation: { items: [ { - name: "About Usecase", + name: `About ${entityLabel}`, content: [""], }, ], }, public: false, + created: null, }); // App categories for dropdowns @@ -458,6 +824,11 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar { value: "assets", label: "Assets" }, { value: "intel", label: "Intel" }, { value: "email", label: "Email" }, + {value: "webhook", label: "Webhook"}, + {value: "schedule", label: "Schedule"}, + {value: "pipelines", label: "Pipelines"}, + {value: "shuffle workflow", label: "Shuffle Workflow"}, + {value: "user input", label: "User Input"}, { value: "other", label: "Other" } ]; @@ -505,7 +876,9 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar description: usecase.mainContent?.description, navigation: usecase.navigation || { items: [] }, categories: usecase.mainContent?.categories || [], - public: usecase?.public || false, + public: usecase?.public || false, + created: usecase?.created || null, + headerImage: getFirstImageFromNavigation(usecase.navigation), })); if (usecases.length > 0) { setUsecaseData(usecases); @@ -526,7 +899,195 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar // Sample categories const categoryOptions = ["Collect","Enrich", "Detect", "Respond", "Verify"]; + // Uploading image to the public bucket + const handleImageUpload = async (imageData) => { + const folderName = "usecase_images"; + const usecaseId = formData?.id || partnerData?.id || "usecase"; + + try { + const response = await fetch(`${globalUrl}/api/v1/image_upload`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + body: JSON.stringify({ + imageData, + folder: folderName, + id: usecaseId, + }), + }); + + // if (response.status !== 200) { + // toast.error("Failed to upload image" + response?.reason); + // return null; + // } + + const responseJson = await response.json(); + if (!responseJson?.success || !responseJson?.url) { + toast.error("Failed to upload image: " + responseJson?.reason); + return null; + } + + return responseJson.url; + } catch (error) { + toast.error("Failed to upload image" + error?.message); + return null; + } + }; + + // This function is used to update the content with the public image url + const updateContentWithImageUrl = (itemIndex, contentIndex, imageUrl) => { + setFormData((prevData) => { + const newItems = [...(prevData.navigation.items || [])]; + const item = newItems[itemIndex]; + if (!item) { + return prevData; + } + + const contents = [...(item.content || [])]; + const existingContent = contents[contentIndex] || ""; + const markdownImage = imageUrl ? `![image](${imageUrl})` : ""; + + const placeholderIndex = existingContent.indexOf(IMAGE_UPLOAD_PLACEHOLDER); + if (placeholderIndex !== -1) { + const before = existingContent.slice(0, placeholderIndex); + const after = existingContent.slice( + placeholderIndex + IMAGE_UPLOAD_PLACEHOLDER.length, + ); + + const trimmedBefore = before.replace(/\s*$/, ""); + const needsNewline = + trimmedBefore.length > 0 && !trimmedBefore.endsWith("\n"); + const separator = needsNewline ? "\n" : ""; + + contents[contentIndex] = `${trimmedBefore}${separator}${markdownImage}${after}`; + } else { + const needsNewline = + existingContent.length > 0 && !existingContent.endsWith("\n"); + const separator = needsNewline ? "\n" : ""; + + contents[contentIndex] = `${existingContent}${separator}${markdownImage}`; + } + + newItems[itemIndex] = { + ...item, + content: contents, + }; + + return { + ...prevData, + navigation: { + items: newItems, + }, + }; + }); + }; + + // This function is used to insert the image placeholder (Uploading image...) in the content to show that the image is being uploaded + const insertImagePlaceholder = (itemIndex, contentIndex) => { + setFormData((prevData) => { + const newItems = [...(prevData.navigation.items || [])]; + const item = newItems[itemIndex]; + if (!item) { + return prevData; + } + + const contents = [...(item.content || [])]; + const existingContent = contents[contentIndex] || ""; + const needsNewline = + existingContent.length > 0 && !existingContent.endsWith("\n"); + const separator = needsNewline ? "\n" : ""; + + contents[contentIndex] = `${existingContent}${separator}${IMAGE_UPLOAD_PLACEHOLDER}`; + + newItems[itemIndex] = { + ...item, + content: contents, + }; + + return { + ...prevData, + navigation: { + items: newItems, + }, + }; + }); + }; + + // This function is used to process the image file and upload it to the public bucket + const processImageFile = (imageFile, itemIndex, contentIndex) => { + const reader = new FileReader(); + reader.onload = async (loadEvent) => { + const imageData = loadEvent.target?.result; + if (!imageData) { + toast.error("Failed to read image"); + updateContentWithImageUrl(itemIndex, contentIndex, ""); + return; + } + + const imageUrl = await handleImageUpload(imageData); + if (!imageUrl) { + // Clean up the placeholder if upload failed + updateContentWithImageUrl(itemIndex, contentIndex, ""); + return; + } + + updateContentWithImageUrl(itemIndex, contentIndex, imageUrl); + }; + reader.readAsDataURL(imageFile); + }; + + // This function is used to handle the image paste event + const handleContentPaste = async (event, itemIndex, contentIndex) => { + const items = event.clipboardData?.items; + if (!items || items.length === 0) { + return; + } + + let imageFile = null; + for (let i = 0; i < items.length; i += 1) { + const item = items[i]; + if (item.kind === "file" && item.type.startsWith("image/")) { + imageFile = item.getAsFile(); + break; + } + } + + if (!imageFile) { + return; + } + + event.preventDefault(); + + insertImagePlaceholder(itemIndex, contentIndex); + processImageFile(imageFile, itemIndex, contentIndex); + }; + + // This function is used to handle the image drop event + const handleContentDrop = async (event, itemIndex, contentIndex) => { + event.preventDefault(); + + const files = event.dataTransfer?.files; + if (!files || files.length === 0) { + return; + } + + const imageFile = Array.from(files).find((file) => file.type.startsWith("image/")); + if (!imageFile) { + toast.error("Failed to upload image, only images are supported"); + return; + } + + insertImagePlaceholder(itemIndex, contentIndex); + processImageFile(imageFile, itemIndex, contentIndex); + }; + const getUserProfileWorkflows = (orgId) => { + if (selectedTab === "articles") { + return; + } setIsWorkflowLoading(true); fetch(`${globalUrl}/api/v1/partners/${orgId}/workflows`, { method: "GET", @@ -713,12 +1274,13 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar navigation: usecase?.navigation || { items: [ { - name: "About Usecase", + name: `About ${entityLabel}`, content: [""], }, ], }, public: usecase?.public || false, + created: usecase?.created || Math.floor(Date.now() / 1000), }); setOpenDialog(true); }; @@ -753,15 +1315,15 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar const validationErrors = []; // Check for public workflow selection - if (!formData.mainContent.publicWorkflowId || formData.mainContent.publicWorkflowId.trim() === "") { + if (!isArticlesTab && (!formData.mainContent.publicWorkflowId || formData.mainContent.publicWorkflowId.trim() === "")) { validationErrors.push("Please select a public workflow"); } - if (!formData.mainContent.sourceAppType || formData.mainContent.sourceAppType.trim() === "") { + if (!isArticlesTab && (!formData.mainContent.sourceAppType || formData.mainContent.sourceAppType.trim() === "")) { validationErrors.push("Source app type is required"); } - if (!formData.mainContent.destinationAppType || formData.mainContent.destinationAppType.trim() === "") { + if (!isArticlesTab && (!formData.mainContent.destinationAppType || formData.mainContent.destinationAppType.trim() === "")) { validationErrors.push("Destination app type is required"); } @@ -783,7 +1345,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar } // Check categories - if (!formData.mainContent.categories || !Array.isArray(formData.mainContent.categories) || formData.mainContent.categories.length === 0) { + if (!isArticlesTab && (!formData.mainContent.categories || !Array.isArray(formData.mainContent.categories) || formData.mainContent.categories.length === 0)) { validationErrors.push("At least one category must be selected"); } @@ -846,7 +1408,6 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar return res.json(); }) .then((responseData) => { - console.log("Response data:", responseData); // Reset loading state setIsSubmitting(false); @@ -863,7 +1424,9 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar description: formData.mainContent?.description.trim(), categories: formData.mainContent?.categories || [], navigation: formData.navigation || {}, - public: formData?.public || false, + public: formData?.public || false, + created: formData?.created || null, + headerImage: getFirstImageFromNavigation(formData.navigation), } setUsecaseData((prevData) => { @@ -921,6 +1484,28 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar }); }; + // This is used to filter the usecases based on the selected tab + const filteredUsecases = Array.isArray(usecaseData) + ? usecaseData.filter((usecase) => { + const hasWorkflowId = + !!usecase.publicWorkflowId && + usecase.publicWorkflowId.toString().trim() !== ""; + + if (selectedTab === "articles") { + // Articles: only items without a public workflow ID + return !hasWorkflowId; + } + + if (selectedTab === "usecases") { + // Usecases: only items with a public workflow ID + return hasWorkflowId; + } + + // Fallback: show all + return true; + }) + : []; + return ( - {/* Add Usecase Button */} + {/* Add Usecase / Article Button */} - Usecases + {selectedTab === "usecases" && "Usecases"} + {selectedTab === "articles" && "Articles"} - {/* Usecases Grid */} + {/* Usecases / Articles Grid */} {isLoading ? ( // Skeleton loading state {/* Display 4 skeleton cards while loading */} {[...Array(4)].map((_, index) => ( - + ))} - ) : usecaseData.length > 0 ? ( - // Actual data display + ) : filteredUsecases.length > 0 ? ( + // Actual data display (filtered based on public workflow ID) - {usecaseData.map((usecase) => ( + {filteredUsecases.map((usecase) => ( ))} @@ -1027,7 +1614,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar color: "#FFFFFF", }} > - No usecases found + {isArticlesTab ? "No articles found" : "No usecases found"} )} @@ -1064,7 +1651,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar }} > - {formData?.id ? "Update" : "Add New"} Usecase : {formData?.public ? "Published" : "Draft"} + {formData?.id ? "Update" : "Add New"} {entityLabel} : {formData?.public ? "Published" : "Draft"} + {!isArticlesTab && ( + )} + + {/* Other data selection (use all data types) */} + {!isArticlesTab && ( + + + + Coming soon: Files (detection) + + + + + + Coming soon: Datastore category (threatlists) + + + + )} {/* App Type Selection */} - + {!isArticlesTab && ( + + + )} + {!isArticlesTab && ( + )} @@ -1648,7 +2271,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar { const newItems = [...formData.navigation.items]; @@ -1659,12 +2282,15 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar navigation: { items: newItems }, }); }} + onPaste={(event) => handleContentPaste(event, itemIndex, contentIndex)} + onDrop={(event) => handleContentDrop(event, itemIndex, contentIndex)} + onDragOver={(event) => event.preventDefault()} placeholder="Content (Markdown supported)" helperText={ - Markdown syntax is supported + Markdown syntax is supported and Use ### for subItem for Table of Contents. Drag and drop or paste images to the content to upload them. } @@ -1708,7 +2334,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar )} ))} - +{/* + */} ))} @@ -1791,7 +2417,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar > {isSubmitting ? (formData?.id ? "Updating..." : "Adding...") - : (formData?.id ? "Update Usecase" : "Add Usecase") + : (formData?.id ? `Update ${entityLabel}` : `Add ${entityLabel}`) } diff --git a/frontend/src/components/Priority.jsx b/frontend/src/components/Priority.jsx index d5fb4432..508ecff6 100644 --- a/frontend/src/components/Priority.jsx +++ b/frontend/src/components/Priority.jsx @@ -25,7 +25,7 @@ const Priority = (props) => { const { globalUrl, clickedFromOrgTab,userdata, serverside, priority, checkLogin, setAdminTab, setCurTab, appFramework, } = props; const { themeMode, supportEmail } = useContext(Context); const theme = getTheme(themeMode); - const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true"); + const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; let navigate = useNavigate(); if (window.location.pathname === "/workflows") { diff --git a/frontend/src/components/RunsOverTimeWidget.jsx b/frontend/src/components/RunsOverTimeWidget.jsx index c68511e0..523a34ee 100644 --- a/frontend/src/components/RunsOverTimeWidget.jsx +++ b/frontend/src/components/RunsOverTimeWidget.jsx @@ -1,7 +1,25 @@ import React, { useEffect, useMemo, useState } from 'react'; -import { Typography, ToggleButton, ToggleButtonGroup, Select, MenuItem, FormControl, InputLabel, Box } from '@mui/material'; +import { makeStyles, } from "@mui/styles"; +import { + Typography, + ToggleButton, + ToggleButtonGroup, + Select, + MenuItem, + FormControl, + InputLabel, + Box, + Autocomplete, + TextField, + Tooltip, + IconButton, +} from '@mui/material'; import { BarChart, BarSeries, Bar, GridlineSeries, Gridline, TooltipArea, ChartTooltip, LinearYAxis, LinearYAxisTickSeries, LinearYAxisTickLabel } from 'reaviz'; import theme from '../theme.jsx'; +import {green, yellow, red} from "../views/AngularWorkflow.jsx" +import { + OpenInNew as OpenInNewIcon, +} from '@mui/icons-material'; // Compact number formatter for axis ticks (e.g. 12,000 -> 12k, 12,000,000 -> 12M) function formatCompactNumber(value) { @@ -59,34 +77,128 @@ function computeTodayValueForOrg(key, orgStats) { const RunsOverTimeWidget = (props) => { const { globalUrl, onLoadingChange, monthOverride, dummyMode, selectedOrganization, selectedOrgForStats, orgStats, orgForLimit, loadingSelectedOrgStats } = props; const [mode, setMode] = useState('workflows'); // 'apps' | 'workflows' + const [selectedStatType, setSelectedStatType] = useState({ + "value": 'total_workflow_executions', + "label": 'Workflows', + "amount": 0, + }) const [series, setSeries] = useState([]); const [days, setDays] = useState(365); // aggregate to last 12 months by default const [selectedMonth, setSelectedMonth] = useState(null); // Date representing first day of target month, or null for yearly view + const [statTypeOptions, setStatTypeOptions] = useState([]) const [loading, setLoading] = useState(false); + const useStyles = makeStyles({ + notchedOutline: { + borderColor: "#FF8544 !important", + }, + root: { + "& .MuiAutocomplete-listbox": { + border: "2px solid #FF8544", + color: theme.palette.text.primary, + fontSize: 18, + "& li:nth-child(even)": { + backgroundColor: "#CCC", + }, + "& li:nth-child(odd)": { + backgroundColor: "#FFF", + }, + }, + }, + inputRoot: { + color: theme.palette.text.primary, + "&:hover .MuiOutlinedInput-notchedOutline": { + borderColor: "#f86a3e", + }, + }, + }); + const classes = useStyles(); // Helper: fetch time series for a specific statistics key const fetchSeriesForKey = async (key) => { + + if (statTypeOptions?.length < 3) { + var foundkeys = {} + for (var key in orgStats) { + if (!key.startsWith("total_")) { + continue + } + + // Remove total + foundkeys[key] = orgStats[key] + } + + for (var foundKey in orgStats?.daily_statistics) { + const dailyStats = orgStats?.daily_statistics[foundKey] + + for (var additionKey in dailyStats?.additions) { + const addition = dailyStats?.additions[additionKey]; + if (foundkeys[addition?.key] === undefined) { + foundkeys[addition?.key] = addition?.value + } else { + foundkeys[addition?.key] += addition?.value + } + } + } + + var newarray = [] + for (var key in foundkeys) { + const foundkey = key + + var parsedname = key + if (parsedname?.startsWith("total_")) { + parsedname = parsedname.replace("total_", "") + } + + if (parsedname?.startsWith("categorylabel")) { + parsedname = parsedname.replace("categorylabel", "") + } + + parsedname = (parsedname.charAt(0).toUpperCase() + parsedname.substring(1)).replaceAll("_", " ") + + newarray.push({ + "value": key, + "label": parsedname, + "amount": foundkeys[key], + }) + } + + if (newarray.length > 0) { + setStatTypeOptions(newarray) + } + } + try { // If specific org is selected and pre-fetched stats are available, use them directly if (selectedOrgForStats && selectedOrgForStats !== 'ALL' && orgStats && Array.isArray(orgStats?.daily_statistics)) { + const dailyStats = orgStats.daily_statistics; const processedEntries = []; const cutoff = new Date(); cutoff.setDate(cutoff.getDate() - days); + if (key.startsWith("total_")) { + key = key.replace("total_", "") + } + for (const day of dailyStats) { if (!day?.date) continue; const dayDate = new Date(day.date); if (dayDate < cutoff) continue; let value = 0; - if (key === 'workflow_executions') { - const finished = Number(day?.workflow_executions_finished || 0); - const failed = Number(day?.workflow_executions_failed || 0); - value = finished + failed; - } else { - value = Number(day?.[key] || 0); - } + + if (day[key] === undefined) { + for (var additionKey in day?.additions) { + const addition = day?.additions[additionKey]; + if (addition?.key === key) { + value += Number(addition?.value || 0); + break + } + } + } else { + value = Number(day[key] || 0); + } + processedEntries.push({ date: day.date, value }); } @@ -185,7 +297,7 @@ function computeTodayValueForOrg(key, orgStats) { }; // Load and transform into monthly aggregation for last 12 months - const load = async (curMode) => { + const loadStats = async (curMode, inputType) => { setLoading(true); try { // Clear current series immediately to avoid any visual overlap while switching views @@ -203,8 +315,10 @@ function computeTodayValueForOrg(key, orgStats) { setSeries(dummy); return; } - const key = curMode === 'apps' ? 'app_executions' : 'workflow_executions'; + + const key = inputType !== undefined ? inputType : selectedStatType?.value?.length > 0 ? selectedStatType?.value : curMode === 'apps' ? 'app_executions' : 'workflow_executions'; const entries = await fetchSeriesForKey(key); + // Normalize variants: {Date, Value} or {date, value} const normalized = (entries || []).map((d) => ({ date: d?.Date ? new Date(d.Date) : (d?.date ? new Date(d.date) : new Date()), @@ -272,6 +386,23 @@ function computeTodayValueForOrg(key, orgStats) { } }; + + useEffect(() => { + setTimeout(() => { + const starterWidgetStatType = localStorage.getItem("runsOverTimeWidgetStatType") + if (starterWidgetStatType) { + setSelectedStatType({ + "value": starterWidgetStatType, + "label": (starterWidgetStatType.charAt(0).toUpperCase() + starterWidgetStatType.substring(1)).replaceAll("_", " "), + "amount": 0, + }) + + loadStats(mode, starterWidgetStatType) + } + + }, 1500) + }, []) + // Apply month override (e.g. onboarding Explore Now) - consolidated with main load effect useEffect(() => { if (monthOverride instanceof Date) { @@ -287,8 +418,9 @@ function computeTodayValueForOrg(key, orgStats) { setSeries([]); return; } - load(mode); - }, [mode, globalUrl, days, selectedMonth, dummyMode, selectedOrgForStats, loadingSelectedOrgStats]); + + loadStats(mode); + }, [selectedStatType, mode, globalUrl, days, selectedMonth, dummyMode, selectedOrgForStats, loadingSelectedOrgStats]); // Notify parent on loading changes useEffect(() => { @@ -341,55 +473,125 @@ function computeTodayValueForOrg(key, orgStats) { } />; + + const barColorscheme = [ + "#f85a3e", // anchor orange + "#ff7a57", // brighter, more playful + "#e14b2e", // slightly darker + redder + "#ff9b6b", // soft peachy highlight + "#c83f24", // deep burnt orange + ] return (
- Runs over time ({mode === "workflows" ? "Workflows" : "Apps"}) + {selectedStatType?.label} ({selectedStatType?.amount}) - v && setMode(v)} - sx={{ - height: 37, - backgroundColor: 'rgba(255,255,255,0.06)', - border: '1px solid rgba(255,255,255,0.22)', - borderRadius: '30px', - padding: '2px', - "& .MuiToggleButton-root": { - border: "none", - borderRadius: "30px", - color: "#fff", - padding: "6px 16px", - textTransform: "none", - fontSize: "14px", - "&.Mui-selected": { - backgroundColor: "#fff", - color: "#222", - fontWeight: "600", - "&:hover": { - backgroundColor: "#fff", - }, - }, - "&:hover": { - backgroundColor: "rgba(255, 255, 255, 0.2)", - }, + + + + + + + + + + - - - Workflows - - - - - Apps - - - + '& .MuiOutlinedInput-notchedOutline': { borderColor: 'rgba(255,255,255,0.22)' }, + }}> + option.id === value.id} + getOptionLabel={(option) => { + if ( + option === undefined || + option === null || + option.label === undefined || + option.label === null + ) { + if (option.value !== undefined && option.value !== null) { + return option.value + } else { + return option + } + } + + const newname = ( + option.label.charAt(0).toUpperCase() + option.label.substring(1) + ).replaceAll("_", " "); + return newname; + }} + options={statTypeOptions} + fullWidth + style={{ + backgroundColor: theme.palette?.inputColor, + borderRadius: theme.palette?.borderRadius, + }} + onChange={(event, newValue) => { + console.log("CHANGE: ", newValue) + }} + renderOption={(props, data, state) => { + // Format to thousand or million + const formattedamount = formatCompactNumber(data?.amount) + const numbercolor = data?.amount >= 1000000 ? red : (data?.amount >= 100000 ? yellow : green) + + return ( + { + setSelectedStatType(data) + + // Set local storage for the key + if (data?.value) { + localStorage.setItem("runsOverTimeWidgetStatType", data?.value) + } + }} + > + {formattedamount} {data?.label} + + ) + }} + renderInput={(params) => { + return ( +
+ +
+ ) + }} + /> + + } />} + series={} />} gridlines={} />} yAxis={ { } const imageSize = 30 const timenowUnix = Math.floor(Date.now() / 1000) - const columns: GridColDef[] = [ + const columns = [ { field: 'execution_source', headerName: 'Source', diff --git a/frontend/src/components/SearchData.jsx b/frontend/src/components/SearchData.jsx index a77560dd..01f619bf 100644 --- a/frontend/src/components/SearchData.jsx +++ b/frontend/src/components/SearchData.jsx @@ -77,7 +77,7 @@ const SearchData = props => { // return null //} - const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true"); + const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; // if (window.location.pathname === "/docs" || window.location.pathname === "/apps" || window.location.pathname === "/usecases" ) { // setModalOpen(false) // } diff --git a/frontend/src/components/UsecaseSearch.jsx b/frontend/src/components/UsecaseSearch.jsx index 2460876d..ff90bc68 100644 --- a/frontend/src/components/UsecaseSearch.jsx +++ b/frontend/src/components/UsecaseSearch.jsx @@ -349,7 +349,7 @@ const UsecaseSearch = (props) => { const [selectedAction, setSelectedAction] = React.useState({}); const [firstRequest, setFirstRequest] = React.useState(true); - const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true"); + const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; //const alert = useAlert() useEffect(() => { diff --git a/frontend/src/components/UserManagmentTab.jsx b/frontend/src/components/UserManagmentTab.jsx index 6293b3ce..41ab96f0 100644 --- a/frontend/src/components/UserManagmentTab.jsx +++ b/frontend/src/components/UserManagmentTab.jsx @@ -25,6 +25,7 @@ import { Skeleton, Switch, Box, + Chip, } from "@mui/material"; import { @@ -158,7 +159,7 @@ const UserManagmentTab = memo((props) => { toast("Failed to update user"); } else { //toast("Set the user field " + field + " to " + value); - toast("Successfully updated user field " + field); + toast.success(`Successfully updated user field '${field}'`) if (field !== "suborgs") { setSelectedUserModalOpen(false); @@ -203,7 +204,7 @@ const UserManagmentTab = memo((props) => { getUsers(); }, 1000); - toast("Invite sent! They will show up in the list when they have accepted the invite.") + toast.success("The user will show up in the list when finished.") } }) ) @@ -260,7 +261,6 @@ const UserManagmentTab = memo((props) => { event.target.value = [] } - console.log("event: ", event.target.value); setMatchingOrganizations(event.target.value); // Workaround for empty orgs if (event.target.value.length === 0) { @@ -336,6 +336,17 @@ const UserManagmentTab = memo((props) => { }) .then((responseJson) => { setUsers(responseJson); + + if (responseJson.success !== false) { + for (var i = 0; i < responseJson.length; i++) { + const data = responseJson[i]; + if (data?.login_type === "DELETED") { + //toast.info("Found lost/half-deleted users you can recover. Please contact support@shuffler.io to learn more.") + break + } + } + } + setShowLoader(false) }) .catch((error) => { @@ -364,13 +375,13 @@ const UserManagmentTab = memo((props) => { }) .then((responseJson) => { if (!responseJson.success && responseJson.reason !== undefined) { - toast("Failed to deactivate user: " + responseJson.reason); + toast.error("Failed to deactivate user: " + responseJson.reason); } else if (responseJson.success === false) { - toast( + toast.error( `Failed to deactivate user. Please contact ${supportEmail} if this persists.`, ); } else { - toast("Changed activation for user " + data.id); + toast.success("Changed activation for user " + data.id); } }) @@ -1529,6 +1540,7 @@ const UserManagmentTab = memo((props) => { { data.login_type === undefined || data?.login_type === null || data?.login_type?.length === 0 - ? "Normal" + ? + "Normal" + : + data?.login_type === "DELETED" ? + { + e.preventDefault() + e.stopPropagation() + + toast.warn("Re-adding the account to the org with 'user' role.") + inviteUser({ + "Username": data.username, + }) + }} + /> : data.login_type } style={{ display:'table-cell',verticalAlign: 'middle', padding: "8px", }} @@ -1647,6 +1682,7 @@ const UserManagmentTab = memo((props) => { ) : null} { setSelectedUserModalOpen(true); setSelectedUser(data); diff --git a/frontend/src/components/WelcomeForm2.jsx b/frontend/src/components/WelcomeForm2.jsx index 3e11cfc2..41d45210 100644 --- a/frontend/src/components/WelcomeForm2.jsx +++ b/frontend/src/components/WelcomeForm2.jsx @@ -161,7 +161,7 @@ const WelcomeForm = (props) => { const [clickdiff, setclickdiff] = useState(0); const [mouseHoverIndex, setMouseHoverIndex] = useState(-1) - const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true"); + const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; //const alert = useAlert(); let navigate = useNavigate(); diff --git a/frontend/src/components/WorkflowGrid.jsx b/frontend/src/components/WorkflowGrid.jsx index 6d3a61b8..751bf465 100644 --- a/frontend/src/components/WorkflowGrid.jsx +++ b/frontend/src/components/WorkflowGrid.jsx @@ -29,7 +29,7 @@ const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b 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") ? true : (process.env.IS_SSR === "true"); + 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([]); diff --git a/frontend/src/components/WorkflowTemplatePopup.jsx b/frontend/src/components/WorkflowTemplatePopup.jsx index 3f02f706..de9f08dc 100644 --- a/frontend/src/components/WorkflowTemplatePopup.jsx +++ b/frontend/src/components/WorkflowTemplatePopup.jsx @@ -47,7 +47,7 @@ const WorkflowTemplatePopup = (props) => { const [requestSent, setRequestSent] = React.useState(false) - const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true"); + const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; let navigate = useNavigate(); useEffect(() => { if (modalOpen !== true) { diff --git a/frontend/src/components/ssoTab.jsx b/frontend/src/components/ssoTab.jsx index d9339ef2..c361e1b1 100644 --- a/frontend/src/components/ssoTab.jsx +++ b/frontend/src/components/ssoTab.jsx @@ -15,6 +15,7 @@ import theme from "../theme.jsx"; import { toast } from "react-toastify"; import { Context } from "../context/ContextApi.jsx"; import { getTheme } from "../theme.jsx"; +import { red } from "../views/AngularWorkflow.jsx" const useStyles = makeStyles({ notchedOutline: { @@ -25,6 +26,14 @@ const useStyles = makeStyles({ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handleEditOrg})=>{ + // Check if user is admin + const isAdmin = userdata?.active_org?.role === "admin" || userdata?.support === true; + + // State for tracking user SSO connection status + const [users, setUsers] = React.useState([]); + const [userSSOConnected, setUserSSOConnected] = React.useState(false); + const [checkingSSOStatus, setCheckingSSOStatus] = React.useState(true); + const classes = useStyles(); const [show2faSetup, setShow2faSetup] = React.useState(false); const [autoPrivision, setAutoProvision] = React.useState(selectedOrganization?.sso_config?.auto_provision) @@ -97,6 +106,53 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle const { themeMode, supportEmail, brandColor } = useContext(Context); const theme = getTheme(themeMode, brandColor); + // Function to fetch users and check current user's SSO status + const checkUserSSOStatus = () => { + setCheckingSSOStatus(true); + fetch(globalUrl + "/api/v1/getusers", { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + }, + credentials: "include", + }) + .then((response) => { + if (response.status !== 200) { + return; + } + return response.json(); + }) + .then((responseJson) => { + if (responseJson && Array.isArray(responseJson)) { + setUsers(responseJson); + + // Find current user and check if they have SSO info for this org + const currentUser = responseJson.find(user => user.id === userdata?.id); + if (currentUser && currentUser.sso_infos && Array.isArray(currentUser.sso_infos)) { + const hasSSOForThisOrg = currentUser.sso_infos.some(ssoInfo => + ssoInfo.org_id === selectedOrganization?.id && ssoInfo.sub + ); + setUserSSOConnected(hasSSOForThisOrg); + } else { + setUserSSOConnected(false); + } + } + setCheckingSSOStatus(false); + }) + .catch((error) => { + console.log("Error fetching users:", error); + setCheckingSSOStatus(false); + }); + }; + + // Check SSO status on component mount and when organization changes + useEffect(() => { + if (userdata?.id && selectedOrganization?.id) { + checkUserSSOStatus(); + } + }, [userdata?.id, selectedOrganization?.id]); + useEffect(()=>{ if (openidClientSecret !== selectedOrganization?.sso_config?.client_secret) { @@ -149,7 +205,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle disabled={ userdata === undefined || userdata === null || - userdata.admin !== "true" + !isAdmin } onClick={() => handleEditOrg( @@ -256,7 +312,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle const url = `${globalUrl}/api/v1/orgs/${selectedOrganization?.id}/change`; const data = { org_id: selectedOrganization?.id, - sso_test: true, + sso: true, }; fetch(url, { @@ -309,6 +365,59 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle }); }; + const HandleDisconnectSSO = () => { + const url = `${globalUrl}/api/v1/disconnect_sso`; + const data = { + org_id: selectedOrganization?.id, + }; + + fetch(url, { + mode: "cors", + credentials: "include", + crossDomain: true, + method: "POST", + body: JSON.stringify(data), + withCredentials: true, + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }) + .then((response) => { + if (response.status !== 200) { + toast.error( + `Failed to disconnect SSO. Please try again later or contact ${supportEmail} if issue persists.`, + { duration: 3000 } + ); + return null; + } + return response.json(); + }) + .then((responseJson) => { + if (!responseJson) return; + + if (responseJson.success === true) { + toast.success( + "Successfully disconnected from SSO.", + { duration: 3000 } + ); + // Refresh the SSO status after disconnecting + checkUserSSOStatus(); + } else { + toast.error( + responseJson.reason || "Failed to disconnect SSO.", + { duration: 3000 } + ); + } + }) + .catch((error) => { + console.error("Error disconnecting SSO:", error); + toast.error( + "An error occurred while disconnecting SSO. Please try again.", + { duration: 3000 } + ); + }); + }; + return (
@@ -316,6 +425,81 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle SSO Configuration + + {/* SSO Connection section - moved to top */} +
+ + {checkingSSOStatus + ? "Checking your SSO connection status..." + : userSSOConnected + ? "Your account is connected with this org's SSO!" + : "Connect your account with this org's SSO!" + } + + 0 || + ssoCertificate?.length > 0 || + openidAuthorization?.length > 0 || + openidClientId?.length > 0 + ) + ? "SSO must be configured for this organization before you can connect." + : "" + } + > +
+ + + + + {userSSOConnected && ( + + + + + + )} +
+
+
+
{SSORequired ? "Required" : "Optional"} @@ -369,7 +554,6 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle sx={{marginBottom: 0.6, marginTop: 0.6}} name="onOffSwitch" color="primary" - title="Disable auto-provisioning of users in SSO" />
@@ -399,6 +583,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle sx={{marginBottom: 0.6, marginTop: 0.6}} name="onOffSwitch" color="primary" + disabled={!isAdmin} title="Disable auto-provisioning of users in SSO" />
@@ -428,58 +613,12 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle sx={{marginBottom: 0.6, marginTop: 0.6}} name="onOffSwitch" color="primary" + disabled={!isAdmin} title="Disable auto-provisioning of users in SSO" />
} - -
- - You can test your SSO configuration by clicking the button below. - Before testing, ensure you have set Open ID Connect or SAML SSO - credentials. - - 0 || - ssoCertificate?.length > 0 || - openidAuthorization?.length > 0 || - openidClientId?.length > 0 - ) - ? "Please ensure all SSO credentials are set before testing." - : "" - } - > - - - - -
OpenID connect @@ -514,6 +653,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle onChange={(e) => setShowOpenIdCred(e.target.checked)} name="showOpenIdCred" color="primary" + disabled={!isAdmin} />
@@ -536,6 +676,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle variant="outlined" placeholder="The OpenID client ID from the identity provider" value={showOpenIdCred ? openidClientId : openidClientId?.length > 0 ? "•".repeat(50) : ""} + disabled={!isAdmin} onChange={(e) => setOpenidClientId(e.target.value)} onFocus={(e) => setShowOpenIdCred(true)} onBlur={(e) => setShowOpenIdCred(false)} @@ -574,6 +715,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle variant="outlined" placeholder="The OpenID client secret - DONT use this if dealing with implicit auth / PKCE" value={showOpenIdCred ? openidClientSecret : openidClientSecret?.length > 0 ? "•".repeat(50) : ""} + disabled={!isAdmin} onChange={(e) => { setOpenidClientSecret(e.target.value); }} @@ -616,6 +758,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle rows={2} placeholder="The OpenID authorization URL (usually ends with /authorize)" value={showOpenIdCred ? openidAuthorization : openidAuthorization?.length > 0 ? "•".repeat(50) : ""} + disabled={!isAdmin} onChange={(e) => { setOpenidAuthorization(e.target.value) }} @@ -656,6 +799,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle rows={2} placeholder="The OpenID token URL (usually ends with /token)" value={showOpenIdCred ? openidToken : openidToken?.length > 0 ? "•".repeat(50) : ""} + disabled={!isAdmin} onChange={(e) => { setOpenidToken(e.target.value) }} @@ -682,8 +826,8 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle {/*isCloud ? null : */} SAML SSO (v1.1) - - IdP URL for Shuffle SAML/SSO: {`${globalUrl}/api/v1/login_sso`} + + Note: Support for SAML SSO was deprecated due to potential security issues. Please consider migrating to OpenID Connect for better compatibility and features.
setShowSamlCred(e.target.checked)} name="showSamlCred" color="primary" + disabled={!isAdmin} />
@@ -725,6 +870,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle variant="outlined" placeholder="The entrypoint URL from your provider" value={showSamlCred ? ssoEntrypoint : ssoEntrypoint?.length > 0 ? "•".repeat(50) : ""} + disabled={!isAdmin} onChange={(e) => { setSsoEntrypoint(e.target.value); }} @@ -765,6 +911,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle rows={2} placeholder="The X509 certificate to use" value={showSamlCred ? ssoCertificate : ssoCertificate?.length > 0 ? "•".repeat(50) : ""} + disabled={!isAdmin} onFocus={(e) => setShowSamlCred(true)} onBlur={(e) => setShowSamlCred(false)} onChange={(e) => { diff --git a/frontend/src/defaultCytoscapeStyle.jsx b/frontend/src/defaultCytoscapeStyle.jsx index e1eece63..777b0c38 100644 --- a/frontend/src/defaultCytoscapeStyle.jsx +++ b/frontend/src/defaultCytoscapeStyle.jsx @@ -1,4 +1,6 @@ -export default function defaultCytoscapeStyle(theme) { +import { red, } from "./views/AngularWorkflow.jsx" + +export default function defaultCytoscapeStyle(theme, apps) { return [ { selector: "node", @@ -575,7 +577,14 @@ export default function defaultCytoscapeStyle(theme) { height: "data(height)", padding: "0px", margin: "0px", - "background-color": "data(iconBackground)", + "background-color": function(element) { + const value = element.data() + if (value?.backgroundcolor === null || value?.backgroundcolor === undefined) { + return "#888888" + } + + return element.data("backgroundcolor") + }, "background-fill": "data(fillstyle)", "background-gradient-direction": "to-right", "background-gradient-stop-colors": "data(fillGradient)", @@ -605,6 +614,48 @@ export default function defaultCytoscapeStyle(theme) { "background-color": "#f85a3e", }, }, + { + selector: `node[large_image="/images/singul_green.png"]`, + css: { + "background-image": function(element) { + const nodeData = element.data() + if (apps === null || apps === undefined) { + return nodeData?.large_image + } + + const params = nodeData["parameters"] + if (params === null || params === undefined) { + return nodeData?.large_image + } + + var foundappname = "" + for (var key in params) { + const param = params[key] + if (param.name === "app_name") { + foundappname = param?.value.toLowerCase().replaceAll(" ", "_") + break + } + } + + if (foundappname == "") { + return nodeData?.large_image + } + + for (var key in apps) { + const app = apps[key] + const newappname = app.name.toLowerCase().replaceAll(" ", "_") + if (newappname == foundappname) { + if (app.large_image !== null && app.large_image !== undefined && app.large_image != "") { + element.data("large_image", app.large_image) + return app.large_image + } + } + } + + return nodeData?.large_image + }, + }, + }, { selector: `node[name="switch"]`, css: { diff --git a/frontend/src/views/AgentUI.jsx b/frontend/src/views/AgentUI.jsx index 9aef5284..17320406 100644 --- a/frontend/src/views/AgentUI.jsx +++ b/frontend/src/views/AgentUI.jsx @@ -24,6 +24,9 @@ import { TextField, Popover, Divider, + + AvatarGroup, + Avatar, } from '@mui/material' import { @@ -40,6 +43,7 @@ import { Refresh as RefreshIcon, Add as AddIcon, Warning as WarningIcon, + Pause as PauseIcon, } from '@mui/icons-material' import { @@ -68,6 +72,27 @@ const AgentUI = (props) => { const [appPickerAnchor, setAppPickerAnchor] = React.useState(null) const [chosenApps, setChosenApps] = useState([]) + const activateApp = (appId) => { + if (appId === undefined || appId === null || appId === "") { + return + } + + const url = `${globalUrl}/api/v1/apps/${appId}/activate` + fetch(url, { + method: "GET", + credentials: "include", + }) + .then((response) => { + return response.json() + }) + .then((responseJson) => { + if (responseJson.success !== false) { + getApps() + } else { + toast.warn("Failed to activate app: " + responseJson.reason) + } + }) + } useEffect(() => { if (newSelectedApp.objectID === undefined || newSelectedApp.objectID === null || newSelectedApp.objectID === "") { @@ -83,9 +108,76 @@ const AgentUI = (props) => { id: newSelectedApp.objectID, image: newSelectedApp.image_url, }])) + + var found = false + if (apps?.length > 0) { + for (var key in apps) { + const app = apps[key] + if (app?.id !== newSelectedApp?.objectID) { + continue + } + + found = true + break + } + } + + if (!found) { + activateApp(newSelectedApp.objectID) + } } }, [newSelectedApp]) + useEffect(() => { + if (chosenApps?.length > 0) { + return + } + + if (apps?.length === 0) { + console.log("No apps loaded yet") + return + } + + if (data === undefined || data === null) { + return + } + + if (data?.allowed_actions === undefined || data?.allowed_actions === null) { + return + } + + console.log("HERE!") + console.log("Data, apps: ", data, apps) + + var newChosenApps = [] + for (var key in data?.allowed_actions) { + const allowedAction = data?.allowed_actions[key] + if (!allowedAction?.startsWith("app:")) { + newChosenApps.push({ + "name": allowedAction, + "large_image": "", + }) + + continue + } + + const appId = allowedAction.split(":")[1] + for (var appKey in apps) { + const app = apps[appKey] + if (app.id !== appId) { + continue + } + + newChosenApps.push(app) + break + } + } + + if (newChosenApps.length > 0) { + setChosenApps(newChosenApps) + } + }, [data]) + const {themeMode} = useContext(Context) const theme = getTheme(themeMode) const navigate = useNavigate(); @@ -95,12 +187,13 @@ const AgentUI = (props) => { } const agentWrapperStyle = { - width: 1000, - height: 1000, + width: "100%", + maxHeight: "100vh", margin: "auto", - paddingTop: 100, - paddingBottom: 1000, backgroundColor: theme.palette.backgroundColor, + + paddingBottom: showAgentStarter ? 0 : 1500, + } if (data.input === undefined || data.input === null) { @@ -412,9 +505,7 @@ const AgentUI = (props) => { } else { for (var key in questionAnswers) { const answer = questionAnswers[key] - if (isContinuation === true) { - newArgument["question_"+(answer.index)] = answer.value - } + newArgument["question_"+(answer.index)] = answer.value } } @@ -678,8 +769,8 @@ const AgentUI = (props) => { - const rerunButton = - + const rerunButton = item?.type === "processing" ? null : + { + var itemLabel = item?.label?.replaceAll("_", " ") || "" + if (item?.details?.reason !== undefined && item?.details?.reason !== null && item?.details?.reason !== "") { + itemLabel = item?.details?.reason + } + return (
{ paddingTop: defaultTopPadding, paddingBottom: defaultTopPadding, }}> - {item.label} + {itemLabel}
- +
{ item?.type === "decision" ?
{rerunButton} - + { : rerunButton } - - - - {open ? - - : - - } - - - + {item?.type === "processing" ? null : + + + + {open ? + + : + + } + + + + + }
@@ -976,13 +1087,17 @@ const AgentUI = (props) => {
: null} @@ -1078,6 +1193,7 @@ const AgentUI = (props) => { } var finishDecisionId = "" + var finishAnswer = "" var sortedTimelineItems = [] for (var key in agent_data?.decisions) { const item = agent_data.decisions[key] @@ -1105,17 +1221,71 @@ const AgentUI = (props) => { if (item?.details?.action === "finish" || item.action === "finish" || item.category == "finish" || item?.details?.action == "finalise") { finishDecisionId = item?.run_details?.id - } + finishAnswer = item?.reason || "" + if (item?.fields !== undefined && item?.fields !== null) { + // Just choosing the first one + for (var fieldKey in item?.fields) { + const field = item?.fields[fieldKey] + finishAnswer = field?.value || finishAnswer + break + } + } + } } - timelineItems.sort((a, b) => { - if (a.start_time < b.start_time) { - return 1; + // Fill blank space between items in timeline by adding "empty" decisions: + // 1. Sort + // 2. Fill blank space + // 3. Sort again + timelineItems.sort((a, b) => a.start_time - b.start_time) + for (var i = 0; i < timelineItems.length - 1; i++) { + if (timelineItems?.length <= 2) { + break } - return 0; - }) + if (i === timelineItems.length - 1) { + break + } + + const currentItem = timelineItems[i] + if (currentItem?.type === "processing") { + continue + } + + const nextItem = timelineItems[i + 1] + if (nextItem?.type === "processing") { + continue + } + + if (nextItem.start_time - currentItem.end_time > 1) { + timelineItems?.push({ + label: "", + type: "processing", + category: "processing", + status: "FINISHED", + start_time: currentItem.end_time, + end_time: nextItem.start_time, + }) + } + } + timelineItems.sort((a, b) => a.start_time - b.start_time) + + const handleKeyDownCont = (e) => { + if (finishDecisionId === "") { + return + } + + const isCmdEnter = e.metaKey && e.key === "Enter"; // macOS + const isCtrlEnter = e.ctrlKey && e.key === "Enter"; // Windows/Linux + if (isCmdEnter || isCtrlEnter) { + e.preventDefault() + + submitQuestions(finishDecisionId, { + "continue": continuationText, + }, true) + } + } return (
@@ -1151,6 +1321,7 @@ const AgentUI = (props) => { { e.preventDefault(); @@ -1161,15 +1332,35 @@ const AgentUI = (props) => { }, true) }} > -
+ {finishAnswer !== "" ? +
+ + {finishAnswer} + +
+ : null} + +
{ console.log("Value: ", e.target.value) //setActionInput(e.target.value) @@ -1195,7 +1386,7 @@ const AgentUI = (props) => { }} /> - Any failed tasks will be set to ignored (TBD). Learn more + Any failed tasks will be set to ignored. Learn more
@@ -1242,7 +1433,7 @@ const AgentUI = (props) => { // 1. Run the execution. Can this be a single-action run? // 2. Get the execution ID and node ID from the response. const uuid = uuidv4() - var parsedAction = "list_tickets,API" // Default action for now + var parsedAction = "API" // Default action for now if (chosenApps.length > 0) { parsedAction = "" for (var appKey in chosenApps) { @@ -1319,155 +1510,253 @@ const AgentUI = (props) => { cursor: "pointer", } + + const typeButtonGroup = + + + + + + return ( -
- - - {showAgentStarter ? - { - e.preventDefault(); - submitInput(actionInput); - }} - > - - -
- - - Shuffle AI Agents - +
+
+
+
+
{ - setActionInput(e.target.value) - }} - InputProps={{ - endAdornment: ( - agentRequestLoading ? - - : - - - - - - ), - }} + id="copy_element_shuffle" + style={{ display: "none" }} /> -
-
- } label="Select Apps" - style={chipStyle} - onClick={() => { - setAppPickerAnchor(document.getElementById("add_app_chip")) + {showAgentStarter ? + { + e.preventDefault(); + submitInput(actionInput); + }} + > + {/* + + */} + +
+ + + What do you want to do? + + { + setActionInput(e.target.value) + }} + InputProps={{ + endAdornment: ( + agentRequestLoading ? + + : + + + + + + ), }} /> - { - setAppPickerAnchor(null) - }} - anchorOrigin={{ - vertical: 'bottom', - horizontal: 'left', - }} - > - +
+ } label="Select Apps / MCPs" + style={chipStyle} + onClick={() => { + setAppPickerAnchor(document.getElementById("add_app_chip")) + }} /> - + { + setAppPickerAnchor(null) + }} + anchorOrigin={{ + vertical: 'bottom', + horizontal: 'left', + }} + > + + +
+ + {chosenApps.map((app, index) => { + const chosenName = (app?.name?.charAt(0).toUpperCase() + app?.name?.slice(1))?.replaceAll("_", " ").replaceAll("-", " ") + const chosenImagePath = app?.image + const chosenImage = + + return ( + { + const newChosenApps = chosenApps.filter((a, i) => i !== index) + setChosenApps(newChosenApps) + }} + /> + ) + })} +
+ +
+ {execution === undefined || execution === null || execution?.execution_id === undefined || execution?.execution_id === null ? null : typeButtonGroup} +
+ + {/* +
+ */} + + : +
+
+
+ {typeButtonGroup} + + + + + + + + + {execution?.status === "EXECUTING" ? + + + + + + : null} + +
+ +
+ + {chosenApps?.map((app, index) => { + return( + + { + window.open(`/apps/${app.id}`, '_blank', 'noopener,noreferrer'); + }} + style={{ + cursor: "pointer", + width: 30, + height: 30, + }} + /> + + ) + })} + +
+
+ + {buttonState === "timeline" ? + + : + null + }
- - {chosenApps.map((app, index) => { - const chosenName = (app?.name?.charAt(0).toUpperCase() + app?.name?.slice(1))?.replaceAll("_", " ").replaceAll("-", " ") - const chosenImagePath = app?.image - const chosenImage = - - return ( - { - const newChosenApps = chosenApps.filter((a, i) => i !== index) - setChosenApps(newChosenApps) - }} - /> - ) - })} -
-
- : -
- - - - - - - - - - - - {buttonState === "timeline" ? - - : - null }
- } +
) } diff --git a/frontend/src/views/AngularWorkflow.jsx b/frontend/src/views/AngularWorkflow.jsx index d6e33cef..fcf3e270 100755 --- a/frontend/src/views/AngularWorkflow.jsx +++ b/frontend/src/views/AngularWorkflow.jsx @@ -463,9 +463,9 @@ const AngularWorkflow = (defaultprops) => { var to_be_copied = ""; const [firstrequest, setFirstrequest] = React.useState(true); - const cystyle = useMemo(() => defaultCytoscapeStyle(theme), [themeMode]); - // const cystyle = useMemo(() => defaultCytoscapeStyle, [themeMode]); + const [apps, setApps] = React.useState([]); + const cystyle = useMemo(() => defaultCytoscapeStyle(theme, apps), [themeMode, apps]); const [cy, setCy] = React.useState(); const useStyles = makeStyles({ @@ -593,7 +593,6 @@ const AngularWorkflow = (defaultprops) => { const [showDropdown, setShowDropdown] = React.useState(false); const [triggerActionList, setTriggerActionList] = React.useState([]); - const [apps, setApps] = React.useState([]); const [filteredApps, setFilteredApps] = React.useState([]); const [prioritizedApps, setPrioritizedApps] = React.useState([]) @@ -2560,7 +2559,7 @@ const AngularWorkflow = (defaultprops) => { // Center and zoom to the node with smooth animation if requested if (config.center) { - cy.animate({ + cy.stop().animate({ center: { eles: cyNode }, @@ -3324,7 +3323,7 @@ const AngularWorkflow = (defaultprops) => { } const animationDuration = 150 - foundnode.animate( + foundnode.stop().animate( { style: parsedStyle, }, @@ -3445,6 +3444,18 @@ const AngularWorkflow = (defaultprops) => { return } + for (var i = 0; i < workflowExecutions.length; i++) { + const exec = workflowExecutions[i] + if (exec.execution_id === curAction.source_execution) { + if (exec?.execution_argument === undefined || exec?.execution_argument === null || exec?.execution_argument.length === 0) { + break + } + + setExecutionText(exec.execution_argument) + break + } + } + setExecutionRunning(true) setExecutionRequestStarted(true) var headers = { @@ -3830,7 +3841,7 @@ const AngularWorkflow = (defaultprops) => { } else { console.log("Closing auth modal? FAIL") - toast("Failed to find new authentication. See details in Oauth2 popup window where auth was attempted."); + toast.error("Failed to find new authentication. See details in Oauth2 popup window where auth was attempted."); shouldClose = false } } else { @@ -4184,7 +4195,7 @@ const AngularWorkflow = (defaultprops) => { } const animationDuration = 150 - node.animate( + node.stop().animate( { style: parsedStyle, }, @@ -4201,7 +4212,7 @@ const AngularWorkflow = (defaultprops) => { "border-color": "#81c784", } - node.animate( + node.stop().animate( { style: parsedStyle, }, @@ -4250,7 +4261,7 @@ const AngularWorkflow = (defaultprops) => { } const animationDuration = 150 - node.animate( + node.stop().animate( { style: parsedStyle, }, @@ -4267,7 +4278,7 @@ const AngularWorkflow = (defaultprops) => { "border-color": "#81c784", } - node.animate( + node.stop().animate( { style: parsedStyle, }, @@ -4298,7 +4309,7 @@ const AngularWorkflow = (defaultprops) => { } const animationDuration = 150 - node.animate( + node.stop().animate( { style: parsedStyle, }, @@ -4314,7 +4325,7 @@ const AngularWorkflow = (defaultprops) => { "line-gradient-stop-colors": ["grey", "grey"], } - node.animate( + node.stop().animate( { style: parsedStyle, }, @@ -4346,7 +4357,7 @@ const AngularWorkflow = (defaultprops) => { } const animationDuration = 150 - node.animate( + node.stop().animate( { style: parsedStyle, }, @@ -4363,7 +4374,7 @@ const AngularWorkflow = (defaultprops) => { "border-color": color, } - node.animate( + node.stop().animate( { style: parsedStyle, }, @@ -4394,7 +4405,7 @@ const AngularWorkflow = (defaultprops) => { } const animationDuration = 150 - node.animate( + node.stop().animate( { style: parsedStyle, }, @@ -4450,7 +4461,7 @@ const AngularWorkflow = (defaultprops) => { } const animationDuration = 150 - node.animate( + node.stop().animate( { style: parsedStyle, }, @@ -4484,7 +4495,7 @@ const AngularWorkflow = (defaultprops) => { } const animationDuration = 150 - node.animate( + node.stop().animate( { style: parsedStyle, }, @@ -8627,7 +8638,7 @@ const AngularWorkflow = (defaultprops) => { }; } - event.target.animate( + event.target.stop().animate( { style: parsedStyle, }, @@ -9423,7 +9434,7 @@ const AngularWorkflow = (defaultprops) => { } if (event.target !== undefined && event.target !== null) { - event.target.animate( + event.target.stop().animate( { style: parsedStyle, }, @@ -9741,7 +9752,7 @@ const AngularWorkflow = (defaultprops) => { } const animationDuration = 150 - foundnode.animate( + foundnode.stop().animate( { style: parsedStyle, }, @@ -9910,7 +9921,7 @@ const AngularWorkflow = (defaultprops) => { } const animationDuration = 150 - foundnode.animate( + foundnode.stop().animate( { style: parsedStyle, }, @@ -12487,7 +12498,7 @@ const AngularWorkflow = (defaultprops) => { //} safeRefine(event.currentTarget.value) }} - limit={5} + limit={15} /> {/*isSearchStalled ? 'My search is stalled' : ''*/} @@ -14360,8 +14371,7 @@ const AngularWorkflow = (defaultprops) => { padding: 30, pointerEvents: "auto", color: theme.palette.text.primary, - minWidth: isMobile ? "90%" : 650, - border: theme.palette.defaultBorder, + minWidth: isMobile ? "90%" : 750, borderRadius: theme.palette.borderRadius, backgroundColor: "black", @@ -14474,18 +14484,24 @@ const AngularWorkflow = (defaultprops) => { { return collapseField(jsonField) }} @@ -14499,10 +14515,11 @@ const AngularWorkflow = (defaultprops) => { } return ( - { setExecutionText(data) - executeWorkflow(data, workflow.start, lastSaved); + //executeWorkflow(data, workflow.start, lastSaved); setExecutionArgumentModalOpen(false) }} @@ -14759,6 +14776,7 @@ const AngularWorkflow = (defaultprops) => { color: theme.palette.textColor, backgroundColor: 'inherit', zIndex: 10000, + fontSize: 12, }} > PS: Conditions can't be used for loops [ .# ]. Use the filters list action.{" "} @@ -15110,7 +15128,9 @@ const AngularWorkflow = (defaultprops) => { key={condition.condition.id} square style={paperVariableStyle} - onClick={() => { }} + onClick={() => { + setLastSaved(false) + }} >
{ return } + setLastSaved(false) setSourceValue({ name: "source", value: "", @@ -15384,17 +15405,15 @@ const AngularWorkflow = (defaultprops) => { // Change Direction of the branch target/source const foundBranch = cy.getElementById(selectedEdge.id) if (foundBranch !== undefined && foundBranch !== null) { - console.log("BRANCH: ", foundBranch) const source = foundBranch.data("source") const target = foundBranch.data("target") var branchdata = JSON.parse(JSON.stringify(foundBranch.data())) - console.log("BEFORE: ", branchdata) - console.log("Start node", workflow.start) const startNode = workflow.start if (source === startNode) { toast("Can't point to Start Node") } else { + setLastSaved(false) const newid = uuidv4() branchdata.source = target branchdata.target = source @@ -19298,7 +19317,7 @@ const AngularWorkflow = (defaultprops) => { }, 2000); } - toast.info("Successfully changed active organisation - refreshing!"); + toast.success("Successfully changed active organisation - refreshing!"); } else { if (responseJson.reason !== undefined && responseJson.reason !== null && responseJson.reason.length > 0) { toast(responseJson.reason); @@ -20649,7 +20668,7 @@ const AngularWorkflow = (defaultprops) => { } const buttonHeights = 45 - const boxSize = buttonHeights + const boxSize = buttonHeights+2 const executionButton = executionRunning ? ( @@ -20737,20 +20756,17 @@ const AngularWorkflow = (defaultprops) => { > { return ( -
+
{ onSelect={(select) => { HandleJsonCopy(validate.result, select, "exec"); }} - name={false} + name={"$exec"} />
) @@ -21787,7 +21803,26 @@ const AngularWorkflow = (defaultprops) => { ) } - if (execution.execution_source === "authgroups") { + if (execution.execution_source?.startsWith("datastore")) { + const iconMargin = 7 + return ( +
+ +
+ ) + } else if (execution.execution_source === "authgroups") { const iconMargin = 7 return (
{
) : null} + {executionData.execution_source !== undefined && executionData.execution_source !== null && executionData.execution_source.length > 0 && @@ -22954,8 +22990,17 @@ const AngularWorkflow = (defaultprops) => { - - {executionData.execution_source === "authgroups" || (executionData.authgroup !== undefined && executionData.authgroup !== null && executionData.authgroup.length > 0) ? + {executionData?.execution_source?.startsWith("datastore") ? + 2 ? "&category="+executionData.execution_source.split("|")[1]+"&key="+executionData.execution_source.split("|")[2] : ""}`} + target="_blank" + style={{ textDecoration: "none", color: theme.palette.linkColor }} + > + Datastore Automation + + : + executionData.execution_source === "authgroups" || (executionData.authgroup !== undefined && executionData.authgroup !== null && executionData.authgroup.length > 0) ? { HandleJsonCopy(showResult, select, data.action.label); console.log("SELECTED!: ", select); }} - name={"Results for " + data.action.label} + name={`$${data?.action?.label?.toLowerCase()}`} /> diff --git a/frontend/src/views/AppCreator.jsx b/frontend/src/views/AppCreator.jsx index d6317b3a..7a954ef2 100755 --- a/frontend/src/views/AppCreator.jsx +++ b/frontend/src/views/AppCreator.jsx @@ -563,7 +563,7 @@ const AppCreator = (defaultprops) => { }; - const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true"); + const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; useEffect(() => { if (window.location.pathname.includes("apps/edit")) { diff --git a/frontend/src/views/AppExplorer.jsx b/frontend/src/views/AppExplorer.jsx index 1e23d70d..54537f7d 100644 --- a/frontend/src/views/AppExplorer.jsx +++ b/frontend/src/views/AppExplorer.jsx @@ -247,7 +247,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)"; read_time: 1, }) - const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true"); + const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; // FIXME: This is used, as useEffect() creates an issue with apps not loading at all diff --git a/frontend/src/views/Docs.jsx b/frontend/src/views/Docs.jsx index f5b3bb82..ef933d16 100755 --- a/frontend/src/views/Docs.jsx +++ b/frontend/src/views/Docs.jsx @@ -62,7 +62,6 @@ const dividerColor = "rgb(225, 228, 232)"; const hrefStyle = { color: "rgba(255, 255, 255, 0.8)", textDecoration: "none", - marginRight: window.location.pathname.includes("/articles/") ? "0.8em" : undefined, }; @@ -387,6 +386,7 @@ export const CodeHandler = (props) => { minWidth: "50%", maxWidth: "100%", backgroundColor: theme.palette.inputColor, + whiteSpace: "pre-wrap", overflowY: "auto", // Have it inline borderRadius: theme.palette?.borderRadius, @@ -1020,7 +1020,7 @@ const Docs = (defaultprops) => { } const fetchDocList = (resetCache = false) => { - var url = `${globalUrl}/api/v1/docs` + var url = `${globalUrl}/api/v1/docs?resetCache=${resetCache}` if (location.pathname.includes("/legal")) { url = `${globalUrl}/api/v1/docs?folder=legal&resetCache=${resetCache}` } else if (location.pathname.includes("/articles")) { @@ -1047,16 +1047,16 @@ const Docs = (defaultprops) => { .catch((error) => { }); }; - const fetchDoc = (docId) => { + const fetchDoc = (docId, resetCache = false) => { if (docId === undefined) { return } - var url = `${globalUrl}/api/v1/docs/${docId}` + var url = `${globalUrl}/api/v1/docs/${docId}?resetCache=${resetCache}` if (location.pathname.includes("/legal")) { - url = `${globalUrl}/api/v1/docs/${docId}?folder=legal` + url = `${globalUrl}/api/v1/docs/${docId}?folder=legal&resetCache=${resetCache}` } else if (location.pathname.includes("/articles")) { - url = `${globalUrl}/api/v1/docs/${docId}?folder=articles` + url = `${globalUrl}/api/v1/docs/${docId}?folder=articles&resetCache=${resetCache}` } fetch(url, { @@ -1082,7 +1082,7 @@ const Docs = (defaultprops) => { autoClose: 2000, }); setTimeout(() => { - navigate(`/articles/2.0_release`) + navigate(`/articles`) }, 2000) return } @@ -1130,7 +1130,8 @@ const Docs = (defaultprops) => { const handleResetCache = () => { fetchDocList(true); - toast("Cache has been reset"); + fetchDoc(props.match.params.key, true); + toast("Cache and list will be reset in a few seconds"); } if (firstrequest) { @@ -1344,6 +1345,21 @@ const Docs = (defaultprops) => { tr: TableRowRenderer, th: TableHeaderCellRenderer, td: TableCellRenderer, + ul: ({ children }) => ( + + {children} + + ), + ol: ({ children }) => ( + + {children} + + ), + li: ({ children }) => ( + + {children} + + ), } @@ -1355,7 +1371,6 @@ const Docs = (defaultprops) => { const activeListItemStyle = { backgroundColor: "rgba(248, 106, 62, 0.08)", // Slight orange tint - marginRight: window.location.pathname.includes("/articles/") ? "0.8em" : undefined, borderLeft: "3px solid #f86a3e", paddingLeft: "13px", // Compensate for the border }; @@ -1415,6 +1430,7 @@ const Docs = (defaultprops) => { { }
- {userdata?.support && isArticlePage && ( - + + )} {tocLines.length > 0 ? ( diff --git a/frontend/src/views/NewDashboard.jsx b/frontend/src/views/NewDashboard.jsx index bb2add48..f4709a84 100644 --- a/frontend/src/views/NewDashboard.jsx +++ b/frontend/src/views/NewDashboard.jsx @@ -111,8 +111,8 @@ const NewDashboard = (props) => { const STATIC_TIME_PERCENT = 'TBD' const STATIC_MONEY_PERCENT = 'TBD' const kpis = [ - { value: timeFmt.display, title: timeFmt.title, label: 'Time saved', icon: , percentage: STATIC_TIME_PERCENT, color: '#5cc879', disabled: true}, - { value: formatCurrencyCompact(totals.moneySavedDollars), label: 'Money saved', icon: , percentage: STATIC_MONEY_PERCENT, color: '#5cc879', disabled: true, }, + //{ value: timeFmt.display, title: timeFmt.title, label: 'Time saved', icon: , percentage: STATIC_TIME_PERCENT, color: '#5cc879', disabled: true}, + //{ value: formatCurrencyCompact(totals.moneySavedDollars), label: 'Money saved', icon: , percentage: STATIC_MONEY_PERCENT, color: '#5cc879', disabled: true, }, { value: String(unreadCount), label: 'Total errors', icon: , percentage: "", color: '#f87171' }, { value: String(readCount), label: 'Errors resolved', icon: , percentage: "", color: '#5cc879' }, ]; @@ -133,12 +133,12 @@ const NewDashboard = (props) => { useEffect(() => { const anyLoading = - loadingSfw || - loadingRot || + //loadingSfw || + //loadingRot || loadingNoti || - loadingSelectedOrgStats || + loadingSelectedOrgStats //|| !selectedOrganization || - !selectedOrgForStats; + //!selectedOrgForStats; setShowOverlay(anyLoading); }, [ loadingSfw, @@ -399,6 +399,7 @@ const NewDashboard = (props) => { headerSubtitle="Complete these steps to start seeing insights." /> )} + {showOverlay && (
@@ -407,6 +408,7 @@ const NewDashboard = (props) => {
)} + {/* Header / Greeting */} {`${getGreeting()}, ${displayName ?? 'User'}!`} diff --git a/frontend/src/views/UpdateAuthentication.jsx b/frontend/src/views/UpdateAuthentication.jsx index b55d844b..9f4702ea 100644 --- a/frontend/src/views/UpdateAuthentication.jsx +++ b/frontend/src/views/UpdateAuthentication.jsx @@ -25,7 +25,7 @@ const SetAuthentication = (props) => { const [loadFail, setLoadFail] = useState(""); const [appAuthentication, setAppAuthentication] = React.useState([]); - const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true"); + const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; //const alert = useAlert(); const parseIncomingOpenapiData = (data) => { diff --git a/frontend/src/views/Welcome.jsx b/frontend/src/views/Welcome.jsx index cb5c24c5..169c58b1 100644 --- a/frontend/src/views/Welcome.jsx +++ b/frontend/src/views/Welcome.jsx @@ -47,7 +47,7 @@ const Welcome = (props) => { } }, [activeStep]) - const isCloud = (window.location.host === "localhost:3002" || window.location.host === "shuffler.io") ? true : (process.env.IS_SSR === "true"); + const isCloud = window.location.host === "localhost:3002" || window.location.host === "shuffler.io"; const [steps, setSteps] = useState([ "Help us get to know you", "Find your Apps", diff --git a/frontend/src/views/Workflows2.jsx b/frontend/src/views/Workflows2.jsx index f94b38b5..1cd1cfd3 100644 --- a/frontend/src/views/Workflows2.jsx +++ b/frontend/src/views/Workflows2.jsx @@ -343,7 +343,7 @@ export const GetIconInfo = (action) => { "protect", ], }, - { key: "intel", values: ["intel", "feed", "threat intel", "threat intelligence", "ti", "t.i.", "t.i", "ti.", "rule", "technique", "tactic", "techniques", "tactics", "ioc", "indicator", "hash", "ip", "url", "domain", ] }, + { key: "intel", values: ["intel", "feed", "threat intel", "threat intelligence", "ti", "t.i.", "t.i", "ti.", "rule", "technique", "tactic", "techniques", "tactics", "ioc", "indicator", "hash", "ip", "url", "domain", "sha", "md5", ] }, ]; var selectedKey = "" @@ -3346,7 +3346,7 @@ const Workflows2 = (props) => { } style={{ textDecoration: "none", - color: "inherit", + color: theme.palette.text.primary, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", @@ -5149,7 +5149,7 @@ const Workflows2 = (props) => { {backgroundWorkflows.length > 0 &&