Merge for 2.2.0 release

This commit is contained in:
Frikky
2026-02-10 13:29:26 +01:00
parent 487927da3e
commit f51848fe65
64 changed files with 12124 additions and 2657 deletions
+367 -82
View File
@@ -5,7 +5,7 @@ import { toast } from 'react-toastify';
import ReactJson from "react-json-view-ssr";
import { validateJson, handleReactJsonClipboard, GetIconInfo, } from "../views/Workflows2.jsx";
import { red } from "../views/AngularWorkflow.jsx";
import { green, red } from "../views/AngularWorkflow.jsx";
import CollectIngestModal from "../components/CollectIngestModal.jsx";
import CorrelationGraph from "../components/CorrelationGraph.jsx";
import { useNavigate, Link, useParams } from "react-router-dom";
@@ -37,9 +37,12 @@ import {
Pagination,
PaginationItem,
Avatar,
ListSubheader,
} from "@mui/material";
import { DataGrid } from '@mui/x-data-grid';
import {
DataGrid,
} from '@mui/x-data-grid';
import {
Link as LinkIcon,
@@ -52,6 +55,7 @@ import {
OpenInNew as OpenInNewIcon,
CloudDownload as CloudDownloadIcon,
Description as DescriptionIcon,
Polymer as PolymerIcon,
CheckCircle as CheckCircleIcon,
Close as CloseIcon,
Apps as AppsIcon,
@@ -59,6 +63,7 @@ import {
Cached as CachedIcon,
AccessibilityNew as AccessibilityNewIcon,
Lock as LockIcon,
Eco as EcoIcon,
Schedule as ScheduleIcon,
Cloud as CloudIcon,
Business as BusinessIcon,
@@ -66,6 +71,7 @@ import {
VisibilityOff as VisibilityOffIcon,
Clear as ClearIcon,
Add as AddIcon,
Remove as RemoveIcon,
Rocket as RocketIcon,
Webhook as WebhookIcon,
Air as AirIcon,
@@ -76,6 +82,8 @@ import {
FilterAlt as FilterAltIcon,
CompareArrows as CompareArrowsIcon,
Hub as HubIcon,
Key as KeyIcon,
FlashOn as FlashOnIcon,
} from "@mui/icons-material";
import { Context } from "../context/ContextApi.jsx";
@@ -133,6 +141,7 @@ const CacheView = memo((props) => {
const [selectAllChecked, setSelectAllChecked] = React.useState(false)
const [renderTextBox, setRenderTextBox] = React.useState(false);
const [datastoreCategories, setDatastoreCategories] = React.useState(["default", "protected"]);
const [datastoreCategoryGroups, setDatastoreCategoryGroups] = React.useState([]);
const [selectedCategory, setSelectedCategory] = React.useState("default");
const [selectedFileId, setSelectedFileId] = React.useState("");
const [updateToThisCategory, setUpdateToThisCategory] = useState("")
@@ -163,6 +172,18 @@ const CacheView = memo((props) => {
var to_be_copied = "";
const defaultAutomation = [
{
"name": "Security Rules",
"description": "Describes security rules that are validated BEFORE an update occurs. This is in order for bad writes to be avoided. Control: allow, deny, merge, overwrite. Logic: if, or, and. Functions: same_shape, is_superset, has_deleted_field",
"options": [{
"key": "rule",
"value": "",
}],
"icon": <KeyIcon />,
"beta": true,
"disabled": false,
"enabled": false,
},
{
"name": "Run workflow",
"description": "Runs one or more workflows with the updated value as runtime argument",
@@ -200,14 +221,16 @@ const CacheView = memo((props) => {
{
"name": "Run AI Agent",
"description": "Runs an AI Agent to process the updated value. Uses built-in ShuffleAI configs. Learn more: https://shuffler.io/docs/AI",
"description": "Runs an AI Agent to process the updated value. Controllable based on your 'action' input, where the full updated value is added on the end. All actions run in order, meaning it needs to finish the previous one before the next is ran. Uses built-in ShuffleAI configs. Learn more: https://shuffler.io/docs/AI",
"type": "singul",
"options": [{
"key": "",
"key": "action-1",
"description": "Describe the action to perform on this item",
"value": "",
}],
"beta": true,
"icon": <SmartToyIcon />,
"enabled": false,
"disabled": true,
"disabled": false,
},
{
@@ -218,22 +241,10 @@ const CacheView = memo((props) => {
"key": "",
"value": "",
}],
"beta": true,
"icon": "/images/logos/singul.svg",
"disabled": false,
},
{
"name": "Send message",
"description": "",
"type": "singul",
"options": [{
"key": "app",
"value": "",
}],
"icon": <SendIcon />,
"disabled": true,
"enabled": false,
},
]
const [categoryAutomations, setCategoryAutomations] = useState(defaultAutomation)
@@ -317,8 +328,21 @@ const CacheView = memo((props) => {
var chosenCategory = selectedCategory
const urlParams = new URLSearchParams(window.location.search)
const categoryParam = urlParams.get("category")
var categoryParam = urlParams.get("category")
if (categoryParam && categoryParam !== undefined && categoryParam !== "default" && categoryParam !== "") {
// In order to make linking weird urls from workflow page work.
if (urlParams.get("src") == "workflow") {
if (categoryParam?.toLowerCase().startsWith("list")) {
const newParam = categoryParam.substring(5).replaceAll("%20", "_")
// Replace the url param. There may be more params.
urlParams.set("category", newParam)
window.history.replaceState({}, '', `${window.location.pathname}?${urlParams.toString()}`)
categoryParam = newParam
}
}
chosenCategory = categoryParam
setSelectedCategory(categoryParam)
}
@@ -447,7 +471,31 @@ const CacheView = memo((props) => {
}
}
var foundstartwords = {}
var categorygroups = []
for (var key in newcategories) {
const category = newcategories[key]
if (!category.includes("_")) {
continue
}
const startword = category.split("_")[0]
if (startword.length <= 2) {
continue
}
if (!foundstartwords.hasOwnProperty(startword)) {
foundstartwords[startword] = 1
} else {
foundstartwords[startword] += 1
if (foundstartwords[startword] === 2) {
categorygroups.push(startword)
}
}
}
setDatastoreCategories(newcategories)
setDatastoreCategoryGroups(categorygroups)
}
if (responseJson?.category_config !== undefined && responseJson?.category_config !== null) {
@@ -463,6 +511,8 @@ const CacheView = memo((props) => {
//if (responseJson.category_config.automations[key].icon === undefined || responseJson.category_config.automations[key].icon === null || responseJson.category_config.automations[key].icon === "") {
const foundItem = defaultAutomation.find((automation) => automation.name === responseJson.category_config.automations[key].name)
if (foundItem) {
responseJson.category_config.automations[key].description = foundItem.description
responseJson.category_config.automations[key].beta = foundItem.beta
responseJson.category_config.automations[key].disabled = foundItem.disabled
responseJson.category_config.automations[key].icon = foundItem.icon
responseJson.category_config.automations[key].type = foundItem?.type
@@ -542,6 +592,7 @@ const CacheView = memo((props) => {
key: dataValue.key,
value: value,
category: selectedCategory,
tags: dataValue?.tags || [],
}
if (dataValue?.category !== undefined && dataValue?.category !== "" && dataValue?.category !== "default") {
@@ -558,7 +609,9 @@ const CacheView = memo((props) => {
setCacheInput([entry]);
fetch(globalUrl + `/api/v1/orgs/${orgId}/set_cache`, {
//entry = [entry]
const parsedUrl = `${globalUrl}/api/v2/datastore`
fetch(parsedUrl, {
method: "POST",
headers: {
@@ -780,6 +833,37 @@ const CacheView = memo((props) => {
Category: {dataValue.category}
</Typography>
: null}
{dataValue?.tags !== undefined && dataValue?.tags !== null && dataValue?.tags?.length > 0 ?
<div style={{display: "flex", marginTop: 12, }}>
<Typography variant="body2" color="textSecondary" style={{ marginRight: 10, marginTop: 4, }}>
Tags
</Typography>
{dataValue.tags?.map((tag, index) => {
return (
<Chip
key={index}
label={tag}
style={{ marginRight: 5, }}
onDelete={() => {
var newTags = dataValue.tags.filter((t) => t !== tag)
if (newTags.length === 0) {
newTags = ["none"]
} else if (newTags.length > 1) {
newTags = newTags.filter((t) => t !== "none")
}
setDataValue({
...dataValue,
tags: newTags,
})
}}
/>
)
})}
</div>
: null}
</div>
: null}
@@ -1065,7 +1149,6 @@ const CacheView = memo((props) => {
saveAutomation(newAutomations)
}
console.log("AUTO: ", automation)
return (
<Tooltip title={
<Typography style={{margin: 10, }}>
@@ -1089,7 +1172,7 @@ const CacheView = memo((props) => {
style={{ marginRight: 10, marginTop: -10, }}
checked={automation.enabled}
// Check if automation options have a value
disabled={automation.name !== "Enrich" && (automation?.disabled === true || automation.options.length === 0 || automation.options.some((option) => option.value === ""))}
disabled={automation.name !== "Enrich" && automation.name != "Run AI Agent" && (automation?.disabled === true || automation.options.length === 0 || automation.options.some((option) => option.value === ""))}
onChange={(e) => {
e.stopPropagation()
e.preventDefault()
@@ -1140,6 +1223,19 @@ const CacheView = memo((props) => {
}}>
{automation.name}
</Typography>
{automation?.beta !== true ? null :
<Chip
label="Beta"
size="small"
style={{
marginLeft: 10,
height: 20,
fontSize: 12,
border: `1px solid ${green}`,
color: "rgba(255,255,255,0.7)",
}}
/>
}
</div>
{automation?.disabled !== true ?
@@ -1166,8 +1262,9 @@ const CacheView = memo((props) => {
: null}
</div>
{showOptions && (
updatedAutomation.options.map((option, optionIndex) => {
{showOptions &&
<div style={{ marginTop: 20, marginBottom: 20, }}>
{updatedAutomation.options.map((option, optionIndex) => {
if (option?.key === "datastore_categories") {
if (datastoreCategories === undefined || datastoreCategories === null || datastoreCategories.length <= 1) {
return (
@@ -1446,6 +1543,87 @@ const CacheView = memo((props) => {
}}
/>
)
} else if (updatedAutomation?.name === "Run AI Agent") {
return (
<ButtonGroup style={{display: "flex", }}>
<TextField
key={optionIndex}
fullWidth
disabled={option?.disabled === true}
style={theme.palette.textFieldStyle}
InputProps={{
disableUnderline: true,
style: {
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
color: theme.palette.textFieldStyle.color,
height: 40,
},
}}
label={option.key}
defaultValue={option.value}
onBlur={(e) => {
if (e.target.value === "") {
updatedAutomation.enabled = false
} else {
updatedAutomation.enabled = true
}
updatedAutomation.options[optionIndex].value = e.target.value;
setUpdatedAutomation(updatedAutomation)
setUpdated(true)
}}
/>
{optionIndex === updatedAutomation.options.length - 1 ?
<Button color="secondary" variant="outlined" style={{
height: 40,
maxWidth: 40,
}}
disabled={option?.value?.length < 20}
onClick={() => {
var newindex = optionIndex + 2
if (updatedAutomation.options.length > 0) {
const lastOption = updatedAutomation.options[updatedAutomation.options.length - 1]
const lastIndex = parseInt(lastOption.key.split("-")[1])
if (!isNaN(lastIndex)) {
newindex = lastIndex + 1
}
}
updatedAutomation.options.push({
"key": `action-${newindex}`,
"description": "Describe the action to perform on this item",
"value": "",
})
setUpdatedAutomation(updatedAutomation)
setUpdate(Math.random())
}}>
<AddIcon/>
</Button>
:
<Tooltip title={option?.disabled !== true ? "Disable action (won't run until re-enabled)" : "Re-Enable action (runs on all new edits)"} placement="top">
<Button color="secondary" variant="outlined" style={{
height: 40,
maxWidth: 40,
}} onClick={() => {
if (option?.disabled === true) {
updatedAutomation.options[optionIndex].disabled = false
} else {
updatedAutomation.options[optionIndex].disabled = true
}
setUpdatedAutomation(updatedAutomation)
setUpdate(Math.random())
}}>
{option?.disabled !== true ? <RemoveIcon /> : <FlashOnIcon />}
</Button>
</Tooltip>
}
</ButtonGroup>
)
}
return (
@@ -1478,8 +1656,9 @@ const CacheView = memo((props) => {
}}
/>
)
})
)}
})}
</div>
}
</div>
</Tooltip>
)
@@ -1506,6 +1685,8 @@ const CacheView = memo((props) => {
)
}
const urlParams = new URLSearchParams(window?.location?.search)
const highlightedKey = urlParams.get("key") || ""
const columns = [
{
field: 'key',
@@ -1513,6 +1694,24 @@ const CacheView = memo((props) => {
width: 200,
filterable: true,
sortable: true,
renderCell: (props) => {
return (
<Typography
variant="body2"
style={{
maxWidth: 180,
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
cursor: "default",
border: props.row.key === highlightedKey ? `2px solid ${theme.palette.primary.main}` : "none",
}}
>
{props.row.key}
</Typography>
)
}
},
{
width: 540,
@@ -1552,8 +1751,10 @@ const CacheView = memo((props) => {
backgroundColor: theme.palette.platformColor,
border: theme.palette.defaultBorder,
padding: 5,
maxWidth: 500,
minWidth: 500,
maxHeight: 500,
overflowX: "auto",
overflowY: "auto",
}}
collapsed={true}
@@ -1735,6 +1936,7 @@ const CacheView = memo((props) => {
"created": data.created,
"workflow_id": data.workflow_id,
"category": data.category,
"tags": data.tags,
})
setValue(newvalue)
setModalOpen(true)
@@ -1914,7 +2116,7 @@ const CacheView = memo((props) => {
},
];
var previousgroup = ""
const isAutomating = categoryAutomations?.find((automation) => automation.enabled) !== undefined
return (
<div style={{
@@ -2067,56 +2269,53 @@ const CacheView = memo((props) => {
</Button>
</ButtonGroup>
<ButtonGroup style={{marginTop: 0, }}>
{datastoreCategories !== undefined &&
<ButtonGroup style={{ position: "absolute", top: -6, }}>
{datastoreCategories !== undefined &&
datastoreCategories !== null &&
datastoreCategories.length > 1 ? (
<FormControl style={{ minWidth: 150, maxWidth: 150, marginTop: 8, }}>
<InputLabel id="category-choice" style={{
color: "rgba(255, 255, 255, 0.65)",
}}>
Category
</InputLabel>
<Select
<FormControl style={{ minWidth: 250, maxWidth: 250, marginTop: 8, }}>
<Autocomplete
labelId="category-choice"
style={{
minWidth: 150,
maxWidth: 150,
height: 35,
borderRadius: "5px 0px 0px 5px",
minWidth: 250,
maxWidth: 250,
}}
ListboxProps={{
style: {
maxHeight: "70vh",
border: "1px solid rgba(255,255,255,0.3)",
}
}}
value={selectedCategory}
onChange={(event) => {
setCategoryConfig(undefined)
setCategoryAutomations(defaultAutomation)
//if (selectAllChecked || listCache.length > 0) {
if (selectAllChecked || selectedFiles.length > 0) {
setUpdateToThisCategory(event.target.value)
return
}
setSelectedCategory(event.target.value)
if (event.target.value === "all" || event.target.value === "default") {
listOrgCache(orgId, "", 0, pageSize, page)
} else {
listOrgCache(orgId, event.target.value, 0, pageSize, page)
}
// Add it to the url as a query
if (window.location.search.includes("category=")) {
const newurl = window.location.href.replace(/category=[^&]+/, `category=${event.target.value}`)
window.history.pushState({ path: newurl }, "", newurl)
} else {
window.history.pushState({ path: window.location.href }, "", `${window.location.href}&category=${event.target.value}`)
}
options={datastoreCategories}
getOptionLabel={(data) => {
const fixedname = data?.charAt(0)?.toUpperCase() + data?.slice(1)?.replaceAll("_", " ")
return fixedname
}}
>
{datastoreCategories.map((data, index) => {
// Should find the icon for things
// Fix uppercase at start of words
groupBy={(data) => {
if (!data.includes("_")) {
return undefined
}
const firstword = data.split("_")[0]
if (datastoreCategoryGroups.includes(firstword)) {
return firstword.charAt(0).toUpperCase() + firstword.slice(1)
}
return undefined
}}
renderInput={(params) => {
return (
<TextField
{...params}
label="Select Category"
variant="outlined"
size="small"
/>
)
}}
renderOption={(props, data, state) => {
const fixedname = data?.charAt(0)?.toUpperCase() + data?.slice(1)?.replaceAll("_", " ")
const iconDetails = GetIconInfo({
"app_name": fixedname,
@@ -2125,13 +2324,40 @@ const CacheView = memo((props) => {
return (
<MenuItem
key={index}
key={data}
value={data}
style={{
color: theme.palette.textFieldStyle.color,
display: "flex",
borderBottom: theme.palette.defaultBorder,
}}
onClick={(e) => {
e.stopPropagation()
e.preventDefault()
setCategoryConfig(undefined)
setCategoryAutomations(defaultAutomation)
if (selectAllChecked || selectedFiles.length > 0) {
setUpdateToThisCategory(data)
return
}
setSelectedCategory(data)
if (data === "all" || data === "default") {
listOrgCache(orgId, "", 0, pageSize, page)
} else {
listOrgCache(orgId, data, 0, pageSize, page)
}
// Add it to the url as a query
if (window.location.search.includes("category=")) {
const newurl = window.location.href.replace(/category=[^&]+/, `category=${data}`)
window.history.pushState({ path: newurl }, "", newurl)
} else {
window.history.pushState({ path: window.location.href }, "", `${window.location.href}&category=${data}`)
}
}}
>
<Typography style={{display: "flex", marginTop: 5, }}>
<div style={{marginRight: 10, }}>
@@ -2143,9 +2369,69 @@ const CacheView = memo((props) => {
{fixedname}
</Typography>
</MenuItem>
)
}}
/>
{/*
{datastoreCategories.map((data, index) => {
var addition = ""
if (data.includes("_")) {
const firstword = data.split("_")[0]
if (firstword !== previousgroup) {
if (datastoreCategoryGroups.includes(firstword)) {
previousgroup = firstword
addition =
<ListSubheader>
{firstword.charAt(0).toUpperCase() + firstword.slice(1)}
</ListSubheader>
} else {
if (previousgroup !== "") {
addition = <div style={{height: 8, backgroundColor: "rgba(0,0,0,0.8)",}} />
previousgroup = ""
}
}
}
} else {
if (previousgroup !== "") {
addition = <div style={{height: 8, backgroundColor: "rgba(0,0,0,0.8)",}} />
previousgroup = ""
}
}
// Should find the icon for things
// Fix uppercase at start of words
const fixedname = data?.charAt(0)?.toUpperCase() + data?.slice(1)?.replaceAll("_", " ")
const iconDetails = GetIconInfo({
"app_name": fixedname,
"name": fixedname,
})
return (
<MenuItem
key={data}
value={data}
style={{
color: theme.palette.textFieldStyle.color,
display: "flex",
borderBottom: theme.palette.defaultBorder,
}}
>
{addition}
<Typography style={{display: "flex", marginTop: 5, }}>
<div style={{marginRight: 10, }}>
{iconDetails?.originalIcon && (
iconDetails?.originalIcon
)}
</div>
{fixedname}
</Typography>
</MenuItem>
);
})}
</Select>
*/}
</FormControl>
) : null}
@@ -2154,7 +2440,7 @@ const CacheView = memo((props) => {
<Tooltip title={"Close"} style={{}} aria-label={""}>
<Button
style={{
height: 35,
height: 40,
borderRadius: 4,
textTransform: 'none',
fontSize: 16,
@@ -2173,14 +2459,13 @@ const CacheView = memo((props) => {
</Button>
</Tooltip>
:
<Tooltip title={"Add or find category"} style={{}} aria-label={""}>
<Tooltip title={"Add new category object"} style={{}} aria-label={""}>
<Button
style={{
whiteSpace: "nowrap",
width: datastoreCategories !== undefined && datastoreCategories !== null && datastoreCategories.length > 1 ? 50 : 169,
height: 35,
height: 40,
textTransform: 'none',
fontSize: 16,
borderRadius: "0px 5px 5px 0px",
}}
@@ -2211,14 +2496,14 @@ const CacheView = memo((props) => {
}}
style={{
height: 35,
height: 40,
width: 200,
marginTop: 0,
}}
InputProps={{
style: {
color: theme.palette.textFieldStyle.color,
height: 35,
height: 40,
fontSize: 16,
borderRadius: 4,
paddingTop: 0,
@@ -2245,7 +2530,7 @@ const CacheView = memo((props) => {
height: 35,
textTransform: 'none',
border: isAutomating ? `1px solid ${theme.palette.primary.main}` : null,
border: isAutomating ? `1px solid ${green}` : null,
}}
variant="contained"
color="secondary"
@@ -2259,8 +2544,8 @@ const CacheView = memo((props) => {
setShowAutomationMenu(true)
}}
>
{isAutomating ? <RocketLaunchIcon style={{color: theme.palette.primary.main, marginRight: 10 }}/> : <RocketIcon style={{marginRight: 10, color: theme.palette.secondary.main, }} />}
Automate (beta)
{isAutomating ? <RocketLaunchIcon style={{color: green, marginRight: 10 }}/> : <RocketIcon style={{marginRight: 10, color: theme.palette.secondary.main, }} />}
Automate
</Button>
</span>
</Tooltip>
@@ -2403,7 +2688,7 @@ const CacheView = memo((props) => {
Subscribing
</Typography>
<Typography variant="body2" style={{ color: theme.palette.text.secondary, marginBottom: 20 }}>
Enabling this feature will allow other organizations to subscribe to this category. This is NOT fully available yet.
Enabling subscriptions allows other organizations to subscribe to this category and receive updates when keys are added or modified. Makes the category searchable.
</Typography>
</div>
</div>