Merge pull request #1911 from yashsinghcodes/nightly
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) {
|
} else if (userdata && isOrgLoaded && isUserDataLoaded && userdata?.active_org?.role !== "admin" && !userdata?.support) {
|
||||||
const queryParams = new URLSearchParams(location.search);
|
const queryParams = new URLSearchParams(location.search);
|
||||||
const tabName = queryParams?.get('admin_tab')?.toLowerCase();
|
const tabName = queryParams?.get('admin_tab')?.toLowerCase();
|
||||||
if (tabName === "sso") {
|
// SSO tab is now accessible to non-admins (removed restriction)
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = new URLSearchParams(location.search);
|
const params = new URLSearchParams(location.search);
|
||||||
const tab = params?.get('tab')?.toLowerCase();
|
const tab = params?.get('tab')?.toLowerCase();
|
||||||
|
|||||||
@@ -1129,7 +1129,7 @@ const AppFramework = (props) => {
|
|||||||
}, [newSelectedApp])
|
}, [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;
|
const imgSize = 50;
|
||||||
var parsedFrameworkData = frameworkData === undefined ? {} : frameworkData
|
var parsedFrameworkData = frameworkData === undefined ? {} : frameworkData
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ const AppSelection = props => {
|
|||||||
document.title = "Choose your apps"
|
document.title = "Choose your apps"
|
||||||
const ref = useRef()
|
const ref = useRef()
|
||||||
let navigate = useNavigate();
|
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(() => {
|
useEffect(() => {
|
||||||
if (newSelectedApp === undefined || newSelectedApp.objectID === undefined || newSelectedApp.objectID === undefined || newSelectedApp.objectID.length === 0) {
|
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 { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, newSelectedApp, setNewSelectedApp, defaultSearch, showSearch, ConfiguredHits, userdata, cy, isCreatorPage, actionImageList, setActionImageList, setUserSpecialzedApp, inputHeight, } = props
|
||||||
const { themeMode } = useContext(Context)
|
const { themeMode } = useContext(Context)
|
||||||
const theme = getTheme(themeMode)
|
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 rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows
|
||||||
const xs = parsedXs === undefined || parsedXs === null ? 12 : parsedXs
|
const xs = parsedXs === undefined || parsedXs === null ? 12 : parsedXs
|
||||||
const [formMail, setFormMail] = React.useState("");
|
const [formMail, setFormMail] = React.useState("");
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import React, { useState, useEffect, useContext, useCallback } from 'react';
|
|||||||
import {getTheme} from '../theme.jsx';
|
import {getTheme} from '../theme.jsx';
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'
|
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 { toast } from "react-toastify"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -602,7 +602,7 @@ const AppStats = (defaultprops) => {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns: GridColDef[] = [
|
const columns = [
|
||||||
{
|
{
|
||||||
field: 'workflow.name',
|
field: 'workflow.name',
|
||||||
headerName: 'Workflow Name',
|
headerName: 'Workflow Name',
|
||||||
|
|||||||
@@ -37,11 +37,11 @@ import {
|
|||||||
Pagination,
|
Pagination,
|
||||||
PaginationItem,
|
PaginationItem,
|
||||||
Avatar,
|
Avatar,
|
||||||
|
ListSubheader,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DataGrid,
|
DataGrid,
|
||||||
GridColDef,
|
|
||||||
} from '@mui/x-data-grid';
|
} from '@mui/x-data-grid';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -138,6 +138,7 @@ const CacheView = memo((props) => {
|
|||||||
const [selectAllChecked, setSelectAllChecked] = React.useState(false)
|
const [selectAllChecked, setSelectAllChecked] = React.useState(false)
|
||||||
const [renderTextBox, setRenderTextBox] = React.useState(false);
|
const [renderTextBox, setRenderTextBox] = React.useState(false);
|
||||||
const [datastoreCategories, setDatastoreCategories] = React.useState(["default", "protected"]);
|
const [datastoreCategories, setDatastoreCategories] = React.useState(["default", "protected"]);
|
||||||
|
const [datastoreCategoryGroups, setDatastoreCategoryGroups] = React.useState([]);
|
||||||
const [selectedCategory, setSelectedCategory] = React.useState("default");
|
const [selectedCategory, setSelectedCategory] = React.useState("default");
|
||||||
const [selectedFileId, setSelectedFileId] = React.useState("");
|
const [selectedFileId, setSelectedFileId] = React.useState("");
|
||||||
const [updateToThisCategory, setUpdateToThisCategory] = useState("")
|
const [updateToThisCategory, setUpdateToThisCategory] = useState("")
|
||||||
@@ -206,13 +207,13 @@ const CacheView = memo((props) => {
|
|||||||
{
|
{
|
||||||
"name": "Run AI Agent",
|
"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",
|
"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": [{
|
"options": [{
|
||||||
"key": "",
|
"key": "",
|
||||||
"value": "",
|
"value": "",
|
||||||
}],
|
}],
|
||||||
"icon": <SmartToyIcon />,
|
"icon": <SmartToyIcon />,
|
||||||
"enabled": false,
|
"disabled": false,
|
||||||
"disabled": true,
|
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -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)
|
setDatastoreCategories(newcategories)
|
||||||
|
setDatastoreCategoryGroups(categorygroups)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (responseJson?.category_config !== undefined && responseJson?.category_config !== null) {
|
if (responseJson?.category_config !== undefined && responseJson?.category_config !== null) {
|
||||||
@@ -1070,7 +1095,6 @@ const CacheView = memo((props) => {
|
|||||||
saveAutomation(newAutomations)
|
saveAutomation(newAutomations)
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("AUTO: ", automation)
|
|
||||||
return (
|
return (
|
||||||
<Tooltip title={
|
<Tooltip title={
|
||||||
<Typography style={{margin: 10, }}>
|
<Typography style={{margin: 10, }}>
|
||||||
@@ -1094,7 +1118,7 @@ const CacheView = memo((props) => {
|
|||||||
style={{ marginRight: 10, marginTop: -10, }}
|
style={{ marginRight: 10, marginTop: -10, }}
|
||||||
checked={automation.enabled}
|
checked={automation.enabled}
|
||||||
// Check if automation options have a value
|
// 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) => {
|
onChange={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
e.preventDefault()
|
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',
|
field: 'key',
|
||||||
headerName: 'Key',
|
headerName: 'Key',
|
||||||
width: 200,
|
width: 200,
|
||||||
filterable: true,
|
filterable: true,
|
||||||
sortable: 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,
|
width: 540,
|
||||||
@@ -1919,7 +1963,7 @@ const CacheView = memo((props) => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
var previousgroup = ""
|
||||||
const isAutomating = categoryAutomations?.find((automation) => automation.enabled) !== undefined
|
const isAutomating = categoryAutomations?.find((automation) => automation.enabled) !== undefined
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -2072,56 +2116,53 @@ const CacheView = memo((props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
|
|
||||||
<ButtonGroup style={{marginTop: 0, }}>
|
<ButtonGroup style={{ position: "absolute", top: -6, }}>
|
||||||
{datastoreCategories !== undefined &&
|
{datastoreCategories !== undefined &&
|
||||||
datastoreCategories !== null &&
|
datastoreCategories !== null &&
|
||||||
datastoreCategories.length > 1 ? (
|
datastoreCategories.length > 1 ? (
|
||||||
|
|
||||||
<FormControl style={{ minWidth: 150, maxWidth: 150, marginTop: 8, }}>
|
<FormControl style={{ minWidth: 250, maxWidth: 250, marginTop: 8, }}>
|
||||||
<InputLabel id="category-choice" style={{
|
<Autocomplete
|
||||||
color: "rgba(255, 255, 255, 0.65)",
|
|
||||||
}}>
|
|
||||||
Category
|
|
||||||
</InputLabel>
|
|
||||||
<Select
|
|
||||||
labelId="category-choice"
|
labelId="category-choice"
|
||||||
style={{
|
style={{
|
||||||
minWidth: 150,
|
minWidth: 250,
|
||||||
maxWidth: 150,
|
maxWidth: 250,
|
||||||
height: 35,
|
}}
|
||||||
borderRadius: "5px 0px 0px 5px",
|
ListboxProps={{
|
||||||
|
style: {
|
||||||
|
maxHeight: "70vh",
|
||||||
|
border: "1px solid rgba(255,255,255,0.3)",
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
value={selectedCategory}
|
value={selectedCategory}
|
||||||
onChange={(event) => {
|
options={datastoreCategories}
|
||||||
setCategoryConfig(undefined)
|
getOptionLabel={(data) => {
|
||||||
setCategoryAutomations(defaultAutomation)
|
const fixedname = data?.charAt(0)?.toUpperCase() + data?.slice(1)?.replaceAll("_", " ")
|
||||||
|
return fixedname
|
||||||
//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}`)
|
|
||||||
}
|
|
||||||
}}
|
}}
|
||||||
>
|
groupBy={(data) => {
|
||||||
{datastoreCategories.map((data, index) => {
|
if (!data.includes("_")) {
|
||||||
// Should find the icon for things
|
return undefined
|
||||||
// Fix uppercase at start of words
|
}
|
||||||
|
|
||||||
|
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 fixedname = data?.charAt(0)?.toUpperCase() + data?.slice(1)?.replaceAll("_", " ")
|
||||||
const iconDetails = GetIconInfo({
|
const iconDetails = GetIconInfo({
|
||||||
"app_name": fixedname,
|
"app_name": fixedname,
|
||||||
@@ -2130,13 +2171,40 @@ const CacheView = memo((props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
key={index}
|
key={data}
|
||||||
value={data}
|
value={data}
|
||||||
style={{
|
style={{
|
||||||
color: theme.palette.textFieldStyle.color,
|
color: theme.palette.textFieldStyle.color,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
borderBottom: theme.palette.defaultBorder,
|
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, }}>
|
<Typography style={{display: "flex", marginTop: 5, }}>
|
||||||
<div style={{marginRight: 10, }}>
|
<div style={{marginRight: 10, }}>
|
||||||
@@ -2148,9 +2216,69 @@ const CacheView = memo((props) => {
|
|||||||
{fixedname}
|
{fixedname}
|
||||||
</Typography>
|
</Typography>
|
||||||
</MenuItem>
|
</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>
|
</FormControl>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
@@ -2159,7 +2287,7 @@ const CacheView = memo((props) => {
|
|||||||
<Tooltip title={"Close"} style={{}} aria-label={""}>
|
<Tooltip title={"Close"} style={{}} aria-label={""}>
|
||||||
<Button
|
<Button
|
||||||
style={{
|
style={{
|
||||||
height: 35,
|
height: 40,
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
textTransform: 'none',
|
textTransform: 'none',
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
@@ -2178,14 +2306,13 @@ const CacheView = memo((props) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
:
|
:
|
||||||
<Tooltip title={"Add or find category"} style={{}} aria-label={""}>
|
<Tooltip title={"Add new category object"} style={{}} aria-label={""}>
|
||||||
<Button
|
<Button
|
||||||
style={{
|
style={{
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
width: datastoreCategories !== undefined && datastoreCategories !== null && datastoreCategories.length > 1 ? 50 : 169,
|
width: datastoreCategories !== undefined && datastoreCategories !== null && datastoreCategories.length > 1 ? 50 : 169,
|
||||||
height: 35,
|
height: 40,
|
||||||
textTransform: 'none',
|
textTransform: 'none',
|
||||||
fontSize: 16,
|
|
||||||
|
|
||||||
borderRadius: "0px 5px 5px 0px",
|
borderRadius: "0px 5px 5px 0px",
|
||||||
}}
|
}}
|
||||||
@@ -2216,14 +2343,14 @@ const CacheView = memo((props) => {
|
|||||||
|
|
||||||
}}
|
}}
|
||||||
style={{
|
style={{
|
||||||
height: 35,
|
height: 40,
|
||||||
width: 200,
|
width: 200,
|
||||||
marginTop: 0,
|
marginTop: 0,
|
||||||
}}
|
}}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
style: {
|
style: {
|
||||||
color: theme.palette.textFieldStyle.color,
|
color: theme.palette.textFieldStyle.color,
|
||||||
height: 35,
|
height: 40,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
paddingTop: 0,
|
paddingTop: 0,
|
||||||
@@ -2408,7 +2535,7 @@ const CacheView = memo((props) => {
|
|||||||
Subscribing
|
Subscribing
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" style={{ color: theme.palette.text.secondary, marginBottom: 20 }}>
|
<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>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+106
-151
@@ -43,6 +43,8 @@ const ChatBot = (props) => {
|
|||||||
const [appname, setAppname] = useState("");
|
const [appname, setAppname] = useState("");
|
||||||
const [threadId, setThreadId] = useState("");
|
const [threadId, setThreadId] = useState("");
|
||||||
const [runId, setRunId] = useState("");
|
const [runId, setRunId] = useState("");
|
||||||
|
const [responseId, setResponseId] = useState("");
|
||||||
|
const [conversationId, setConversationId] = useState("");
|
||||||
|
|
||||||
// New state for thread management
|
// New state for thread management
|
||||||
const [isLoadingThread, setIsLoadingThread] = useState(false);
|
const [isLoadingThread, setIsLoadingThread] = useState(false);
|
||||||
@@ -54,7 +56,7 @@ const ChatBot = (props) => {
|
|||||||
const [showAppSearch, setShowAppSearch] = useState(false);
|
const [showAppSearch, setShowAppSearch] = useState(false);
|
||||||
|
|
||||||
// Get thread ID from URL params
|
// Get thread ID from URL params
|
||||||
const { threadId: urlThreadId } = useParams();
|
const { conversationId: urlConversationId } = useParams();
|
||||||
let navigate = useNavigate();
|
let navigate = useNavigate();
|
||||||
|
|
||||||
const waitingMsg = "Processing..."
|
const waitingMsg = "Processing..."
|
||||||
@@ -93,24 +95,24 @@ const ChatBot = (props) => {
|
|||||||
}
|
}
|
||||||
}, [appname])
|
}, [appname])
|
||||||
|
|
||||||
// Load existing thread if threadId is in URL
|
// Load existing conversation if conversationId is in URL
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (urlThreadId && urlThreadId !== threadId) {
|
if (urlConversationId && urlConversationId !== conversationId) {
|
||||||
loadExistingThread(urlThreadId);
|
loadExistingThread(urlConversationId);
|
||||||
}
|
}
|
||||||
}, [urlThreadId]);
|
}, [urlConversationId]);
|
||||||
|
|
||||||
const loadExistingThread = (threadIdToLoad) => {
|
const loadExistingThread = (conversationIdToLoad) => {
|
||||||
setIsLoadingThread(true);
|
setIsLoadingThread(true);
|
||||||
setThreadError("");
|
setThreadError("");
|
||||||
|
|
||||||
fetch(`${globalUrl}/api/v1/conversation/thread`, {
|
fetch(`${globalUrl}/api/v1/conversation/history`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
body: JSON.stringify({ thread_id: threadIdToLoad }),
|
body: JSON.stringify({ conversation_id: conversationIdToLoad }),
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
if (response.status === 403) {
|
if (response.status === 403) {
|
||||||
@@ -138,22 +140,22 @@ const ChatBot = (props) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set thread data - this is crucial for continuing the conversation
|
// Set conversation data - this is crucial for continuing the conversation
|
||||||
setThreadId(data.thread_id);
|
setConversationId(data.conversation_id);
|
||||||
console.log("Loaded existing thread:", data.thread_id);
|
|
||||||
|
|
||||||
// Transform API message format to UI format
|
// Transform API message format to UI format
|
||||||
const transformedMessages = (data.messages || []).map((msg, index) => ({
|
const transformedMessages = (data.messages || []).map((msg, index) => ({
|
||||||
id: `${data.thread_id}_${index}`,
|
id: `${data.conversation_id}_${index}`,
|
||||||
status: msg.role === "user" ? "sent" : "received",
|
status: msg.role === "user" ? "sent" : "received",
|
||||||
message: msg.content,
|
message: msg.content,
|
||||||
timestamp: msg.timestamp
|
timestamp: msg.timestamp
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setMessages(transformedMessages);
|
setMessages(transformedMessages);
|
||||||
setThreadOrgId(data.thread_org_id);
|
setThreadOrgId(data.org_id);
|
||||||
setIsActiveOrg(data.is_active_org);
|
setIsActiveOrg(data.is_active_org);
|
||||||
|
|
||||||
|
// Disable chat if not in active org
|
||||||
if (!data.is_active_org) {
|
if (!data.is_active_org) {
|
||||||
setChatDisabled(true);
|
setChatDisabled(true);
|
||||||
} else {
|
} else {
|
||||||
@@ -260,12 +262,9 @@ const ChatBot = (props) => {
|
|||||||
const sentId = uuidv4();
|
const sentId = uuidv4();
|
||||||
var parsedData = {
|
var parsedData = {
|
||||||
"query": inputmsg,
|
"query": inputmsg,
|
||||||
"thread_id": threadId,
|
"conversation_id": conversationId,
|
||||||
"run_id": runId,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("Sending message with thread_id:", threadId);
|
|
||||||
|
|
||||||
if (appname !== undefined && appname !== null && appname !== "") {
|
if (appname !== undefined && appname !== null && appname !== "") {
|
||||||
parsedData["app_name"] = appname
|
parsedData["app_name"] = appname
|
||||||
}
|
}
|
||||||
@@ -315,169 +314,125 @@ const ChatBot = (props) => {
|
|||||||
|
|
||||||
setMessages(newmessages);
|
setMessages(newmessages);
|
||||||
|
|
||||||
//fetch(`http://localhost:8080/api/v1/conversation`, {
|
// Use streaming endpoint
|
||||||
fetch(`${globalUrl}/api/v1/conversation`, {
|
fetch(`${globalUrl}/api/v1/conversation/stream`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "text/event-stream",
|
||||||
},
|
},
|
||||||
credentials: "include",
|
credentials: "include",
|
||||||
body: JSON.stringify(parsedData),
|
body: JSON.stringify(parsedData),
|
||||||
})
|
})
|
||||||
.then((res) => res.text())
|
.then(async (response) => {
|
||||||
.then((resText) => {
|
if (!response.ok) {
|
||||||
setLoading(false)
|
const errorText = await response.text();
|
||||||
var data = {}
|
throw new Error(`Server returned status ${response.status}: ${errorText}`);
|
||||||
|
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 !== "") {
|
// Keep waiting message until first chunk arrives
|
||||||
setRunId(data.run_id)
|
const streamMessageId = uuidv4();
|
||||||
}
|
let firstChunkReceived = false;
|
||||||
|
|
||||||
if (data.thread_id !== undefined && data.thread_id !== null && data.thread_id !== "") {
|
while (true) {
|
||||||
setThreadId(data.thread_id)
|
const { done, value } = await reader.read();
|
||||||
|
|
||||||
// Update URL if this is a new thread (not already in URL)
|
if (done) {
|
||||||
if (!urlThreadId && data.thread_id !== threadId) {
|
setLoading(false);
|
||||||
console.log("Navigating to new thread URL:", data.thread_id);
|
break;
|
||||||
navigate(`/chat/${data.thread_id}`, { replace: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.success === undefined) {
|
|
||||||
newmessages = newmessages.filter((msg) => msg.message !== waitingMsg);
|
|
||||||
newmessages.push({
|
|
||||||
"status": "received",
|
|
||||||
"message": resText,
|
|
||||||
"id": uuidv4(),
|
|
||||||
});
|
|
||||||
|
|
||||||
setMessages(newmessages);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// authentication for app
|
|
||||||
// app validation (choose one)
|
|
||||||
const defaultMessage = `Default output. The feature you're interacting with may not have been implemented yet. Contact ${supportEmail} with a screenshot of this and your input please.`
|
|
||||||
|
|
||||||
var outputmessage = defaultMessage;
|
|
||||||
var status = "received";
|
|
||||||
var action = ""
|
|
||||||
if (data.success === false) {
|
|
||||||
if (data.reason !== undefined) {
|
|
||||||
outputmessage = data.reason
|
|
||||||
}
|
}
|
||||||
|
|
||||||
status = "error"
|
buffer += decoder.decode(value, { stream: true });
|
||||||
} else {
|
const lines = buffer.split('\n');
|
||||||
if (data.reason !== undefined) {
|
buffer = lines.pop() || ""; // Keep incomplete line in buffer
|
||||||
outputmessage = data.reason
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.action !== undefined) {
|
for (const line of lines) {
|
||||||
//console.log("Action is defined: ", data.action);
|
if (line.startsWith('data: ')) {
|
||||||
action = data.action
|
const jsonStr = line.replace('data: ', '');
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(jsonStr);
|
||||||
|
const eventType = data.type;
|
||||||
|
|
||||||
if (data.action === "app_authentication") {
|
if (eventType === "chunk") {
|
||||||
// If success & app auth -> say auth success and show available labels
|
// On first chunk, remove waiting message and add streaming message
|
||||||
// If !success & app auth -> do authentication
|
if (!firstChunkReceived) {
|
||||||
if (data.success === true) {
|
newmessages = newmessages.filter((msg) => msg.message !== waitingMsg);
|
||||||
newmessages = newmessages.filter((msg) => msg.message !== waitingMsg);
|
newmessages.push({
|
||||||
// No action for this.
|
"id": streamMessageId,
|
||||||
// "action": action,
|
"status": "received",
|
||||||
var appname = ""
|
"message": "",
|
||||||
if (data.apps !== undefined && data.apps !== null && data.apps.length > 0) {
|
});
|
||||||
appname = data.apps[0].name.replaceAll("_", " ")
|
firstChunkReceived = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var outputmessage = `**Please specify which ${appname} action you want to use**: \n`
|
// Append chunk to streamed text
|
||||||
if (data.available_labels !== undefined && data.available_labels !== null && data.available_labels.length > 0) {
|
streamedText += data.chunk || "";
|
||||||
for (var i = 0; i < data.available_labels.length; i++) {
|
|
||||||
outputmessage += "* " + data.available_labels[i] + "\n"
|
// Update the message with accumulated text
|
||||||
|
newmessages = newmessages.map(msg =>
|
||||||
|
msg.id === streamMessageId
|
||||||
|
? { ...msg, message: streamedText }
|
||||||
|
: msg
|
||||||
|
);
|
||||||
|
setMessages([...newmessages]);
|
||||||
|
|
||||||
|
} else if (eventType === "done") {
|
||||||
|
// Capture conversation ID for next turn
|
||||||
|
newConversationId = data.data;
|
||||||
|
if (newConversationId) {
|
||||||
|
setConversationId(newConversationId);
|
||||||
|
|
||||||
|
// Update URL if this is a new conversation
|
||||||
|
if (!urlConversationId && newConversationId !== conversationId) {
|
||||||
|
navigate(`/chat/${newConversationId}`, { replace: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (eventType === "error") {
|
||||||
|
// Handle error event
|
||||||
|
const errorMsg = data.data || "An error occurred";
|
||||||
|
console.error("[Stream Error]:", errorMsg);
|
||||||
|
|
||||||
|
newmessages = newmessages.map(msg =>
|
||||||
|
msg.id === streamMessageId
|
||||||
|
? { ...msg, status: "error", message: errorMsg }
|
||||||
|
: msg
|
||||||
|
);
|
||||||
|
setMessages([...newmessages]);
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
outputmessage += "* Reauthenticate ([see auth](/admin?tab=app_auth))"
|
|
||||||
}
|
|
||||||
//Some opavailable actions: " + data.apps.map((app) => app.name).join(", ")
|
|
||||||
const parsedmessage = {
|
|
||||||
"status": status,
|
|
||||||
"message": outputmessage,
|
|
||||||
"id": uuidv4(),
|
|
||||||
"category": data.category,
|
|
||||||
|
|
||||||
"thread_id": data.thread_id,
|
} catch (e) {
|
||||||
"run_id": data.run_id,
|
console.error("[ERROR] Failed to parse SSE data:", e, jsonStr);
|
||||||
}
|
|
||||||
newmessages.push(parsedmessage);
|
|
||||||
setMessages(newmessages);
|
|
||||||
|
|
||||||
return
|
|
||||||
|
|
||||||
} else {
|
|
||||||
if (data.apps !== undefined) {
|
|
||||||
setInputAuth(data.apps)
|
|
||||||
|
|
||||||
setMessage(inputmsg);
|
|
||||||
|
|
||||||
setForceReauthentication(true)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} 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);
|
setLoading(false);
|
||||||
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)
|
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
console.log("Problem: ", err);
|
console.log("Problem: ", err);
|
||||||
|
|
||||||
setMessage(message);
|
setMessage(inputmsg);
|
||||||
newmessages = newmessages.filter((msg) => msg.message !== waitingMsg);
|
newmessages = newmessages.filter((msg) => msg.message !== waitingMsg);
|
||||||
|
|
||||||
// Find the message with the sentId and change the status to error
|
// Add error message
|
||||||
newmessages.push({
|
newmessages.push({
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"message": message,
|
"message": inputmsg,
|
||||||
"error_message": "Failed to send: "+err,
|
"error_message": "Failed to send: "+err.message,
|
||||||
"id": sentId,
|
"id": sentId,
|
||||||
});
|
});
|
||||||
setMessages(newmessages);
|
setMessages([...newmessages]);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -654,21 +609,21 @@ const ChatBot = (props) => {
|
|||||||
<Card style={{backgroundColor: theme.palette.surfaceColor, borderRadius: theme.palette?.borderRadius,}}>
|
<Card style={{backgroundColor: theme.palette.surfaceColor, borderRadius: theme.palette?.borderRadius,}}>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Typography variant="h6">
|
<Typography variant="h6">
|
||||||
How many incidents did we get last week?
|
What is the difference between a trigger and an action?
|
||||||
</Typography>
|
</Typography>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card style={{backgroundColor: theme.palette.surfaceColor, borderRadius: theme.palette?.borderRadius, marginTop: 10, }}>
|
<Card style={{backgroundColor: theme.palette.surfaceColor, borderRadius: theme.palette?.borderRadius, marginTop: 10, }}>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Typography variant="h6">
|
<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>
|
</Typography>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
<Card style={{backgroundColor: theme.palette.surfaceColor, borderRadius: theme.palette?.borderRadius, marginTop: 10, }}>
|
<Card style={{backgroundColor: theme.palette.surfaceColor, borderRadius: theme.palette?.borderRadius, marginTop: 10, }}>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Typography variant="h6">
|
<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>
|
</Typography>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -1007,9 +962,9 @@ const ChatBot = (props) => {
|
|||||||
padding: "0 20px"
|
padding: "0 20px"
|
||||||
}}>
|
}}>
|
||||||
{[
|
{[
|
||||||
"How many incidents did we get last week?",
|
"What is the difference between a trigger and an action?",
|
||||||
"Answer the last email from Jim about the new project",
|
"How do I create a new workflow?",
|
||||||
"Is the IP 1.2.3.4 blocked? If not, block it."
|
"Is there a way to mass abort executions of a specific workflow?"
|
||||||
].map((sample, index) => (
|
].map((sample, index) => (
|
||||||
<Card key={index} style={{
|
<Card key={index} style={{
|
||||||
backgroundColor: theme.palette.surfaceColor,
|
backgroundColor: theme.palette.surfaceColor,
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const EditOrgTab = (props) => {
|
|||||||
const [organizationFeatures, setOrganizationFeatures] = React.useState({});
|
const [organizationFeatures, setOrganizationFeatures] = React.useState({});
|
||||||
const [users, setUsers] = React.useState([]);
|
const [users, setUsers] = React.useState([]);
|
||||||
const [orgRequest, setOrgRequest] = React.useState(true);
|
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(() => {
|
useEffect(() => {
|
||||||
if(users.length === 0) {
|
if(users.length === 0) {
|
||||||
getUsers();
|
getUsers();
|
||||||
|
|||||||
@@ -633,7 +633,7 @@ const EditWorkflow = (props) => {
|
|||||||
<AutoAwesomeIcon style={{ marginRight: 8 }} />
|
<AutoAwesomeIcon style={{ marginRight: 8 }} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
AI Generate
|
AI Generate (beta)
|
||||||
</Button>
|
</Button>
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -206,7 +206,11 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
|
|||||||
setOpenAutocomplete(false)
|
setOpenAutocomplete(false)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Add suborg
|
<Button variant="outlined" startIcon={<AddIcon />} sx={{
|
||||||
|
textTransform: 'none',
|
||||||
|
}}>
|
||||||
|
Add suborg
|
||||||
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
</Link>
|
</Link>
|
||||||
</Popper>
|
</Popper>
|
||||||
|
|||||||
@@ -1319,8 +1319,14 @@ const LicencePopup = (props) => {
|
|||||||
>
|
>
|
||||||
{`${
|
{`${
|
||||||
isPaidPlan ? "Next billing: " : "App runs resets on "
|
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, {
|
).toLocaleDateString(undefined, {
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
month: "short",
|
month: "short",
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ const menuData = {
|
|||||||
title: "Articles",
|
title: "Articles",
|
||||||
icon: "/images/icons/articles.svg",
|
icon: "/images/icons/articles.svg",
|
||||||
hoverIcon: "/images/icons/articles_hover.svg",
|
hoverIcon: "/images/icons/articles_hover.svg",
|
||||||
link: "/articles/2.0_release",
|
link: "/articles",
|
||||||
gaData: {
|
gaData: {
|
||||||
category: "navbar",
|
category: "navbar",
|
||||||
action: "resources_click",
|
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
|
// This is more a guess than anything
|
||||||
// Should be handled in getAppAuthentication()
|
// 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 url = `${globalUrl}/api/v1/orgs/${selectedOrganization?.id}/change`;
|
||||||
const data = {
|
const data = {
|
||||||
org_id: selectedOrganization?.id,
|
org_id: selectedOrganization?.id,
|
||||||
sso_test: true,
|
sso: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
fetch(url, {
|
fetch(url, {
|
||||||
|
|||||||
@@ -62,11 +62,8 @@ const OrganizationTab = (props) => {
|
|||||||
if (isIntegrationPartner && isChildOrg && !isGlobalUser) {
|
if (isIntegrationPartner && isChildOrg && !isGlobalUser) {
|
||||||
setVisibleTabs(items.filter((item) => item !== 'Branding' && item !== 'SSO'));
|
setVisibleTabs(items.filter((item) => item !== 'Branding' && item !== 'SSO'));
|
||||||
} else {
|
} else {
|
||||||
if (userdata && userdata.active_org && userdata.active_org.role === 'admin') {
|
// Show SSO tab for all users now
|
||||||
setVisibleTabs(items);
|
setVisibleTabs(items);
|
||||||
} else {
|
|
||||||
setVisibleTabs(items.filter((item) => item !== 'SSO'));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//if (isCloud) {
|
//if (isCloud) {
|
||||||
|
|||||||
@@ -1911,7 +1911,7 @@ const ParsedAction = (props) => {
|
|||||||
<Tooltip
|
<Tooltip
|
||||||
title={
|
title={
|
||||||
<Typography variant="body2" style={{margin: 3, }}>
|
<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>
|
</Typography>
|
||||||
}
|
}
|
||||||
placement="top"
|
placement="top"
|
||||||
@@ -1923,8 +1923,8 @@ const ParsedAction = (props) => {
|
|||||||
marginTop: "auto",
|
marginTop: "auto",
|
||||||
marginBottom: "auto",
|
marginBottom: "auto",
|
||||||
height: 30,
|
height: 30,
|
||||||
marginLeft: 98,
|
|
||||||
textTransform: "none",
|
textTransform: "none",
|
||||||
|
marginLeft: 115,
|
||||||
}}
|
}}
|
||||||
disabled={autoCompleting}
|
disabled={autoCompleting}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -1935,7 +1935,7 @@ const ParsedAction = (props) => {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PlayArrowIcon style={{marginRight: 5, }}/>
|
<CachedIcon style={{marginRight: 5, }}/>
|
||||||
Rerun
|
Rerun
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -2814,6 +2814,8 @@ const ParsedAction = (props) => {
|
|||||||
style: {
|
style: {
|
||||||
backgroundColor: theme.palette.platformColor,
|
backgroundColor: theme.palette.platformColor,
|
||||||
color: theme.palette.textColor,
|
color: theme.palette.textColor,
|
||||||
|
border: "1px solid rgba(255,255,255,0.3)",
|
||||||
|
paddingRight: 20,
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
filterOptions={(options, { inputValue }) => {
|
filterOptions={(options, { inputValue }) => {
|
||||||
@@ -3092,6 +3094,14 @@ const ParsedAction = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{apps.map((app, appIndex) => {
|
{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) {
|
if (app.categories === undefined || app.categories === null || app.categories.length === 0) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -3177,7 +3187,7 @@ const ParsedAction = (props) => {
|
|||||||
marginRight: 5,
|
marginRight: 5,
|
||||||
borderRadius: 5,
|
borderRadius: 5,
|
||||||
cursor: "pointer",
|
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>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ const PartnerApps = () => {
|
|||||||
minHeight: "500px",
|
minHeight: "500px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography>Partner Apps</Typography>
|
<Typography>Partner's Apps</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -126,9 +126,9 @@ const PartnerTab = (props) => {
|
|||||||
case `apps`:
|
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} />;
|
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' :
|
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} />;
|
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 `ai_agents`:
|
case `aiagents`:
|
||||||
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 <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':
|
case 'branding':
|
||||||
return <Branding
|
return <Branding
|
||||||
isCloud={isCloud}
|
isCloud={isCloud}
|
||||||
@@ -157,8 +157,7 @@ const PartnerTab = (props) => {
|
|||||||
!isCloud &&
|
!isCloud &&
|
||||||
(tabName === "Usecases" ||
|
(tabName === "Usecases" ||
|
||||||
tabName === "Apps" ||
|
tabName === "Apps" ||
|
||||||
tabName === "AI Agents" ||
|
tabName === "AI Agents")
|
||||||
tabName === "Articles")
|
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -166,7 +165,6 @@ const PartnerTab = (props) => {
|
|||||||
// Disable Apps, Articles, and AI Agents for all non-support users
|
// Disable Apps, Articles, and AI Agents for all non-support users
|
||||||
if (
|
if (
|
||||||
tabName === "Apps" ||
|
tabName === "Apps" ||
|
||||||
tabName === "Articles" ||
|
|
||||||
tabName === "AI Agents"
|
tabName === "AI Agents"
|
||||||
) {
|
) {
|
||||||
return true;
|
return true;
|
||||||
@@ -196,7 +194,7 @@ const PartnerTab = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isSupportOnlyTab = (tabName) => {
|
const isSupportOnlyTab = (tabName) => {
|
||||||
return tabName === "Apps" || tabName === "Articles" || tabName === "AI Agents";
|
return tabName === "Apps" || tabName === "AI Agents";
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { Box, Typography } from '@mui/material'
|
import { Box, Typography } from '@mui/material'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
const PartnersArticles = () => {
|
const PartnersArticles = (props) => {
|
||||||
|
const {tabName = "Partner's Articles"} = props;
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -13,7 +14,7 @@ const PartnersArticles = () => {
|
|||||||
minHeight: "500px",
|
minHeight: "500px",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography>Partner Articles</Typography>
|
<Typography>{tabName}</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
import React, { useContext, useEffect, useState } from "react";
|
import React, { useContext, useEffect, useState } from "react";
|
||||||
import { getTheme } from "../theme.jsx";
|
import { getTheme } from "../theme.jsx";
|
||||||
import { Context } from "../context/ContextApi.jsx";
|
import { Context } from "../context/ContextApi.jsx";
|
||||||
|
import { triggers } from "../views/AngularWorkflow.jsx";
|
||||||
import AddIcon from "@mui/icons-material/Add";
|
import AddIcon from "@mui/icons-material/Add";
|
||||||
import CloseIcon from "@mui/icons-material/Close";
|
import CloseIcon from "@mui/icons-material/Close";
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
@@ -37,6 +38,35 @@ import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp";
|
|||||||
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
|
||||||
import InfoIcon from "@mui/icons-material/Info";
|
import InfoIcon from "@mui/icons-material/Info";
|
||||||
import { Link, useNavigate } from "react-router-dom";
|
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
|
// Helper function to get the correct image path based on app category
|
||||||
export const getCategoryImagePath = (category) => {
|
export const getCategoryImagePath = (category) => {
|
||||||
@@ -62,16 +92,132 @@ export const getCategoryImagePath = (category) => {
|
|||||||
return "/images/appCategories/intel.svg";
|
return "/images/appCategories/intel.svg";
|
||||||
} else if (lowerCategory.includes("email")) {
|
} else if (lowerCategory.includes("email")) {
|
||||||
return "/images/appCategories/email.svg";
|
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 {
|
} else {
|
||||||
return "/images/appCategories/other.svg";
|
return "/images/appCategories/other.svg";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Skeleton component for loading state
|
// Skeleton component for loading state
|
||||||
const UsecaseCardSkeleton = () => {
|
const UsecaseCardSkeleton = ({ isArticlesTab }) => {
|
||||||
const { themeMode } = useContext(Context);
|
const { themeMode } = useContext(Context);
|
||||||
const theme = getTheme(themeMode);
|
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 (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
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 [anchorEl, setAnchorEl] = useState(null);
|
||||||
const open = Boolean(anchorEl);
|
const open = Boolean(anchorEl);
|
||||||
const { themeMode } = useContext(Context);
|
const { themeMode } = useContext(Context);
|
||||||
@@ -209,9 +356,215 @@ const UsecaseCard = ({ usecase, handleToggle, handleOpenDialog, handleDeleteUsec
|
|||||||
navigate(`/usecases/${usecase.id}`);
|
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 (
|
return (
|
||||||
<Box
|
<Box
|
||||||
onClick={handleCardClick}
|
|
||||||
sx={{
|
sx={{
|
||||||
textDecoration: "none",
|
textDecoration: "none",
|
||||||
backgroundColor: theme.palette.usecaseCardColor,
|
backgroundColor: theme.palette.usecaseCardColor,
|
||||||
@@ -241,7 +594,6 @@ const UsecaseCard = ({ usecase, handleToggle, handleOpenDialog, handleDeleteUsec
|
|||||||
sm: "300px",
|
sm: "300px",
|
||||||
},
|
},
|
||||||
gap: "20px",
|
gap: "20px",
|
||||||
cursor: "pointer",
|
|
||||||
"&:hover": {
|
"&:hover": {
|
||||||
backgroundColor: theme.palette.usecaseCardHoverColor,
|
backgroundColor: theme.palette.usecaseCardHoverColor,
|
||||||
},
|
},
|
||||||
@@ -302,6 +654,17 @@ const UsecaseCard = ({ usecase, handleToggle, handleOpenDialog, handleDeleteUsec
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
|
<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
|
<Switch
|
||||||
checked={usecase.public}
|
checked={usecase.public}
|
||||||
onClick={(e) => {
|
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 { themeMode } = useContext(Context);
|
||||||
const theme = getTheme(themeMode);
|
const theme = getTheme(themeMode);
|
||||||
|
const isArticlesTab = selectedTab?.toLowerCase() === "articles";
|
||||||
|
const entityLabel = isArticlesTab ? "Article" : "Usecase";
|
||||||
|
|
||||||
// Dialog state
|
// Dialog state
|
||||||
const [openDialog, setOpenDialog] = useState(false);
|
const [openDialog, setOpenDialog] = useState(false);
|
||||||
@@ -438,12 +803,13 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
|||||||
navigation: {
|
navigation: {
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
name: "About Usecase",
|
name: `About ${entityLabel}`,
|
||||||
content: [""],
|
content: [""],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
public: false,
|
public: false,
|
||||||
|
created: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
// App categories for dropdowns
|
// App categories for dropdowns
|
||||||
@@ -458,6 +824,11 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
|||||||
{ value: "assets", label: "Assets" },
|
{ value: "assets", label: "Assets" },
|
||||||
{ value: "intel", label: "Intel" },
|
{ value: "intel", label: "Intel" },
|
||||||
{ value: "email", label: "Email" },
|
{ 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" }
|
{ value: "other", label: "Other" }
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -506,6 +877,8 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
|||||||
navigation: usecase.navigation || { items: [] },
|
navigation: usecase.navigation || { items: [] },
|
||||||
categories: usecase.mainContent?.categories || [],
|
categories: usecase.mainContent?.categories || [],
|
||||||
public: usecase?.public || false,
|
public: usecase?.public || false,
|
||||||
|
created: usecase?.created || null,
|
||||||
|
headerImage: getFirstImageFromNavigation(usecase.navigation),
|
||||||
}));
|
}));
|
||||||
if (usecases.length > 0) {
|
if (usecases.length > 0) {
|
||||||
setUsecaseData(usecases);
|
setUsecaseData(usecases);
|
||||||
@@ -526,7 +899,195 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
|||||||
// Sample categories
|
// Sample categories
|
||||||
const categoryOptions = ["Collect","Enrich", "Detect", "Respond", "Verify"];
|
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) => {
|
const getUserProfileWorkflows = (orgId) => {
|
||||||
|
if (selectedTab === "articles") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
setIsWorkflowLoading(true);
|
setIsWorkflowLoading(true);
|
||||||
fetch(`${globalUrl}/api/v1/partners/${orgId}/workflows`, {
|
fetch(`${globalUrl}/api/v1/partners/${orgId}/workflows`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@@ -713,12 +1274,13 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
|||||||
navigation: usecase?.navigation || {
|
navigation: usecase?.navigation || {
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
name: "About Usecase",
|
name: `About ${entityLabel}`,
|
||||||
content: [""],
|
content: [""],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
public: usecase?.public || false,
|
public: usecase?.public || false,
|
||||||
|
created: usecase?.created || Math.floor(Date.now() / 1000),
|
||||||
});
|
});
|
||||||
setOpenDialog(true);
|
setOpenDialog(true);
|
||||||
};
|
};
|
||||||
@@ -753,15 +1315,15 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
|||||||
const validationErrors = [];
|
const validationErrors = [];
|
||||||
|
|
||||||
// Check for public workflow selection
|
// 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");
|
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");
|
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");
|
validationErrors.push("Destination app type is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -783,7 +1345,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check categories
|
// 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");
|
validationErrors.push("At least one category must be selected");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -846,7 +1408,6 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
|||||||
return res.json();
|
return res.json();
|
||||||
})
|
})
|
||||||
.then((responseData) => {
|
.then((responseData) => {
|
||||||
console.log("Response data:", responseData);
|
|
||||||
// Reset loading state
|
// Reset loading state
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
|
|
||||||
@@ -864,6 +1425,8 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
|||||||
categories: formData.mainContent?.categories || [],
|
categories: formData.mainContent?.categories || [],
|
||||||
navigation: formData.navigation || {},
|
navigation: formData.navigation || {},
|
||||||
public: formData?.public || false,
|
public: formData?.public || false,
|
||||||
|
created: formData?.created || null,
|
||||||
|
headerImage: getFirstImageFromNavigation(formData.navigation),
|
||||||
}
|
}
|
||||||
|
|
||||||
setUsecaseData((prevData) => {
|
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 (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -934,7 +1519,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
|||||||
justifyContent: "flex-start",
|
justifyContent: "flex-start",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Add Usecase Button */}
|
{/* Add Usecase / Article Button */}
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
@@ -946,13 +1531,13 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography sx={{ fontSize: 20, fontWeight: 500, color: "#FFFFFF" }}>
|
<Typography sx={{ fontSize: 20, fontWeight: 500, color: "#FFFFFF" }}>
|
||||||
Usecases
|
{selectedTab === "usecases" && "Usecases"}
|
||||||
|
{selectedTab === "articles" && "Articles"}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
color="primary"
|
color="primary"
|
||||||
startIcon={<AddIcon />}
|
startIcon={<AddIcon />}
|
||||||
disabled={isWorkflowLoading}
|
|
||||||
onClick={handleOpenDialog}
|
onClick={handleOpenDialog}
|
||||||
sx={{
|
sx={{
|
||||||
px: 3,
|
px: 3,
|
||||||
@@ -961,11 +1546,11 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
|||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Add Usecase
|
{isArticlesTab ? "Add Article" : "Add Usecase"}
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Usecases Grid */}
|
{/* Usecases / Articles Grid */}
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
// Skeleton loading state
|
// Skeleton loading state
|
||||||
<Box
|
<Box
|
||||||
@@ -981,11 +1566,11 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
|||||||
>
|
>
|
||||||
{/* Display 4 skeleton cards while loading */}
|
{/* Display 4 skeleton cards while loading */}
|
||||||
{[...Array(4)].map((_, index) => (
|
{[...Array(4)].map((_, index) => (
|
||||||
<UsecaseCardSkeleton key={index} />
|
<UsecaseCardSkeleton key={index} isArticlesTab={isArticlesTab} partnerData={partnerData} />
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
) : usecaseData.length > 0 ? (
|
) : filteredUsecases.length > 0 ? (
|
||||||
// Actual data display
|
// Actual data display (filtered based on public workflow ID)
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@@ -997,13 +1582,15 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
|||||||
pt: 4,
|
pt: 4,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{usecaseData.map((usecase) => (
|
{filteredUsecases.map((usecase) => (
|
||||||
<UsecaseCard
|
<UsecaseCard
|
||||||
handleOpenDialog={handleOpenDialog}
|
handleOpenDialog={handleOpenDialog}
|
||||||
key={usecase.id}
|
key={usecase.id}
|
||||||
usecase={usecase}
|
usecase={usecase}
|
||||||
handleToggle={handleToggle}
|
handleToggle={handleToggle}
|
||||||
handleDeleteUsecase={handleDeleteUsecase}
|
handleDeleteUsecase={handleDeleteUsecase}
|
||||||
|
isArticlesTab={isArticlesTab}
|
||||||
|
partnerData={partnerData}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Box>
|
</Box>
|
||||||
@@ -1027,7 +1614,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
|||||||
color: "#FFFFFF",
|
color: "#FFFFFF",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
No usecases found
|
{isArticlesTab ? "No articles found" : "No usecases found"}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
@@ -1064,7 +1651,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Typography sx={{ fontSize: 20, fontWeight: 500, color: theme.palette.text.primary }}>
|
<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>
|
</Typography>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={handleCloseDialog}
|
onClick={handleCloseDialog}
|
||||||
@@ -1088,6 +1675,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
|||||||
paddingTop: 3,
|
paddingTop: 3,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{!isArticlesTab && (
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@@ -1207,9 +1795,40 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
|||||||
)}
|
)}
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</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 */}
|
{/* 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>
|
<FormControl fullWidth>
|
||||||
<Typography
|
<Typography
|
||||||
sx={{
|
sx={{
|
||||||
@@ -1286,6 +1905,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
|||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|
||||||
|
|
||||||
<FormControl fullWidth>
|
<FormControl fullWidth>
|
||||||
<Typography
|
<Typography
|
||||||
sx={{
|
sx={{
|
||||||
@@ -1362,6 +1982,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
|||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
</Box>
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
<Box sx={{ mb: 2, mt: 2 }}>
|
<Box sx={{ mb: 2, mt: 2 }}>
|
||||||
<Typography
|
<Typography
|
||||||
@@ -1433,6 +2054,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
{!isArticlesTab && (
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Typography
|
<Typography
|
||||||
sx={{ color: theme.palette.text.primary, mb: 1, fontSize: "14px" }}
|
sx={{ color: theme.palette.text.primary, mb: 1, fontSize: "14px" }}
|
||||||
@@ -1513,6 +2135,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
|||||||
))}
|
))}
|
||||||
</Select>
|
</Select>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@@ -1648,7 +2271,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
|||||||
<Box key={contentIndex} sx={{ mb: 1 }}>
|
<Box key={contentIndex} sx={{ mb: 1 }}>
|
||||||
<TextField
|
<TextField
|
||||||
multiline
|
multiline
|
||||||
rows={6}
|
rows={14}
|
||||||
value={content}
|
value={content}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const newItems = [...formData.navigation.items];
|
const newItems = [...formData.navigation.items];
|
||||||
@@ -1659,12 +2282,15 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
|||||||
navigation: { items: newItems },
|
navigation: { items: newItems },
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
onPaste={(event) => handleContentPaste(event, itemIndex, contentIndex)}
|
||||||
|
onDrop={(event) => handleContentDrop(event, itemIndex, contentIndex)}
|
||||||
|
onDragOver={(event) => event.preventDefault()}
|
||||||
placeholder="Content (Markdown supported)"
|
placeholder="Content (Markdown supported)"
|
||||||
helperText={
|
helperText={
|
||||||
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5, mt: 0.5 }}>
|
<Box sx={{ display: "flex", alignItems: "center", gap: 0.5, mt: 0.5 }}>
|
||||||
<InfoIcon sx={{ fontSize: "14px", opacity: 0.7 }} />
|
<InfoIcon sx={{ fontSize: "14px", opacity: 0.7 }} />
|
||||||
<Typography variant="caption" sx={{ 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>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
@@ -1708,7 +2334,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
|||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
|
{/*
|
||||||
<Button
|
<Button
|
||||||
startIcon={<AddCircleOutlineIcon />}
|
startIcon={<AddCircleOutlineIcon />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -1725,7 +2351,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Add Paragraph
|
Add Paragraph
|
||||||
</Button>
|
</Button> */}
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
@@ -1791,7 +2417,7 @@ const PartnersUsecasesTab = ({ isCloud, globalUrl, userdata, partnerData, setPar
|
|||||||
>
|
>
|
||||||
{isSubmitting
|
{isSubmitting
|
||||||
? (formData?.id ? "Updating..." : "Adding...")
|
? (formData?.id ? "Updating..." : "Adding...")
|
||||||
: (formData?.id ? "Update Usecase" : "Add Usecase")
|
: (formData?.id ? `Update ${entityLabel}` : `Add ${entityLabel}`)
|
||||||
}
|
}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const Priority = (props) => {
|
|||||||
const { globalUrl, clickedFromOrgTab,userdata, serverside, priority, checkLogin, setAdminTab, setCurTab, appFramework, } = props;
|
const { globalUrl, clickedFromOrgTab,userdata, serverside, priority, checkLogin, setAdminTab, setCurTab, appFramework, } = props;
|
||||||
const { themeMode, supportEmail } = useContext(Context);
|
const { themeMode, supportEmail } = useContext(Context);
|
||||||
const theme = getTheme(themeMode);
|
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();
|
let navigate = useNavigate();
|
||||||
|
|
||||||
if (window.location.pathname === "/workflows") {
|
if (window.location.pathname === "/workflows") {
|
||||||
|
|||||||
@@ -1,7 +1,25 @@
|
|||||||
import React, { useEffect, useMemo, useState } from 'react';
|
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 { BarChart, BarSeries, Bar, GridlineSeries, Gridline, TooltipArea, ChartTooltip, LinearYAxis, LinearYAxisTickSeries, LinearYAxisTickLabel } from 'reaviz';
|
||||||
import theme from '../theme.jsx';
|
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)
|
// Compact number formatter for axis ticks (e.g. 12,000 -> 12k, 12,000,000 -> 12M)
|
||||||
function formatCompactNumber(value) {
|
function formatCompactNumber(value) {
|
||||||
@@ -59,34 +77,128 @@ function computeTodayValueForOrg(key, orgStats) {
|
|||||||
const RunsOverTimeWidget = (props) => {
|
const RunsOverTimeWidget = (props) => {
|
||||||
const { globalUrl, onLoadingChange, monthOverride, dummyMode, selectedOrganization, selectedOrgForStats, orgStats, orgForLimit, loadingSelectedOrgStats } = props;
|
const { globalUrl, onLoadingChange, monthOverride, dummyMode, selectedOrganization, selectedOrgForStats, orgStats, orgForLimit, loadingSelectedOrgStats } = props;
|
||||||
const [mode, setMode] = useState('workflows'); // 'apps' | 'workflows'
|
const [mode, setMode] = useState('workflows'); // 'apps' | 'workflows'
|
||||||
|
const [selectedStatType, setSelectedStatType] = useState({
|
||||||
|
"value": 'total_workflow_executions',
|
||||||
|
"label": 'Workflows',
|
||||||
|
"amount": 0,
|
||||||
|
})
|
||||||
const [series, setSeries] = useState([]);
|
const [series, setSeries] = useState([]);
|
||||||
const [days, setDays] = useState(365); // aggregate to last 12 months by default
|
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 [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 [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
|
// Helper: fetch time series for a specific statistics key
|
||||||
const fetchSeriesForKey = async (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 {
|
try {
|
||||||
// If specific org is selected and pre-fetched stats are available, use them directly
|
// If specific org is selected and pre-fetched stats are available, use them directly
|
||||||
if (selectedOrgForStats && selectedOrgForStats !== 'ALL' && orgStats && Array.isArray(orgStats?.daily_statistics)) {
|
if (selectedOrgForStats && selectedOrgForStats !== 'ALL' && orgStats && Array.isArray(orgStats?.daily_statistics)) {
|
||||||
|
|
||||||
const dailyStats = orgStats.daily_statistics;
|
const dailyStats = orgStats.daily_statistics;
|
||||||
const processedEntries = [];
|
const processedEntries = [];
|
||||||
const cutoff = new Date();
|
const cutoff = new Date();
|
||||||
cutoff.setDate(cutoff.getDate() - days);
|
cutoff.setDate(cutoff.getDate() - days);
|
||||||
|
|
||||||
|
if (key.startsWith("total_")) {
|
||||||
|
key = key.replace("total_", "")
|
||||||
|
}
|
||||||
|
|
||||||
for (const day of dailyStats) {
|
for (const day of dailyStats) {
|
||||||
if (!day?.date) continue;
|
if (!day?.date) continue;
|
||||||
const dayDate = new Date(day.date);
|
const dayDate = new Date(day.date);
|
||||||
if (dayDate < cutoff) continue;
|
if (dayDate < cutoff) continue;
|
||||||
|
|
||||||
let value = 0;
|
let value = 0;
|
||||||
if (key === 'workflow_executions') {
|
|
||||||
const finished = Number(day?.workflow_executions_finished || 0);
|
if (day[key] === undefined) {
|
||||||
const failed = Number(day?.workflow_executions_failed || 0);
|
for (var additionKey in day?.additions) {
|
||||||
value = finished + failed;
|
const addition = day?.additions[additionKey];
|
||||||
} else {
|
if (addition?.key === key) {
|
||||||
value = Number(day?.[key] || 0);
|
value += Number(addition?.value || 0);
|
||||||
}
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
value = Number(day[key] || 0);
|
||||||
|
}
|
||||||
|
|
||||||
processedEntries.push({ date: day.date, value });
|
processedEntries.push({ date: day.date, value });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,7 +297,7 @@ function computeTodayValueForOrg(key, orgStats) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Load and transform into monthly aggregation for last 12 months
|
// Load and transform into monthly aggregation for last 12 months
|
||||||
const load = async (curMode) => {
|
const loadStats = async (curMode, inputType) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
// Clear current series immediately to avoid any visual overlap while switching views
|
// Clear current series immediately to avoid any visual overlap while switching views
|
||||||
@@ -203,8 +315,10 @@ function computeTodayValueForOrg(key, orgStats) {
|
|||||||
setSeries(dummy);
|
setSeries(dummy);
|
||||||
return;
|
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);
|
const entries = await fetchSeriesForKey(key);
|
||||||
|
|
||||||
// Normalize variants: {Date, Value} or {date, value}
|
// Normalize variants: {Date, Value} or {date, value}
|
||||||
const normalized = (entries || []).map((d) => ({
|
const normalized = (entries || []).map((d) => ({
|
||||||
date: d?.Date ? new Date(d.Date) : (d?.date ? new Date(d.date) : new Date()),
|
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
|
// Apply month override (e.g. onboarding Explore Now) - consolidated with main load effect
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (monthOverride instanceof Date) {
|
if (monthOverride instanceof Date) {
|
||||||
@@ -287,8 +418,9 @@ function computeTodayValueForOrg(key, orgStats) {
|
|||||||
setSeries([]);
|
setSeries([]);
|
||||||
return;
|
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
|
// Notify parent on loading changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -342,54 +474,124 @@ function computeTodayValueForOrg(key, orgStats) {
|
|||||||
/>;
|
/>;
|
||||||
|
|
||||||
|
|
||||||
|
const barColorscheme = [
|
||||||
|
"#f85a3e", // anchor orange
|
||||||
|
"#ff7a57", // brighter, more playful
|
||||||
|
"#e14b2e", // slightly darker + redder
|
||||||
|
"#ff9b6b", // soft peachy highlight
|
||||||
|
"#c83f24", // deep burnt orange
|
||||||
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
<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' }}>
|
<Box sx={{ display: 'flex', gap: 1.5, alignItems: 'center', justifyContent: 'center' }}>
|
||||||
<ToggleButtonGroup
|
|
||||||
exclusive
|
<Tooltip title="Learn about custom stats" arrow>
|
||||||
size="large"
|
<a href="/docs/API#count-stats-for-custom-key" target="_blank" rel="noopener noreferrer" style={{ textDecoration: 'none' }}>
|
||||||
value={mode}
|
<IconButton>
|
||||||
onChange={(e, v) => v && setMode(v)}
|
<OpenInNewIcon style={{ color: theme.palette.text.secondary, }} />
|
||||||
sx={{
|
</IconButton>
|
||||||
height: 37,
|
</a>
|
||||||
backgroundColor: 'rgba(255,255,255,0.06)',
|
</Tooltip>
|
||||||
border: '1px solid rgba(255,255,255,0.22)',
|
|
||||||
borderRadius: '30px',
|
<FormControl size="small" variant="outlined" style={{ minWidth: 350, }} sx={{
|
||||||
padding: '2px',
|
'& .MuiInputBase-root': {
|
||||||
"& .MuiToggleButton-root": {
|
height: 40,
|
||||||
border: "none",
|
backgroundColor: 'rgba(255,255,255,0.06)',
|
||||||
borderRadius: "30px",
|
borderRadius: '20px',
|
||||||
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)",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}}
|
'& .MuiOutlinedInput-notchedOutline': { borderColor: 'rgba(255,255,255,0.22)' },
|
||||||
>
|
}}>
|
||||||
<ToggleButton value="workflows">
|
<Autocomplete
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
labelId="data-type-choice"
|
||||||
Workflows
|
label="Select Datatype"
|
||||||
</Box>
|
autoHighlight
|
||||||
</ToggleButton>
|
value={selectedStatType}
|
||||||
<ToggleButton value="apps">
|
classes={{ inputRoot: classes.inputRoot }}
|
||||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
ListboxProps={{
|
||||||
Apps
|
style: {
|
||||||
</Box>
|
backgroundColor: theme.palette.inputColor,
|
||||||
</ToggleButton>
|
color: theme.palette.text.primary,
|
||||||
</ToggleButtonGroup>
|
},
|
||||||
|
}}
|
||||||
|
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={{
|
<FormControl size="small" variant="outlined" style={{ minWidth: 170 }} sx={{
|
||||||
'& .MuiInputBase-root': {
|
'& .MuiInputBase-root': {
|
||||||
height: 40,
|
height: 40,
|
||||||
@@ -438,7 +640,7 @@ function computeTodayValueForOrg(key, orgStats) {
|
|||||||
key={`${mode}-${selectedMonth ? `${selectedMonth.getFullYear()}-${selectedMonth.getMonth()}` : 'yearly'}`}
|
key={`${mode}-${selectedMonth ? `${selectedMonth.getFullYear()}-${selectedMonth.getMonth()}` : 'yearly'}`}
|
||||||
height={300}
|
height={300}
|
||||||
data={barData}
|
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" />} />}
|
gridlines={<GridlineSeries line={<Gridline direction="y" />} />}
|
||||||
yAxis={
|
yAxis={
|
||||||
<LinearYAxis
|
<LinearYAxis
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ import {
|
|||||||
Send as SendIcon,
|
Send as SendIcon,
|
||||||
} from '@mui/icons-material';
|
} from '@mui/icons-material';
|
||||||
|
|
||||||
import { DataGrid, GridColDef, GridValueGetterParams } from '@mui/x-data-grid'
|
import { DataGrid, GridValueGetterParams } from '@mui/x-data-grid'
|
||||||
import {
|
import {
|
||||||
Search as SearchIcon,
|
Search as SearchIcon,
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
@@ -335,7 +335,7 @@ const RuntimeDebugger = (props) => {
|
|||||||
}
|
}
|
||||||
const imageSize = 30
|
const imageSize = 30
|
||||||
const timenowUnix = Math.floor(Date.now() / 1000)
|
const timenowUnix = Math.floor(Date.now() / 1000)
|
||||||
const columns: GridColDef[] = [
|
const columns = [
|
||||||
{
|
{
|
||||||
field: 'execution_source',
|
field: 'execution_source',
|
||||||
headerName: 'Source',
|
headerName: 'Source',
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ const SearchData = props => {
|
|||||||
// return null
|
// 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" ) {
|
// if (window.location.pathname === "/docs" || window.location.pathname === "/apps" || window.location.pathname === "/usecases" ) {
|
||||||
// setModalOpen(false)
|
// setModalOpen(false)
|
||||||
// }
|
// }
|
||||||
|
|||||||
@@ -349,7 +349,7 @@ const UsecaseSearch = (props) => {
|
|||||||
const [selectedAction, setSelectedAction] = React.useState({});
|
const [selectedAction, setSelectedAction] = React.useState({});
|
||||||
const [firstRequest, setFirstRequest] = React.useState(true);
|
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()
|
//const alert = useAlert()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
Skeleton,
|
Skeleton,
|
||||||
Switch,
|
Switch,
|
||||||
Box,
|
Box,
|
||||||
|
Chip,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -158,7 +159,7 @@ const UserManagmentTab = memo((props) => {
|
|||||||
toast("Failed to update user");
|
toast("Failed to update user");
|
||||||
} else {
|
} else {
|
||||||
//toast("Set the user field " + field + " to " + value);
|
//toast("Set the user field " + field + " to " + value);
|
||||||
toast("Successfully updated user field " + field);
|
toast.success(`Successfully updated user field '${field}'`)
|
||||||
|
|
||||||
if (field !== "suborgs") {
|
if (field !== "suborgs") {
|
||||||
setSelectedUserModalOpen(false);
|
setSelectedUserModalOpen(false);
|
||||||
@@ -203,7 +204,7 @@ const UserManagmentTab = memo((props) => {
|
|||||||
getUsers();
|
getUsers();
|
||||||
}, 1000);
|
}, 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 = []
|
event.target.value = []
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("event: ", event.target.value);
|
|
||||||
setMatchingOrganizations(event.target.value);
|
setMatchingOrganizations(event.target.value);
|
||||||
// Workaround for empty orgs
|
// Workaround for empty orgs
|
||||||
if (event.target.value.length === 0) {
|
if (event.target.value.length === 0) {
|
||||||
@@ -336,6 +336,17 @@ const UserManagmentTab = memo((props) => {
|
|||||||
})
|
})
|
||||||
.then((responseJson) => {
|
.then((responseJson) => {
|
||||||
setUsers(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)
|
setShowLoader(false)
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
@@ -364,13 +375,13 @@ const UserManagmentTab = memo((props) => {
|
|||||||
})
|
})
|
||||||
.then((responseJson) => {
|
.then((responseJson) => {
|
||||||
if (!responseJson.success && responseJson.reason !== undefined) {
|
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) {
|
} else if (responseJson.success === false) {
|
||||||
toast(
|
toast.error(
|
||||||
`Failed to deactivate user. Please contact ${supportEmail} if this persists.`,
|
`Failed to deactivate user. Please contact ${supportEmail} if this persists.`,
|
||||||
);
|
);
|
||||||
} else {
|
} 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
|
<ListItemText
|
||||||
primary={
|
primary={
|
||||||
<Select
|
<Select
|
||||||
|
disabled={data?.login_type === "DELETED"}
|
||||||
SelectDisplayProps={{
|
SelectDisplayProps={{
|
||||||
style: {
|
style: {
|
||||||
// marginLeft: 10,
|
// marginLeft: 10,
|
||||||
@@ -1611,7 +1623,30 @@ const UserManagmentTab = memo((props) => {
|
|||||||
data.login_type === undefined ||
|
data.login_type === undefined ||
|
||||||
data?.login_type === null ||
|
data?.login_type === null ||
|
||||||
data?.login_type?.length === 0
|
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
|
: data.login_type
|
||||||
}
|
}
|
||||||
style={{ display:'table-cell',verticalAlign: 'middle', padding: "8px", }}
|
style={{ display:'table-cell',verticalAlign: 'middle', padding: "8px", }}
|
||||||
@@ -1647,6 +1682,7 @@ const UserManagmentTab = memo((props) => {
|
|||||||
) : null}
|
) : null}
|
||||||
<ListItemText style={{ display:'table-cell', textAlign: "left", verticalAlign: 'middle', padding: "8px", }}>
|
<ListItemText style={{ display:'table-cell', textAlign: "left", verticalAlign: 'middle', padding: "8px", }}>
|
||||||
<IconButton
|
<IconButton
|
||||||
|
disabled={data?.login_type === "DELETED"}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setSelectedUserModalOpen(true);
|
setSelectedUserModalOpen(true);
|
||||||
setSelectedUser(data);
|
setSelectedUser(data);
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ const WelcomeForm = (props) => {
|
|||||||
const [clickdiff, setclickdiff] = useState(0);
|
const [clickdiff, setclickdiff] = useState(0);
|
||||||
const [mouseHoverIndex, setMouseHoverIndex] = useState(-1)
|
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();
|
//const alert = useAlert();
|
||||||
let navigate = useNavigate();
|
let navigate = useNavigate();
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b
|
|||||||
const AppGrid = props => {
|
const AppGrid = props => {
|
||||||
const { maxRows, showName, showSuggestion, isMobile, globalUrl, parsedXs, alternativeView, onlyResults, inputsearch } = 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 rowHandler = maxRows === undefined || maxRows === null ? 50 : maxRows
|
||||||
const xs = parsedXs === undefined || parsedXs === null ? isMobile ? 6 : 4 : parsedXs
|
const xs = parsedXs === undefined || parsedXs === null ? isMobile ? 6 : 4 : parsedXs
|
||||||
//const [apps, setApps] = React.useState([]);
|
//const [apps, setApps] = React.useState([]);
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ const WorkflowTemplatePopup = (props) => {
|
|||||||
|
|
||||||
const [requestSent, setRequestSent] = React.useState(false)
|
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();
|
let navigate = useNavigate();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (modalOpen !== true) {
|
if (modalOpen !== true) {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import theme from "../theme.jsx";
|
|||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
import { Context } from "../context/ContextApi.jsx";
|
import { Context } from "../context/ContextApi.jsx";
|
||||||
import { getTheme } from "../theme.jsx";
|
import { getTheme } from "../theme.jsx";
|
||||||
|
import { red } from "../views/AngularWorkflow.jsx"
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
const useStyles = makeStyles({
|
||||||
notchedOutline: {
|
notchedOutline: {
|
||||||
@@ -25,6 +26,14 @@ const useStyles = makeStyles({
|
|||||||
|
|
||||||
const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handleEditOrg})=>{
|
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 classes = useStyles();
|
||||||
const [show2faSetup, setShow2faSetup] = React.useState(false);
|
const [show2faSetup, setShow2faSetup] = React.useState(false);
|
||||||
const [autoPrivision, setAutoProvision] = React.useState(selectedOrganization?.sso_config?.auto_provision)
|
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 { themeMode, supportEmail, brandColor } = useContext(Context);
|
||||||
const theme = getTheme(themeMode, brandColor);
|
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(()=>{
|
useEffect(()=>{
|
||||||
|
|
||||||
if (openidClientSecret !== selectedOrganization?.sso_config?.client_secret) {
|
if (openidClientSecret !== selectedOrganization?.sso_config?.client_secret) {
|
||||||
@@ -149,7 +205,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
|
|||||||
disabled={
|
disabled={
|
||||||
userdata === undefined ||
|
userdata === undefined ||
|
||||||
userdata === null ||
|
userdata === null ||
|
||||||
userdata.admin !== "true"
|
!isAdmin
|
||||||
}
|
}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleEditOrg(
|
handleEditOrg(
|
||||||
@@ -256,7 +312,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
|
|||||||
const url = `${globalUrl}/api/v1/orgs/${selectedOrganization?.id}/change`;
|
const url = `${globalUrl}/api/v1/orgs/${selectedOrganization?.id}/change`;
|
||||||
const data = {
|
const data = {
|
||||||
org_id: selectedOrganization?.id,
|
org_id: selectedOrganization?.id,
|
||||||
sso_test: true,
|
sso: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
fetch(url, {
|
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 (
|
return (
|
||||||
<div style={{ width: "100%", height: "100%",boxSizing: 'border-box', padding: "27px 10px 19px 27px", backgroundColor: theme.palette.platformColor , borderRadius: '16px', }}>
|
<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'}} >
|
<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}}>
|
<Typography variant="h5" style={{ width: "100%", fontWeight: 500, fontSize: 24}}>
|
||||||
SSO Configuration
|
SSO Configuration
|
||||||
</Typography>
|
</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
|
<div
|
||||||
style={{
|
style={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@@ -339,6 +523,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
|
|||||||
sx={{marginBottom: 0.6, marginTop: 0.6}}
|
sx={{marginBottom: 0.6, marginTop: 0.6}}
|
||||||
name="onOffSwitch"
|
name="onOffSwitch"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
disabled={!isAdmin}
|
||||||
title="Make SAML SSO or OpenID Authentication Required or Optional for Your Organization"
|
title="Make SAML SSO or OpenID Authentication Required or Optional for Your Organization"
|
||||||
/>
|
/>
|
||||||
{SSORequired ? "Required" : "Optional"}
|
{SSORequired ? "Required" : "Optional"}
|
||||||
@@ -369,7 +554,6 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
|
|||||||
sx={{marginBottom: 0.6, marginTop: 0.6}}
|
sx={{marginBottom: 0.6, marginTop: 0.6}}
|
||||||
name="onOffSwitch"
|
name="onOffSwitch"
|
||||||
color="primary"
|
color="primary"
|
||||||
title="Disable auto-provisioning of users in SSO"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -399,6 +583,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
|
|||||||
sx={{marginBottom: 0.6, marginTop: 0.6}}
|
sx={{marginBottom: 0.6, marginTop: 0.6}}
|
||||||
name="onOffSwitch"
|
name="onOffSwitch"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
disabled={!isAdmin}
|
||||||
title="Disable auto-provisioning of users in SSO"
|
title="Disable auto-provisioning of users in SSO"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -428,58 +613,12 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
|
|||||||
sx={{marginBottom: 0.6, marginTop: 0.6}}
|
sx={{marginBottom: 0.6, marginTop: 0.6}}
|
||||||
name="onOffSwitch"
|
name="onOffSwitch"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
disabled={!isAdmin}
|
||||||
title="Disable auto-provisioning of users in SSO"
|
title="Disable auto-provisioning of users in SSO"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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}}>
|
<Grid item xs={12} sx={{marginTop: 2}}>
|
||||||
<span style={{ display: "flex", flexDirection: "column" }}>
|
<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>
|
<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)}
|
onChange={(e) => setShowOpenIdCred(e.target.checked)}
|
||||||
name="showOpenIdCred"
|
name="showOpenIdCred"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
disabled={!isAdmin}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Grid container style={{ marginTop: 8, }} spacing={2}>
|
<Grid container style={{ marginTop: 8, }} spacing={2}>
|
||||||
@@ -536,6 +676,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
placeholder="The OpenID client ID from the identity provider"
|
placeholder="The OpenID client ID from the identity provider"
|
||||||
value={showOpenIdCred ? openidClientId : openidClientId?.length > 0 ? "•".repeat(50) : ""}
|
value={showOpenIdCred ? openidClientId : openidClientId?.length > 0 ? "•".repeat(50) : ""}
|
||||||
|
disabled={!isAdmin}
|
||||||
onChange={(e) => setOpenidClientId(e.target.value)}
|
onChange={(e) => setOpenidClientId(e.target.value)}
|
||||||
onFocus={(e) => setShowOpenIdCred(true)}
|
onFocus={(e) => setShowOpenIdCred(true)}
|
||||||
onBlur={(e) => setShowOpenIdCred(false)}
|
onBlur={(e) => setShowOpenIdCred(false)}
|
||||||
@@ -574,6 +715,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
placeholder="The OpenID client secret - DONT use this if dealing with implicit auth / PKCE"
|
placeholder="The OpenID client secret - DONT use this if dealing with implicit auth / PKCE"
|
||||||
value={showOpenIdCred ? openidClientSecret : openidClientSecret?.length > 0 ? "•".repeat(50) : ""}
|
value={showOpenIdCred ? openidClientSecret : openidClientSecret?.length > 0 ? "•".repeat(50) : ""}
|
||||||
|
disabled={!isAdmin}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setOpenidClientSecret(e.target.value);
|
setOpenidClientSecret(e.target.value);
|
||||||
}}
|
}}
|
||||||
@@ -616,6 +758,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
|
|||||||
rows={2}
|
rows={2}
|
||||||
placeholder="The OpenID authorization URL (usually ends with /authorize)"
|
placeholder="The OpenID authorization URL (usually ends with /authorize)"
|
||||||
value={showOpenIdCred ? openidAuthorization : openidAuthorization?.length > 0 ? "•".repeat(50) : ""}
|
value={showOpenIdCred ? openidAuthorization : openidAuthorization?.length > 0 ? "•".repeat(50) : ""}
|
||||||
|
disabled={!isAdmin}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setOpenidAuthorization(e.target.value)
|
setOpenidAuthorization(e.target.value)
|
||||||
}}
|
}}
|
||||||
@@ -656,6 +799,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
|
|||||||
rows={2}
|
rows={2}
|
||||||
placeholder="The OpenID token URL (usually ends with /token)"
|
placeholder="The OpenID token URL (usually ends with /token)"
|
||||||
value={showOpenIdCred ? openidToken : openidToken?.length > 0 ? "•".repeat(50) : ""}
|
value={showOpenIdCred ? openidToken : openidToken?.length > 0 ? "•".repeat(50) : ""}
|
||||||
|
disabled={!isAdmin}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setOpenidToken(e.target.value)
|
setOpenidToken(e.target.value)
|
||||||
}}
|
}}
|
||||||
@@ -682,8 +826,8 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
|
|||||||
{/*isCloud ? null : */}
|
{/*isCloud ? null : */}
|
||||||
<Grid item xs={12} sx={{ marginTop: 3.5 }} >
|
<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="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, }}>
|
<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 }}>
|
||||||
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>
|
Note: Support for SAML SSO was deprecated due to potential security issues. Please consider migrating to OpenID Connect for better compatibility and features.
|
||||||
</Typography>
|
</Typography>
|
||||||
<div style={{ display: 'flex', marginTop: 10, }}>
|
<div style={{ display: 'flex', marginTop: 10, }}>
|
||||||
<Typography
|
<Typography
|
||||||
@@ -702,6 +846,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
|
|||||||
onChange={(e) => setShowSamlCred(e.target.checked)}
|
onChange={(e) => setShowSamlCred(e.target.checked)}
|
||||||
name="showSamlCred"
|
name="showSamlCred"
|
||||||
color="primary"
|
color="primary"
|
||||||
|
disabled={!isAdmin}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Grid container style={{ marginTop: 10, }} spacing={2}>
|
<Grid container style={{ marginTop: 10, }} spacing={2}>
|
||||||
@@ -725,6 +870,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
|
|||||||
variant="outlined"
|
variant="outlined"
|
||||||
placeholder="The entrypoint URL from your provider"
|
placeholder="The entrypoint URL from your provider"
|
||||||
value={showSamlCred ? ssoEntrypoint : ssoEntrypoint?.length > 0 ? "•".repeat(50) : ""}
|
value={showSamlCred ? ssoEntrypoint : ssoEntrypoint?.length > 0 ? "•".repeat(50) : ""}
|
||||||
|
disabled={!isAdmin}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setSsoEntrypoint(e.target.value);
|
setSsoEntrypoint(e.target.value);
|
||||||
}}
|
}}
|
||||||
@@ -765,6 +911,7 @@ const SSOTab = ({selectedOrganization, userdata, isEditOrgTab, globalUrl, handle
|
|||||||
rows={2}
|
rows={2}
|
||||||
placeholder="The X509 certificate to use"
|
placeholder="The X509 certificate to use"
|
||||||
value={showSamlCred ? ssoCertificate : ssoCertificate?.length > 0 ? "•".repeat(50) : ""}
|
value={showSamlCred ? ssoCertificate : ssoCertificate?.length > 0 ? "•".repeat(50) : ""}
|
||||||
|
disabled={!isAdmin}
|
||||||
onFocus={(e) => setShowSamlCred(true)}
|
onFocus={(e) => setShowSamlCred(true)}
|
||||||
onBlur={(e) => setShowSamlCred(false)}
|
onBlur={(e) => setShowSamlCred(false)}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
export default function defaultCytoscapeStyle(theme) {
|
import { red, } from "./views/AngularWorkflow.jsx"
|
||||||
|
|
||||||
|
export default function defaultCytoscapeStyle(theme, apps) {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
selector: "node",
|
selector: "node",
|
||||||
@@ -575,7 +577,14 @@ export default function defaultCytoscapeStyle(theme) {
|
|||||||
height: "data(height)",
|
height: "data(height)",
|
||||||
padding: "0px",
|
padding: "0px",
|
||||||
margin: "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-fill": "data(fillstyle)",
|
||||||
"background-gradient-direction": "to-right",
|
"background-gradient-direction": "to-right",
|
||||||
"background-gradient-stop-colors": "data(fillGradient)",
|
"background-gradient-stop-colors": "data(fillGradient)",
|
||||||
@@ -605,6 +614,48 @@ export default function defaultCytoscapeStyle(theme) {
|
|||||||
"background-color": "#f85a3e",
|
"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"]`,
|
selector: `node[name="switch"]`,
|
||||||
css: {
|
css: {
|
||||||
|
|||||||
+468
-179
@@ -24,6 +24,9 @@ import {
|
|||||||
TextField,
|
TextField,
|
||||||
Popover,
|
Popover,
|
||||||
Divider,
|
Divider,
|
||||||
|
|
||||||
|
AvatarGroup,
|
||||||
|
Avatar,
|
||||||
} from '@mui/material'
|
} from '@mui/material'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -40,6 +43,7 @@ import {
|
|||||||
Refresh as RefreshIcon,
|
Refresh as RefreshIcon,
|
||||||
Add as AddIcon,
|
Add as AddIcon,
|
||||||
Warning as WarningIcon,
|
Warning as WarningIcon,
|
||||||
|
Pause as PauseIcon,
|
||||||
} from '@mui/icons-material'
|
} from '@mui/icons-material'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -68,6 +72,27 @@ const AgentUI = (props) => {
|
|||||||
const [appPickerAnchor, setAppPickerAnchor] = React.useState(null)
|
const [appPickerAnchor, setAppPickerAnchor] = React.useState(null)
|
||||||
const [chosenApps, setChosenApps] = useState([])
|
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(() => {
|
useEffect(() => {
|
||||||
if (newSelectedApp.objectID === undefined || newSelectedApp.objectID === null || newSelectedApp.objectID === "") {
|
if (newSelectedApp.objectID === undefined || newSelectedApp.objectID === null || newSelectedApp.objectID === "") {
|
||||||
@@ -83,9 +108,76 @@ const AgentUI = (props) => {
|
|||||||
id: newSelectedApp.objectID,
|
id: newSelectedApp.objectID,
|
||||||
image: newSelectedApp.image_url,
|
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])
|
}, [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 {themeMode} = useContext(Context)
|
||||||
const theme = getTheme(themeMode)
|
const theme = getTheme(themeMode)
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -95,12 +187,13 @@ const AgentUI = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const agentWrapperStyle = {
|
const agentWrapperStyle = {
|
||||||
width: 1000,
|
width: "100%",
|
||||||
height: 1000,
|
maxHeight: "100vh",
|
||||||
margin: "auto",
|
margin: "auto",
|
||||||
paddingTop: 100,
|
|
||||||
paddingBottom: 1000,
|
|
||||||
backgroundColor: theme.palette.backgroundColor,
|
backgroundColor: theme.palette.backgroundColor,
|
||||||
|
|
||||||
|
paddingBottom: showAgentStarter ? 0 : 1500,
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (data.input === undefined || data.input === null) {
|
if (data.input === undefined || data.input === null) {
|
||||||
@@ -412,9 +505,7 @@ const AgentUI = (props) => {
|
|||||||
} else {
|
} else {
|
||||||
for (var key in questionAnswers) {
|
for (var key in questionAnswers) {
|
||||||
const answer = questionAnswers[key]
|
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>
|
</Tooltip>
|
||||||
|
|
||||||
|
|
||||||
const rerunButton =
|
const rerunButton = item?.type === "processing" ? null :
|
||||||
<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">
|
<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>
|
<span>
|
||||||
<IconButton
|
<IconButton
|
||||||
disabled={item.type !== "decision" || disableButtons}
|
disabled={item.type !== "decision" || disableButtons}
|
||||||
@@ -698,6 +789,11 @@ const AgentUI = (props) => {
|
|||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|
||||||
|
var itemLabel = item?.label?.replaceAll("_", " ") || ""
|
||||||
|
if (item?.details?.reason !== undefined && item?.details?.reason !== null && item?.details?.reason !== "") {
|
||||||
|
itemLabel = item?.details?.reason
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -778,10 +874,22 @@ const AgentUI = (props) => {
|
|||||||
paddingTop: defaultTopPadding,
|
paddingTop: defaultTopPadding,
|
||||||
paddingBottom: defaultTopPadding,
|
paddingBottom: defaultTopPadding,
|
||||||
}}>
|
}}>
|
||||||
{item.label}
|
{itemLabel}
|
||||||
</div>
|
</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={{
|
<div style={{
|
||||||
minWidth: maxTimelineWidth,
|
minWidth: maxTimelineWidth,
|
||||||
maxWidth: maxTimelineWidth,
|
maxWidth: maxTimelineWidth,
|
||||||
@@ -874,7 +982,7 @@ const AgentUI = (props) => {
|
|||||||
item?.type === "decision" ?
|
item?.type === "decision" ?
|
||||||
<div style={{display: "flex", }}>
|
<div style={{display: "flex", }}>
|
||||||
{rerunButton}
|
{rerunButton}
|
||||||
<Tooltip title="Explore/debug execution" placement="left">
|
<Tooltip title="Explore/debug app run" placement="left">
|
||||||
<span>
|
<span>
|
||||||
<IconButton
|
<IconButton
|
||||||
disabled={item?.details?.run_details?.debug_url === undefined || item?.details?.run_details?.debug_url === null || item?.details?.run_details?.debug_url === ""}
|
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
|
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>
|
{item?.type === "processing" ? null :
|
||||||
</span>
|
<Tooltip title="Explore results" placement="right">
|
||||||
</Tooltip>
|
<span>
|
||||||
|
<IconButton
|
||||||
|
disabled={item.details === undefined || item.details === null || item.details === ""}
|
||||||
|
style={{marginLeft: 10, }}
|
||||||
|
>
|
||||||
|
{open ?
|
||||||
|
<ExpandLessIcon />
|
||||||
|
:
|
||||||
|
<ExpandMoreIcon />
|
||||||
|
}
|
||||||
|
|
||||||
|
</IconButton>
|
||||||
|
</span>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -976,13 +1087,17 @@ const AgentUI = (props) => {
|
|||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
style={{marginTop: 16, }}
|
style={{marginTop: 16, }}
|
||||||
disabled={questionSubmitDisabled}
|
disabled={questionSubmitDisabled || agentRequestLoading}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
submitQuestions(item?.details?.run_details?.id, questionAnswers)
|
submitQuestions(item?.details?.run_details?.id, questionAnswers)
|
||||||
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Submit
|
{agentRequestLoading ?
|
||||||
|
<CircularProgress size={20} style={{marginRight: 10, }} />
|
||||||
|
:
|
||||||
|
"Submit"
|
||||||
|
}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
: null}
|
: null}
|
||||||
@@ -1078,6 +1193,7 @@ const AgentUI = (props) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var finishDecisionId = ""
|
var finishDecisionId = ""
|
||||||
|
var finishAnswer = ""
|
||||||
var sortedTimelineItems = []
|
var sortedTimelineItems = []
|
||||||
for (var key in agent_data?.decisions) {
|
for (var key in agent_data?.decisions) {
|
||||||
const item = agent_data.decisions[key]
|
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") {
|
if (item?.details?.action === "finish" || item.action === "finish" || item.category == "finish" || item?.details?.action == "finalise") {
|
||||||
finishDecisionId = item?.run_details?.id
|
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) => {
|
// Fill blank space between items in timeline by adding "empty" decisions:
|
||||||
if (a.start_time < b.start_time) {
|
// 1. Sort
|
||||||
return 1;
|
// 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 (
|
return (
|
||||||
<div style={{marginTop: 20, }}>
|
<div style={{marginTop: 20, }}>
|
||||||
@@ -1151,6 +1321,7 @@ const AgentUI = (props) => {
|
|||||||
<Box
|
<Box
|
||||||
component="form"
|
component="form"
|
||||||
style={{width: "100%", textAlign: "center",}}
|
style={{width: "100%", textAlign: "center",}}
|
||||||
|
onKeyDown={handleKeyDownCont}
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
@@ -1161,15 +1332,35 @@ const AgentUI = (props) => {
|
|||||||
}, true)
|
}, 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>
|
<div>
|
||||||
<TextField
|
<TextField
|
||||||
label="Add more details to the current task"
|
label="Add more details to continue the current task"
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
disabled={agentRequestLoading}
|
disabled={agentRequestLoading}
|
||||||
style={{width: 400, margin: "auto", }}
|
style={{width: 450, margin: "auto", }}
|
||||||
multiline
|
multiline
|
||||||
minRows={1}
|
minRows={2}
|
||||||
|
value={continuationText}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
console.log("Value: ", e.target.value)
|
console.log("Value: ", e.target.value)
|
||||||
//setActionInput(e.target.value)
|
//setActionInput(e.target.value)
|
||||||
@@ -1195,7 +1386,7 @@ const AgentUI = (props) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Typography variant="body2" color="textSecondary" style={{marginTop: 10, }}>
|
<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>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
<Typography color="textSecondary" variant="body1" style={{marginTop: 25, marginLeft: 20, }}>
|
<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?
|
// 1. Run the execution. Can this be a single-action run?
|
||||||
// 2. Get the execution ID and node ID from the response.
|
// 2. Get the execution ID and node ID from the response.
|
||||||
const uuid = uuidv4()
|
const uuid = uuidv4()
|
||||||
var parsedAction = "list_tickets,API" // Default action for now
|
var parsedAction = "API" // Default action for now
|
||||||
if (chosenApps.length > 0) {
|
if (chosenApps.length > 0) {
|
||||||
parsedAction = ""
|
parsedAction = ""
|
||||||
for (var appKey in chosenApps) {
|
for (var appKey in chosenApps) {
|
||||||
@@ -1319,155 +1510,253 @@ const AgentUI = (props) => {
|
|||||||
cursor: "pointer",
|
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 (
|
return (
|
||||||
<div style={agentWrapperStyle}>
|
<div>
|
||||||
<TextField
|
<div style={{height: showAgentStarter ? "25vh" : "5vh", }}>
|
||||||
id="copy_element_shuffle"
|
</div>
|
||||||
style={{ display: "none" }}
|
<div style={agentWrapperStyle}>
|
||||||
/>
|
<div style={{
|
||||||
|
minWidth: 1000, maxWidth: 1000, margin: "auto",
|
||||||
{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>
|
|
||||||
<TextField
|
<TextField
|
||||||
label="What do you want to do?"
|
id="copy_element_shuffle"
|
||||||
variant="outlined"
|
style={{ display: "none" }}
|
||||||
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>
|
|
||||||
),
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div style={{display: "flex", margin: "auto", paddingTop: 10, minWidth: 300, maxWidth: 300, justifyContent: "center", overflowWrap: "wrap", }}>
|
{showAgentStarter ?
|
||||||
<div>
|
<Box
|
||||||
<Chip
|
component="form"
|
||||||
id="add_app_chip"
|
style={{textAlign: "center", }}
|
||||||
icon={<AddIcon />} label="Select Apps"
|
onKeyDown={handleKeyDownRoot}
|
||||||
style={chipStyle}
|
onSubmit={(e) => {
|
||||||
onClick={() => {
|
e.preventDefault();
|
||||||
setAppPickerAnchor(document.getElementById("add_app_chip"))
|
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}
|
<div style={{display: "flex", margin: "auto", paddingTop: 30, minWidth: 300, maxWidth: 300, justifyContent: "center", overflowWrap: "wrap", }}>
|
||||||
anchorEl={appPickerAnchor}
|
<div>
|
||||||
onClose={() => {
|
<Chip
|
||||||
setAppPickerAnchor(null)
|
id="add_app_chip"
|
||||||
}}
|
icon={<AddIcon />} label="Select Apps / MCPs"
|
||||||
anchorOrigin={{
|
style={chipStyle}
|
||||||
vertical: 'bottom',
|
onClick={() => {
|
||||||
horizontal: 'left',
|
setAppPickerAnchor(document.getElementById("add_app_chip"))
|
||||||
}}
|
}}
|
||||||
>
|
|
||||||
<AppSearch
|
|
||||||
userdata={userdata}
|
|
||||||
defaultSearch={""}
|
|
||||||
newSelectedApp={newSelectedApp}
|
|
||||||
setNewSelectedApp={setNewSelectedApp}
|
|
||||||
inputHeight={200}
|
|
||||||
/>
|
/>
|
||||||
</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>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -463,9 +463,9 @@ const AngularWorkflow = (defaultprops) => {
|
|||||||
|
|
||||||
var to_be_copied = "";
|
var to_be_copied = "";
|
||||||
const [firstrequest, setFirstrequest] = React.useState(true);
|
const [firstrequest, setFirstrequest] = React.useState(true);
|
||||||
const cystyle = useMemo(() => defaultCytoscapeStyle(theme), [themeMode]);
|
const [apps, setApps] = React.useState([]);
|
||||||
// const cystyle = useMemo(() => defaultCytoscapeStyle, [themeMode]);
|
|
||||||
|
|
||||||
|
const cystyle = useMemo(() => defaultCytoscapeStyle(theme, apps), [themeMode, apps]);
|
||||||
const [cy, setCy] = React.useState();
|
const [cy, setCy] = React.useState();
|
||||||
|
|
||||||
const useStyles = makeStyles({
|
const useStyles = makeStyles({
|
||||||
@@ -593,7 +593,6 @@ const AngularWorkflow = (defaultprops) => {
|
|||||||
const [showDropdown, setShowDropdown] = React.useState(false);
|
const [showDropdown, setShowDropdown] = React.useState(false);
|
||||||
const [triggerActionList, setTriggerActionList] = React.useState([]);
|
const [triggerActionList, setTriggerActionList] = React.useState([]);
|
||||||
|
|
||||||
const [apps, setApps] = React.useState([]);
|
|
||||||
const [filteredApps, setFilteredApps] = React.useState([]);
|
const [filteredApps, setFilteredApps] = React.useState([]);
|
||||||
const [prioritizedApps, setPrioritizedApps] = 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
|
// Center and zoom to the node with smooth animation if requested
|
||||||
if (config.center) {
|
if (config.center) {
|
||||||
cy.animate({
|
cy.stop().animate({
|
||||||
center: {
|
center: {
|
||||||
eles: cyNode
|
eles: cyNode
|
||||||
},
|
},
|
||||||
@@ -3324,7 +3323,7 @@ const AngularWorkflow = (defaultprops) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const animationDuration = 150
|
const animationDuration = 150
|
||||||
foundnode.animate(
|
foundnode.stop().animate(
|
||||||
{
|
{
|
||||||
style: parsedStyle,
|
style: parsedStyle,
|
||||||
},
|
},
|
||||||
@@ -3445,6 +3444,18 @@ const AngularWorkflow = (defaultprops) => {
|
|||||||
return
|
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)
|
setExecutionRunning(true)
|
||||||
setExecutionRequestStarted(true)
|
setExecutionRequestStarted(true)
|
||||||
var headers = {
|
var headers = {
|
||||||
@@ -3830,7 +3841,7 @@ const AngularWorkflow = (defaultprops) => {
|
|||||||
} else {
|
} else {
|
||||||
console.log("Closing auth modal? FAIL")
|
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
|
shouldClose = false
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -4184,7 +4195,7 @@ const AngularWorkflow = (defaultprops) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const animationDuration = 150
|
const animationDuration = 150
|
||||||
node.animate(
|
node.stop().animate(
|
||||||
{
|
{
|
||||||
style: parsedStyle,
|
style: parsedStyle,
|
||||||
},
|
},
|
||||||
@@ -4201,7 +4212,7 @@ const AngularWorkflow = (defaultprops) => {
|
|||||||
"border-color": "#81c784",
|
"border-color": "#81c784",
|
||||||
}
|
}
|
||||||
|
|
||||||
node.animate(
|
node.stop().animate(
|
||||||
{
|
{
|
||||||
style: parsedStyle,
|
style: parsedStyle,
|
||||||
},
|
},
|
||||||
@@ -4250,7 +4261,7 @@ const AngularWorkflow = (defaultprops) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const animationDuration = 150
|
const animationDuration = 150
|
||||||
node.animate(
|
node.stop().animate(
|
||||||
{
|
{
|
||||||
style: parsedStyle,
|
style: parsedStyle,
|
||||||
},
|
},
|
||||||
@@ -4267,7 +4278,7 @@ const AngularWorkflow = (defaultprops) => {
|
|||||||
"border-color": "#81c784",
|
"border-color": "#81c784",
|
||||||
}
|
}
|
||||||
|
|
||||||
node.animate(
|
node.stop().animate(
|
||||||
{
|
{
|
||||||
style: parsedStyle,
|
style: parsedStyle,
|
||||||
},
|
},
|
||||||
@@ -4298,7 +4309,7 @@ const AngularWorkflow = (defaultprops) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const animationDuration = 150
|
const animationDuration = 150
|
||||||
node.animate(
|
node.stop().animate(
|
||||||
{
|
{
|
||||||
style: parsedStyle,
|
style: parsedStyle,
|
||||||
},
|
},
|
||||||
@@ -4314,7 +4325,7 @@ const AngularWorkflow = (defaultprops) => {
|
|||||||
"line-gradient-stop-colors": ["grey", "grey"],
|
"line-gradient-stop-colors": ["grey", "grey"],
|
||||||
}
|
}
|
||||||
|
|
||||||
node.animate(
|
node.stop().animate(
|
||||||
{
|
{
|
||||||
style: parsedStyle,
|
style: parsedStyle,
|
||||||
},
|
},
|
||||||
@@ -4346,7 +4357,7 @@ const AngularWorkflow = (defaultprops) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const animationDuration = 150
|
const animationDuration = 150
|
||||||
node.animate(
|
node.stop().animate(
|
||||||
{
|
{
|
||||||
style: parsedStyle,
|
style: parsedStyle,
|
||||||
},
|
},
|
||||||
@@ -4363,7 +4374,7 @@ const AngularWorkflow = (defaultprops) => {
|
|||||||
"border-color": color,
|
"border-color": color,
|
||||||
}
|
}
|
||||||
|
|
||||||
node.animate(
|
node.stop().animate(
|
||||||
{
|
{
|
||||||
style: parsedStyle,
|
style: parsedStyle,
|
||||||
},
|
},
|
||||||
@@ -4394,7 +4405,7 @@ const AngularWorkflow = (defaultprops) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const animationDuration = 150
|
const animationDuration = 150
|
||||||
node.animate(
|
node.stop().animate(
|
||||||
{
|
{
|
||||||
style: parsedStyle,
|
style: parsedStyle,
|
||||||
},
|
},
|
||||||
@@ -4450,7 +4461,7 @@ const AngularWorkflow = (defaultprops) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const animationDuration = 150
|
const animationDuration = 150
|
||||||
node.animate(
|
node.stop().animate(
|
||||||
{
|
{
|
||||||
style: parsedStyle,
|
style: parsedStyle,
|
||||||
},
|
},
|
||||||
@@ -4484,7 +4495,7 @@ const AngularWorkflow = (defaultprops) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const animationDuration = 150
|
const animationDuration = 150
|
||||||
node.animate(
|
node.stop().animate(
|
||||||
{
|
{
|
||||||
style: parsedStyle,
|
style: parsedStyle,
|
||||||
},
|
},
|
||||||
@@ -8627,7 +8638,7 @@ const AngularWorkflow = (defaultprops) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
event.target.animate(
|
event.target.stop().animate(
|
||||||
{
|
{
|
||||||
style: parsedStyle,
|
style: parsedStyle,
|
||||||
},
|
},
|
||||||
@@ -9423,7 +9434,7 @@ const AngularWorkflow = (defaultprops) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (event.target !== undefined && event.target !== null) {
|
if (event.target !== undefined && event.target !== null) {
|
||||||
event.target.animate(
|
event.target.stop().animate(
|
||||||
{
|
{
|
||||||
style: parsedStyle,
|
style: parsedStyle,
|
||||||
},
|
},
|
||||||
@@ -9741,7 +9752,7 @@ const AngularWorkflow = (defaultprops) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const animationDuration = 150
|
const animationDuration = 150
|
||||||
foundnode.animate(
|
foundnode.stop().animate(
|
||||||
{
|
{
|
||||||
style: parsedStyle,
|
style: parsedStyle,
|
||||||
},
|
},
|
||||||
@@ -9910,7 +9921,7 @@ const AngularWorkflow = (defaultprops) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const animationDuration = 150
|
const animationDuration = 150
|
||||||
foundnode.animate(
|
foundnode.stop().animate(
|
||||||
{
|
{
|
||||||
style: parsedStyle,
|
style: parsedStyle,
|
||||||
},
|
},
|
||||||
@@ -12487,7 +12498,7 @@ const AngularWorkflow = (defaultprops) => {
|
|||||||
//}
|
//}
|
||||||
safeRefine(event.currentTarget.value)
|
safeRefine(event.currentTarget.value)
|
||||||
}}
|
}}
|
||||||
limit={5}
|
limit={15}
|
||||||
/>
|
/>
|
||||||
{/*isSearchStalled ? 'My search is stalled' : ''*/}
|
{/*isSearchStalled ? 'My search is stalled' : ''*/}
|
||||||
</form>
|
</form>
|
||||||
@@ -14360,8 +14371,7 @@ const AngularWorkflow = (defaultprops) => {
|
|||||||
padding: 30,
|
padding: 30,
|
||||||
pointerEvents: "auto",
|
pointerEvents: "auto",
|
||||||
color: theme.palette.text.primary,
|
color: theme.palette.text.primary,
|
||||||
minWidth: isMobile ? "90%" : 650,
|
minWidth: isMobile ? "90%" : 750,
|
||||||
border: theme.palette.defaultBorder,
|
|
||||||
|
|
||||||
borderRadius: theme.palette.borderRadius,
|
borderRadius: theme.palette.borderRadius,
|
||||||
backgroundColor: "black",
|
backgroundColor: "black",
|
||||||
@@ -14474,18 +14484,24 @@ const AngularWorkflow = (defaultprops) => {
|
|||||||
<Button
|
<Button
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
color="primary"
|
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={() => {
|
onClick={() => {
|
||||||
setExecutionText(data)
|
setExecutionText(data)
|
||||||
executeWorkflow(data, workflow.start, lastSaved);
|
executeWorkflow(data, workflow.start, lastSaved)
|
||||||
|
|
||||||
setExecutionArgumentModalOpen(false)
|
setExecutionArgumentModalOpen(false)
|
||||||
}}
|
}}
|
||||||
>Select</Button>
|
>Select</Button>
|
||||||
<ReactJson
|
<ReactJson
|
||||||
src={validate.result}
|
src={validate.result}
|
||||||
theme={theme.palette.jsonTheme}
|
theme={"summerfruit"}
|
||||||
style={theme.palette.reactJsonStyle}
|
style={{
|
||||||
|
padding: 5,
|
||||||
|
width: "98%",
|
||||||
|
borderRadius: 5,
|
||||||
|
border: "1px solid rgba(255,255,255,0.7)",
|
||||||
|
overflowX: "auto",
|
||||||
|
}}
|
||||||
shouldCollapse={(jsonField) => {
|
shouldCollapse={(jsonField) => {
|
||||||
return collapseField(jsonField)
|
return collapseField(jsonField)
|
||||||
}}
|
}}
|
||||||
@@ -14499,10 +14515,11 @@ const AngularWorkflow = (defaultprops) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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={() => {
|
onClick={() => {
|
||||||
setExecutionText(data)
|
setExecutionText(data)
|
||||||
executeWorkflow(data, workflow.start, lastSaved);
|
//executeWorkflow(data, workflow.start, lastSaved);
|
||||||
|
|
||||||
setExecutionArgumentModalOpen(false)
|
setExecutionArgumentModalOpen(false)
|
||||||
}}
|
}}
|
||||||
@@ -14759,6 +14776,7 @@ const AngularWorkflow = (defaultprops) => {
|
|||||||
color: theme.palette.textColor,
|
color: theme.palette.textColor,
|
||||||
backgroundColor: 'inherit',
|
backgroundColor: 'inherit',
|
||||||
zIndex: 10000,
|
zIndex: 10000,
|
||||||
|
fontSize: 12,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<b>PS: Conditions can't be used for loops [ .# ]. Use the filters list action.{" "}</b>
|
<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}
|
key={condition.condition.id}
|
||||||
square
|
square
|
||||||
style={paperVariableStyle}
|
style={paperVariableStyle}
|
||||||
onClick={() => { }}
|
onClick={() => {
|
||||||
|
setLastSaved(false)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -15339,6 +15359,7 @@ const AngularWorkflow = (defaultprops) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setLastSaved(false)
|
||||||
setSourceValue({
|
setSourceValue({
|
||||||
name: "source",
|
name: "source",
|
||||||
value: "",
|
value: "",
|
||||||
@@ -15384,17 +15405,15 @@ const AngularWorkflow = (defaultprops) => {
|
|||||||
// Change Direction of the branch target/source
|
// Change Direction of the branch target/source
|
||||||
const foundBranch = cy.getElementById(selectedEdge.id)
|
const foundBranch = cy.getElementById(selectedEdge.id)
|
||||||
if (foundBranch !== undefined && foundBranch !== null) {
|
if (foundBranch !== undefined && foundBranch !== null) {
|
||||||
console.log("BRANCH: ", foundBranch)
|
|
||||||
const source = foundBranch.data("source")
|
const source = foundBranch.data("source")
|
||||||
const target = foundBranch.data("target")
|
const target = foundBranch.data("target")
|
||||||
|
|
||||||
var branchdata = JSON.parse(JSON.stringify(foundBranch.data()))
|
var branchdata = JSON.parse(JSON.stringify(foundBranch.data()))
|
||||||
console.log("BEFORE: ", branchdata)
|
|
||||||
console.log("Start node", workflow.start)
|
|
||||||
const startNode = workflow.start
|
const startNode = workflow.start
|
||||||
if (source === startNode) {
|
if (source === startNode) {
|
||||||
toast("Can't point to Start Node")
|
toast("Can't point to Start Node")
|
||||||
} else {
|
} else {
|
||||||
|
setLastSaved(false)
|
||||||
const newid = uuidv4()
|
const newid = uuidv4()
|
||||||
branchdata.source = target
|
branchdata.source = target
|
||||||
branchdata.target = source
|
branchdata.target = source
|
||||||
@@ -19298,7 +19317,7 @@ const AngularWorkflow = (defaultprops) => {
|
|||||||
}, 2000);
|
}, 2000);
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.info("Successfully changed active organisation - refreshing!");
|
toast.success("Successfully changed active organisation - refreshing!");
|
||||||
} else {
|
} else {
|
||||||
if (responseJson.reason !== undefined && responseJson.reason !== null && responseJson.reason.length > 0) {
|
if (responseJson.reason !== undefined && responseJson.reason !== null && responseJson.reason.length > 0) {
|
||||||
toast(responseJson.reason);
|
toast(responseJson.reason);
|
||||||
@@ -20649,7 +20668,7 @@ const AngularWorkflow = (defaultprops) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const buttonHeights = 45
|
const buttonHeights = 45
|
||||||
const boxSize = buttonHeights
|
const boxSize = buttonHeights+2
|
||||||
const executionButton = executionRunning ? (
|
const executionButton = executionRunning ? (
|
||||||
<Tooltip color="primary" title="Stop execution" placement="top">
|
<Tooltip color="primary" title="Stop execution" placement="top">
|
||||||
<span>
|
<span>
|
||||||
@@ -20737,20 +20756,17 @@ const AngularWorkflow = (defaultprops) => {
|
|||||||
>
|
>
|
||||||
<TextField
|
<TextField
|
||||||
id="execution_argument_input_field"
|
id="execution_argument_input_field"
|
||||||
style={{
|
variant="outlined"
|
||||||
...theme.palette.textFieldStyle,
|
|
||||||
height: buttonHeights + 2,
|
|
||||||
}}
|
|
||||||
InputProps={{
|
InputProps={{
|
||||||
style: {
|
style: {
|
||||||
...theme.palette.innerTextfieldStyle,
|
...theme.palette.innerTextfieldStyle,
|
||||||
height: buttonHeights + 2,
|
height: buttonHeights+2,
|
||||||
marginTop: -1,
|
marginTop: 0,
|
||||||
border: "none",
|
|
||||||
|
|
||||||
// Remove left side borderRadius
|
// Remove left side borderRadius
|
||||||
borderTopLeftRadius: 0,
|
borderTopLeftRadius: 0,
|
||||||
borderBottomLeftRadius: 0,
|
borderBottomLeftRadius: 0,
|
||||||
|
borderTop: "0px solid",
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={workflow.public}
|
disabled={workflow.public}
|
||||||
@@ -21687,7 +21703,7 @@ const AngularWorkflow = (defaultprops) => {
|
|||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ display: "flex" }}>
|
<div style={{ display: "flex", maxHeight: 500, overflow: "auto",}}>
|
||||||
<IconButton
|
<IconButton
|
||||||
style={{
|
style={{
|
||||||
marginTop: "auto",
|
marginTop: "auto",
|
||||||
@@ -21749,7 +21765,7 @@ const AngularWorkflow = (defaultprops) => {
|
|||||||
onSelect={(select) => {
|
onSelect={(select) => {
|
||||||
HandleJsonCopy(validate.result, select, "exec");
|
HandleJsonCopy(validate.result, select, "exec");
|
||||||
}}
|
}}
|
||||||
name={false}
|
name={"$exec"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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
|
const iconMargin = 7
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -22943,6 +22978,7 @@ const AngularWorkflow = (defaultprops) => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{executionData.execution_source !== undefined &&
|
{executionData.execution_source !== undefined &&
|
||||||
executionData.execution_source !== null &&
|
executionData.execution_source !== null &&
|
||||||
executionData.execution_source.length > 0 &&
|
executionData.execution_source.length > 0 &&
|
||||||
@@ -22954,8 +22990,17 @@ const AngularWorkflow = (defaultprops) => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body1" color="textSecondary">
|
<Typography variant="body1" color="textSecondary">
|
||||||
|
|
||||||
|
{executionData?.execution_source?.startsWith("datastore") ?
|
||||||
{executionData.execution_source === "authgroups" || (executionData.authgroup !== undefined && executionData.authgroup !== null && executionData.authgroup.length > 0) ?
|
<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
|
<a
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
href={`/admin?tab=app_auth`}
|
href={`/admin?tab=app_auth`}
|
||||||
@@ -23614,7 +23659,7 @@ const AngularWorkflow = (defaultprops) => {
|
|||||||
HandleJsonCopy(showResult, select, data.action.label);
|
HandleJsonCopy(showResult, select, data.action.label);
|
||||||
console.log("SELECTED!: ", select);
|
console.log("SELECTED!: ", select);
|
||||||
}}
|
}}
|
||||||
name={"Results for " + data.action.label}
|
name={`$${data?.action?.label?.toLowerCase()}`}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</span>
|
</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(() => {
|
useEffect(() => {
|
||||||
if (window.location.pathname.includes("apps/edit")) {
|
if (window.location.pathname.includes("apps/edit")) {
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ const buttonBackground = "linear-gradient(to right, #f86a3e, #f34079)";
|
|||||||
read_time: 1,
|
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
|
// 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 = {
|
const hrefStyle = {
|
||||||
color: "rgba(255, 255, 255, 0.8)",
|
color: "rgba(255, 255, 255, 0.8)",
|
||||||
textDecoration: "none",
|
textDecoration: "none",
|
||||||
marginRight: window.location.pathname.includes("/articles/") ? "0.8em" : undefined,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -387,6 +386,7 @@ export const CodeHandler = (props) => {
|
|||||||
minWidth: "50%",
|
minWidth: "50%",
|
||||||
maxWidth: "100%",
|
maxWidth: "100%",
|
||||||
backgroundColor: theme.palette.inputColor,
|
backgroundColor: theme.palette.inputColor,
|
||||||
|
whiteSpace: "pre-wrap",
|
||||||
overflowY: "auto",
|
overflowY: "auto",
|
||||||
// Have it inline
|
// Have it inline
|
||||||
borderRadius: theme.palette?.borderRadius,
|
borderRadius: theme.palette?.borderRadius,
|
||||||
@@ -1020,7 +1020,7 @@ const Docs = (defaultprops) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fetchDocList = (resetCache = false) => {
|
const fetchDocList = (resetCache = false) => {
|
||||||
var url = `${globalUrl}/api/v1/docs`
|
var url = `${globalUrl}/api/v1/docs?resetCache=${resetCache}`
|
||||||
if (location.pathname.includes("/legal")) {
|
if (location.pathname.includes("/legal")) {
|
||||||
url = `${globalUrl}/api/v1/docs?folder=legal&resetCache=${resetCache}`
|
url = `${globalUrl}/api/v1/docs?folder=legal&resetCache=${resetCache}`
|
||||||
} else if (location.pathname.includes("/articles")) {
|
} else if (location.pathname.includes("/articles")) {
|
||||||
@@ -1047,16 +1047,16 @@ const Docs = (defaultprops) => {
|
|||||||
.catch((error) => { });
|
.catch((error) => { });
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchDoc = (docId) => {
|
const fetchDoc = (docId, resetCache = false) => {
|
||||||
if (docId === undefined) {
|
if (docId === undefined) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var url = `${globalUrl}/api/v1/docs/${docId}`
|
var url = `${globalUrl}/api/v1/docs/${docId}?resetCache=${resetCache}`
|
||||||
if (location.pathname.includes("/legal")) {
|
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")) {
|
} 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, {
|
fetch(url, {
|
||||||
@@ -1082,7 +1082,7 @@ const Docs = (defaultprops) => {
|
|||||||
autoClose: 2000,
|
autoClose: 2000,
|
||||||
});
|
});
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
navigate(`/articles/2.0_release`)
|
navigate(`/articles`)
|
||||||
}, 2000)
|
}, 2000)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1130,7 +1130,8 @@ const Docs = (defaultprops) => {
|
|||||||
|
|
||||||
const handleResetCache = () => {
|
const handleResetCache = () => {
|
||||||
fetchDocList(true);
|
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) {
|
if (firstrequest) {
|
||||||
@@ -1344,6 +1345,21 @@ const Docs = (defaultprops) => {
|
|||||||
tr: TableRowRenderer,
|
tr: TableRowRenderer,
|
||||||
th: TableHeaderCellRenderer,
|
th: TableHeaderCellRenderer,
|
||||||
td: TableCellRenderer,
|
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 = {
|
const activeListItemStyle = {
|
||||||
backgroundColor: "rgba(248, 106, 62, 0.08)", // Slight orange tint
|
backgroundColor: "rgba(248, 106, 62, 0.08)", // Slight orange tint
|
||||||
marginRight: window.location.pathname.includes("/articles/") ? "0.8em" : undefined,
|
|
||||||
borderLeft: "3px solid #f86a3e",
|
borderLeft: "3px solid #f86a3e",
|
||||||
paddingLeft: "13px", // Compensate for the border
|
paddingLeft: "13px", // Compensate for the border
|
||||||
};
|
};
|
||||||
@@ -1415,6 +1430,7 @@ const Docs = (defaultprops) => {
|
|||||||
<List style={{
|
<List style={{
|
||||||
listStyle: "none",
|
listStyle: "none",
|
||||||
paddingLeft: "0",
|
paddingLeft: "0",
|
||||||
|
paddingRight: isArticlePage ? 15 : undefined,
|
||||||
paddingTop: !isArticlePage ? "60px" : undefined,
|
paddingTop: !isArticlePage ? "60px" : undefined,
|
||||||
paddingBottom: !isArticlePage ? "30px" : undefined,
|
paddingBottom: !isArticlePage ? "30px" : undefined,
|
||||||
display: isArticlePage ? sidebarOpen ? "block" : "none" : undefined,
|
display: isArticlePage ? sidebarOpen ? "block" : "none" : undefined,
|
||||||
@@ -1524,15 +1540,32 @@ const Docs = (defaultprops) => {
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div style={IndexBar}>
|
<div style={IndexBar}>
|
||||||
{userdata?.support && isArticlePage && (
|
{userdata?.support && (
|
||||||
<Button
|
<Tooltip
|
||||||
variant="contained"
|
title="Wait about 5 minutes after updating the file on GitHub, then click to reset the cache. (Support only) "
|
||||||
color="secondary"
|
placement="top"
|
||||||
onClick={handleResetCache}
|
componentsProps={{
|
||||||
style={{marginBottom: "15px"}}
|
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 ?
|
{tocLines.length > 0 ?
|
||||||
(
|
(
|
||||||
|
|||||||
@@ -111,8 +111,8 @@ const NewDashboard = (props) => {
|
|||||||
const STATIC_TIME_PERCENT = 'TBD'
|
const STATIC_TIME_PERCENT = 'TBD'
|
||||||
const STATIC_MONEY_PERCENT = 'TBD'
|
const STATIC_MONEY_PERCENT = 'TBD'
|
||||||
const kpis = [
|
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: 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: 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(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' },
|
{ 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(() => {
|
useEffect(() => {
|
||||||
const anyLoading =
|
const anyLoading =
|
||||||
loadingSfw ||
|
//loadingSfw ||
|
||||||
loadingRot ||
|
//loadingRot ||
|
||||||
loadingNoti ||
|
loadingNoti ||
|
||||||
loadingSelectedOrgStats ||
|
loadingSelectedOrgStats //||
|
||||||
!selectedOrganization ||
|
!selectedOrganization ||
|
||||||
!selectedOrgForStats;
|
//!selectedOrgForStats;
|
||||||
setShowOverlay(anyLoading);
|
setShowOverlay(anyLoading);
|
||||||
}, [
|
}, [
|
||||||
loadingSfw,
|
loadingSfw,
|
||||||
@@ -399,6 +399,7 @@ const NewDashboard = (props) => {
|
|||||||
headerSubtitle="Complete these steps to start seeing insights."
|
headerSubtitle="Complete these steps to start seeing insights."
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{showOverlay && (
|
{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={{ 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 }}>
|
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 12 }}>
|
||||||
@@ -407,6 +408,7 @@ const NewDashboard = (props) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Header / Greeting */}
|
{/* Header / Greeting */}
|
||||||
<Box style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', margin: '8px 0 16px 0' }}>
|
<Box style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', margin: '8px 0 16px 0' }}>
|
||||||
<Typography variant="h5">{`${getGreeting()}, ${displayName ?? 'User'}!`}</Typography>
|
<Typography variant="h5">{`${getGreeting()}, ${displayName ?? 'User'}!`}</Typography>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const SetAuthentication = (props) => {
|
|||||||
const [loadFail, setLoadFail] = useState("");
|
const [loadFail, setLoadFail] = useState("");
|
||||||
const [appAuthentication, setAppAuthentication] = React.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 alert = useAlert();
|
||||||
|
|
||||||
const parseIncomingOpenapiData = (data) => {
|
const parseIncomingOpenapiData = (data) => {
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ const Welcome = (props) => {
|
|||||||
}
|
}
|
||||||
}, [activeStep])
|
}, [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([
|
const [steps, setSteps] = useState([
|
||||||
"Help us get to know you",
|
"Help us get to know you",
|
||||||
"Find your Apps",
|
"Find your Apps",
|
||||||
|
|||||||
@@ -343,7 +343,7 @@ export const GetIconInfo = (action) => {
|
|||||||
"protect",
|
"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 = ""
|
var selectedKey = ""
|
||||||
@@ -3346,7 +3346,7 @@ const Workflows2 = (props) => {
|
|||||||
}
|
}
|
||||||
style={{
|
style={{
|
||||||
textDecoration: "none",
|
textDecoration: "none",
|
||||||
color: "inherit",
|
color: theme.palette.text.primary,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
textOverflow: "ellipsis",
|
textOverflow: "ellipsis",
|
||||||
whiteSpace: "nowrap",
|
whiteSpace: "nowrap",
|
||||||
@@ -5149,7 +5149,7 @@ const Workflows2 = (props) => {
|
|||||||
|
|
||||||
{backgroundWorkflows.length > 0 &&
|
{backgroundWorkflows.length > 0 &&
|
||||||
<Tab
|
<Tab
|
||||||
label={`Background Processes`}
|
label={`Security Bundle`}
|
||||||
value={4}
|
value={4}
|
||||||
style={{
|
style={{
|
||||||
...tabStyle,
|
...tabStyle,
|
||||||
|
|||||||
Reference in New Issue
Block a user