Merge pull request #1911 from yashsinghcodes/nightly

cloud frontend sync
This commit is contained in:
Yash Singh
2025-12-19 18:16:54 +05:30
committed by GitHub
41 changed files with 2189 additions and 668 deletions
+1 -9
View File
@@ -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();
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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) {
+1 -1
View File
@@ -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("");
+2 -2
View File
@@ -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',
+185 -58
View File
@@ -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": <SmartToyIcon />,
"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 (
<Tooltip title={
<Typography style={{margin: 10, }}>
@@ -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 (
<Typography
variant="body2"
style={{
maxWidth: 180,
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
cursor: "default",
border: props.row.key === highlightedKey ? `2px solid ${theme.palette.primary.main}` : "none",
}}
>
{props.row.key}
</Typography>
)
}
},
{
width: 540,
@@ -1919,7 +1963,7 @@ const CacheView = memo((props) => {
},
];
var previousgroup = ""
const isAutomating = categoryAutomations?.find((automation) => automation.enabled) !== undefined
return (
<div style={{
@@ -2072,56 +2116,53 @@ const CacheView = memo((props) => {
</Button>
</ButtonGroup>
<ButtonGroup style={{marginTop: 0, }}>
{datastoreCategories !== undefined &&
<ButtonGroup style={{ position: "absolute", top: -6, }}>
{datastoreCategories !== undefined &&
datastoreCategories !== null &&
datastoreCategories.length > 1 ? (
<FormControl style={{ minWidth: 150, maxWidth: 150, marginTop: 8, }}>
<InputLabel id="category-choice" style={{
color: "rgba(255, 255, 255, 0.65)",
}}>
Category
</InputLabel>
<Select
<FormControl style={{ minWidth: 250, maxWidth: 250, marginTop: 8, }}>
<Autocomplete
labelId="category-choice"
style={{
minWidth: 150,
maxWidth: 150,
height: 35,
borderRadius: "5px 0px 0px 5px",
minWidth: 250,
maxWidth: 250,
}}
ListboxProps={{
style: {
maxHeight: "70vh",
border: "1px solid rgba(255,255,255,0.3)",
}
}}
value={selectedCategory}
onChange={(event) => {
setCategoryConfig(undefined)
setCategoryAutomations(defaultAutomation)
//if (selectAllChecked || listCache.length > 0) {
if (selectAllChecked || selectedFiles.length > 0) {
setUpdateToThisCategory(event.target.value)
return
}
setSelectedCategory(event.target.value)
if (event.target.value === "all" || event.target.value === "default") {
listOrgCache(orgId, "", 0, pageSize, page)
} else {
listOrgCache(orgId, event.target.value, 0, pageSize, page)
}
// Add it to the url as a query
if (window.location.search.includes("category=")) {
const newurl = window.location.href.replace(/category=[^&]+/, `category=${event.target.value}`)
window.history.pushState({ path: newurl }, "", newurl)
} else {
window.history.pushState({ path: window.location.href }, "", `${window.location.href}&category=${event.target.value}`)
}
options={datastoreCategories}
getOptionLabel={(data) => {
const fixedname = data?.charAt(0)?.toUpperCase() + data?.slice(1)?.replaceAll("_", " ")
return fixedname
}}
>
{datastoreCategories.map((data, index) => {
// Should find the icon for things
// Fix uppercase at start of words
groupBy={(data) => {
if (!data.includes("_")) {
return undefined
}
const firstword = data.split("_")[0]
if (datastoreCategoryGroups.includes(firstword)) {
return firstword.charAt(0).toUpperCase() + firstword.slice(1)
}
return undefined
}}
renderInput={(params) => {
return (
<TextField
{...params}
label="Select Category"
variant="outlined"
size="small"
/>
)
}}
renderOption={(props, data, state) => {
const fixedname = data?.charAt(0)?.toUpperCase() + data?.slice(1)?.replaceAll("_", " ")
const iconDetails = GetIconInfo({
"app_name": fixedname,
@@ -2130,13 +2171,40 @@ const CacheView = memo((props) => {
return (
<MenuItem
key={index}
key={data}
value={data}
style={{
color: theme.palette.textFieldStyle.color,
display: "flex",
borderBottom: theme.palette.defaultBorder,
}}
onClick={(e) => {
e.stopPropagation()
e.preventDefault()
setCategoryConfig(undefined)
setCategoryAutomations(defaultAutomation)
if (selectAllChecked || selectedFiles.length > 0) {
setUpdateToThisCategory(data)
return
}
setSelectedCategory(data)
if (data === "all" || data === "default") {
listOrgCache(orgId, "", 0, pageSize, page)
} else {
listOrgCache(orgId, data, 0, pageSize, page)
}
// Add it to the url as a query
if (window.location.search.includes("category=")) {
const newurl = window.location.href.replace(/category=[^&]+/, `category=${data}`)
window.history.pushState({ path: newurl }, "", newurl)
} else {
window.history.pushState({ path: window.location.href }, "", `${window.location.href}&category=${data}`)
}
}}
>
<Typography style={{display: "flex", marginTop: 5, }}>
<div style={{marginRight: 10, }}>
@@ -2148,9 +2216,69 @@ const CacheView = memo((props) => {
{fixedname}
</Typography>
</MenuItem>
)
}}
/>
{/*
{datastoreCategories.map((data, index) => {
var addition = ""
if (data.includes("_")) {
const firstword = data.split("_")[0]
if (firstword !== previousgroup) {
if (datastoreCategoryGroups.includes(firstword)) {
previousgroup = firstword
addition =
<ListSubheader>
{firstword.charAt(0).toUpperCase() + firstword.slice(1)}
</ListSubheader>
} else {
if (previousgroup !== "") {
addition = <div style={{height: 8, backgroundColor: "rgba(0,0,0,0.8)",}} />
previousgroup = ""
}
}
}
} else {
if (previousgroup !== "") {
addition = <div style={{height: 8, backgroundColor: "rgba(0,0,0,0.8)",}} />
previousgroup = ""
}
}
// Should find the icon for things
// Fix uppercase at start of words
const fixedname = data?.charAt(0)?.toUpperCase() + data?.slice(1)?.replaceAll("_", " ")
const iconDetails = GetIconInfo({
"app_name": fixedname,
"name": fixedname,
})
return (
<MenuItem
key={data}
value={data}
style={{
color: theme.palette.textFieldStyle.color,
display: "flex",
borderBottom: theme.palette.defaultBorder,
}}
>
{addition}
<Typography style={{display: "flex", marginTop: 5, }}>
<div style={{marginRight: 10, }}>
{iconDetails?.originalIcon && (
iconDetails?.originalIcon
)}
</div>
{fixedname}
</Typography>
</MenuItem>
);
})}
</Select>
*/}
</FormControl>
) : null}
@@ -2159,7 +2287,7 @@ const CacheView = memo((props) => {
<Tooltip title={"Close"} style={{}} aria-label={""}>
<Button
style={{
height: 35,
height: 40,
borderRadius: 4,
textTransform: 'none',
fontSize: 16,
@@ -2178,14 +2306,13 @@ const CacheView = memo((props) => {
</Button>
</Tooltip>
:
<Tooltip title={"Add or find category"} style={{}} aria-label={""}>
<Tooltip title={"Add new category object"} style={{}} aria-label={""}>
<Button
style={{
whiteSpace: "nowrap",
width: datastoreCategories !== undefined && datastoreCategories !== null && datastoreCategories.length > 1 ? 50 : 169,
height: 35,
height: 40,
textTransform: 'none',
fontSize: 16,
borderRadius: "0px 5px 5px 0px",
}}
@@ -2216,14 +2343,14 @@ const CacheView = memo((props) => {
}}
style={{
height: 35,
height: 40,
width: 200,
marginTop: 0,
}}
InputProps={{
style: {
color: theme.palette.textFieldStyle.color,
height: 35,
height: 40,
fontSize: 16,
borderRadius: 4,
paddingTop: 0,
@@ -2408,7 +2535,7 @@ const CacheView = memo((props) => {
Subscribing
</Typography>
<Typography variant="body2" style={{ color: theme.palette.text.secondary, marginBottom: 20 }}>
Enabling this feature will allow other organizations to subscribe to this category. This is NOT fully available yet.
Enabling subscriptions allows other organizations to subscribe to this category and receive updates when keys are added or modified. Makes the category searchable.
</Typography>
</div>
</div>
+106 -151
View File
@@ -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 (data.success === undefined) {
newmessages = newmessages.filter((msg) => msg.message !== waitingMsg);
newmessages.push({
"status": "received",
"message": resText,
"id": uuidv4(),
});
setMessages(newmessages);
return;
}
// 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
if (done) {
setLoading(false);
break;
}
status = "error"
} else {
if (data.reason !== undefined) {
outputmessage = data.reason
}
}
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || ""; // Keep incomplete line in buffer
if (data.action !== undefined) {
//console.log("Action is defined: ", data.action);
action = data.action
for (const line of lines) {
if (line.startsWith('data: ')) {
const jsonStr = line.replace('data: ', '');
try {
const data = JSON.parse(jsonStr);
const eventType = data.type;
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("_", " ")
}
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;
}
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"
// 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]);
} 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 });
}
}
} 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) => {
<Card style={{backgroundColor: theme.palette.surfaceColor, borderRadius: theme.palette?.borderRadius,}}>
<CardContent>
<Typography variant="h6">
How many incidents did we get last week?
What is the difference between a trigger and an action?
</Typography>
</CardContent>
</Card>
<Card style={{backgroundColor: theme.palette.surfaceColor, borderRadius: theme.palette?.borderRadius, marginTop: 10, }}>
<CardContent>
<Typography variant="h6">
Answer the last email from Jim about the new project, and say we're on it
How do I create a new workflow?
</Typography>
</CardContent>
</Card>
<Card style={{backgroundColor: theme.palette.surfaceColor, borderRadius: theme.palette?.borderRadius, marginTop: 10, }}>
<CardContent>
<Typography variant="h6">
Is the IP 1.2.3.4 blocked? If not, block it.
Is there a way to mass abort executions of a specific workflow?
</Typography>
</CardContent>
</Card>
@@ -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) => (
<Card key={index} style={{
backgroundColor: theme.palette.surfaceColor,
+1 -1
View File
@@ -30,7 +30,7 @@ const EditOrgTab = (props) => {
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();
+1 -1
View File
@@ -633,7 +633,7 @@ const EditWorkflow = (props) => {
<AutoAwesomeIcon style={{ marginRight: 8 }} />
)}
AI Generate
AI Generate (beta)
</Button>
</span>
</Tooltip>
+5 -1
View File
@@ -206,7 +206,11 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
setOpenAutocomplete(false)
}}
>
Add suborg
<Button variant="outlined" startIcon={<AddIcon />} sx={{
textTransform: 'none',
}}>
Add suborg
</Button>
</Box>
</Link>
</Popper>
+8 -2
View File
@@ -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",
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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()
@@ -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, {
+2 -5
View File
@@ -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) {
+14 -4
View File
@@ -1911,7 +1911,7 @@ const ParsedAction = (props) => {
<Tooltip
title={
<Typography variant="body2" style={{margin: 3, }}>
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.
</Typography>
}
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) => {
}
}}
>
<PlayArrowIcon style={{marginRight: 5, }}/>
<CachedIcon style={{marginRight: 5, }}/>
Rerun
</Button>
</Tooltip>
@@ -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) => {
</div>
{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)",
}} />
</Tooltip>
</div>
+1 -1
View File
@@ -13,7 +13,7 @@ const PartnerApps = () => {
minHeight: "500px",
}}
>
<Typography>Partner Apps</Typography>
<Typography>Partner's Apps</Typography>
</Box>
)
}
File diff suppressed because one or more lines are too long
+5 -7
View File
@@ -126,9 +126,9 @@ const PartnerTab = (props) => {
case `apps`:
return <PartnersApps isCloud={isCloud} partnerData={partnerData} setPartnerData={setPartnerData} handleStatusChange={handleStatusChange} userdata={userdata} globalUrl={globalUrl} serverside={serverside} handleGetOrg={handleGetOrg} setSelectedOrganization={setSelectedOrganization} selectedOrganization={selectedOrganization} selectedTab={selectedTab} />;
case 'articles' :
return <PartnerArticles isCloud={isCloud} partnerData={partnerData} setPartnerData={setPartnerData} handleStatusChange={handleStatusChange} userdata={userdata} globalUrl={globalUrl} serverside={serverside} handleGetOrg={handleGetOrg} setSelectedOrganization={setSelectedOrganization} selectedOrganization={selectedOrganization} selectedTab={selectedTab} />;
case `ai_agents`:
return <PartnerArticles isCloud={isCloud} partnerData={partnerData} setPartnerData={setPartnerData} handleStatusChange={handleStatusChange} userdata={userdata} globalUrl={globalUrl} serverside={serverside} handleGetOrg={handleGetOrg} setSelectedOrganization={setSelectedOrganization} selectedOrganization={selectedOrganization} selectedTab={selectedTab} />;
return <PartnersUsecasesTab isCloud={isCloud} partnerData={partnerData} setPartnerData={setPartnerData} handleStatusChange={handleStatusChange} userdata={userdata} globalUrl={globalUrl} serverside={serverside} handleGetOrg={handleGetOrg} setSelectedOrganization={setSelectedOrganization} selectedOrganization={selectedOrganization} selectedTab={selectedTab} />;
case `aiagents`:
return <PartnerArticles tabName="AI Agents, Coming Soon.." isCloud={isCloud} partnerData={partnerData} setPartnerData={setPartnerData} handleStatusChange={handleStatusChange} userdata={userdata} globalUrl={globalUrl} serverside={serverside} handleGetOrg={handleGetOrg} setSelectedOrganization={setSelectedOrganization} selectedOrganization={selectedOrganization} selectedTab={selectedTab} />;
case 'branding':
return <Branding
isCloud={isCloud}
@@ -157,8 +157,7 @@ const PartnerTab = (props) => {
!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 (
+3 -2
View File
@@ -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 (
<Box
sx={{
@@ -13,7 +14,7 @@ const PartnersArticles = () => {
minHeight: "500px",
}}
>
<Typography>Partner Articles</Typography>
<Typography>{tabName}</Typography>
</Box>
)
}
+655 -29
View File
@@ -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 (
<Box
sx={{
textDecoration: "none",
backgroundColor: theme.palette.usecaseCardColor,
borderRadius: "16px",
width: {
xs: "230px",
sm: "320px",
},
minWidth: {
xs: "230px",
sm: "320px",
},
overflow: "hidden",
display: "flex",
flexDirection: "column",
}}
>
{/* Header image skeleton */}
<Skeleton
variant="rectangular"
height={160}
sx={{
width: "100%",
bgcolor:
themeMode === "dark"
? "rgba(255, 255, 255, 0.08)"
: "rgba(0, 0, 0, 0.06)",
}}
/>
{/* Body skeleton */}
<Box
sx={{
p: 2,
pt: 2.5,
display: "flex",
flexDirection: "column",
gap: 1.5,
}}
>
<Skeleton
variant="text"
width="80%"
height={26}
sx={{
bgcolor:
themeMode === "dark"
? "rgba(255, 255, 255, 0.1)"
: "rgba(0, 0, 0, 0.08)",
}}
/>
<Skeleton
variant="text"
width="40%"
height={20}
sx={{
bgcolor:
themeMode === "dark"
? "rgba(255, 255, 255, 0.08)"
: "rgba(0, 0, 0, 0.06)",
}}
/>
<Box
sx={{
mt: "auto",
display: "flex",
justifyContent: "flex-end",
alignItems: "center",
gap: 1,
}}
>
<Skeleton
variant="rectangular"
width={38}
height={22}
sx={{
borderRadius: "12px",
bgcolor:
themeMode === "dark"
? "rgba(255, 255, 255, 0.1)"
: "rgba(0, 0, 0, 0.06)",
}}
/>
<Skeleton
variant="circular"
width={24}
height={24}
sx={{
bgcolor:
themeMode === "dark"
? "rgba(255, 255, 255, 0.1)"
: "rgba(0, 0, 0, 0.06)",
}}
/>
</Box>
</Box>
</Box>
);
}
// Skeleton for Usecases Tab
return (
<Box
sx={{
@@ -177,7 +323,8 @@ const UsecaseCardSkeleton = () => {
);
};
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 (
<Box
sx={{
textDecoration: "none",
backgroundColor: theme.palette.usecaseCardColor,
borderRadius: "16px",
width: {
xs: "230px",
sm: "320px",
},
minWidth: {
xs: "230px",
sm: "320px",
},
overflow: "hidden",
display: "flex",
flexDirection: "column",
"&:hover": {
backgroundColor: theme.palette.usecaseCardHoverColor,
},
}}
>
<Box
component="img"
src={usecase.headerImage || "/images/no_image.png"}
alt={usecase.name}
sx={{
width: "100%",
height: 160,
objectFit: "cover",
}}
/>
<Box
sx={{
p: 2,
pt: 2.5,
display: "flex",
flexDirection: "column",
gap: 1.5,
height: "100%",
}}
>
<Typography
sx={{
color: theme.palette.text.primary,
fontSize: {
xs: "15px",
lg: "16px",
},
fontWeight: 600,
fontFamily: theme.typography.fontFamily,
display: "-webkit-box",
WebkitLineClamp: 2,
WebkitBoxOrient: "vertical",
overflow: "hidden",
}}
>
{usecase.name}
</Typography>
{usecase.created && (
<Typography
sx={{
color: theme.palette.text.secondary,
fontSize: "13px",
}}
>
{new Date(usecase.created * 1000).toLocaleDateString("en-US", {
year: "numeric",
month: "short",
day: "numeric",
})}
</Typography>
)}
<Box
sx={{
mt: -1,
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
gap: 0.5,
}}
onClick={(e) => e.stopPropagation()}
>
<OpenInNewIcon
fontSize="small"
onClick={(e) => {
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",
}}
/>
<Switch
checked={usecase.public}
onClick={(e) => {
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",
},
}}
/>
<IconButton
onClick={(e) => {
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)",
},
}}
>
<MoreVertIcon fontSize="small" />
</IconButton>
</Box>
<Menu
anchorEl={anchorEl}
open={open}
onClose={handleClose}
onClick={(e) => 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" }}
>
<MenuItem
onClick={(e) => {
e.stopPropagation();
handleOpenDialog(usecase);
setAnchorEl(null);
}}
>
<ListItemIcon>
<EditIcon
fontSize="small"
sx={{
color: theme.palette.text.primary,
fontFamily: theme.typography.fontFamily,
}}
/>
</ListItemIcon>
<ListItemText>Edit Article</ListItemText>
</MenuItem>
<MenuItem
onClick={(e) => {
e.stopPropagation();
handleDeleteUsecase(usecase.id);
}}
>
<ListItemIcon>
<DeleteIcon
fontSize="small"
sx={{
color: theme.palette.text.primary,
fontFamily: theme.typography.fontFamily,
}}
/>
</ListItemIcon>
<ListItemText>Delete Article</ListItemText>
</MenuItem>
</Menu>
</Box>
</Box>
);
}
// Usecase Card Layout
return (
<Box
onClick={handleCardClick}
sx={{
textDecoration: "none",
backgroundColor: theme.palette.usecaseCardColor,
@@ -241,7 +594,6 @@ const UsecaseCard = ({ usecase, handleToggle, handleOpenDialog, handleDeleteUsec
sm: "300px",
},
gap: "20px",
cursor: "pointer",
"&:hover": {
backgroundColor: theme.palette.usecaseCardHoverColor,
},
@@ -302,6 +654,17 @@ const UsecaseCard = ({ usecase, handleToggle, handleOpenDialog, handleDeleteUsec
</Tooltip>
</div>
<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
<OpenInNewIcon
fontSize="small"
onClick={(e) => {
e.stopPropagation();
window.open(`${window.location.origin}/usecases/${usecase.name.toLowerCase().replaceAll(" ", "_")}`, "_blank");
}}
sx={{
color: theme.palette.primary.main,
cursor: "pointer",
}}
/>
<Switch
checked={usecase.public}
onClick={(e) => {
@@ -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" }
];
@@ -506,6 +877,8 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
navigation: usecase.navigation || { items: [] },
categories: usecase.mainContent?.categories || [],
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);
@@ -864,6 +1425,8 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
categories: formData.mainContent?.categories || [],
navigation: formData.navigation || {},
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 (
<Box
sx={{
@@ -934,7 +1519,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
justifyContent: "flex-start",
}}
>
{/* Add Usecase Button */}
{/* Add Usecase / Article Button */}
<Box
sx={{
width: "100%",
@@ -946,13 +1531,13 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
}}
>
<Typography sx={{ fontSize: 20, fontWeight: 500, color: "#FFFFFF" }}>
Usecases
{selectedTab === "usecases" && "Usecases"}
{selectedTab === "articles" && "Articles"}
</Typography>
<Button
variant="contained"
color="primary"
startIcon={<AddIcon />}
disabled={isWorkflowLoading}
onClick={handleOpenDialog}
sx={{
px: 3,
@@ -961,11 +1546,11 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
fontWeight: 600,
}}
>
Add Usecase
{isArticlesTab ? "Add Article" : "Add Usecase"}
</Button>
</Box>
{/* Usecases Grid */}
{/* Usecases / Articles Grid */}
{isLoading ? (
// Skeleton loading state
<Box
@@ -981,11 +1566,11 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
>
{/* Display 4 skeleton cards while loading */}
{[...Array(4)].map((_, index) => (
<UsecaseCardSkeleton key={index} />
<UsecaseCardSkeleton key={index} isArticlesTab={isArticlesTab} partnerData={partnerData} />
))}
</Box>
) : usecaseData.length > 0 ? (
// Actual data display
) : filteredUsecases.length > 0 ? (
// Actual data display (filtered based on public workflow ID)
<Box
sx={{
display: "flex",
@@ -997,13 +1582,15 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
pt: 4,
}}
>
{usecaseData.map((usecase) => (
{filteredUsecases.map((usecase) => (
<UsecaseCard
handleOpenDialog={handleOpenDialog}
key={usecase.id}
usecase={usecase}
handleToggle={handleToggle}
handleDeleteUsecase={handleDeleteUsecase}
isArticlesTab={isArticlesTab}
partnerData={partnerData}
/>
))}
</Box>
@@ -1027,7 +1614,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
color: "#FFFFFF",
}}
>
No usecases found
{isArticlesTab ? "No articles found" : "No usecases found"}
</Typography>
</Box>
)}
@@ -1064,7 +1651,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
}}
>
<Typography sx={{ fontSize: 20, fontWeight: 500, color: theme.palette.text.primary }}>
{formData?.id ? "Update" : "Add New"} Usecase : {formData?.public ? "Published" : "Draft"}
{formData?.id ? "Update" : "Add New"} {entityLabel} : {formData?.public ? "Published" : "Draft"}
</Typography>
<IconButton
onClick={handleCloseDialog}
@@ -1088,6 +1675,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
paddingTop: 3,
}}
>
{!isArticlesTab && (
<FormControl>
<Box
sx={{
@@ -1207,9 +1795,40 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
)}
</Select>
</FormControl>
)}
{/* Other data selection (use all data types) */}
{!isArticlesTab && (
<Box sx={{ display: "flex", flexDirection: "row", gap: 2, mt: 2 }}>
<FormControl fullWidth>
<Typography
sx={{
color: grey,
mb: 1,
fontSize: "16px",
}}
>
<b>Coming soon:</b> Files (detection)
</Typography>
</FormControl>
<FormControl fullWidth>
<Typography
sx={{
color: grey,
mb: 1,
fontSize: "16px",
}}
>
<b>Coming soon:</b> Datastore category (threatlists)
</Typography>
</FormControl>
</Box>
)}
{/* App Type Selection */}
<Box sx={{ display: "flex", flexDirection: "row", gap: 2, mt: 2 }}>
{!isArticlesTab && (
<Box sx={{ display: "flex", flexDirection: "row", gap: 2, mt: isArticlesTab ? 0 : 2 }}>
<FormControl fullWidth>
<Typography
sx={{
@@ -1286,6 +1905,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
</Select>
</FormControl>
<FormControl fullWidth>
<Typography
sx={{
@@ -1362,6 +1982,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
</Select>
</FormControl>
</Box>
)}
<Box sx={{ mb: 2, mt: 2 }}>
<Typography
@@ -1433,6 +2054,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
}}
/>
</FormControl>
{!isArticlesTab && (
<FormControl>
<Typography
sx={{ color: theme.palette.text.primary, mb: 1, fontSize: "14px" }}
@@ -1513,6 +2135,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
))}
</Select>
</FormControl>
)}
</Box>
</Box>
@@ -1648,7 +2271,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
<Box key={contentIndex} sx={{ mb: 1 }}>
<TextField
multiline
rows={6}
rows={14}
value={content}
onChange={(e) => {
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={
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5, mt: 0.5 }}>
<InfoIcon sx={{ fontSize: "14px", opacity: 0.7 }} />
<Typography variant="caption" sx={{ opacity: 0.7 }}>
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.
</Typography>
</Box>
}
@@ -1708,7 +2334,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
)}
</Box>
))}
{/*
<Button
startIcon={<AddCircleOutlineIcon />}
onClick={() => {
@@ -1725,7 +2351,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
}}
>
Add Paragraph
</Button>
</Button> */}
</Box>
))}
@@ -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}`)
}
</Button>
</DialogActions>
+1 -1
View File
@@ -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") {
+258 -56
View File
@@ -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(() => {
@@ -342,54 +474,124 @@ 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 (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Typography sx={{ fontSize: 18, fontWeight: 500, fontFamily: theme.typography.fontFamily, paddingLeft: 1 }}>Runs over time ({mode === "workflows" ? "Workflows" : "Apps"})</Typography>
<Typography sx={{ fontSize: 18, fontWeight: 500, fontFamily: theme.typography.fontFamily, paddingLeft: 1 }}>{selectedStatType?.label} ({selectedStatType?.amount})</Typography>
<Box sx={{ display: 'flex', gap: 1.5, alignItems: 'center', justifyContent: 'center' }}>
<ToggleButtonGroup
exclusive
size="large"
value={mode}
onChange={(e, v) => 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)",
},
<Tooltip title="Learn about custom stats" arrow>
<a href="/docs/API#count-stats-for-custom-key" target="_blank" rel="noopener noreferrer" style={{ textDecoration: 'none' }}>
<IconButton>
<OpenInNewIcon style={{ color: theme.palette.text.secondary, }} />
</IconButton>
</a>
</Tooltip>
<FormControl size="small" variant="outlined" style={{ minWidth: 350, }} sx={{
'& .MuiInputBase-root': {
height: 40,
backgroundColor: 'rgba(255,255,255,0.06)',
borderRadius: '20px',
},
}}
>
<ToggleButton value="workflows">
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
Workflows
</Box>
</ToggleButton>
<ToggleButton value="apps">
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
Apps
</Box>
</ToggleButton>
</ToggleButtonGroup>
'& .MuiOutlinedInput-notchedOutline': { borderColor: 'rgba(255,255,255,0.22)' },
}}>
<Autocomplete
labelId="data-type-choice"
label="Select Datatype"
autoHighlight
value={selectedStatType}
classes={{ inputRoot: classes.inputRoot }}
ListboxProps={{
style: {
backgroundColor: theme.palette.inputColor,
color: theme.palette.text.primary,
},
}}
sx={{
'& .MuiOutlinedInput-root': {
height: 40, // Adjust the input height
},
'& .MuiAutocomplete-input': {
padding: '8px', // Adjust the text padding
},
}}
getOptionSelected={(option, value) => 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 (
<MenuItem
style={{
backgroundColor: theme.palette.inputColor,
}}
value={data}
onClick={() => {
setSelectedStatType(data)
// Set local storage for the key
if (data?.value) {
localStorage.setItem("runsOverTimeWidgetStatType", data?.value)
}
}}
>
<span style={{minWidth: 60, maxWidth: 60, color: numbercolor, }}>{formattedamount}</span> {data?.label}
</MenuItem>
)
}}
renderInput={(params) => {
return (
<div style={{ display: "flex", }}>
<TextField
style={theme.palette.textFieldStyle}
{...params}
label="Find your Stat"
variant="outlined"
/>
</div>
)
}}
/>
</FormControl>
<FormControl size="small" variant="outlined" style={{ minWidth: 170 }} sx={{
'& .MuiInputBase-root': {
height: 40,
@@ -438,7 +640,7 @@ function computeTodayValueForOrg(key, orgStats) {
key={`${mode}-${selectedMonth ? `${selectedMonth.getFullYear()}-${selectedMonth.getMonth()}` : 'yearly'}`}
height={300}
data={barData}
series={<BarSeries tooltip={tooltip} bar={<Bar rounded={true} />} />}
series={<BarSeries colorScheme={barColorscheme} tooltip={tooltip} bar={<Bar rounded={true} />} />}
gridlines={<GridlineSeries line={<Gridline direction="y" />} />}
yAxis={
<LinearYAxis
+2 -2
View File
@@ -44,7 +44,7 @@ import {
Send as SendIcon,
} from '@mui/icons-material';
import { DataGrid, GridColDef, GridValueGetterParams } from '@mui/x-data-grid'
import { DataGrid, GridValueGetterParams } from '@mui/x-data-grid'
import {
Search as SearchIcon,
} from "@mui/icons-material";
@@ -335,7 +335,7 @@ const RuntimeDebugger = (props) => {
}
const imageSize = 30
const timenowUnix = Math.floor(Date.now() / 1000)
const columns: GridColDef[] = [
const columns = [
{
field: 'execution_source',
headerName: 'Source',
+1 -1
View File
@@ -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)
// }
+1 -1
View File
@@ -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(() => {
+43 -7
View File
@@ -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) => {
<ListItemText
primary={
<Select
disabled={data?.login_type === "DELETED"}
SelectDisplayProps={{
style: {
// marginLeft: 10,
@@ -1611,7 +1623,30 @@ const UserManagmentTab = memo((props) => {
data.login_type === undefined ||
data?.login_type === null ||
data?.login_type?.length === 0
? "Normal"
?
"Normal"
:
data?.login_type === "DELETED" ?
<Chip
style={{
marginLeft: 0,
padding: 0,
marginRight: 0,
cursor: 'pointer',
}}
label={"Recover"}
variant="outlined"
onClick={(e) => {
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}
<ListItemText style={{ display:'table-cell', textAlign: "left", verticalAlign: 'middle', padding: "8px", }}>
<IconButton
disabled={data?.login_type === "DELETED"}
onClick={() => {
setSelectedUserModalOpen(true);
setSelectedUser(data);
+1 -1
View File
@@ -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();
+1 -1
View File
@@ -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([]);
@@ -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) {
+199 -52
View File
@@ -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 (
<div style={{ width: "100%", height: "100%",boxSizing: 'border-box', padding: "27px 10px 19px 27px", backgroundColor: theme.palette.platformColor , borderRadius: '16px', }}>
<div style={{ height: "100%", width: "100%", overflowX: 'hidden', scrollbarColor: theme.palette.scrollbarColorTransparent, scrollbarWidth: 'thin'}} >
@@ -316,6 +425,81 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
<Typography variant="h5" style={{ width: "100%", fontWeight: 500, fontSize: 24}}>
SSO Configuration
</Typography>
{/* SSO Connection section - moved to top */}
<div
style={{
display: "flex",
flexDirection: "column",
marginTop: 20,
width: "100%",
paddingBottom: 10,
}}
>
<Typography variant="body2" color="textSecondary" style={{ margin: "5px 0px 5px 0px", fontSize: 16, fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)" }}>
{checkingSSOStatus
? "Checking your SSO connection status..."
: userSSOConnected
? "Your account is connected with this org's SSO!"
: "Connect your account with this org's SSO!"
}
</Typography>
<Tooltip
title={
checkingSSOStatus
? "Checking SSO connection status..."
: userSSOConnected
? "Your account is already connected to SSO"
: !(
ssoEntrypoint?.length > 0 ||
ssoCertificate?.length > 0 ||
openidAuthorization?.length > 0 ||
openidClientId?.length > 0
)
? "SSO must be configured for this organization before you can connect."
: ""
}
>
<div style={{ display: 'flex', gap: '10px', alignItems: 'center' }}>
<span style={{ width: 150 }}>
<Button
variant={userSSOConnected ? "contained" : "outlined"}
color={userSSOConnected ? "success" : "primary"}
style={{ width: 150, textTransform: "none", margin: "10px 10px 10px 0px", whiteSpace: "nowrap" }}
disabled={
checkingSSOStatus ||
userSSOConnected ||
!(
ssoEntrypoint?.length > 0 ||
ssoCertificate?.length > 0 ||
openidAuthorization?.length > 0 ||
openidClientId?.length > 0
)
}
onClick={HandleTestSSO}
>
{checkingSSOStatus ? "Checking..." : userSSOConnected ? "Connected" : "Connect with SSO"}
</Button>
</span>
{userSSOConnected && (
<Tooltip title="Disconnect your account from SSO">
<span style={{ width: 120 }}>
<Button
variant="outlined"
color="error"
style={{ width: 120, textTransform: "none", margin: "10px 0px 10px 0px", whiteSpace: "nowrap" }}
onClick={HandleDisconnectSSO}
>
Disconnect
</Button>
</span>
</Tooltip>
)}
</div>
</Tooltip>
</div>
<div
style={{
display: "flex",
@@ -339,6 +523,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
sx={{marginBottom: 0.6, marginTop: 0.6}}
name="onOffSwitch"
color="primary"
disabled={!isAdmin}
title="Make SAML SSO or OpenID Authentication Required or Optional for Your Organization"
/>
{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"
/>
</div>
@@ -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"
/>
</div>
@@ -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"
/>
</div>
</div>
}
<div
style={{
display: "flex",
flexDirection: "column",
marginTop: 30,
width: "100%",
paddingBottom: 10,
}}
>
<Typography variant="body2" color="textSecondary" style={{ margin: "5px 0px 5px 0px", fontSize: 16, fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)" }}>
You can test your SSO configuration by clicking the button below.
Before testing, ensure you have set Open ID Connect or SAML SSO
credentials.
</Typography>
<Tooltip
title={
!(
ssoEntrypoint?.length > 0 ||
ssoCertificate?.length > 0 ||
openidAuthorization?.length > 0 ||
openidClientId?.length > 0
)
? "Please ensure all SSO credentials are set before testing."
: ""
}
>
<span style={{ width: 100 }}>
<Button
variant="outlined"
color="primary"
style={{ width: 100, textTransform: "none", margin: "10px 10px 10px 0px", whiteSpace: "nowrap" }}
disabled={
!(
ssoEntrypoint?.length > 0 ||
ssoCertificate?.length > 0 ||
openidAuthorization?.length > 0 ||
openidClientId?.length > 0
)
}
onClick={HandleTestSSO}
>
Test SSO
</Button>
</span>
</Tooltip>
</div>
<Grid item xs={12} sx={{marginTop: 2}}>
<span style={{ display: "flex", flexDirection: "column" }}>
<Typography variant="h5" color="textPrimary" style={{ textAlign: "left", fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)", fontSize: 24, fontWeight: 500, }}>OpenID connect</Typography>
@@ -514,6 +653,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
onChange={(e) => setShowOpenIdCred(e.target.checked)}
name="showOpenIdCred"
color="primary"
disabled={!isAdmin}
/>
</div>
<Grid container style={{ marginTop: 8, }} spacing={2}>
@@ -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 : */}
<Grid item xs={12} sx={{ marginTop: 3.5 }} >
<Typography variant="h5" color="textPrimary" style={{ textAlign: "left", fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)", fontSize: 24, fontWeight: 500, }}>SAML SSO (v1.1)</Typography>
<Typography variant="body2" color="textSecondary" style={{ textAlign: "left", marginTop: 8, fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)", fontSize: 16, fontWeight: 400, }}>
IdP URL for Shuffle SAML/SSO: <Link to={`${globalUrl}/api/v1/login_sso`} target="_blank" style={{ color: theme.palette.text.secondary, textDecoration: "none" }}>{`${globalUrl}/api/v1/login_sso`}</Link>
<Typography variant="body2" color="textSecondary" style={{ textAlign: "left", marginTop: 4, fontFamily: "var(--zds-typography-base,Inter,Helvetica,arial,sans-serif)", fontSize: 16, fontWeight: 400, fontStyle: "italic", color: red }}>
Note: Support for SAML SSO was deprecated due to potential security issues. Please consider migrating to OpenID Connect for better compatibility and features.
</Typography>
<div style={{ display: 'flex', marginTop: 10, }}>
<Typography
@@ -702,6 +846,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
onChange={(e) => setShowSamlCred(e.target.checked)}
name="showSamlCred"
color="primary"
disabled={!isAdmin}
/>
</div>
<Grid container style={{ marginTop: 10, }} spacing={2}>
@@ -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) => {
+53 -2
View File
@@ -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: {
+468 -179
View File
@@ -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) => {
</Tooltip>
const rerunButton =
<Tooltip title="Rerun FROM this decision. This can be used if an agent decision action somehow stopped and didn't get a result. Clears out all decisions AFTER this one." placement="right">
const rerunButton = item?.type === "processing" ? null :
<Tooltip title={item?.type === "decision" ? "Rerun FROM this decision. This can be used if an agent decision action somehow stopped and didn't get a result. Clears out all decisions AFTER this one." : ""} placement="right">
<span>
<IconButton
disabled={item.type !== "decision" || disableButtons}
@@ -698,6 +789,11 @@ const AgentUI = (props) => {
</span>
</Tooltip>
var itemLabel = item?.label?.replaceAll("_", " ") || ""
if (item?.details?.reason !== undefined && item?.details?.reason !== null && item?.details?.reason !== "") {
itemLabel = item?.details?.reason
}
return (
<div
style={{
@@ -778,10 +874,22 @@ const AgentUI = (props) => {
paddingTop: defaultTopPadding,
paddingBottom: defaultTopPadding,
}}>
{item.label}
{itemLabel}
</div>
<Tooltip title={`Time taken: ${currentDuration} seconds. Started: ${new Date(itemStartTime * 1000).toLocaleString()}\nFinished: ${new Date(itemEndTime * 1000).toLocaleString()}`} placement="right">
<Tooltip title={`Time taken: ${currentDuration} seconds. Started: ${new Date(itemStartTime * 1000).toLocaleString()}\nFinished: ${new Date(itemEndTime * 1000).toLocaleString()}`} placement="right"
PopperProps={{
modifiers: [
{
name: 'offset',
options: {
offset: [0, 130], // [skid, distance]
},
},
],
}}
style={theme.palette.tooltip}
>
<div style={{
minWidth: maxTimelineWidth,
maxWidth: maxTimelineWidth,
@@ -874,7 +982,7 @@ const AgentUI = (props) => {
item?.type === "decision" ?
<div style={{display: "flex", }}>
{rerunButton}
<Tooltip title="Explore/debug execution" placement="left">
<Tooltip title="Explore/debug app run" placement="left">
<span>
<IconButton
disabled={item?.details?.run_details?.debug_url === undefined || item?.details?.run_details?.debug_url === null || item?.details?.run_details?.debug_url === ""}
@@ -895,21 +1003,24 @@ const AgentUI = (props) => {
:
rerunButton
}
<Tooltip title="Explore results" placement="right">
<span>
<IconButton
disabled={item.details === undefined || item.details === null || item.details === ""}
style={{marginLeft: 10, }}
>
{open ?
<ExpandLessIcon />
:
<ExpandMoreIcon />
}
</IconButton>
</span>
</Tooltip>
{item?.type === "processing" ? null :
<Tooltip title="Explore results" placement="right">
<span>
<IconButton
disabled={item.details === undefined || item.details === null || item.details === ""}
style={{marginLeft: 10, }}
>
{open ?
<ExpandLessIcon />
:
<ExpandMoreIcon />
}
</IconButton>
</span>
</Tooltip>
}
</div>
</div>
@@ -976,13 +1087,17 @@ const AgentUI = (props) => {
<Button
variant="contained"
style={{marginTop: 16, }}
disabled={questionSubmitDisabled}
disabled={questionSubmitDisabled || agentRequestLoading}
onClick={() => {
submitQuestions(item?.details?.run_details?.id, questionAnswers)
}}
>
Submit
{agentRequestLoading ?
<CircularProgress size={20} style={{marginRight: 10, }} />
:
"Submit"
}
</Button>
</div>
: 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 (
<div style={{marginTop: 20, }}>
@@ -1151,6 +1321,7 @@ const AgentUI = (props) => {
<Box
component="form"
style={{width: "100%", textAlign: "center",}}
onKeyDown={handleKeyDownCont}
onSubmit={(e) => {
e.preventDefault();
@@ -1161,15 +1332,35 @@ const AgentUI = (props) => {
}, true)
}}
>
<div style={{display: "flex", maxWidth: 550, minWidth: 550, margin: "auto", marginTop: 50, }}>
{finishAnswer !== "" ?
<div style={{marginTop: 50, textAlign: "left", }}>
<Markdown
components={markdownComponents}
id="markdown_wrapper"
className={"style.reactMarkdown"}
escapeHtml={false}
skipHtml={false}
remarkPlugins={[remarkGfm]}
style={{
maxWidth: "100%",
minWidth: "100%",
}}
>
{finishAnswer}
</Markdown>
</div>
: null}
<div style={{display: "flex", maxWidth: 600, minWidth: 600, margin: "auto", marginTop: 50, }}>
<div>
<TextField
label="Add more details to the current task"
label="Add more details to continue the current task"
variant="outlined"
disabled={agentRequestLoading}
style={{width: 400, margin: "auto", }}
style={{width: 450, margin: "auto", }}
multiline
minRows={1}
minRows={2}
value={continuationText}
onChange={(e) => {
console.log("Value: ", e.target.value)
//setActionInput(e.target.value)
@@ -1195,7 +1386,7 @@ const AgentUI = (props) => {
}}
/>
<Typography variant="body2" color="textSecondary" style={{marginTop: 10, }}>
Any failed tasks will be set to ignored (TBD). <a href="/docs/AI#agent-continuations" target="_blank" rel="noreferrer" style={{color: theme.palette.main, textDecoration: "none", }}>Learn more</a>
Any failed tasks will be set to ignored. <a href="/docs/AI#agent-continuations" target="_blank" rel="noreferrer" style={{color: theme.palette.main, textDecoration: "none", }}>Learn more</a>
</Typography>
</div>
<Typography color="textSecondary" variant="body1" style={{marginTop: 25, marginLeft: 20, }}>
@@ -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 =
<ButtonGroup style={{}}>
<Button
variant={showAgentStarter ? "contained" : "outlined"}
color="secondary"
onClick={() => {
if (actionInput === "" || actionInput === undefined || actionInput === null) {
setActionInput(data?.original_input || "")
}
//setChosenApps(chosenApps.concat([{
setTimeout(() => {
setShowAgentStarter(true)
}, 200)
}}
>
Restart
</Button>
<Button
variant={buttonState === "default" ? "contained" : "outlined"}
color="secondary"
disabled
onClick={() => {
setButtonState("default");
setShowAgentStarter(false)
}}
>
Default
</Button>
<Button
variant={buttonState === "timeline" ? "contained" : "outlined"}
color="secondary"
onClick={() => {
setButtonState("timeline");
setShowAgentStarter(false)
}}
>
Timeline
</Button>
</ButtonGroup>
return (
<div style={agentWrapperStyle}>
<TextField
id="copy_element_shuffle"
style={{ display: "none" }}
/>
{showAgentStarter ?
<Box
component="form"
style={{textAlign: "center", }}
onKeyDown={handleKeyDownRoot}
onSubmit={(e) => {
e.preventDefault();
submitInput(actionInput);
}}
>
<img src="/images/logos/agent.svg" style={{
width: 200,
height: 200,
borderRadius: theme.palette.borderRadius,
}} />
<div />
<Typography variant="h5" style={{marginTop: 30, }}>
Shuffle AI Agents
</Typography>
<div>
<div style={{height: showAgentStarter ? "25vh" : "5vh", }}>
</div>
<div style={agentWrapperStyle}>
<div style={{
minWidth: 1000, maxWidth: 1000, margin: "auto",
}}>
<TextField
label="What do you want to do?"
variant="outlined"
disabled={agentRequestLoading}
style={{width: 450, marginRight: 20, marginTop: 30, }}
multiline
minRows={1}
defaultValue={actionInput || ""}
onChange={(e) => {
setActionInput(e.target.value)
}}
InputProps={{
endAdornment: (
agentRequestLoading ?
<CircularProgress size={24} style={{marginRight: 10, }} />
:
<Tooltip title="This is the input for the AI Agent. It can be any valid JSON.">
<IconButton type="submit">
<SendIcon
color="primary"
/>
</IconButton>
</Tooltip>
),
}}
id="copy_element_shuffle"
style={{ display: "none" }}
/>
<div style={{display: "flex", margin: "auto", paddingTop: 10, minWidth: 300, maxWidth: 300, justifyContent: "center", overflowWrap: "wrap", }}>
<div>
<Chip
id="add_app_chip"
icon={<AddIcon />} label="Select Apps"
style={chipStyle}
onClick={() => {
setAppPickerAnchor(document.getElementById("add_app_chip"))
{showAgentStarter ?
<Box
component="form"
style={{textAlign: "center", }}
onKeyDown={handleKeyDownRoot}
onSubmit={(e) => {
e.preventDefault();
submitInput(actionInput);
}}
>
{/*
<img src="/images/logos/agent.svg" style={{
width: 200,
height: 200,
borderRadius: theme.palette.borderRadius,
}} />
*/}
<div />
<Typography variant="h2" style={{marginTop: 30, }}>
What do you want to do?
</Typography>
<TextField
label="Get my emails for today and summarise them"
variant="outlined"
disabled={agentRequestLoading}
style={{width: 500, marginTop: 30, }}
multiline
minRows={1}
defaultValue={actionInput || ""}
onChange={(e) => {
setActionInput(e.target.value)
}}
InputProps={{
endAdornment: (
agentRequestLoading ?
<CircularProgress size={24} style={{marginRight: 10, }} />
:
<Tooltip title="This is the input for the AI Agent. It can be any valid JSON.">
<IconButton type="submit">
<SendIcon
color="primary"
/>
</IconButton>
</Tooltip>
),
}}
/>
<Popover
open={appPickerAnchor !== null}
anchorEl={appPickerAnchor}
onClose={() => {
setAppPickerAnchor(null)
}}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
>
<AppSearch
userdata={userdata}
defaultSearch={""}
newSelectedApp={newSelectedApp}
setNewSelectedApp={setNewSelectedApp}
inputHeight={200}
<div style={{display: "flex", margin: "auto", paddingTop: 30, minWidth: 300, maxWidth: 300, justifyContent: "center", overflowWrap: "wrap", }}>
<div>
<Chip
id="add_app_chip"
icon={<AddIcon />} label="Select Apps / MCPs"
style={chipStyle}
onClick={() => {
setAppPickerAnchor(document.getElementById("add_app_chip"))
}}
/>
</Popover>
<Popover
open={appPickerAnchor !== null}
anchorEl={appPickerAnchor}
onClose={() => {
setAppPickerAnchor(null)
}}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
>
<AppSearch
userdata={userdata}
defaultSearch={""}
newSelectedApp={newSelectedApp}
setNewSelectedApp={setNewSelectedApp}
inputHeight={200}
/>
</Popover>
</div>
{chosenApps.map((app, index) => {
const chosenName = (app?.name?.charAt(0).toUpperCase() + app?.name?.slice(1))?.replaceAll("_", " ").replaceAll("-", " ")
const chosenImagePath = app?.image
const chosenImage = <img src={chosenImagePath} style={{width: 24, height: 24, borderRadius: 20, marginRight: 1, }} />
return (
<Chip icon={chosenImage} label={chosenName} variant="outlined"
style={chipStyle}
onDelete={() => {
const newChosenApps = chosenApps.filter((a, i) => i !== index)
setChosenApps(newChosenApps)
}}
/>
)
})}
</div>
<div style={{marginTop: 20, }}>
{execution === undefined || execution === null || execution?.execution_id === undefined || execution?.execution_id === null ? null : typeButtonGroup}
</div>
{/*
<div style={{
height: 300,
width: "100%",
background: showAgentStarter ? `linear-gradient(to bottom, ${theme.palette.backgroundColor}, #C9591D)` : "inherit",
backgroundSize: "100% 100%",
backgroundRepeat: "no-repeat",
backgroundPosition: "0px 100px",
}} />
*/}
</Box>
:
<div>
<div style={{display: "flex", }}>
<div style={{flex: 2, }}>
{typeButtonGroup}
<ButtonGroup style={{marginLeft: 25, }}>
<Tooltip title="Reload the agent data" placement="top">
<span>
<Button
disabled={execution === null || Object.keys(execution).length === 0}
style={{}}
variant={"outlined"}
color="secondary"
onClick={() => {
GetExecution(execution?.execution_id, agentActionResult?.action?.id, execution?.authorization)
}}
>
<RefreshIcon />
</Button>
</span>
</Tooltip>
{execution?.status === "EXECUTING" ?
<Tooltip title="Stop the agent" placement="top">
<span>
<Button
disabled={execution === null || true}
style={{}}
variant={"outlined"}
color="secondary"
onClick={() => {
toast.warn("Not implemented. Should run abort.")
}}
>
<PauseIcon />
</Button>
</span>
</Tooltip>
: null}
</ButtonGroup>
</div>
<div style={{flex: 1, }}>
<AvatarGroup>
{chosenApps?.map((app, index) => {
return(
<Tooltip title={`Constraint: ${app?.name}`} key={index} placement="top">
<Avatar
key={index}
alt={app?.name}
src={app?.large_image}
onClick={() => {
window.open(`/apps/${app.id}`, '_blank', 'noopener,noreferrer');
}}
style={{
cursor: "pointer",
width: 30,
height: 30,
}}
/>
</Tooltip>
)
})}
</AvatarGroup>
</div>
</div>
{buttonState === "timeline" ?
<TimelineRender agent_data={data} />
:
null
}
</div>
{chosenApps.map((app, index) => {
const chosenName = (app?.name?.charAt(0).toUpperCase() + app?.name?.slice(1))?.replaceAll("_", " ").replaceAll("-", " ")
const chosenImagePath = app?.image
const chosenImage = <img src={chosenImagePath} style={{width: 24, height: 24, borderRadius: 20, marginRight: 1, }} />
return (
<Chip icon={chosenImage} label={chosenName} variant="outlined"
style={chipStyle}
onDelete={() => {
const newChosenApps = chosenApps.filter((a, i) => i !== index)
setChosenApps(newChosenApps)
}}
/>
)
})}
</div>
</Box>
:
<div>
<ButtonGroup style={{marginTop: 50, }}>
<Button
variant={buttonState === "default" ? "contained" : "outlined"}
color="secondary"
onClick={() => {
setButtonState("default");
}}
>
Default
</Button>
<Button
variant={buttonState === "timeline" ? "contained" : "outlined"}
color="secondary"
onClick={() => {
setButtonState("timeline");
}}
>
Timeline
</Button>
</ButtonGroup>
<Tooltip title="Reload the agent data" placement="top">
<span>
<Button
disabled={execution === null || Object.keys(execution).length === 0}
style={{marginLeft: 25, }}
variant={"outlined"}
color="secondary"
onClick={() => {
GetExecution(execution.execution_id, agentActionResult.action.id, execution.authorization)
}}
>
<RefreshIcon />
</Button>
</span>
</Tooltip>
{buttonState === "timeline" ?
<TimelineRender agent_data={data} />
:
null
}
</div>
}
</div>
</div>
)
}
+94 -49
View File
@@ -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' : ''*/}
</form>
@@ -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) => {
<Button
variant="outlined"
color="primary"
style={{position: "sticky", top: 35, maxHeight: 40, maxWidth: 150, marginRight: 5, }}
style={{position: "sticky", top: 50, maxHeight: 40, maxWidth: 150, marginRight: 5, }}
onClick={() => {
setExecutionText(data)
executeWorkflow(data, workflow.start, lastSaved);
executeWorkflow(data, workflow.start, lastSaved)
setExecutionArgumentModalOpen(false)
}}
>Select</Button>
<ReactJson
src={validate.result}
theme={theme.palette.jsonTheme}
style={theme.palette.reactJsonStyle}
theme={"summerfruit"}
style={{
padding: 5,
width: "98%",
borderRadius: 5,
border: "1px solid rgba(255,255,255,0.7)",
overflowX: "auto",
}}
shouldCollapse={(jsonField) => {
return collapseField(jsonField)
}}
@@ -14499,10 +14515,11 @@ const AngularWorkflow = (defaultprops) => {
}
return (
<Paper style={{ padding: 10, marginTop: 10, backgroundColor: theme.palette.platformColor, maxHeight: 100, overflow: "auto", cursor: "pointer", position: "relative", }}
<Paper
style={{ padding: 10, marginTop: 10, backgroundColor: theme.palette.platformColor, maxHeight: 150, overflow: "auto", position: "relative", border: `2px solid rgba(255,255,255,0.3)`, }}
onClick={() => {
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,
}}
>
<b>PS: Conditions can't be used for loops [ .# ]. Use the filters list action.{" "}</b>
@@ -15110,7 +15128,9 @@ const AngularWorkflow = (defaultprops) => {
key={condition.condition.id}
square
style={paperVariableStyle}
onClick={() => { }}
onClick={() => {
setLastSaved(false)
}}
>
<div
style={{
@@ -15339,6 +15359,7 @@ const AngularWorkflow = (defaultprops) => {
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 ? (
<Tooltip color="primary" title="Stop execution" placement="top">
<span>
@@ -20737,20 +20756,17 @@ const AngularWorkflow = (defaultprops) => {
>
<TextField
id="execution_argument_input_field"
style={{
...theme.palette.textFieldStyle,
height: buttonHeights + 2,
}}
variant="outlined"
InputProps={{
style: {
...theme.palette.innerTextfieldStyle,
height: buttonHeights + 2,
marginTop: -1,
border: "none",
height: buttonHeights+2,
marginTop: 0,
// Remove left side borderRadius
borderTopLeftRadius: 0,
borderBottomLeftRadius: 0,
borderTop: "0px solid",
}
}}
disabled={workflow.public}
@@ -21687,7 +21703,7 @@ const AngularWorkflow = (defaultprops) => {
return (
<div style={{ display: "flex" }}>
<div style={{ display: "flex", maxHeight: 500, overflow: "auto",}}>
<IconButton
style={{
marginTop: "auto",
@@ -21749,7 +21765,7 @@ const AngularWorkflow = (defaultprops) => {
onSelect={(select) => {
HandleJsonCopy(validate.result, select, "exec");
}}
name={false}
name={"$exec"}
/>
</div>
)
@@ -21787,7 +21803,26 @@ const AngularWorkflow = (defaultprops) => {
)
}
if (execution.execution_source === "authgroups") {
if (execution.execution_source?.startsWith("datastore")) {
const iconMargin = 7
return (
<div style={{
width: size,
height: size,
borderRadius: borderRadius,
backgroundColor: green,
}}>
<StorageIcon
style={{
width: size / 3 * 2,
height: size / 3 * 2,
marginLeft: iconMargin,
marginTop: iconMargin,
}}
/>
</div>
)
} else if (execution.execution_source === "authgroups") {
const iconMargin = 7
return (
<div style={{
@@ -22943,6 +22978,7 @@ const AngularWorkflow = (defaultprops) => {
</Typography>
</div>
) : null}
{executionData.execution_source !== undefined &&
executionData.execution_source !== null &&
executionData.execution_source.length > 0 &&
@@ -22954,8 +22990,17 @@ const AngularWorkflow = (defaultprops) => {
</Typography>
<Typography variant="body1" color="textSecondary">
{executionData.execution_source === "authgroups" || (executionData.authgroup !== undefined && executionData.authgroup !== null && executionData.authgroup.length > 0) ?
{executionData?.execution_source?.startsWith("datastore") ?
<a
rel="noopener noreferrer"
href={`/admin?tab=datastore${executionData.execution_source.split("|").length > 2 ? "&category="+executionData.execution_source.split("|")[1]+"&key="+executionData.execution_source.split("|")[2] : ""}`}
target="_blank"
style={{ textDecoration: "none", color: theme.palette.linkColor }}
>
Datastore Automation
</a>
:
executionData.execution_source === "authgroups" || (executionData.authgroup !== undefined && executionData.authgroup !== null && executionData.authgroup.length > 0) ?
<a
rel="noopener noreferrer"
href={`/admin?tab=app_auth`}
@@ -23614,7 +23659,7 @@ const AngularWorkflow = (defaultprops) => {
HandleJsonCopy(showResult, select, data.action.label);
console.log("SELECTED!: ", select);
}}
name={"Results for " + data.action.label}
name={`$${data?.action?.label?.toLowerCase()}`}
/>
</span>
+1 -1
View File
@@ -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")) {
+1 -1
View File
@@ -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
+50 -17
View File
@@ -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 }) => (
<Box component="ul" sx={{ my: 0.5, fontFamily: '"Segoe UI", Inter, sans-serif', fontSize: "17.6px" }}>
{children}
</Box>
),
ol: ({ children }) => (
<Box component="ol" sx={{ my: 0.5, fontFamily: '"Segoe UI", Inter, sans-serif', fontSize: "17.6px" }}>
{children}
</Box>
),
li: ({ children }) => (
<Typography component="li" sx={{ my: 0.5, color: "inherit", fontFamily: '"Segoe UI", Inter, sans-serif', fontSize: "17.6px" }}>
{children}
</Typography>
),
}
@@ -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) => {
<List style={{
listStyle: "none",
paddingLeft: "0",
paddingRight: isArticlePage ? 15 : undefined,
paddingTop: !isArticlePage ? "60px" : undefined,
paddingBottom: !isArticlePage ? "30px" : undefined,
display: isArticlePage ? sidebarOpen ? "block" : "none" : undefined,
@@ -1524,15 +1540,32 @@ const Docs = (defaultprops) => {
}
</div>
<div style={IndexBar}>
{userdata?.support && isArticlePage && (
<Button
variant="contained"
color="secondary"
onClick={handleResetCache}
style={{marginBottom: "15px"}}
{userdata?.support && (
<Tooltip
title="Wait about 5 minutes after updating the file on GitHub, then click to reset the cache. (Support only) "
placement="top"
componentsProps={{
tooltip: {
sx: {
backgroundColor: "rgba(33, 33, 33, 1)",
color: "rgba(241, 241, 241, 1)",
fontSize: 14,
border: "1px solid rgba(73, 73, 73, 1)",
fontFamily: theme?.typography?.fontFamily,
}
},
}}
arrow
>
Reset Cache
</Button>
<Button
variant="contained"
color="secondary"
onClick={handleResetCache}
style={{marginBottom: "15px"}}
>
Reset Cache
</Button>
</Tooltip>
)}
{tocLines.length > 0 ?
(
+8 -6
View File
@@ -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: <TrendingUpIcon sx={{ color: '#5cc879', fontSize: 34 }} />, percentage: STATIC_TIME_PERCENT, color: '#5cc879', disabled: true},
{ value: formatCurrencyCompact(totals.moneySavedDollars), label: 'Money saved', icon: <TrendingUpIcon sx={{ color: '#5cc879', fontSize: 34 }} />, percentage: STATIC_MONEY_PERCENT, color: '#5cc879', disabled: true, },
//{ value: timeFmt.display, title: timeFmt.title, label: 'Time saved', icon: <TrendingUpIcon sx={{ color: '#5cc879', fontSize: 34 }} />, percentage: STATIC_TIME_PERCENT, color: '#5cc879', disabled: true},
//{ value: formatCurrencyCompact(totals.moneySavedDollars), label: 'Money saved', icon: <TrendingUpIcon sx={{ color: '#5cc879', fontSize: 34 }} />, percentage: STATIC_MONEY_PERCENT, color: '#5cc879', disabled: true, },
{ value: String(unreadCount), label: 'Total errors', icon: <ErrorOutlineIcon sx={{ color: '#f87171', fontSize: 34, opacity: 0.9 }} />, percentage: "", color: '#f87171' },
{ value: String(readCount), label: 'Errors resolved', icon: <TaskAltIcon sx={{ color: '#5cc879', fontSize: 34, opacity: 0.9 }} />, 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 && (
<div style={{ position: 'absolute', inset: 0, background: 'rgba(17,17,17,0.6)', backdropFilter: 'blur(2px)', zIndex: 10, display: 'flex', alignItems: 'center', justifyContent: 'center', borderRadius: 12 }}>
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 12 }}>
@@ -407,6 +408,7 @@ const NewDashboard = (props) => {
</div>
</div>
)}
{/* Header / Greeting */}
<Box style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', margin: '8px 0 16px 0' }}>
<Typography variant="h5">{`${getGreeting()}, ${displayName ?? 'User'}!`}</Typography>
+1 -1
View File
@@ -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) => {
+1 -1
View File
@@ -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",
+3 -3
View File
@@ -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 &&
<Tab
label={`Background Processes`}
label={`Security Bundle`}
value={4}
style={{
...tabStyle,