cloud frontend sync
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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("");
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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>
|
||||
|
||||
+104
-149
@@ -43,6 +43,8 @@ const ChatBot = (props) => {
|
||||
const [appname, setAppname] = useState("");
|
||||
const [threadId, setThreadId] = useState("");
|
||||
const [runId, setRunId] = useState("");
|
||||
const [responseId, setResponseId] = useState("");
|
||||
const [conversationId, setConversationId] = useState("");
|
||||
|
||||
// New state for thread management
|
||||
const [isLoadingThread, setIsLoadingThread] = useState(false);
|
||||
@@ -54,7 +56,7 @@ const ChatBot = (props) => {
|
||||
const [showAppSearch, setShowAppSearch] = useState(false);
|
||||
|
||||
// Get thread ID from URL params
|
||||
const { threadId: urlThreadId } = useParams();
|
||||
const { conversationId: urlConversationId } = useParams();
|
||||
let navigate = useNavigate();
|
||||
|
||||
const waitingMsg = "Processing..."
|
||||
@@ -93,24 +95,24 @@ const ChatBot = (props) => {
|
||||
}
|
||||
}, [appname])
|
||||
|
||||
// Load existing thread if threadId is in URL
|
||||
// Load existing conversation if conversationId is in URL
|
||||
useEffect(() => {
|
||||
if (urlThreadId && urlThreadId !== threadId) {
|
||||
loadExistingThread(urlThreadId);
|
||||
if (urlConversationId && urlConversationId !== conversationId) {
|
||||
loadExistingThread(urlConversationId);
|
||||
}
|
||||
}, [urlThreadId]);
|
||||
}, [urlConversationId]);
|
||||
|
||||
const loadExistingThread = (threadIdToLoad) => {
|
||||
const loadExistingThread = (conversationIdToLoad) => {
|
||||
setIsLoadingThread(true);
|
||||
setThreadError("");
|
||||
|
||||
fetch(`${globalUrl}/api/v1/conversation/thread`, {
|
||||
fetch(`${globalUrl}/api/v1/conversation/history`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify({ thread_id: threadIdToLoad }),
|
||||
body: JSON.stringify({ conversation_id: conversationIdToLoad }),
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status === 403) {
|
||||
@@ -138,22 +140,22 @@ const ChatBot = (props) => {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set thread data - this is crucial for continuing the conversation
|
||||
setThreadId(data.thread_id);
|
||||
console.log("Loaded existing thread:", data.thread_id);
|
||||
// Set conversation data - this is crucial for continuing the conversation
|
||||
setConversationId(data.conversation_id);
|
||||
|
||||
// Transform API message format to UI format
|
||||
const transformedMessages = (data.messages || []).map((msg, index) => ({
|
||||
id: `${data.thread_id}_${index}`,
|
||||
id: `${data.conversation_id}_${index}`,
|
||||
status: msg.role === "user" ? "sent" : "received",
|
||||
message: msg.content,
|
||||
timestamp: msg.timestamp
|
||||
}));
|
||||
|
||||
setMessages(transformedMessages);
|
||||
setThreadOrgId(data.thread_org_id);
|
||||
setThreadOrgId(data.org_id);
|
||||
setIsActiveOrg(data.is_active_org);
|
||||
|
||||
// Disable chat if not in active org
|
||||
if (!data.is_active_org) {
|
||||
setChatDisabled(true);
|
||||
} else {
|
||||
@@ -260,12 +262,9 @@ const ChatBot = (props) => {
|
||||
const sentId = uuidv4();
|
||||
var parsedData = {
|
||||
"query": inputmsg,
|
||||
"thread_id": threadId,
|
||||
"run_id": runId,
|
||||
"conversation_id": conversationId,
|
||||
}
|
||||
|
||||
console.log("Sending message with thread_id:", threadId);
|
||||
|
||||
if (appname !== undefined && appname !== null && appname !== "") {
|
||||
parsedData["app_name"] = appname
|
||||
}
|
||||
@@ -315,169 +314,125 @@ const ChatBot = (props) => {
|
||||
|
||||
setMessages(newmessages);
|
||||
|
||||
//fetch(`http://localhost:8080/api/v1/conversation`, {
|
||||
fetch(`${globalUrl}/api/v1/conversation`, {
|
||||
// Use streaming endpoint
|
||||
fetch(`${globalUrl}/api/v1/conversation/stream`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "text/event-stream",
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify(parsedData),
|
||||
})
|
||||
.then((res) => res.text())
|
||||
.then((resText) => {
|
||||
setLoading(false)
|
||||
var data = {}
|
||||
|
||||
// JSON parse
|
||||
try {
|
||||
data = JSON.parse(resText);
|
||||
} catch (e) {
|
||||
console.log("Error parsing response as JSON: ", e);
|
||||
|
||||
newmessages = newmessages.filter((msg) => msg.message !== waitingMsg);
|
||||
newmessages.push({
|
||||
"status": "received",
|
||||
"message": resText,
|
||||
"id": uuidv4(),
|
||||
});
|
||||
|
||||
setMessages(newmessages);
|
||||
|
||||
return;
|
||||
.then(async (response) => {
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Server returned status ${response.status}: ${errorText}`);
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
let streamedText = "";
|
||||
let newConversationId = "";
|
||||
|
||||
if (data.run_id !== undefined && data.run_id !== null && data.run_id !== "") {
|
||||
setRunId(data.run_id)
|
||||
}
|
||||
// Keep waiting message until first chunk arrives
|
||||
const streamMessageId = uuidv4();
|
||||
let firstChunkReceived = false;
|
||||
|
||||
if (data.thread_id !== undefined && data.thread_id !== null && data.thread_id !== "") {
|
||||
setThreadId(data.thread_id)
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
// Update URL if this is a new thread (not already in URL)
|
||||
if (!urlThreadId && data.thread_id !== threadId) {
|
||||
console.log("Navigating to new thread URL:", data.thread_id);
|
||||
navigate(`/chat/${data.thread_id}`, { replace: true });
|
||||
if (done) {
|
||||
setLoading(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (data.success === undefined) {
|
||||
newmessages = newmessages.filter((msg) => msg.message !== waitingMsg);
|
||||
newmessages.push({
|
||||
"status": "received",
|
||||
"message": resText,
|
||||
"id": uuidv4(),
|
||||
});
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || ""; // Keep incomplete line in buffer
|
||||
|
||||
setMessages(newmessages);
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
const jsonStr = line.replace('data: ', '');
|
||||
try {
|
||||
const data = JSON.parse(jsonStr);
|
||||
const eventType = data.type;
|
||||
|
||||
return;
|
||||
}
|
||||
if (eventType === "chunk") {
|
||||
// On first chunk, remove waiting message and add streaming message
|
||||
if (!firstChunkReceived) {
|
||||
newmessages = newmessages.filter((msg) => msg.message !== waitingMsg);
|
||||
newmessages.push({
|
||||
"id": streamMessageId,
|
||||
"status": "received",
|
||||
"message": "",
|
||||
});
|
||||
firstChunkReceived = true;
|
||||
}
|
||||
|
||||
// authentication for app
|
||||
// app validation (choose one)
|
||||
const defaultMessage = `Default output. The feature you're interacting with may not have been implemented yet. Contact ${supportEmail} with a screenshot of this and your input please.`
|
||||
|
||||
var outputmessage = defaultMessage;
|
||||
var status = "received";
|
||||
var action = ""
|
||||
if (data.success === false) {
|
||||
if (data.reason !== undefined) {
|
||||
outputmessage = data.reason
|
||||
}
|
||||
// Append chunk to streamed text
|
||||
streamedText += data.chunk || "";
|
||||
|
||||
// Update the message with accumulated text
|
||||
newmessages = newmessages.map(msg =>
|
||||
msg.id === streamMessageId
|
||||
? { ...msg, message: streamedText }
|
||||
: msg
|
||||
);
|
||||
setMessages([...newmessages]);
|
||||
|
||||
status = "error"
|
||||
} else {
|
||||
if (data.reason !== undefined) {
|
||||
outputmessage = data.reason
|
||||
}
|
||||
}
|
||||
} else if (eventType === "done") {
|
||||
// Capture conversation ID for next turn
|
||||
newConversationId = data.data;
|
||||
if (newConversationId) {
|
||||
setConversationId(newConversationId);
|
||||
|
||||
// Update URL if this is a new conversation
|
||||
if (!urlConversationId && newConversationId !== conversationId) {
|
||||
navigate(`/chat/${newConversationId}`, { replace: true });
|
||||
}
|
||||
}
|
||||
|
||||
if (data.action !== undefined) {
|
||||
//console.log("Action is defined: ", data.action);
|
||||
action = data.action
|
||||
|
||||
if (data.action === "app_authentication") {
|
||||
// If success & app auth -> say auth success and show available labels
|
||||
// If !success & app auth -> do authentication
|
||||
if (data.success === true) {
|
||||
newmessages = newmessages.filter((msg) => msg.message !== waitingMsg);
|
||||
// No action for this.
|
||||
// "action": action,
|
||||
var appname = ""
|
||||
if (data.apps !== undefined && data.apps !== null && data.apps.length > 0) {
|
||||
appname = data.apps[0].name.replaceAll("_", " ")
|
||||
}
|
||||
|
||||
var outputmessage = `**Please specify which ${appname} action you want to use**: \n`
|
||||
if (data.available_labels !== undefined && data.available_labels !== null && data.available_labels.length > 0) {
|
||||
for (var i = 0; i < data.available_labels.length; i++) {
|
||||
outputmessage += "* " + data.available_labels[i] + "\n"
|
||||
} else if (eventType === "error") {
|
||||
// Handle error event
|
||||
const errorMsg = data.data || "An error occurred";
|
||||
console.error("[Stream Error]:", errorMsg);
|
||||
|
||||
newmessages = newmessages.map(msg =>
|
||||
msg.id === streamMessageId
|
||||
? { ...msg, status: "error", message: errorMsg }
|
||||
: msg
|
||||
);
|
||||
setMessages([...newmessages]);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
outputmessage += "* Reauthenticate ([see auth](/admin?tab=app_auth))"
|
||||
}
|
||||
//Some opavailable actions: " + data.apps.map((app) => app.name).join(", ")
|
||||
const parsedmessage = {
|
||||
"status": status,
|
||||
"message": outputmessage,
|
||||
"id": uuidv4(),
|
||||
"category": data.category,
|
||||
|
||||
"thread_id": data.thread_id,
|
||||
"run_id": data.run_id,
|
||||
}
|
||||
newmessages.push(parsedmessage);
|
||||
setMessages(newmessages);
|
||||
|
||||
return
|
||||
|
||||
} else {
|
||||
if (data.apps !== undefined) {
|
||||
setInputAuth(data.apps)
|
||||
|
||||
setMessage(inputmsg);
|
||||
|
||||
setForceReauthentication(true)
|
||||
} catch (e) {
|
||||
console.error("[ERROR] Failed to parse SSE data:", e, jsonStr);
|
||||
}
|
||||
}
|
||||
} else if (data.action === "select_category" || data.action === "select_app") {
|
||||
console.log("[DEBUG] APP SELECTION! Should help them choose an app to use")
|
||||
// Show a search field
|
||||
setShowAppSearch(true)
|
||||
}
|
||||
}
|
||||
|
||||
newmessages = newmessages.filter((msg) => msg.message !== waitingMsg);
|
||||
const parsedmessage = {
|
||||
"status": status,
|
||||
"message": outputmessage,
|
||||
"id": uuidv4(),
|
||||
"action": action,
|
||||
"category": data.category,
|
||||
|
||||
"thread_id": data.thread_id,
|
||||
"run_id": data.run_id,
|
||||
}
|
||||
newmessages.push(parsedmessage);
|
||||
setMessages(newmessages);
|
||||
console.log("New message: ", parsedmessage)
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((err) => {
|
||||
setLoading(false)
|
||||
console.log("Problem: ", err);
|
||||
|
||||
setMessage(message);
|
||||
setMessage(inputmsg);
|
||||
newmessages = newmessages.filter((msg) => msg.message !== waitingMsg);
|
||||
|
||||
// Find the message with the sentId and change the status to error
|
||||
// Add error message
|
||||
newmessages.push({
|
||||
"status": "error",
|
||||
"message": message,
|
||||
"error_message": "Failed to send: "+err,
|
||||
"message": inputmsg,
|
||||
"error_message": "Failed to send: "+err.message,
|
||||
"id": sentId,
|
||||
});
|
||||
setMessages(newmessages);
|
||||
setMessages([...newmessages]);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -654,21 +609,21 @@ const ChatBot = (props) => {
|
||||
<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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -633,7 +633,7 @@ const EditWorkflow = (props) => {
|
||||
<AutoAwesomeIcon style={{ marginRight: 8 }} />
|
||||
)}
|
||||
|
||||
AI Generate
|
||||
AI Generate (beta)
|
||||
</Button>
|
||||
</span>
|
||||
</Tooltip>
|
||||
|
||||
@@ -127,7 +127,7 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
|
||||
setCurrentSelectedTheme(userdata?.theme);
|
||||
}
|
||||
}, [userdata]);
|
||||
|
||||
|
||||
|
||||
|
||||
const CustomPopper = (props) => {
|
||||
@@ -206,7 +206,11 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
|
||||
setOpenAutocomplete(false)
|
||||
}}
|
||||
>
|
||||
Add suborg
|
||||
<Button variant="outlined" startIcon={<AddIcon />} sx={{
|
||||
textTransform: 'none',
|
||||
}}>
|
||||
Add suborg
|
||||
</Button>
|
||||
</Box>
|
||||
</Link>
|
||||
</Popper>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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, {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -13,9 +13,9 @@ const PartnerApps = () => {
|
||||
minHeight: "500px",
|
||||
}}
|
||||
>
|
||||
<Typography>Partner Apps</Typography>
|
||||
<Typography>Partner's Apps</Typography>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default PartnerApps
|
||||
export default PartnerApps
|
||||
File diff suppressed because one or more lines are too long
@@ -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 (
|
||||
|
||||
@@ -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,9 +14,9 @@ const PartnersArticles = () => {
|
||||
minHeight: "500px",
|
||||
}}
|
||||
>
|
||||
<Typography>Partner Articles</Typography>
|
||||
<Typography>{tabName}</Typography>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
export default PartnersArticles
|
||||
export default PartnersArticles
|
||||
@@ -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" }
|
||||
];
|
||||
|
||||
@@ -505,7 +876,9 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
||||
description: usecase.mainContent?.description,
|
||||
navigation: usecase.navigation || { items: [] },
|
||||
categories: usecase.mainContent?.categories || [],
|
||||
public: usecase?.public || false,
|
||||
public: usecase?.public || false,
|
||||
created: usecase?.created || null,
|
||||
headerImage: getFirstImageFromNavigation(usecase.navigation),
|
||||
}));
|
||||
if (usecases.length > 0) {
|
||||
setUsecaseData(usecases);
|
||||
@@ -526,7 +899,195 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
||||
// Sample categories
|
||||
const categoryOptions = ["Collect","Enrich", "Detect", "Respond", "Verify"];
|
||||
|
||||
// Uploading image to the public bucket
|
||||
const handleImageUpload = async (imageData) => {
|
||||
const folderName = "usecase_images";
|
||||
const usecaseId = formData?.id || partnerData?.id || "usecase";
|
||||
|
||||
try {
|
||||
const response = await fetch(`${globalUrl}/api/v1/image_upload`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify({
|
||||
imageData,
|
||||
folder: folderName,
|
||||
id: usecaseId,
|
||||
}),
|
||||
});
|
||||
|
||||
// if (response.status !== 200) {
|
||||
// toast.error("Failed to upload image" + response?.reason);
|
||||
// return null;
|
||||
// }
|
||||
|
||||
const responseJson = await response.json();
|
||||
if (!responseJson?.success || !responseJson?.url) {
|
||||
toast.error("Failed to upload image: " + responseJson?.reason);
|
||||
return null;
|
||||
}
|
||||
|
||||
return responseJson.url;
|
||||
} catch (error) {
|
||||
toast.error("Failed to upload image" + error?.message);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// This function is used to update the content with the public image url
|
||||
const updateContentWithImageUrl = (itemIndex, contentIndex, imageUrl) => {
|
||||
setFormData((prevData) => {
|
||||
const newItems = [...(prevData.navigation.items || [])];
|
||||
const item = newItems[itemIndex];
|
||||
if (!item) {
|
||||
return prevData;
|
||||
}
|
||||
|
||||
const contents = [...(item.content || [])];
|
||||
const existingContent = contents[contentIndex] || "";
|
||||
const markdownImage = imageUrl ? `` : "";
|
||||
|
||||
const placeholderIndex = existingContent.indexOf(IMAGE_UPLOAD_PLACEHOLDER);
|
||||
if (placeholderIndex !== -1) {
|
||||
const before = existingContent.slice(0, placeholderIndex);
|
||||
const after = existingContent.slice(
|
||||
placeholderIndex + IMAGE_UPLOAD_PLACEHOLDER.length,
|
||||
);
|
||||
|
||||
const trimmedBefore = before.replace(/\s*$/, "");
|
||||
const needsNewline =
|
||||
trimmedBefore.length > 0 && !trimmedBefore.endsWith("\n");
|
||||
const separator = needsNewline ? "\n" : "";
|
||||
|
||||
contents[contentIndex] = `${trimmedBefore}${separator}${markdownImage}${after}`;
|
||||
} else {
|
||||
const needsNewline =
|
||||
existingContent.length > 0 && !existingContent.endsWith("\n");
|
||||
const separator = needsNewline ? "\n" : "";
|
||||
|
||||
contents[contentIndex] = `${existingContent}${separator}${markdownImage}`;
|
||||
}
|
||||
|
||||
newItems[itemIndex] = {
|
||||
...item,
|
||||
content: contents,
|
||||
};
|
||||
|
||||
return {
|
||||
...prevData,
|
||||
navigation: {
|
||||
items: newItems,
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// This function is used to insert the image placeholder (Uploading image...) in the content to show that the image is being uploaded
|
||||
const insertImagePlaceholder = (itemIndex, contentIndex) => {
|
||||
setFormData((prevData) => {
|
||||
const newItems = [...(prevData.navigation.items || [])];
|
||||
const item = newItems[itemIndex];
|
||||
if (!item) {
|
||||
return prevData;
|
||||
}
|
||||
|
||||
const contents = [...(item.content || [])];
|
||||
const existingContent = contents[contentIndex] || "";
|
||||
const needsNewline =
|
||||
existingContent.length > 0 && !existingContent.endsWith("\n");
|
||||
const separator = needsNewline ? "\n" : "";
|
||||
|
||||
contents[contentIndex] = `${existingContent}${separator}${IMAGE_UPLOAD_PLACEHOLDER}`;
|
||||
|
||||
newItems[itemIndex] = {
|
||||
...item,
|
||||
content: contents,
|
||||
};
|
||||
|
||||
return {
|
||||
...prevData,
|
||||
navigation: {
|
||||
items: newItems,
|
||||
},
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
// This function is used to process the image file and upload it to the public bucket
|
||||
const processImageFile = (imageFile, itemIndex, contentIndex) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = async (loadEvent) => {
|
||||
const imageData = loadEvent.target?.result;
|
||||
if (!imageData) {
|
||||
toast.error("Failed to read image");
|
||||
updateContentWithImageUrl(itemIndex, contentIndex, "");
|
||||
return;
|
||||
}
|
||||
|
||||
const imageUrl = await handleImageUpload(imageData);
|
||||
if (!imageUrl) {
|
||||
// Clean up the placeholder if upload failed
|
||||
updateContentWithImageUrl(itemIndex, contentIndex, "");
|
||||
return;
|
||||
}
|
||||
|
||||
updateContentWithImageUrl(itemIndex, contentIndex, imageUrl);
|
||||
};
|
||||
reader.readAsDataURL(imageFile);
|
||||
};
|
||||
|
||||
// This function is used to handle the image paste event
|
||||
const handleContentPaste = async (event, itemIndex, contentIndex) => {
|
||||
const items = event.clipboardData?.items;
|
||||
if (!items || items.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
let imageFile = null;
|
||||
for (let i = 0; i < items.length; i += 1) {
|
||||
const item = items[i];
|
||||
if (item.kind === "file" && item.type.startsWith("image/")) {
|
||||
imageFile = item.getAsFile();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!imageFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
insertImagePlaceholder(itemIndex, contentIndex);
|
||||
processImageFile(imageFile, itemIndex, contentIndex);
|
||||
};
|
||||
|
||||
// This function is used to handle the image drop event
|
||||
const handleContentDrop = async (event, itemIndex, contentIndex) => {
|
||||
event.preventDefault();
|
||||
|
||||
const files = event.dataTransfer?.files;
|
||||
if (!files || files.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const imageFile = Array.from(files).find((file) => file.type.startsWith("image/"));
|
||||
if (!imageFile) {
|
||||
toast.error("Failed to upload image, only images are supported");
|
||||
return;
|
||||
}
|
||||
|
||||
insertImagePlaceholder(itemIndex, contentIndex);
|
||||
processImageFile(imageFile, itemIndex, contentIndex);
|
||||
};
|
||||
|
||||
const getUserProfileWorkflows = (orgId) => {
|
||||
if (selectedTab === "articles") {
|
||||
return;
|
||||
}
|
||||
setIsWorkflowLoading(true);
|
||||
fetch(`${globalUrl}/api/v1/partners/${orgId}/workflows`, {
|
||||
method: "GET",
|
||||
@@ -713,12 +1274,13 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
||||
navigation: usecase?.navigation || {
|
||||
items: [
|
||||
{
|
||||
name: "About Usecase",
|
||||
name: `About ${entityLabel}`,
|
||||
content: [""],
|
||||
},
|
||||
],
|
||||
},
|
||||
public: usecase?.public || false,
|
||||
created: usecase?.created || Math.floor(Date.now() / 1000),
|
||||
});
|
||||
setOpenDialog(true);
|
||||
};
|
||||
@@ -753,15 +1315,15 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
||||
const validationErrors = [];
|
||||
|
||||
// Check for public workflow selection
|
||||
if (!formData.mainContent.publicWorkflowId || formData.mainContent.publicWorkflowId.trim() === "") {
|
||||
if (!isArticlesTab && (!formData.mainContent.publicWorkflowId || formData.mainContent.publicWorkflowId.trim() === "")) {
|
||||
validationErrors.push("Please select a public workflow");
|
||||
}
|
||||
|
||||
if (!formData.mainContent.sourceAppType || formData.mainContent.sourceAppType.trim() === "") {
|
||||
if (!isArticlesTab && (!formData.mainContent.sourceAppType || formData.mainContent.sourceAppType.trim() === "")) {
|
||||
validationErrors.push("Source app type is required");
|
||||
}
|
||||
|
||||
if (!formData.mainContent.destinationAppType || formData.mainContent.destinationAppType.trim() === "") {
|
||||
if (!isArticlesTab && (!formData.mainContent.destinationAppType || formData.mainContent.destinationAppType.trim() === "")) {
|
||||
validationErrors.push("Destination app type is required");
|
||||
}
|
||||
|
||||
@@ -783,7 +1345,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
||||
}
|
||||
|
||||
// Check categories
|
||||
if (!formData.mainContent.categories || !Array.isArray(formData.mainContent.categories) || formData.mainContent.categories.length === 0) {
|
||||
if (!isArticlesTab && (!formData.mainContent.categories || !Array.isArray(formData.mainContent.categories) || formData.mainContent.categories.length === 0)) {
|
||||
validationErrors.push("At least one category must be selected");
|
||||
}
|
||||
|
||||
@@ -846,7 +1408,6 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
||||
return res.json();
|
||||
})
|
||||
.then((responseData) => {
|
||||
console.log("Response data:", responseData);
|
||||
// Reset loading state
|
||||
setIsSubmitting(false);
|
||||
|
||||
@@ -863,7 +1424,9 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
||||
description: formData.mainContent?.description.trim(),
|
||||
categories: formData.mainContent?.categories || [],
|
||||
navigation: formData.navigation || {},
|
||||
public: formData?.public || false,
|
||||
public: formData?.public || false,
|
||||
created: formData?.created || null,
|
||||
headerImage: getFirstImageFromNavigation(formData.navigation),
|
||||
}
|
||||
|
||||
setUsecaseData((prevData) => {
|
||||
@@ -921,6 +1484,28 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
||||
});
|
||||
};
|
||||
|
||||
// This is used to filter the usecases based on the selected tab
|
||||
const filteredUsecases = Array.isArray(usecaseData)
|
||||
? usecaseData.filter((usecase) => {
|
||||
const hasWorkflowId =
|
||||
!!usecase.publicWorkflowId &&
|
||||
usecase.publicWorkflowId.toString().trim() !== "";
|
||||
|
||||
if (selectedTab === "articles") {
|
||||
// Articles: only items without a public workflow ID
|
||||
return !hasWorkflowId;
|
||||
}
|
||||
|
||||
if (selectedTab === "usecases") {
|
||||
// Usecases: only items with a public workflow ID
|
||||
return hasWorkflowId;
|
||||
}
|
||||
|
||||
// Fallback: show all
|
||||
return true;
|
||||
})
|
||||
: [];
|
||||
|
||||
return (
|
||||
<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>
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -1,7 +1,25 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { Typography, ToggleButton, ToggleButtonGroup, Select, MenuItem, FormControl, InputLabel, Box } from '@mui/material';
|
||||
import { makeStyles, } from "@mui/styles";
|
||||
import {
|
||||
Typography,
|
||||
ToggleButton,
|
||||
ToggleButtonGroup,
|
||||
Select,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Box,
|
||||
Autocomplete,
|
||||
TextField,
|
||||
Tooltip,
|
||||
IconButton,
|
||||
} from '@mui/material';
|
||||
import { BarChart, BarSeries, Bar, GridlineSeries, Gridline, TooltipArea, ChartTooltip, LinearYAxis, LinearYAxisTickSeries, LinearYAxisTickLabel } from 'reaviz';
|
||||
import theme from '../theme.jsx';
|
||||
import {green, yellow, red} from "../views/AngularWorkflow.jsx"
|
||||
import {
|
||||
OpenInNew as OpenInNewIcon,
|
||||
} from '@mui/icons-material';
|
||||
|
||||
// Compact number formatter for axis ticks (e.g. 12,000 -> 12k, 12,000,000 -> 12M)
|
||||
function formatCompactNumber(value) {
|
||||
@@ -59,34 +77,128 @@ function computeTodayValueForOrg(key, orgStats) {
|
||||
const RunsOverTimeWidget = (props) => {
|
||||
const { globalUrl, onLoadingChange, monthOverride, dummyMode, selectedOrganization, selectedOrgForStats, orgStats, orgForLimit, loadingSelectedOrgStats } = props;
|
||||
const [mode, setMode] = useState('workflows'); // 'apps' | 'workflows'
|
||||
const [selectedStatType, setSelectedStatType] = useState({
|
||||
"value": 'total_workflow_executions',
|
||||
"label": 'Workflows',
|
||||
"amount": 0,
|
||||
})
|
||||
const [series, setSeries] = useState([]);
|
||||
const [days, setDays] = useState(365); // aggregate to last 12 months by default
|
||||
const [selectedMonth, setSelectedMonth] = useState(null); // Date representing first day of target month, or null for yearly view
|
||||
const [statTypeOptions, setStatTypeOptions] = useState([])
|
||||
const [loading, setLoading] = useState(false);
|
||||
const useStyles = makeStyles({
|
||||
notchedOutline: {
|
||||
borderColor: "#FF8544 !important",
|
||||
},
|
||||
root: {
|
||||
"& .MuiAutocomplete-listbox": {
|
||||
border: "2px solid #FF8544",
|
||||
color: theme.palette.text.primary,
|
||||
fontSize: 18,
|
||||
"& li:nth-child(even)": {
|
||||
backgroundColor: "#CCC",
|
||||
},
|
||||
"& li:nth-child(odd)": {
|
||||
backgroundColor: "#FFF",
|
||||
},
|
||||
},
|
||||
},
|
||||
inputRoot: {
|
||||
color: theme.palette.text.primary,
|
||||
"&:hover .MuiOutlinedInput-notchedOutline": {
|
||||
borderColor: "#f86a3e",
|
||||
},
|
||||
},
|
||||
});
|
||||
const classes = useStyles();
|
||||
|
||||
// Helper: fetch time series for a specific statistics key
|
||||
const fetchSeriesForKey = async (key) => {
|
||||
|
||||
if (statTypeOptions?.length < 3) {
|
||||
var foundkeys = {}
|
||||
for (var key in orgStats) {
|
||||
if (!key.startsWith("total_")) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Remove total
|
||||
foundkeys[key] = orgStats[key]
|
||||
}
|
||||
|
||||
for (var foundKey in orgStats?.daily_statistics) {
|
||||
const dailyStats = orgStats?.daily_statistics[foundKey]
|
||||
|
||||
for (var additionKey in dailyStats?.additions) {
|
||||
const addition = dailyStats?.additions[additionKey];
|
||||
if (foundkeys[addition?.key] === undefined) {
|
||||
foundkeys[addition?.key] = addition?.value
|
||||
} else {
|
||||
foundkeys[addition?.key] += addition?.value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var newarray = []
|
||||
for (var key in foundkeys) {
|
||||
const foundkey = key
|
||||
|
||||
var parsedname = key
|
||||
if (parsedname?.startsWith("total_")) {
|
||||
parsedname = parsedname.replace("total_", "")
|
||||
}
|
||||
|
||||
if (parsedname?.startsWith("categorylabel")) {
|
||||
parsedname = parsedname.replace("categorylabel", "")
|
||||
}
|
||||
|
||||
parsedname = (parsedname.charAt(0).toUpperCase() + parsedname.substring(1)).replaceAll("_", " ")
|
||||
|
||||
newarray.push({
|
||||
"value": key,
|
||||
"label": parsedname,
|
||||
"amount": foundkeys[key],
|
||||
})
|
||||
}
|
||||
|
||||
if (newarray.length > 0) {
|
||||
setStatTypeOptions(newarray)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// If specific org is selected and pre-fetched stats are available, use them directly
|
||||
if (selectedOrgForStats && selectedOrgForStats !== 'ALL' && orgStats && Array.isArray(orgStats?.daily_statistics)) {
|
||||
|
||||
const dailyStats = orgStats.daily_statistics;
|
||||
const processedEntries = [];
|
||||
const cutoff = new Date();
|
||||
cutoff.setDate(cutoff.getDate() - days);
|
||||
|
||||
if (key.startsWith("total_")) {
|
||||
key = key.replace("total_", "")
|
||||
}
|
||||
|
||||
for (const day of dailyStats) {
|
||||
if (!day?.date) continue;
|
||||
const dayDate = new Date(day.date);
|
||||
if (dayDate < cutoff) continue;
|
||||
|
||||
let value = 0;
|
||||
if (key === 'workflow_executions') {
|
||||
const finished = Number(day?.workflow_executions_finished || 0);
|
||||
const failed = Number(day?.workflow_executions_failed || 0);
|
||||
value = finished + failed;
|
||||
} else {
|
||||
value = Number(day?.[key] || 0);
|
||||
}
|
||||
|
||||
if (day[key] === undefined) {
|
||||
for (var additionKey in day?.additions) {
|
||||
const addition = day?.additions[additionKey];
|
||||
if (addition?.key === key) {
|
||||
value += Number(addition?.value || 0);
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
value = Number(day[key] || 0);
|
||||
}
|
||||
|
||||
processedEntries.push({ date: day.date, value });
|
||||
}
|
||||
|
||||
@@ -185,7 +297,7 @@ function computeTodayValueForOrg(key, orgStats) {
|
||||
};
|
||||
|
||||
// Load and transform into monthly aggregation for last 12 months
|
||||
const load = async (curMode) => {
|
||||
const loadStats = async (curMode, inputType) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
// Clear current series immediately to avoid any visual overlap while switching views
|
||||
@@ -203,8 +315,10 @@ function computeTodayValueForOrg(key, orgStats) {
|
||||
setSeries(dummy);
|
||||
return;
|
||||
}
|
||||
const key = curMode === 'apps' ? 'app_executions' : 'workflow_executions';
|
||||
|
||||
const key = inputType !== undefined ? inputType : selectedStatType?.value?.length > 0 ? selectedStatType?.value : curMode === 'apps' ? 'app_executions' : 'workflow_executions';
|
||||
const entries = await fetchSeriesForKey(key);
|
||||
|
||||
// Normalize variants: {Date, Value} or {date, value}
|
||||
const normalized = (entries || []).map((d) => ({
|
||||
date: d?.Date ? new Date(d.Date) : (d?.date ? new Date(d.date) : new Date()),
|
||||
@@ -272,6 +386,23 @@ function computeTodayValueForOrg(key, orgStats) {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
const starterWidgetStatType = localStorage.getItem("runsOverTimeWidgetStatType")
|
||||
if (starterWidgetStatType) {
|
||||
setSelectedStatType({
|
||||
"value": starterWidgetStatType,
|
||||
"label": (starterWidgetStatType.charAt(0).toUpperCase() + starterWidgetStatType.substring(1)).replaceAll("_", " "),
|
||||
"amount": 0,
|
||||
})
|
||||
|
||||
loadStats(mode, starterWidgetStatType)
|
||||
}
|
||||
|
||||
}, 1500)
|
||||
}, [])
|
||||
|
||||
// Apply month override (e.g. onboarding Explore Now) - consolidated with main load effect
|
||||
useEffect(() => {
|
||||
if (monthOverride instanceof Date) {
|
||||
@@ -287,8 +418,9 @@ function computeTodayValueForOrg(key, orgStats) {
|
||||
setSeries([]);
|
||||
return;
|
||||
}
|
||||
load(mode);
|
||||
}, [mode, globalUrl, days, selectedMonth, dummyMode, selectedOrgForStats, loadingSelectedOrgStats]);
|
||||
|
||||
loadStats(mode);
|
||||
}, [selectedStatType, mode, globalUrl, days, selectedMonth, dummyMode, selectedOrgForStats, loadingSelectedOrgStats]);
|
||||
|
||||
// Notify parent on loading changes
|
||||
useEffect(() => {
|
||||
@@ -341,55 +473,125 @@ function computeTodayValueForOrg(key, orgStats) {
|
||||
}
|
||||
/>;
|
||||
|
||||
|
||||
const barColorscheme = [
|
||||
"#f85a3e", // anchor orange
|
||||
"#ff7a57", // brighter, more playful
|
||||
"#e14b2e", // slightly darker + redder
|
||||
"#ff9b6b", // soft peachy highlight
|
||||
"#c83f24", // deep burnt orange
|
||||
]
|
||||
|
||||
return (
|
||||
<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
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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)
|
||||
// }
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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")) {
|
||||
|
||||
@@ -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
@@ -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 ?
|
||||
(
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user