2875 lines
92 KiB
React
2875 lines
92 KiB
React
import React, { useState, useEffect, useContext, memo } from "react";
|
|
import { makeStyles } from "@mui/styles";
|
|
import { getTheme } from "../theme.jsx";
|
|
import SubOrgDistributionDialog from "./SubOrgDistributionDialog.jsx";
|
|
import DeleteConfirmDialog from "./DeleteConfirmDialog.jsx";
|
|
import { toast } from 'react-toastify';
|
|
|
|
import ReactJson from "react-json-view-ssr";
|
|
import { validateJson, handleReactJsonClipboard, GetIconInfo, } from "../views/Workflows2.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";
|
|
import {
|
|
Typography,
|
|
Tooltip,
|
|
Divider,
|
|
TextField,
|
|
Button,
|
|
Tabs,
|
|
Tab,
|
|
ListItemText,
|
|
IconButton,
|
|
Dialog,
|
|
DialogTitle,
|
|
DialogActions,
|
|
Skeleton,
|
|
Chip,
|
|
Checkbox,
|
|
MenuItem,
|
|
DialogContent,
|
|
FormControl,
|
|
Select,
|
|
Autocomplete,
|
|
ButtonGroup,
|
|
InputLabel,
|
|
Pagination,
|
|
PaginationItem,
|
|
Avatar,
|
|
ListSubheader,
|
|
} from "@mui/material";
|
|
|
|
import {
|
|
DataGrid,
|
|
} from '@mui/x-data-grid';
|
|
|
|
import {
|
|
Link as LinkIcon,
|
|
AutoFixHigh as AutoFixHighIcon,
|
|
AutoFixNormal as AutoFixNormalIcon,
|
|
Edit as EditIcon,
|
|
FileCopy as FileCopyIcon,
|
|
SelectAll as SelectAllIcon,
|
|
DeleteOutline as DeleteOutlineIcon,
|
|
OpenInNew as OpenInNewIcon,
|
|
CloudDownload as CloudDownloadIcon,
|
|
Description as DescriptionIcon,
|
|
CheckCircle as CheckCircleIcon,
|
|
Close as CloseIcon,
|
|
Apps as AppsIcon,
|
|
Image as ImageIcon,
|
|
Cached as CachedIcon,
|
|
AccessibilityNew as AccessibilityNewIcon,
|
|
Lock as LockIcon,
|
|
Schedule as ScheduleIcon,
|
|
Cloud as CloudIcon,
|
|
Business as BusinessIcon,
|
|
Visibility as VisibilityIcon,
|
|
VisibilityOff as VisibilityOffIcon,
|
|
Clear as ClearIcon,
|
|
Add as AddIcon,
|
|
Remove as RemoveIcon,
|
|
Rocket as RocketIcon,
|
|
Webhook as WebhookIcon,
|
|
Air as AirIcon,
|
|
RocketLaunch as RocketLaunchIcon,
|
|
Send as SendIcon,
|
|
SmartToy as SmartToyIcon,
|
|
Settings as SettingsIcon,
|
|
FilterAlt as FilterAltIcon,
|
|
CompareArrows as CompareArrowsIcon,
|
|
Hub as HubIcon,
|
|
Key as KeyIcon,
|
|
FlashOn as FlashOnIcon,
|
|
Search as SearchIcon,
|
|
} from "@mui/icons-material";
|
|
import { Context } from "../context/ContextApi.jsx";
|
|
|
|
const scrollStyle1 = {
|
|
height: 100,
|
|
width: 225,
|
|
overflow: "hidden",
|
|
position: "relative",
|
|
}
|
|
|
|
const scrollStyle2 = {
|
|
position: "absolute",
|
|
top: 0,
|
|
left: 0,
|
|
bottom: "-20px",
|
|
right: "-20px",
|
|
overflow: "scroll",
|
|
}
|
|
|
|
const useStyles = makeStyles({
|
|
notchedOutline: {
|
|
borderColor: "#f85a3e !important",
|
|
},
|
|
});
|
|
|
|
//
|
|
|
|
//const CacheView = (props) => {
|
|
const CacheView = memo((props) => {
|
|
const { globalUrl, userdata, serverside, orgId, isSelectedDataStore, selectedOrganization } = props;
|
|
const [orgCache, setOrgCache] = React.useState("");
|
|
const [listCache, setListCache] = React.useState([]);
|
|
const [addCache, setAddCache] = React.useState("");
|
|
const [editedCache, setEditedCache] = React.useState("");
|
|
const [modalOpen, setModalOpen] = React.useState(false);
|
|
const [key, setKey] = React.useState("");
|
|
const [value, setValue] = React.useState("");
|
|
const [cacheInput, setCacheInput] = React.useState("");
|
|
const [dataValue, setDataValue] = React.useState({});
|
|
const [editCache, setEditCache] = React.useState(false);
|
|
const [cachedLoaded, setCachedLoaded] = React.useState(false);
|
|
const [show, setShow] = useState({});
|
|
const [showDistributionPopup, setShowDistributionPopup] = useState(false);
|
|
const [selectedSubOrg, setSelectedSubOrg] = useState([]);
|
|
const [selectedCacheKey, setSelectedCacheKey] = useState("");
|
|
const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false);
|
|
const [deleteConfirmTarget, setDeleteConfirmTarget] = useState(null);
|
|
const [distribOrgOrder, setDistribOrgOrder] = useState([]);
|
|
const [totalAmount, setTotalAmount] = useState(0);
|
|
const [page, setPage] = useState(0);
|
|
const [pageSize, setPageSize] = useState(50)
|
|
const [cursors, setCursors] = useState({
|
|
0: "",
|
|
})
|
|
const [_, setUpdate] = useState(Math.random())
|
|
const [selectedRows, setSelectedRows] = useState([]);
|
|
// Direct category migration from ../components/Files.jsx
|
|
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("")
|
|
const [workflows, setWorkflows] = useState([]);
|
|
const [apps, setApps] = useState([]);
|
|
|
|
const [selectedFiles, setSelectedFiles] = useState([]);
|
|
const [showAutomationMenu, setShowAutomationMenu] = useState(false);
|
|
const [showSettingsMenu, setShowSettingsMenu] = useState(false);
|
|
const [showCollectIngestMenu, setShowCollectIngestMenu] = useState(false);
|
|
const [showCorrelations, setShowCorrelations] = useState(undefined);
|
|
|
|
let navigate = useNavigate();
|
|
|
|
useEffect(() => {
|
|
if (selectedCategory === "" || selectedCategory === null || selectedCategory === undefined || selectedCategory === "default") {
|
|
return
|
|
}
|
|
|
|
if (datastoreCategories === undefined || datastoreCategories === null || datastoreCategories.length === 0) {
|
|
return
|
|
}
|
|
|
|
if (!datastoreCategories.includes(selectedCategory)) {
|
|
setDatastoreCategories([...datastoreCategories, selectedCategory])
|
|
}
|
|
}, [datastoreCategories, selectedCategory])
|
|
|
|
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",
|
|
"options": [{
|
|
"key": "workflow_id",
|
|
"value": "",
|
|
}],
|
|
"icon": <AirIcon />,
|
|
"enabled": false,
|
|
},
|
|
/*
|
|
{
|
|
"name": "Correlate Categories",
|
|
"description": "",
|
|
"type": "singul",
|
|
"options": [{
|
|
"key": "datastore_categories",
|
|
"value": "",
|
|
}],
|
|
"icon": <CompareArrowsIcon />,
|
|
"enabled": false,
|
|
"disabled": false,
|
|
},
|
|
*/
|
|
{
|
|
"name": "Send webhook",
|
|
"description": "Sends the updated value to a specified webhook URL as a POST request",
|
|
"options": [{
|
|
"key": "webhook_url",
|
|
"value": "",
|
|
}],
|
|
"icon": <WebhookIcon />,
|
|
"enabled": false,
|
|
},
|
|
|
|
{
|
|
"name": "Run AI Agent",
|
|
"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": "action-1",
|
|
"description": "Describe the action to perform on this item",
|
|
"value": "",
|
|
}],
|
|
"beta": true,
|
|
"icon": <SmartToyIcon />,
|
|
"disabled": false,
|
|
},
|
|
|
|
{
|
|
"name": "Enrich",
|
|
"description": "Enriches the data. Uses regex keys and runs a workflow in the background. Added to the 'enrichments' key.",
|
|
"type": "singul",
|
|
"options": [{
|
|
"key": "",
|
|
"value": "",
|
|
}],
|
|
"beta": true,
|
|
"icon": "/images/logos/singul.svg",
|
|
"disabled": false,
|
|
},
|
|
]
|
|
|
|
const [categoryAutomations, setCategoryAutomations] = useState(defaultAutomation)
|
|
const [categoryConfig, setCategoryConfig] = useState(undefined)
|
|
|
|
const { themeMode, brandColor } = useContext(Context);
|
|
const theme = getTheme(themeMode, brandColor);
|
|
const classes = useStyles();
|
|
|
|
const getApps = () => {
|
|
const url = `${globalUrl}/api/v1/apps`
|
|
fetch(url, {
|
|
method: "GET",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Accept: "application/json",
|
|
},
|
|
credentials: "include",
|
|
})
|
|
.then((response) => {
|
|
if (response.status !== 200) {
|
|
console.log("Status not 200 for workflows :O!");
|
|
return;
|
|
}
|
|
|
|
return response.json();
|
|
})
|
|
.then((responseJson) => {
|
|
if (responseJson?.success === false) {
|
|
toast.warn("Failed to load apps. Please try again or contact support@shuffler if this persists.")
|
|
} else {
|
|
setApps(responseJson)
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
toast(error.toString());
|
|
});
|
|
}
|
|
|
|
const getWorkflows = () => {
|
|
const url = `${globalUrl}/api/v1/workflows`
|
|
fetch(url, {
|
|
method: "GET",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Accept: "application/json",
|
|
},
|
|
credentials: "include",
|
|
})
|
|
.then((response) => {
|
|
if (response.status !== 200) {
|
|
console.log("Status not 200 for workflows :O!");
|
|
return;
|
|
}
|
|
|
|
return response.json();
|
|
})
|
|
.then((responseJson) => {
|
|
if (responseJson?.success !== true) {
|
|
setWorkflows(responseJson)
|
|
} else {
|
|
toast.warn("Failed to load workflows. Please try again or contact support@shuffler if this persists.")
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
toast(error.toString());
|
|
});
|
|
}
|
|
|
|
useEffect(() => {
|
|
setCursors({
|
|
0: "",
|
|
})
|
|
setPage(0)
|
|
setSelectedRows([])
|
|
}, [selectedCategory])
|
|
|
|
useEffect(() => {
|
|
getWorkflows()
|
|
getApps()
|
|
|
|
var chosenCategory = selectedCategory
|
|
const urlParams = new URLSearchParams(window.location.search)
|
|
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 === "OCSF") {
|
|
const newParam = "shuffle-security incidents"
|
|
|
|
urlParams.set("category", newParam)
|
|
window.history.replaceState({}, '', `${window.location.pathname}?${urlParams.toString()}`)
|
|
categoryParam = newParam
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
listOrgCache(orgId, "", 0, pageSize, page)
|
|
setTimeout(() => {
|
|
listOrgCache(orgId, chosenCategory, 0, pageSize, page)
|
|
}, 100)
|
|
}, [])
|
|
|
|
|
|
const handleKeyDown = (event) => {
|
|
if (event.key === 'Enter') {
|
|
datastoreCategories.push(event.target.value);
|
|
setSelectedCategory(event.target.value);
|
|
setRenderTextBox(false);
|
|
}
|
|
|
|
if (event.key === 'Escape'){ // not working for some reasons
|
|
console.log('escape pressed')
|
|
setRenderTextBox(false);
|
|
}
|
|
}
|
|
|
|
const listOrgCache = (orgId, category, index, amount, page, keyValue) => {
|
|
setCachedLoaded(false)
|
|
if (index === undefined || index === null) {
|
|
index = 0
|
|
}
|
|
|
|
var url = `${globalUrl}/api/v1/orgs/${orgId}/list_cache`
|
|
if (category !== undefined && category !== null && category !== "default" && category !== "") {
|
|
url += "?category=" + category.replaceAll(" ", "_")
|
|
} else {
|
|
url += "?category=default"
|
|
category = "default"
|
|
}
|
|
|
|
if (amount !== undefined && amount !== null && amount > 0) {
|
|
url += "&top=" + amount
|
|
}
|
|
|
|
if (page !== undefined && page !== null && page >= 0) {
|
|
if (cursors[page-1] !== undefined && cursors[page-1] !== null && cursors[page-1] !== "") {
|
|
url += "&cursor=" + cursors[page-1]
|
|
}
|
|
}
|
|
|
|
if (keyValue !== undefined && keyValue !== null && keyValue !== "") {
|
|
url += "&key=" + keyValue
|
|
|
|
setCursors({
|
|
0: "",
|
|
})
|
|
setPage(0)
|
|
setSelectedRows([])
|
|
}
|
|
|
|
fetch(url, {
|
|
method: "GET",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Accept: "application/json",
|
|
},
|
|
credentials: "include",
|
|
})
|
|
.then((response) => {
|
|
if (response.status !== 200) {
|
|
console.log("Status not 200 for list cache :O!");
|
|
return;
|
|
}
|
|
|
|
return response.json();
|
|
})
|
|
.then((responseJson) => {
|
|
setCachedLoaded(true);
|
|
if (responseJson?.success === true) {
|
|
setListCache(responseJson.keys);
|
|
|
|
if (responseJson.total_amount !== undefined && responseJson.total_amount !== null && responseJson.total_amount > 0) {
|
|
setTotalAmount(responseJson.total_amount)
|
|
} else {
|
|
setTotalAmount(responseJson.keys.length)
|
|
}
|
|
|
|
if (responseJson?.cursor !== undefined && responseJson?.cursor !== null && responseJson?.cursor !== "") {
|
|
cursors[page] = responseJson.cursor
|
|
}
|
|
|
|
// Especially important during first load
|
|
if (index < 2 && (category === "default" || category === "" || category === undefined)) {
|
|
// If it exists and isn't blank/default, load it
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const categoryParam = urlParams.get("category");
|
|
if (categoryParam && categoryParam !== undefined && categoryParam !== "default" && categoryParam !== "") {
|
|
setSelectedCategory(categoryParam);
|
|
if (index === undefined || index === null) {
|
|
index = 0
|
|
}
|
|
|
|
listOrgCache(orgId, categoryParam, index+1, amount, page)
|
|
} else {
|
|
setSelectedCategory("default");
|
|
}
|
|
}
|
|
|
|
if ((category === undefined || category === "default" || category === "") && datastoreCategories.length === 2 && datastoreCategories[0] === "default") {
|
|
var newcategories = ["default", "protected"]
|
|
for (var key in responseJson.keys) {
|
|
var foundcategory = responseJson.keys[key].category
|
|
if (foundcategory !== undefined && foundcategory !== null && foundcategory !== ""){
|
|
foundcategory = category.replaceAll(" ", "_")
|
|
|
|
if (!newcategories.includes(foundcategory)) {
|
|
newcategories.push(foundcategory)
|
|
}
|
|
}
|
|
}
|
|
|
|
if (responseJson?.categories !== undefined && responseJson?.categories !== null && responseJson?.categories.length > 0) {
|
|
for (var i = 0; i < responseJson.categories.length; i++) {
|
|
const foundcategory = responseJson.categories[i].replaceAll(" ", "_")
|
|
if (foundcategory !== undefined && foundcategory !== null && foundcategory !== "" && foundcategory !== "default" && !newcategories.includes(foundcategory)) {
|
|
newcategories.push(responseJson.categories[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
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) {
|
|
|
|
if (responseJson?.category_config?.id !== undefined && responseJson?.category_config?.id !== null && responseJson?.category_config?.id !== "") {
|
|
setCategoryConfig(responseJson.category_config)
|
|
}
|
|
|
|
// Handle other configs here.
|
|
if (responseJson?.category_config?.automations !== undefined && responseJson?.category_config?.automations !== null && responseJson?.category_config?.automations.length > 0) {
|
|
// Find icons if they exist
|
|
for (var key in responseJson.category_config.automations) {
|
|
//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
|
|
} else {
|
|
responseJson.category_config.automations[key].icon = <RocketIcon />
|
|
}
|
|
}
|
|
|
|
for (var key in defaultAutomation) {
|
|
if (!responseJson.category_config.automations.some((automation) => automation.name === defaultAutomation[key].name)) {
|
|
// If the automation doesn't exist in the response, add it with default values
|
|
responseJson.category_config.automations.push(defaultAutomation[key])
|
|
}
|
|
}
|
|
|
|
setCategoryAutomations(responseJson.category_config.automations)
|
|
} else {
|
|
setCategoryAutomations(defaultAutomation)
|
|
}
|
|
}
|
|
} else {
|
|
//toast.warn("Failed to load keys. Please try again or contact support@shuffler if this persists.")
|
|
|
|
if (category !== undefined && category !== null && category !== "" && category !== "default") {
|
|
toast.info(`No keys to load in category ${category}`)
|
|
setSelectedCategory(category)
|
|
}
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
toast(error.toString());
|
|
});
|
|
};
|
|
|
|
|
|
const deleteEntry = (orgId, key, itemCategory, refreshList) => {
|
|
const method = "POST"
|
|
const url = `${globalUrl}/api/v1/orgs/${orgId}/delete_cache`
|
|
var parsed = {
|
|
"org_id": orgId,
|
|
"key": key,
|
|
"category": selectedCategory === "" || selectedCategory === "default" ? "" : selectedCategory,
|
|
}
|
|
|
|
if (itemCategory !== undefined) {
|
|
parsed["category"] = itemCategory.replaceAll(" ", "_");
|
|
}
|
|
|
|
fetch(url, {
|
|
method: method,
|
|
headers: {
|
|
Accept: "application/json",
|
|
},
|
|
body: JSON.stringify(parsed),
|
|
credentials: "include",
|
|
})
|
|
.then((response) => {
|
|
if (response.status === 200) {
|
|
if (refreshList === undefined || refreshList === null || refreshList === true) {
|
|
toast.success("Deleted datastore entry");
|
|
setTimeout(() => {
|
|
listOrgCache(orgId, selectedCategory, 0, pageSize, page)
|
|
}, 1000);
|
|
}
|
|
} else {
|
|
if (refreshList === undefined || refreshList === null || refreshList === true) {
|
|
setTimeout(() => {
|
|
listOrgCache(orgId, selectedCategory, 0, pageSize, page)
|
|
}, 1000);
|
|
}
|
|
toast.error(`Failed deleting entry ${key} in category ${itemCategory || selectedCategory}. If this persists, please contact support@shuffler.io.`)
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
if (refreshList === undefined || refreshList === null || refreshList === true) {
|
|
setTimeout(() => {
|
|
listOrgCache(orgId, selectedCategory, 0, pageSize, page)
|
|
}, 1000);
|
|
}
|
|
toast(error.toString());
|
|
});
|
|
};
|
|
|
|
const editOrgCache = (orgId) => {
|
|
var entry = {
|
|
key: dataValue.key,
|
|
value: value,
|
|
category: selectedCategory,
|
|
tags: dataValue?.tags || [],
|
|
}
|
|
|
|
if (dataValue?.category !== undefined && dataValue?.category !== "" && dataValue?.category !== "default") {
|
|
entry.category = dataValue?.category?.replaceAll(" ", "_");
|
|
|
|
}
|
|
|
|
if (listCache.length > 0) {
|
|
const selectedCache = listCache.find((data) => data.key === dataValue.key);
|
|
if (selectedCache?.suborg_distribution?.length > 0) {
|
|
entry.suborg_distribution = selectedCache.suborg_distribution;
|
|
}
|
|
}
|
|
|
|
setCacheInput([entry]);
|
|
|
|
//entry = [entry]
|
|
const parsedUrl = `${globalUrl}/api/v2/datastore`
|
|
fetch(parsedUrl, {
|
|
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Accept: "application/json",
|
|
},
|
|
credentials: "include",
|
|
body: JSON.stringify(entry),
|
|
})
|
|
.then((response) => {
|
|
if (response.status !== 200) {
|
|
console.log("Status not 200 for Cache :O!");
|
|
return;
|
|
}
|
|
|
|
return response.json();
|
|
})
|
|
.then((responseJson) => {
|
|
setAddCache(responseJson);
|
|
toast.success("Edit saved");
|
|
setTimeout(() => {
|
|
listOrgCache(orgId, selectedCategory, 0, pageSize, page);
|
|
}, 7500);
|
|
|
|
setModalOpen(false);
|
|
})
|
|
.catch((error) => {
|
|
toast(error.toString());
|
|
});
|
|
};
|
|
|
|
const addOrgCache = (orgId) => {
|
|
const cache = {
|
|
key: key,
|
|
value: value,
|
|
category: selectedCategory,
|
|
}
|
|
|
|
setCacheInput([cache]);
|
|
|
|
fetch(globalUrl + `/api/v1/orgs/${orgId}/set_cache`, {
|
|
|
|
method: "POST",
|
|
body: JSON.stringify(cache),
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Accept: "application/json",
|
|
},
|
|
credentials: "include",
|
|
})
|
|
.then((response) => {
|
|
if (response.status !== 200) {
|
|
console.log("Status not 200 for apps :O!");
|
|
return;
|
|
}
|
|
|
|
return response.json();
|
|
})
|
|
.then((responseJson) => {
|
|
setAddCache(responseJson);
|
|
toast.success("New key added!");
|
|
|
|
setTimeout(() => {
|
|
listOrgCache(orgId, selectedCategory, 0, pageSize, page);
|
|
}, 5000);
|
|
setModalOpen(false);
|
|
})
|
|
.catch((error) => {
|
|
toast(error.toString());
|
|
});
|
|
};
|
|
|
|
const isValidJson = validateJson(value)
|
|
const autoFixJson = (inputvalue) => {
|
|
console.log("inputvalue: ", inputvalue)
|
|
try {
|
|
var parsedjson = JSON.parse(inputvalue)
|
|
|
|
// setValue() with the parsed json as string
|
|
setValue(JSON.stringify(parsedjson, null, 2))
|
|
} catch (e) {
|
|
console.log("Error parsing JSON: ", e)
|
|
toast.info("Invalid JSON.", {
|
|
autoClose: 1500,
|
|
})
|
|
}
|
|
}
|
|
|
|
const timestamp = (timestamp) => {
|
|
if (timestamp === undefined || timestamp === null || timestamp === "") {
|
|
return null
|
|
}
|
|
|
|
const date = new Date(timestamp * 1000);
|
|
if (date.toString() === "Invalid Date" || date.toString() === "Invalid Date NaN") {
|
|
return null
|
|
}
|
|
|
|
return date.toISOString()?.slice(0, 19)?.replace("T", " ")
|
|
}
|
|
|
|
const modalView = (
|
|
// console.log("key:", dataValue.key),
|
|
//console.log("value:",dataValue.value),
|
|
<Dialog
|
|
open={modalOpen}
|
|
onClose={() => {
|
|
setModalOpen(false);
|
|
}}
|
|
PaperProps={{
|
|
sx: {
|
|
borderRadius: theme?.palette?.DialogStyle?.borderRadius,
|
|
border: theme?.palette?.DialogStyle?.border,
|
|
minWidth: "800px",
|
|
minHeight: "320px",
|
|
fontFamily: theme?.typography?.fontFamily,
|
|
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
|
zIndex: 1000,
|
|
'& .MuiDialogContent-root': {
|
|
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
|
},
|
|
'& .MuiDialogTitle-root': {
|
|
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
|
},
|
|
'& .MuiDialogActions-root': {
|
|
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
|
},
|
|
},
|
|
}}
|
|
>
|
|
<DialogTitle>
|
|
<span style={{ color: theme.palette.text.primary }}>
|
|
{ editCache ? "Edit Key" : "Add Key"}{selectedCategory === "" || selectedCategory === "default" ? "" : ` in category '${selectedCategory}'`}
|
|
</span>
|
|
|
|
</DialogTitle>
|
|
<div style={{ paddingLeft: "30px", paddingRight: '30px', backgroundColor: theme.palette.DialogStyle.backgroundColor, }}>
|
|
Key
|
|
<TextField
|
|
color="primary"
|
|
disabled={editCache}
|
|
style={{ backgroundColor: theme.palette.textFieldStyle.backgroundColor }}
|
|
autoFocus
|
|
InputProps={{
|
|
style: {
|
|
height: "50px",
|
|
color: theme.palette.textFieldStyle.color,
|
|
fontSize: "1em",
|
|
},
|
|
}}
|
|
required
|
|
fullWidth={true}
|
|
autoComplete="Key"
|
|
placeholder="abc"
|
|
id="keyfield"
|
|
margin="normal"
|
|
variant="outlined"
|
|
value={editCache ? dataValue.key : key}
|
|
onChange={(e) => setKey(e.target.value)}
|
|
/>
|
|
</div>
|
|
<div style={{ paddingLeft: 30, paddingRight: 30, backgroundColor: theme.palette.DialogStyle.backgroundColor }}>
|
|
<div style={{display: "flex", }}>
|
|
<Typography style={{marginTop: 25, marginBottom: 0, flex: 20, }}>
|
|
Value - ({isValidJson.valid === true ? "Valid" : "Invalid"} JSON)
|
|
</Typography>
|
|
<Tooltip title="Auto Fix JSON" placement="right">
|
|
<IconButton
|
|
style={{flex: 1, }}
|
|
onClick={() => {
|
|
autoFixJson(value)
|
|
}}
|
|
>
|
|
<AutoFixNormalIcon />
|
|
</IconButton>
|
|
</Tooltip>
|
|
</div>
|
|
|
|
<TextField
|
|
color="primary"
|
|
style={{ backgroundColor: theme.palette.textFieldStyle.backgroundColor, marginTop: 0, }}
|
|
InputProps={{
|
|
style: {
|
|
color: theme.palette.textFieldStyle.color,
|
|
fontSize: "1em",
|
|
},
|
|
}}
|
|
required
|
|
fullWidth
|
|
autoComplete="Value"
|
|
placeholder="123"
|
|
id="Valuefield"
|
|
margin="normal"
|
|
variant="outlined"
|
|
multiline
|
|
minRows={4}
|
|
maxRows={12}
|
|
//defaultValue={editCache ? dataValue.value : ""}
|
|
value={value}
|
|
onChange={(e) => setValue(e.target.value)}
|
|
/>
|
|
|
|
|
|
{editCache ?
|
|
<div>
|
|
<Typography variant="body2" color="textSecondary" style={{ marginTop: 10, }}>
|
|
Created: {timestamp(dataValue?.created)}
|
|
</Typography>
|
|
<Typography variant="body2" color="textSecondary" style={{ }}>
|
|
Edited: {timestamp(dataValue?.edited)}
|
|
</Typography>
|
|
{dataValue?.workflow_id !== "" ?
|
|
<Typography variant="body2" color="textSecondary" style={{ }}>
|
|
Workflow: {dataValue.workflow_id}
|
|
</Typography>
|
|
: null}
|
|
{dataValue?.category !== "" && dataValue?.category !== "default" ?
|
|
<Typography variant="body2" color="textSecondary" style={{ }}>
|
|
Category: {dataValue.category}
|
|
</Typography>
|
|
: null}
|
|
{dataValue?.enrichments !== undefined && dataValue?.enrichments !== null && dataValue.enrichments.length > 0 ?
|
|
<Typography variant="body2" color="textSecondary" style={{ }}>
|
|
Enrichments: {dataValue.enrichments.length}
|
|
</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}
|
|
</div>
|
|
<DialogActions style={{ paddingLeft: "30px", paddingRight: '30px' }}>
|
|
<Button
|
|
style={{ borderRadius: "2px", fontSize: 16, color: theme.palette.primary.main, textTransform:"none" }}
|
|
onClick={() => {
|
|
setModalOpen(false)
|
|
setKey("")
|
|
setValue("")
|
|
setDataValue({})
|
|
}}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
variant="contained"
|
|
style={{ borderRadius: "2px", textTransform:"none" }}
|
|
onClick={() => {
|
|
if (value === "") {
|
|
toast("Key or Value can not be empty");
|
|
return;
|
|
}
|
|
{editCache ? editOrgCache(orgId) : addOrgCache(orgId)}
|
|
setKey("")
|
|
setValue("")
|
|
setDataValue({})
|
|
}}
|
|
color="primary"
|
|
>
|
|
{editCache ? "Edit":"Submit"}
|
|
</Button>
|
|
|
|
</DialogActions>
|
|
</Dialog>
|
|
);
|
|
|
|
const changeDistribution = (id, selectedSubOrg) => {
|
|
|
|
editFileConfig(id, [...new Set(selectedSubOrg)], selectedCategory)
|
|
}
|
|
|
|
|
|
const editFileConfig = (id, selectedSubOrg, category) => {
|
|
const data = {
|
|
Key: id,
|
|
action: "suborg_distribute",
|
|
selected_suborgs: selectedSubOrg,
|
|
category: category === undefined || category === "" || category === "default" ? "" : category,
|
|
}
|
|
|
|
const url = `${globalUrl}/api/v1/orgs/${orgId}/cache/config`;
|
|
|
|
fetch(url, {
|
|
mode: "cors",
|
|
method: "POST",
|
|
body: JSON.stringify(data),
|
|
credentials: "include",
|
|
crossDomain: true,
|
|
withCredentials: true,
|
|
headers: {
|
|
"Content-Type": "application/json; charset=utf-8",
|
|
},
|
|
})
|
|
.then((response) =>
|
|
response.json().then((responseJson) => {
|
|
if (responseJson["success"] === false) {
|
|
toast.error("Failed overwriting datastore");
|
|
} else {
|
|
toast.success("Successfully updated datastore!");
|
|
setTimeout(() => {
|
|
listOrgCache(orgId, selectedCategory, 0, pageSize, page);
|
|
setShowDistributionPopup(false);
|
|
}, 1000);
|
|
}
|
|
})
|
|
)
|
|
.catch((error) => {
|
|
toast("Err: " + error.toString());
|
|
});
|
|
};
|
|
|
|
|
|
|
|
|
|
const deleteConfirmDialog = (
|
|
<DeleteConfirmDialog
|
|
open={deleteConfirmOpen}
|
|
onClose={() => { setDeleteConfirmOpen(false); setDeleteConfirmTarget(null); }}
|
|
onConfirm={() => {
|
|
if (deleteConfirmTarget?.bulk) {
|
|
const itemsToDelete = selectedRows.map(rowId =>
|
|
listCache.find(item => `${item.key}_${item.category || ""}` === rowId)
|
|
).filter(Boolean);
|
|
|
|
const count = itemsToDelete.length;
|
|
setSelectedRows([]);
|
|
itemsToDelete.forEach(item => deleteEntry(orgId, item.key, item.category, false));
|
|
|
|
setTimeout(() => {
|
|
listOrgCache(orgId, selectedCategory, 0, pageSize, page);
|
|
toast.success("Deleted " + count + " keys from datastore");
|
|
}, 3000);
|
|
} else {
|
|
deleteEntry(orgId, deleteConfirmTarget.key, deleteConfirmTarget.category);
|
|
}
|
|
setDeleteConfirmOpen(false);
|
|
setDeleteConfirmTarget(null);
|
|
}}
|
|
title={deleteConfirmTarget?.bulk ? `Delete ${selectedRows?.length} Key${selectedRows?.length > 1 ? "s" : ""}?` : "Delete Key?"}
|
|
description={
|
|
deleteConfirmTarget?.bulk
|
|
? <>Are you sure you want to delete <b>{selectedRows?.length} key{selectedRows?.length > 1 ? "s" : ""}</b>?</>
|
|
: <>Are you sure you want to delete <b>{deleteConfirmTarget?.key}</b>?</>
|
|
}
|
|
warningText="This cannot be undone. Any workflows using these keys will lose access."
|
|
/>
|
|
);
|
|
|
|
const saveAutomation = (allAutomation, settings) => {
|
|
// Check if icon is a string. Otherwise make it empty.
|
|
var removedIcons = {}
|
|
allAutomation.forEach((automation) => {
|
|
const originalIcon = automation.icon
|
|
if (typeof automation.icon !== "string") {
|
|
automation.icon = "";
|
|
}
|
|
})
|
|
|
|
const url = `${globalUrl}/api/v2/datastore/automate`
|
|
const data = {
|
|
"category": selectedCategory === "" || selectedCategory === "default" ? "" : selectedCategory,
|
|
"automations": allAutomation,
|
|
}
|
|
|
|
if (settings !== undefined && settings !== null && Object.keys(settings).length > 0) {
|
|
data["settings"] = settings
|
|
}
|
|
|
|
fetch(url, {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
Accept: "application/json",
|
|
},
|
|
credentials: "include",
|
|
body: JSON.stringify(data),
|
|
})
|
|
.then((response) => {
|
|
if (response.status !== 200) {
|
|
console.log("Status not 200 for automations :O!");
|
|
return;
|
|
}
|
|
|
|
return response.json();
|
|
})
|
|
.then((responseJson) => {
|
|
if (responseJson?.success === true) {
|
|
toast.success("Saved successfully!")
|
|
} else {
|
|
toast.warn("Failed to save automations. Please try again or contact support@shuffler if this persists.")
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
toast.error(error.toString());
|
|
})
|
|
}
|
|
|
|
|
|
const AutomationOptions = ({ automation, index }) => {
|
|
const [showOptions, setShowOptions] = useState(false);
|
|
const [hovered, setHovered] = useState(false);
|
|
|
|
const [updated, setUpdated] = useState(false);
|
|
const [updatedAutomation, setUpdatedAutomation] = useState(automation);
|
|
const [_, setUpdate] = useState(Math.random()) // Force re-render
|
|
|
|
if (automation.icon === undefined || automation.icon === null || automation.icon === "") {
|
|
for (var i = 0; i < defaultAutomation.length; i++) {
|
|
if (defaultAutomation[i].name === automation.name) {
|
|
automation.icon = defaultAutomation[i].icon
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
const runSave = () => {
|
|
setUpdated(false)
|
|
|
|
const newAutomations = [...categoryAutomations]
|
|
newAutomations[index] = updatedAutomation
|
|
|
|
setCategoryAutomations(newAutomations)
|
|
setUpdate(Math.random()) // Force re-render
|
|
|
|
saveAutomation(newAutomations)
|
|
}
|
|
|
|
return (
|
|
<Tooltip title={
|
|
<Typography style={{margin: 10, }}>
|
|
{automation?.description}
|
|
</Typography>
|
|
}
|
|
placement="right"
|
|
style={theme.palette.tooltip}
|
|
>
|
|
<div key={index} style={{
|
|
marginBottom: 10,
|
|
backgroundColor: hovered ? theme.palette.hoverColor : "transparent",
|
|
paddingTop: 5,
|
|
}}
|
|
onMouseEnter={() => setHovered(true)}
|
|
onMouseLeave={() => setHovered(false)}
|
|
>
|
|
<div style={{display: "flex", }}>
|
|
<Tooltip title={automation.enabled ? "Disable Automation" : "Enable Automation"} style={{}} aria-label={""}>
|
|
<Checkbox
|
|
style={{ marginRight: 10, marginTop: -10, }}
|
|
checked={automation.enabled}
|
|
// Check if automation options have a 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()
|
|
|
|
updatedAutomation.enabled = !updatedAutomation.enabled
|
|
setUpdatedAutomation(updatedAutomation)
|
|
setUpdated(true)
|
|
|
|
setUpdate(Math.random())
|
|
}}
|
|
/>
|
|
</Tooltip>
|
|
<div
|
|
style={{
|
|
display: "flex",
|
|
cursor: !automation?.disabled ? "pointer" : "not-allowed",
|
|
width: "100%",
|
|
}}
|
|
disabled={automation?.disabled === true}
|
|
onClick={() => {
|
|
if (automation?.disabled === true) {
|
|
return
|
|
}
|
|
|
|
if (automation?.name === "Enrich") {
|
|
automation.enabled = !automation.enabled
|
|
runSave()
|
|
return
|
|
}
|
|
|
|
setShowOptions(!showOptions)
|
|
|
|
// Auto saves when the options are closed/saved
|
|
if (updated && showOptions) {
|
|
runSave()
|
|
}
|
|
}}
|
|
>
|
|
{typeof automation?.icon === "string" && automation?.icon?.length > 0 ?
|
|
<img src={automation.icon} alt={automation.name} style={{ width: 20, height: 20, }} />
|
|
:
|
|
automation.icon
|
|
}
|
|
|
|
<Typography variant="body1" style={{
|
|
color: automation?.disabled === true ? theme.palette.text.secondary : theme.palette.text.primary,
|
|
marginLeft: 10,
|
|
}}>
|
|
{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 ?
|
|
<Button
|
|
style={{
|
|
borderRadius: theme.palette.borderRadius,
|
|
textTransform: 'none',
|
|
color: updated ? theme.palette.primary.main : theme.palette.text.secondary,
|
|
marginTop: -6,
|
|
|
|
position: "absolute",
|
|
right: 25,
|
|
}}
|
|
disabled={!updated}
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
|
|
runSave()
|
|
}}
|
|
>
|
|
Save
|
|
</Button>
|
|
: null}
|
|
</div>
|
|
|
|
{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 (
|
|
<Typography key={optionIndex} style={{ color: theme.palette.text.secondary, marginTop: 10 }}>
|
|
No categories available. Please add categories in the settings.
|
|
</Typography>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<Autocomplete
|
|
key={optionIndex}
|
|
|
|
multiple
|
|
label="Choose Datastore Categories"
|
|
id="datastore_category_search"
|
|
autoHighlight
|
|
freeSolo
|
|
value={datastoreCategories?.filter(c => option?.value.includes(c)) || []}
|
|
classes={{ inputRoot: classes.inputRoot }}
|
|
ListboxProps={{
|
|
style: {
|
|
backgroundColor: theme.palette.surfaceColor,
|
|
color: theme.palette.text.primary,
|
|
borderRadius: theme.palette.borderRadius,
|
|
},
|
|
}}
|
|
onChange={(event, newValue) => {
|
|
console.log("New Value: ", newValue)
|
|
|
|
option.value = ""
|
|
for (var i = 0; i < newValue.length; i++) {
|
|
option.value += newValue[i] + ","
|
|
}
|
|
|
|
if (newValue.length > 0) {
|
|
updatedAutomation.enabled = true
|
|
} else {
|
|
updatedAutomation.enabled = false
|
|
}
|
|
|
|
updatedAutomation.options[optionIndex] = option
|
|
setUpdatedAutomation(updatedAutomation)
|
|
setUpdated(true)
|
|
|
|
setUpdate(Math.random()) // Force re-render
|
|
}}
|
|
|
|
getOptionLabel={(option) => {
|
|
if (option === undefined || option === null) {
|
|
return "No Categories Selected";
|
|
}
|
|
|
|
return option
|
|
}}
|
|
options={datastoreCategories}
|
|
fullWidth
|
|
style={{
|
|
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
|
borderRadius: theme.palette.textFieldStyle.borderRadius,
|
|
color: theme.palette.textFieldStyle.color,
|
|
height: 35,
|
|
marginBottom: 40,
|
|
}}
|
|
renderOption={(props, data, state) => {
|
|
const fixedname = data?.charAt(0)?.toUpperCase() + data?.slice(1)?.replaceAll("_", " ")
|
|
const iconDetails = GetIconInfo({
|
|
"app_name": fixedname,
|
|
"name": fixedname,
|
|
})
|
|
|
|
const keyfound = option?.value.includes(data)
|
|
|
|
return (
|
|
<Tooltip arrow placement="left" title={
|
|
<span style={{}}>
|
|
{data.image !== undefined && data.image !== null && data.image.length > 0 ?
|
|
<img src={data.image} alt={data.name} style={{ backgroundColor: theme.palette.surfaceColor, maxHeight: 200, minHeigth: 200, borderRadius: theme.palette?.borderRadius, }} />
|
|
: null}
|
|
<Typography>
|
|
Choose {data}
|
|
</Typography>
|
|
</span>
|
|
} >
|
|
<MenuItem
|
|
{...props}
|
|
style={{
|
|
backgroundColor: theme.palette.surfaceColor,
|
|
}}
|
|
value={data}
|
|
>
|
|
<Typography style={{
|
|
display: "flex",
|
|
marginTop: 5,
|
|
color: keyfound ? red : theme.palette.text.primary,
|
|
}}>
|
|
<div style={{marginRight: 10, }}>
|
|
{iconDetails?.originalIcon && (
|
|
iconDetails?.originalIcon
|
|
)}
|
|
</div>
|
|
|
|
{fixedname}
|
|
</Typography>
|
|
</MenuItem>
|
|
</Tooltip>
|
|
)
|
|
}}
|
|
renderInput={(params) => {
|
|
return (
|
|
<TextField
|
|
{...params}
|
|
style={{
|
|
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
|
color: theme.palette.textFieldStyle.color,
|
|
borderRadius: theme.palette.textFieldStyle.borderRadius,
|
|
height: 35,
|
|
fontSize: 16,
|
|
marginTop: "16px"
|
|
}}
|
|
InputProps={{
|
|
...params.InputProps,
|
|
style: {
|
|
height: 35,
|
|
display: "flex",
|
|
alignItems: "center",
|
|
padding: "0px 8px",
|
|
fontSize: 16,
|
|
borderRadius: 4,
|
|
},
|
|
inputProps: {
|
|
...params.inputProps,
|
|
style: {
|
|
height: "100%",
|
|
boxSizing: "border-box",
|
|
}
|
|
|
|
}
|
|
}}
|
|
variant="outlined"
|
|
placeholder="Select Categories to Correlate"
|
|
/>
|
|
)
|
|
}}
|
|
/>
|
|
)
|
|
} else if (option?.key === "workflow_id") {
|
|
return (
|
|
<Autocomplete
|
|
key={optionIndex}
|
|
|
|
multiple
|
|
label="Find a workflow"
|
|
id="workflow_search"
|
|
autoHighlight
|
|
freeSolo
|
|
value={workflows?.filter(w => option?.value.includes(w.id)) || []}
|
|
classes={{ inputRoot: classes.inputRoot }}
|
|
ListboxProps={{
|
|
style: {
|
|
backgroundColor: theme.palette.surfaceColor,
|
|
color: theme.palette.text.primary,
|
|
borderRadius: theme.palette.borderRadius,
|
|
},
|
|
}}
|
|
onChange={(event, newValue) => {
|
|
option.value = ""
|
|
for (var i = 0; i < newValue.length; i++) {
|
|
option.value += newValue[i].id + ","
|
|
}
|
|
|
|
if (newValue.length > 0) {
|
|
updatedAutomation.enabled = true
|
|
} else {
|
|
updatedAutomation.enabled = false
|
|
}
|
|
|
|
updatedAutomation.options[optionIndex] = option
|
|
setUpdatedAutomation(updatedAutomation)
|
|
setUpdated(true)
|
|
|
|
setUpdate(Math.random()) // Force re-render
|
|
}}
|
|
|
|
getOptionLabel={(option) => {
|
|
if (
|
|
option === undefined ||
|
|
option === null ||
|
|
option?.name === undefined ||
|
|
option?.name === null
|
|
) {
|
|
return "No Workflows Selected";
|
|
}
|
|
|
|
const newname = (
|
|
option.name.charAt(0).toUpperCase() + option.name.substring(1)
|
|
).replaceAll("_", " ")
|
|
|
|
return newname
|
|
}}
|
|
options={workflows}
|
|
fullWidth
|
|
style={{
|
|
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
|
borderRadius: theme.palette.textFieldStyle.borderRadius,
|
|
color: theme.palette.textFieldStyle.color,
|
|
height: 35,
|
|
marginBottom: 40,
|
|
}}
|
|
renderOption={(props, data, state) => {
|
|
/*
|
|
if (data.id === option?.value) {
|
|
data = workflow;
|
|
}
|
|
*/
|
|
|
|
return (
|
|
<Tooltip arrow placement="left" title={
|
|
<span style={{}}>
|
|
{data.image !== undefined && data.image !== null && data.image.length > 0 ?
|
|
<img src={data.image} alt={data.name} style={{ backgroundColor: theme.palette.surfaceColor, maxHeight: 200, minHeigth: 200, borderRadius: theme.palette?.borderRadius, }} />
|
|
: null}
|
|
<Typography>
|
|
Choose {data.name}
|
|
</Typography>
|
|
</span>
|
|
} >
|
|
<MenuItem
|
|
{...props}
|
|
style={{
|
|
backgroundColor: theme.palette.surfaceColor,
|
|
color: data.id === option?.value ? red : theme.palette.text.primary,
|
|
}}
|
|
value={data}
|
|
>
|
|
{data.name}
|
|
</MenuItem>
|
|
</Tooltip>
|
|
)
|
|
}}
|
|
renderInput={(params) => {
|
|
return (
|
|
<TextField
|
|
{...params}
|
|
style={{
|
|
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
|
color: theme.palette.textFieldStyle.color,
|
|
borderRadius: theme.palette.textFieldStyle.borderRadius,
|
|
height: 35,
|
|
fontSize: 16,
|
|
marginTop: "16px"
|
|
}}
|
|
InputProps={{
|
|
...params.InputProps,
|
|
style: {
|
|
height: 35,
|
|
display: "flex",
|
|
alignItems: "center",
|
|
padding: "0px 8px",
|
|
fontSize: 16,
|
|
borderRadius: 4,
|
|
},
|
|
inputProps: {
|
|
...params.inputProps,
|
|
style: {
|
|
height: "100%",
|
|
boxSizing: "border-box",
|
|
}
|
|
|
|
}
|
|
}}
|
|
variant="outlined"
|
|
placeholder="Select workflows to run"
|
|
/>
|
|
)
|
|
}}
|
|
/>
|
|
)
|
|
} 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 (
|
|
<TextField
|
|
key={optionIndex}
|
|
fullWidth
|
|
style={{
|
|
...theme.palette.textFieldStyle,
|
|
marginBottom: 20,
|
|
marginTop: 10,
|
|
}}
|
|
InputProps={{
|
|
style: {
|
|
backgroundColor: theme.palette.textFieldStyle.backgroundColor,
|
|
color: theme.palette.textFieldStyle.color,
|
|
},
|
|
}}
|
|
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)
|
|
}}
|
|
/>
|
|
)
|
|
})}
|
|
</div>
|
|
}
|
|
</div>
|
|
</Tooltip>
|
|
)
|
|
}
|
|
|
|
|
|
const setCategorySettingsField = (field, value) => {
|
|
// Check if categoryConfig.settings is set or not. Otherwise set it.
|
|
var categoryConfig2 = categoryConfig
|
|
if (categoryConfig === undefined || categoryConfig === null) {
|
|
categoryConfig2 = {}
|
|
}
|
|
|
|
if (categoryConfig?.settings === undefined || categoryConfig?.settings === null) {
|
|
categoryConfig2.settings = {}
|
|
}
|
|
|
|
categoryConfig2.settings[field] = value
|
|
setCategoryConfig(categoryConfig2)
|
|
|
|
saveAutomation(
|
|
categoryAutomations,
|
|
categoryConfig2.settings,
|
|
)
|
|
}
|
|
|
|
const urlParams = new URLSearchParams(window?.location?.search)
|
|
const highlightedKey = urlParams.get("key") || ""
|
|
const columns = [
|
|
{
|
|
field: 'key',
|
|
headerName: 'Key',
|
|
width: 200,
|
|
filterable: true,
|
|
sortable: true,
|
|
renderCell: (props) => {
|
|
return (
|
|
<Typography
|
|
variant="body2"
|
|
style={{
|
|
maxWidth: 180,
|
|
whiteSpace: "nowrap",
|
|
overflow: "hidden",
|
|
textOverflow: "ellipsis",
|
|
cursor: "default",
|
|
|
|
border: props.row.key === highlightedKey ? `2px solid ${theme.palette.primary.main}` : "none",
|
|
}}
|
|
>
|
|
{props.row.key}
|
|
</Typography>
|
|
)
|
|
}
|
|
},
|
|
{
|
|
width: 540,
|
|
field: 'value',
|
|
filterable: true,
|
|
headerName: 'Value',
|
|
renderCell: (props) => {
|
|
const data = props.row
|
|
|
|
if (data?.category?.toLowerCase() === "protected") {
|
|
return (
|
|
<Typography
|
|
variant="body2"
|
|
type="password"
|
|
style={{maxHeight: 200, overflow: "hidden", }}
|
|
>
|
|
***************
|
|
</Typography>
|
|
)
|
|
}
|
|
|
|
const validate = validateJson(data.value)
|
|
|
|
return (
|
|
<div onClick={(e) => {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
}}
|
|
>
|
|
{validate.valid ?
|
|
<ReactJson
|
|
id={"reactjson_" + data.key}
|
|
src={validate.result}
|
|
theme={theme.palette.jsonTheme}
|
|
style={{
|
|
...theme.palette.reactJsonStyle,
|
|
backgroundColor: theme.palette.platformColor,
|
|
border: theme.palette.defaultBorder,
|
|
padding: 5,
|
|
maxWidth: 500,
|
|
minWidth: 500,
|
|
maxHeight: 500,
|
|
overflowX: "auto",
|
|
overflowY: "auto",
|
|
}}
|
|
collapsed={true}
|
|
enableClipboard={(copy) => {
|
|
handleReactJsonClipboard(copy)
|
|
}}
|
|
onSelect={(select) => {
|
|
//currentParams.set("category", selectedCategory);
|
|
//HandleJsonCopy(validate.result, select, "exec");
|
|
}}
|
|
collapseStringsAfterLength={theme.palette.jsonCollapseStringsAfterLength}
|
|
iconStyle={theme.palette.jsonIconStyle}
|
|
displayDataTypes={false}
|
|
name={null}
|
|
/>
|
|
:
|
|
<Typography variant="body2" style={{maxWidth: 500, maxHeight: 200, overflow: "auto", }}>
|
|
{data.value}
|
|
</Typography>
|
|
}
|
|
</div>
|
|
)
|
|
}
|
|
},
|
|
{
|
|
field: 'category',
|
|
headerName: 'Category',
|
|
description: 'Category for this key.',
|
|
width: 75,
|
|
filterable: false,
|
|
sortable: true,
|
|
renderCell: (props) => {
|
|
// Return avatar with hover for the category
|
|
const data = props.row
|
|
const clickCategory = (e) => {
|
|
setCategoryConfig(undefined)
|
|
setCategoryAutomations(defaultAutomation)
|
|
|
|
if (selectAllChecked || selectedFiles.length > 0) {
|
|
setUpdateToThisCategory(data.category)
|
|
return
|
|
}
|
|
|
|
setSelectedCategory(data.category)
|
|
if (data.category === "all" || data.category === "default") {
|
|
listOrgCache(orgId, "", 0, pageSize, page)
|
|
} else {
|
|
listOrgCache(orgId, data.category, 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.category}`)
|
|
window.history.pushState({ path: newurl }, "", newurl)
|
|
} else {
|
|
window.history.pushState({ path: window.location.href }, "", `${window.location.href}&category=${data.category}`)
|
|
}
|
|
}
|
|
|
|
|
|
const iconDetails = GetIconInfo({
|
|
"app_name": data.category,
|
|
"name": data.category,
|
|
})
|
|
|
|
const avatarLetter = (data.category === "" || data.category === "default" ? " " : data.category.charAt(0).toUpperCase())[0]
|
|
return (
|
|
<Tooltip title={data.category === "" || data.category === "default" ? "No category" : `Category name: ${data.category}`} placement="left">
|
|
<Avatar
|
|
onClick={(e) => {
|
|
clickCategory(e)
|
|
}}
|
|
style={{
|
|
color: "white",
|
|
backgroundColor: iconDetails?.iconBackgroundColor || theme.palette.primary.secondary,
|
|
marginLeft: 15,
|
|
height: 30,
|
|
width: 30,
|
|
cursor: data.category !== "" && data.category !== "default" ? "pointer" : "default",
|
|
}}
|
|
variant="rounded"
|
|
>
|
|
{iconDetails?.originalIcon ?
|
|
iconDetails?.originalIcon
|
|
:
|
|
avatarLetter
|
|
}
|
|
</Avatar>
|
|
</Tooltip>
|
|
)
|
|
}
|
|
},
|
|
{
|
|
field: 'actions',
|
|
headerName: 'Actions',
|
|
description: 'Actions for this key.',
|
|
width: 175,
|
|
filterable: false,
|
|
sortable: false,
|
|
|
|
renderCell: (props) => {
|
|
const data = props.row
|
|
|
|
return (
|
|
<span style={{ display: "flex" }}>
|
|
{data?.workflow_id === "" || data?.workflow_id === null || data?.workflow_id === undefined || data?.workflow_id?.length !== 36 ?
|
|
<IconButton
|
|
disabled={data.workflow_id?.length === 0}
|
|
style={{}}
|
|
>
|
|
<OpenInNewIcon
|
|
style={{
|
|
color:
|
|
data.workflow_id?.length === 36
|
|
? "#FF8444"
|
|
: "grey",
|
|
}}
|
|
/>
|
|
</IconButton>
|
|
: (
|
|
<Tooltip
|
|
title={"Go to workflow"}
|
|
style={{}}
|
|
aria-label={"Download"}
|
|
placement="left"
|
|
>
|
|
<span>
|
|
<a
|
|
rel="noopener noreferrer"
|
|
style={{
|
|
textDecoration: "none",
|
|
color: "#f85a3e",
|
|
}}
|
|
href={`/workflows/${data.workflow_id}`}
|
|
target="_blank"
|
|
>
|
|
<IconButton
|
|
disabled={data.workflow_id?.length ===0}
|
|
style={{marginLeft: 0}}
|
|
>
|
|
<OpenInNewIcon
|
|
style={{
|
|
width: 24, height: 24,
|
|
color:
|
|
data.workflow_id?.length === 36
|
|
? "#FF8444"
|
|
: "grey",
|
|
}}
|
|
/>
|
|
</IconButton>
|
|
</a>
|
|
</span>
|
|
</Tooltip>
|
|
)}
|
|
|
|
<Tooltip
|
|
title={data?.org_id !== selectedOrganization.id ? "You can not edit this cache as it is controlled by parent organization." : "Edit this key" }
|
|
style={{}}
|
|
aria-label={"Edit"}
|
|
>
|
|
<span>
|
|
<IconButton
|
|
style={{ padding: "6px" }}
|
|
disabled={data.org_id !== selectedOrganization.id ? true : false}
|
|
onClick={(e) => {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
// Try to make the value JSON indented
|
|
const valid = validateJson(data.value)
|
|
var newvalue = data.value
|
|
if (valid.valid) {
|
|
// JSON stringify with indentation
|
|
newvalue = JSON.stringify(valid.result, null, 2)
|
|
}
|
|
|
|
|
|
setEditCache(true)
|
|
setDataValue({
|
|
"key": data.key,
|
|
"value": newvalue,
|
|
|
|
"edited": data.edited,
|
|
"created": data.created,
|
|
"workflow_id": data.workflow_id,
|
|
"category": data.category,
|
|
"tags": data.tags,
|
|
"enrichments": data.enrichments,
|
|
})
|
|
setValue(newvalue)
|
|
setModalOpen(true)
|
|
}}
|
|
>
|
|
<svg
|
|
width="24"
|
|
height="24"
|
|
viewBox="0 0 24 24"
|
|
fill="none"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
>
|
|
<path
|
|
d="M16.1038 4.66848C16.3158 4.45654 16.5674 4.28843 16.8443 4.17373C17.1212 4.05903 17.418 4 17.7177 4C18.0174 4 18.3142 4.05903 18.5911 4.17373C18.868 4.28843 19.1196 4.45654 19.3315 4.66848C19.5435 4.88041 19.7116 5.13201 19.8263 5.40891C19.941 5.68582 20 5.9826 20 6.28232C20 6.58204 19.941 6.87882 19.8263 7.15573C19.7116 7.43263 19.5435 7.68423 19.3315 7.89617L8.43807 18.7896L4 20L5.21038 15.5619L16.1038 4.66848Z"
|
|
stroke={themeMode=== "dark" ? "#F1F1F1" : "#333"}
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
/>
|
|
</svg>
|
|
</IconButton>
|
|
</span>
|
|
</Tooltip>
|
|
|
|
<Tooltip
|
|
title={"Explore correlations: Requires a category."}
|
|
style={{}}
|
|
aria-label={"Edit"}
|
|
>
|
|
<span>
|
|
<IconButton
|
|
style={{ padding: "6px" }}
|
|
disabled={userdata?.support !== true || data?.category === "" || data?.category === null || data?.category === undefined}
|
|
onClick={(e) => {
|
|
if (userdata?.support !== true) {
|
|
toast.warn("Correlation graphs are a beta feature.")
|
|
}
|
|
|
|
toast.info("Opening correlations window")
|
|
|
|
// add the key we are correlating
|
|
const currentParams = new URLSearchParams(window.location.search);
|
|
currentParams.set("category", selectedCategory);
|
|
currentParams.set("correlations", "true");
|
|
currentParams.set("key", data.key);
|
|
navigate(`${window.location.pathname}?${currentParams.toString()}`);
|
|
setShowCorrelations(data)
|
|
}}
|
|
>
|
|
<HubIcon />
|
|
</IconButton>
|
|
</span>
|
|
</Tooltip>
|
|
|
|
<Tooltip
|
|
title={data?.org_id !== selectedOrganization.id ? "You can not access public URL for this key as it is controlled by parent organization." : "Public URL (types: text, raw, json)" }
|
|
style={{ marginLeft: 0, }}
|
|
aria-label={"Public URL"}
|
|
>
|
|
<span>
|
|
<IconButton
|
|
style={{ padding: "6px" }}
|
|
disabled={data.public_authorization === undefined || data.public_authorization === null || data.public_authorization === "" || data.org_id !== selectedOrganization.id ? true : false || data.category === "protected"}
|
|
onClick={(e) => {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
window.open(`${globalUrl}/api/v1/orgs/${orgId}/cache/${data.key}?type=text&authorization=${data.public_authorization}`, "_blank");
|
|
}}
|
|
>
|
|
<LinkIcon
|
|
/>
|
|
</IconButton>
|
|
</span>
|
|
</Tooltip>
|
|
<Tooltip
|
|
title={selectedOrganization?.id !== undefined && data?.org_id !== selectedOrganization.id ? "You can not delete this key as it is controlled by parent organization." : "Delete this key" }
|
|
aria-label={"Delete"}
|
|
>
|
|
<span>
|
|
<IconButton
|
|
style={{ padding: "6px" }}
|
|
disabled={selectedOrganization?.id === undefined ? false : data.org_id !== selectedOrganization.id ? true : false}
|
|
onClick={(e) => {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
setDeleteConfirmTarget({ key: data.key, category: data.category })
|
|
setDeleteConfirmOpen(true)
|
|
}}
|
|
>
|
|
<svg
|
|
width="24"
|
|
height="24"
|
|
viewBox="0 0 24 24"
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
style={{
|
|
stroke: data.org_id === selectedOrganization.id ? "#fd4c62" : "#c8c8c8",
|
|
}}
|
|
>
|
|
<path
|
|
d="M5 7.20001H6.6H19.4"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
fill="none"
|
|
/>
|
|
<path
|
|
d="M17.7996 7.2V18.4C17.7996 18.8243 17.631 19.2313 17.331 19.5314C17.0309 19.8314 16.624 20 16.1996 20H8.19961C7.77526 20 7.3683 19.8314 7.06824 19.5314C6.76818 19.2313 6.59961 18.8243 6.59961 18.4V7.2M8.99961 7.2V5.6C8.99961 5.17565 9.16818 4.76869 9.46824 4.46863C9.7683 4.16857 10.1753 4 10.5996 4H13.7996C14.224 4 14.6309 4.16857 14.931 4.46863C15.231 4.76869 15.3996 5.17565 15.3996 5.6V7.2"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
fill="none"
|
|
/>
|
|
</svg>
|
|
</IconButton>
|
|
</span>
|
|
</Tooltip>
|
|
</span>
|
|
)
|
|
}
|
|
},
|
|
{
|
|
field: 'distribution',
|
|
headerName: 'Distribution',
|
|
description: 'Controls whether this key is distributed to sub-organizations.',
|
|
width: 100,
|
|
filterable: false,
|
|
sortable: false,
|
|
|
|
renderCell: (props) => {
|
|
const data = props.row
|
|
const isDistributed = data?.suborg_distribution?.length > 0 ? true : false;
|
|
|
|
return (
|
|
<div>
|
|
{selectedOrganization.id !== undefined && data?.org_id !== selectedOrganization.id ?
|
|
<ListItemText
|
|
primary={
|
|
<Tooltip
|
|
title="Parent organization controlled datastore. You can use, but not modify this key. Contact an admin of your parent organization if you need changes to this."
|
|
placement="top"
|
|
>
|
|
<Chip
|
|
label={"Parent"}
|
|
variant="contained"
|
|
color="secondary"
|
|
/>
|
|
</Tooltip>
|
|
}
|
|
style={{display: "table-cell", textAlign: 'center', verticalAlign: 'middle', }}
|
|
/>
|
|
:
|
|
<ListItemText
|
|
primary={
|
|
<Tooltip
|
|
title="Distributed to sub-organizations. This means the sub organizations can use this datastore key, but can not modify it."
|
|
placement="top"
|
|
>
|
|
<Checkbox
|
|
disabled={ userdata?.active_org?.role !== "admin" || (selectedOrganization.creator_org !== undefined && selectedOrganization.creator_org !== null && selectedOrganization.creator_org !== "" )? true : false}
|
|
checked={isDistributed}
|
|
style={{ margin: "auto" }}
|
|
color="secondary"
|
|
onClick={() => {
|
|
setShowDistributionPopup(true);
|
|
let initialSelected = [];
|
|
if(data?.suborg_distribution?.length > 0){
|
|
initialSelected = data.suborg_distribution;
|
|
}
|
|
setSelectedSubOrg(initialSelected);
|
|
setSelectedCacheKey(data.key);
|
|
const suborgs = (userdata?.orgs || []).filter(o => o.creator_org === userdata?.active_org?.id);
|
|
const sorted = [...suborgs].sort((a, b) => {
|
|
const aS = initialSelected.includes(a.id);
|
|
const bS = initialSelected.includes(b.id);
|
|
if (aS !== bS) return bS - aS;
|
|
return a.name.localeCompare(b.name);
|
|
});
|
|
setDistribOrgOrder(sorted.map(o => o.id));
|
|
}}
|
|
/>
|
|
</Tooltip>
|
|
}
|
|
style={{display: "table-cell", textAlign: 'center', verticalAlign: 'middle', }}
|
|
/>
|
|
}
|
|
</div>
|
|
)
|
|
}
|
|
},
|
|
];
|
|
|
|
var previousgroup = ""
|
|
const isAutomating = categoryAutomations?.find((automation) => automation.enabled) !== undefined
|
|
const isAutomatingAccess = categoryConfig?.settings?.timeout >= 60 || categoryConfig?.settings?.public === true ? true : false
|
|
return (
|
|
<div style={{
|
|
minHeight: 2000,
|
|
height: isSelectedDataStore?"100%":null,
|
|
|
|
paddingBottom: isSelectedDataStore?null:250,
|
|
boxSizing: "border-box",
|
|
width: isSelectedDataStore? "100%" :null,
|
|
transition: "width 0.3s ease",
|
|
padding:isSelectedDataStore?"27px 10px 27px 27px":null,
|
|
color: isSelectedDataStore?'#ffffff':null,
|
|
backgroundColor: isSelectedDataStore? theme.palette.platformColor :null,
|
|
borderTopRightRadius: isSelectedDataStore?'8px':null,
|
|
borderBottomRightRadius: isSelectedDataStore?'8px':null,
|
|
borderLeft: theme.palette.defaultBorder
|
|
}}>
|
|
{modalView}
|
|
|
|
{showCorrelations !== undefined ?
|
|
<Dialog
|
|
open={showCorrelations !== undefined}
|
|
onClose={() => setShowCorrelations(undefined)}
|
|
PaperProps={{
|
|
sx: {
|
|
borderRadius: theme?.palette?.DialogStyle?.borderRadius,
|
|
border: theme?.palette?.DialogStyle?.border,
|
|
minWidth: 1000,
|
|
minHeight: 768,
|
|
fontFamily: theme?.typography?.fontFamily,
|
|
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
|
zIndex: 1000,
|
|
'& .MuiDialogContent-root': {
|
|
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
|
},
|
|
'& .MuiDialogTitle-root': {
|
|
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
|
},
|
|
'& .MuiDialogActions-root': {
|
|
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
|
},
|
|
},
|
|
}}
|
|
>
|
|
<DialogTitle>
|
|
Correlations
|
|
</DialogTitle>
|
|
<DialogContent>
|
|
<CorrelationGraph
|
|
globalUrl={globalUrl}
|
|
datastoreData={showCorrelations}
|
|
userdata={userdata}
|
|
/>
|
|
</DialogContent>
|
|
</Dialog>
|
|
: null}
|
|
|
|
<CollectIngestModal
|
|
globalUrl={globalUrl}
|
|
open={showCollectIngestMenu}
|
|
setOpen={setShowCollectIngestMenu}
|
|
|
|
workflows={workflows}
|
|
getWorkflows={getWorkflows}
|
|
|
|
apps={apps}
|
|
/>
|
|
|
|
<SubOrgDistributionDialog
|
|
open={showDistributionPopup}
|
|
onClose={() => { setShowDistributionPopup(false); setSelectedCacheKey(""); }}
|
|
title="Distribute Datastore Key to Sub-Organizations"
|
|
extraInfo={selectedCacheKey ? `Selected Key: ${selectedCacheKey}` : null}
|
|
orgs={distribOrgOrder.map(id => (userdata?.orgs || []).find(o => o.id === id)).filter(Boolean)}
|
|
selectedOrgIds={selectedSubOrg}
|
|
onSelectionChange={setSelectedSubOrg}
|
|
onSave={(ids) => { changeDistribution(selectedCacheKey, ids); }}
|
|
/>
|
|
{deleteConfirmDialog}
|
|
|
|
<div style={{height: "100%", overflowY: "auto", scrollbarColor: theme.palette.scrollbarColorTransparent, scrollbarWidth: 'thin'}}>
|
|
<div style={{ height: "100%", width: "calc(100% - 20px)", scrollbarColor: theme.palette.scrollbarColorTransparent, scrollbarWidth: 'thin' }}>
|
|
<div style={{ marginTop: isSelectedDataStore?null:20, marginBottom: 20 }}>
|
|
|
|
<div style={{display: "flex", position: "relative", }}>
|
|
|
|
<Typography variant="h5" style={{ display: isSelectedDataStore?null: "inline", fontWeight: 500 }}>
|
|
Shuffle Datastore
|
|
</Typography>
|
|
|
|
{userdata?.support === true ?
|
|
<Tooltip title={"Collect (support only)"} style={{}} aria-label={""}>
|
|
<span style={{position: "absolute", right: 0, }}>
|
|
<Button
|
|
style={{
|
|
whiteSpace: "nowrap",
|
|
height: 35,
|
|
textTransform: 'none',
|
|
|
|
}}
|
|
variant="outlined"
|
|
color="secondary"
|
|
onClick={() => {
|
|
setShowCollectIngestMenu(true)
|
|
}}
|
|
>
|
|
<FilterAltIcon style={{marginRight: 10, }}/>
|
|
Collect (beta)
|
|
</Button>
|
|
</span>
|
|
</Tooltip>
|
|
: null}
|
|
</div>
|
|
|
|
<Typography variant="body2" color="textSecondary" style={{ marginLeft: isSelectedDataStore?null:25, marginTop: 10}}>
|
|
Datastore is a permanent key-value database for storing data which can be used for automation.
|
|
<a
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
href="/docs/organizations#datastore"
|
|
style={{ textDecoration: isSelectedDataStore?null:"none", color: theme.palette.linkColor }}
|
|
>
|
|
Learn more
|
|
</a>
|
|
|
|
{selectedCategory === "protected" ?
|
|
<div style={{ color: red, }}>
|
|
Protected keys are encrypted, only available to admins, and will be masked when used in workflows. If you want unreadable secrets, use <a href="/admin?tab=app_auth" style={{ color: theme.palette.linkColor }}>App Auth</a>.
|
|
</div>
|
|
: null}
|
|
|
|
</Typography>
|
|
</div>
|
|
|
|
<div style={{width: "100%", position: "relative", }}>
|
|
<ButtonGroup>
|
|
<Button
|
|
style={{textTransform: isSelectedDataStore ? 'capitalize':null, width : isSelectedDataStore ? 120 : null, height:isSelectedDataStore?40:null}}
|
|
variant="contained"
|
|
color="primary"
|
|
onClick={() =>{
|
|
setEditCache(false)
|
|
setModalOpen(true)
|
|
|
|
setValue("")
|
|
}}
|
|
>
|
|
Add Key
|
|
</Button>
|
|
<Button
|
|
style={{ marginRight: 15, width:isSelectedDataStore ? 81:null, height:isSelectedDataStore?40:null, }}
|
|
variant="contained"
|
|
color="secondary"
|
|
onClick={() => {
|
|
listOrgCache(orgId, selectedCategory, 0, pageSize, page);
|
|
}}
|
|
>
|
|
<CachedIcon />
|
|
</Button>
|
|
</ButtonGroup>
|
|
|
|
<ButtonGroup style={{ position: "absolute", top: -6, }}>
|
|
{datastoreCategories !== undefined &&
|
|
datastoreCategories !== null &&
|
|
datastoreCategories.length > 1 ? (
|
|
|
|
<FormControl style={{ minWidth: 275, maxWidth: 275, marginTop: 8, }}>
|
|
<Autocomplete
|
|
labelId="category-choice"
|
|
style={{
|
|
minWidth: 275,
|
|
maxWidth: 275,
|
|
}}
|
|
ListboxProps={{
|
|
style: {
|
|
maxHeight: "70vh",
|
|
border: "1px solid rgba(255,255,255,0.3)",
|
|
}
|
|
}}
|
|
value={selectedCategory}
|
|
options={datastoreCategories}
|
|
getOptionLabel={(data) => {
|
|
const fixedname = data?.charAt(0)?.toUpperCase() + data?.slice(1)?.replaceAll("_", " ")
|
|
return fixedname
|
|
}}
|
|
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,
|
|
"name": fixedname,
|
|
})
|
|
|
|
return (
|
|
<MenuItem
|
|
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, }}>
|
|
{iconDetails?.originalIcon && (
|
|
iconDetails?.originalIcon
|
|
)}
|
|
</div>
|
|
|
|
{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>
|
|
);
|
|
})}
|
|
*/}
|
|
</FormControl>
|
|
) : null}
|
|
|
|
<div style={{display: "inline-flex", position:"relative", top: 8}}>
|
|
{renderTextBox ?
|
|
<Tooltip title={"Close"} style={{}} aria-label={""}>
|
|
<Button
|
|
style={{
|
|
height: 40,
|
|
borderRadius: 4,
|
|
textTransform: 'none',
|
|
fontSize: 16,
|
|
borderRadius: "0px 5px 5px 0px",
|
|
|
|
marginRight: 10,
|
|
}}
|
|
color="secondary"
|
|
variant="contained"
|
|
onClick={() => {
|
|
setRenderTextBox(false);
|
|
console.log(" close clicked")
|
|
}}
|
|
>
|
|
<ClearIcon/>
|
|
</Button>
|
|
</Tooltip>
|
|
:
|
|
<Tooltip title={"Add new category object"} style={{}} aria-label={""}>
|
|
<Button
|
|
style={{
|
|
whiteSpace: "nowrap",
|
|
width: datastoreCategories !== undefined && datastoreCategories !== null && datastoreCategories.length > 1 ? 50 : 169,
|
|
height: 40,
|
|
textTransform: 'none',
|
|
|
|
borderRadius: "0px 5px 5px 0px",
|
|
}}
|
|
variant="outlined"
|
|
color="secondary"
|
|
onClick={() => {
|
|
setRenderTextBox(true);
|
|
}}
|
|
>
|
|
<AddIcon/>
|
|
{datastoreCategories !== undefined && datastoreCategories !== null && datastoreCategories.length > 1 ? "" : "Category"}
|
|
</Button>
|
|
</Tooltip>
|
|
}
|
|
|
|
{renderTextBox && <TextField
|
|
onKeyPress={(event)=>{
|
|
handleKeyDown(event);
|
|
|
|
// Check value of the field
|
|
const foundValue = event.target.value.trim();
|
|
if(event.key === 'Enter' && foundValue?.length > 0){
|
|
setUpdateToThisCategory(event.target.value)
|
|
|
|
listOrgCache(orgId, event.target.value, 0, pageSize, 0)
|
|
setPage(0)
|
|
}
|
|
|
|
}}
|
|
style={{
|
|
height: 40,
|
|
width: 200,
|
|
marginTop: 0,
|
|
}}
|
|
InputProps={{
|
|
style: {
|
|
color: theme.palette.textFieldStyle.color,
|
|
height: 40,
|
|
fontSize: 16,
|
|
borderRadius: 4,
|
|
paddingTop: 0,
|
|
},
|
|
}}
|
|
id=""
|
|
color="primary"
|
|
placeholder="Category name"
|
|
required
|
|
margin="dense"
|
|
defaultValue={""}
|
|
autoFocus
|
|
/>}
|
|
</div>
|
|
</ButtonGroup>
|
|
|
|
|
|
<ButtonGroup style={{ position: "absolute", right: 0, top: 5, }}>
|
|
<Tooltip title={"Automate current Category"} style={{}} aria-label={""}>
|
|
<span>
|
|
<Button
|
|
style={{
|
|
whiteSpace: "nowrap",
|
|
height: 35,
|
|
textTransform: 'none',
|
|
|
|
border: isAutomating ? `1px solid ${green}` : null,
|
|
}}
|
|
variant="contained"
|
|
color="secondary"
|
|
disabled={selectedCategory === undefined || selectedCategory === "" || selectedCategory === "default"}
|
|
onClick={() => {
|
|
// Set up:
|
|
// Workflow triggers for when a key is added/edited/deleted.
|
|
// Automatic ingest mechanisms:
|
|
// - IOCs (monitor URLs)
|
|
// - Singul ingest
|
|
setShowAutomationMenu(true)
|
|
}}
|
|
>
|
|
{isAutomating ? <RocketLaunchIcon style={{color: green, marginRight: 10 }}/> : <RocketIcon style={{marginRight: 10, color: theme.palette.secondary.main, }} />}
|
|
Automate
|
|
</Button>
|
|
</span>
|
|
</Tooltip>
|
|
|
|
<Tooltip title={"Other category settings"} style={{}} aria-label={""}>
|
|
<Button
|
|
style={{
|
|
whiteSpace: "nowrap",
|
|
height: 35,
|
|
textTransform: 'none',
|
|
marginLeft: 3,
|
|
}}
|
|
variant="outlined"
|
|
color={isAutomatingAccess ? "primary" : "secondary"}
|
|
disabled={selectedCategory === undefined || selectedCategory === "" || selectedCategory === "default"}
|
|
onClick={() => {
|
|
setShowSettingsMenu(true)
|
|
}}
|
|
>
|
|
<SettingsIcon style={{}} />
|
|
</Button>
|
|
</Tooltip>
|
|
</ButtonGroup>
|
|
|
|
{showAutomationMenu || showSettingsMenu ?
|
|
<Dialog
|
|
open={showAutomationMenu || showSettingsMenu}
|
|
onClose={() => {
|
|
setShowAutomationMenu(false)
|
|
setShowSettingsMenu(false)
|
|
}}
|
|
PaperProps={{
|
|
sx: {
|
|
borderRadius: theme?.palette?.DialogStyle?.borderRadius,
|
|
border: theme?.palette?.DialogStyle?.border,
|
|
minWidth: 500,
|
|
minHeight: 700,
|
|
fontFamily: theme?.typography?.fontFamily,
|
|
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
|
zIndex: 1000,
|
|
'& .MuiDialogContent-root': {
|
|
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
|
},
|
|
'& .MuiDialogTitle-root': {
|
|
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
|
},
|
|
'& .MuiDialogActions-root': {
|
|
backgroundColor: theme?.palette?.DialogStyle?.backgroundColor,
|
|
},
|
|
},
|
|
}}
|
|
>
|
|
<DialogTitle>
|
|
</DialogTitle>
|
|
<DialogContent style={{ paddingLeft: "30px", paddingRight: '30px', backgroundColor: theme.palette.DialogStyle.backgroundColor, }}>
|
|
{showSettingsMenu === true ?
|
|
<div>
|
|
<Typography variant="h6" style={{ color: theme.palette.text.primary, marginBottom: 20 }}>
|
|
<SettingsIcon style={{ verticalAlign: "middle", marginRight: 10 }} />
|
|
Settings for category '{selectedCategory}'
|
|
</Typography>
|
|
|
|
<div style={{display: "flex", marginTop: 50, }}>
|
|
<div style={{flex: 2, }}>
|
|
<Typography variant="body1" style={{ color: theme.palette.text.primary, }}>
|
|
Timeout
|
|
</Typography>
|
|
<Typography variant="body2" style={{ color: theme.palette.text.secondary, marginBottom: 20 }}>
|
|
You can set a timeout for the category. This will delete all keys in this category after the specified time. Timeout is in seconds and based on last <b>EDITED</b> time.
|
|
</Typography>
|
|
</div>
|
|
<TextField
|
|
style={{ backgroundColor: theme.palette.textFieldStyle.backgroundColor, marginLeft: 20, flex: 1, }}
|
|
InputProps={{
|
|
style: {
|
|
color: theme.palette.textFieldStyle.color,
|
|
fontSize: "1em",
|
|
},
|
|
}}
|
|
fullWidth
|
|
autoComplete="Timeout"
|
|
placeholder="86400"
|
|
id="Timeoutfield"
|
|
margin="normal"
|
|
variant="outlined"
|
|
label="Timeout (seconds)"
|
|
defaultValue={categoryConfig?.settings?.timeout || ""}
|
|
onBlur={(e) => {
|
|
// Check if it's a number or not
|
|
var timeoutValue = 0
|
|
if (isNaN(e.target.value) || e.target.value === "" || e.target.value === null) {
|
|
toast.info("Timeout must be a number. Setting to 0.")
|
|
} else {
|
|
timeoutValue = parseInt(e.target.value, 10)
|
|
if (timeoutValue < 60) {
|
|
toast.info("Timeout must be between 60 seconds or more. Setting to 0.")
|
|
}
|
|
|
|
if (timeoutValue === categoryConfig?.settings?.timeout) {
|
|
return
|
|
}
|
|
}
|
|
|
|
setCategorySettingsField("timeout", timeoutValue)
|
|
}}
|
|
/>
|
|
</div>
|
|
|
|
<div style={{display: "flex", marginTop: 25, }}>
|
|
<div style={{flex: 3, }}>
|
|
<Typography variant="body1" style={{ color: theme.palette.text.primary, }}>
|
|
{categoryConfig?.settings?.public === true ? "" : "NOT"} Public
|
|
</Typography>
|
|
<Typography variant="body2" style={{ color: theme.palette.text.secondary, marginBottom: 20 }}>
|
|
This will make the url for this category public. Metadata will be cleared, except for timestamps. Types: keys,ndjson,csv,values,json,meta
|
|
|
|
<div style={{marginTop: 20, }}>
|
|
<div><b>URL (when public):</b></div> {globalUrl}/api/v2/datastore/category/{selectedCategory}?top=10000&type=keys&org_id={orgId}
|
|
</div>
|
|
</Typography>
|
|
</div>
|
|
<div style={{flex: 1, marginLeft: 60, marginTop: 25, }}>
|
|
<Tooltip title={categoryConfig?.settings?.public === true ? "Disable public access for this category" : "Enable public access to data in this category"} style={{}} aria-label={""}>
|
|
<Checkbox
|
|
checked={categoryConfig?.settings?.public === true}
|
|
onChange={(e) => {
|
|
setCategorySettingsField("public", e.target.checked)
|
|
setUpdate(Math.random()) // Force update to re-render the component
|
|
}}
|
|
style={{marginTop: 10, }}
|
|
color="secondary"
|
|
/>
|
|
</Tooltip>
|
|
</div>
|
|
</div>
|
|
|
|
<div style={{display: "flex", marginTop: 25, }}>
|
|
<div style={{flex: 2, }}>
|
|
<Typography variant="body1" style={{ color: theme.palette.text.primary, }}>
|
|
Subscribing
|
|
</Typography>
|
|
<Typography variant="body2" style={{ color: theme.palette.text.secondary, marginBottom: 20 }}>
|
|
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>
|
|
</div>
|
|
:
|
|
<div>
|
|
<Typography variant="h6" style={{ color: theme.palette.text.primary, marginBottom: 20 }}>
|
|
<RocketIcon style={{ verticalAlign: "middle", marginRight: 10 }} />
|
|
Automation for category '{selectedCategory}'
|
|
</Typography>
|
|
|
|
<Typography variant="body2" style={{ color: theme.palette.text.secondary, marginBottom: 20, marginTop: 20, }}>
|
|
When
|
|
</Typography>
|
|
<Typography variant="body1" style={{ color: theme.palette.text.primary, marginBottom: 20 }}>
|
|
A key is edited
|
|
</Typography>
|
|
|
|
<Typography variant="body2" style={{ color: theme.palette.text.secondary, marginTop: 100, marginBottom: 20 }}>
|
|
Do
|
|
</Typography>
|
|
{categoryAutomations.map((automation, index) => {
|
|
|
|
return (
|
|
<AutomationOptions
|
|
key={index}
|
|
automation={automation}
|
|
index={index}
|
|
/>
|
|
)
|
|
})}
|
|
</div>
|
|
}
|
|
|
|
</DialogContent>
|
|
</Dialog>
|
|
: null}
|
|
</div>
|
|
|
|
{isSelectedDataStore? null :<Divider
|
|
style={{
|
|
marginTop: 20,
|
|
marginBottom: 20,
|
|
}}
|
|
/>}
|
|
|
|
<DataGrid
|
|
rows={listCache}
|
|
columns={columns}
|
|
checkboxSelection
|
|
disableRowSelectionOnClick
|
|
rowSelectionModel={selectedRows}
|
|
onSelectionModelChange={(newSelection) => {
|
|
setSelectedRows(newSelection);
|
|
}}
|
|
onRowSelectionModelChange={(newSelection) => {
|
|
setSelectedRows(newSelection);
|
|
}}
|
|
keepNonExistentRowsSelected={false}
|
|
getRowId={(row) => `${row?.key}_${row?.category}`}
|
|
|
|
autoHeight={true}
|
|
sx={{
|
|
marginTop: 1,
|
|
height: listCache.length*52+500,
|
|
width: "100%",
|
|
'.MuiTablePagination-selectLabel, .MuiTablePagination-select, .MuiTablePagination-selectIcon': {
|
|
display: 'none',
|
|
},
|
|
marginBottom: 20,
|
|
}}
|
|
|
|
loading={cachedLoaded === false}
|
|
pagination
|
|
paginationMode="server"
|
|
page={page}
|
|
rowCount={totalAmount}
|
|
onPageChange={(newPage, second) => {
|
|
listOrgCache(orgId, selectedCategory, 0, pageSize, newPage)
|
|
|
|
setPage(newPage)
|
|
}}
|
|
onPageSizeChange={(newSize) => {
|
|
setPageSize(newSize);
|
|
setPage(0)
|
|
setSelectedRows([])
|
|
|
|
setCursors({
|
|
0: "",
|
|
})
|
|
}}
|
|
|
|
|
|
filterMode="client"
|
|
onFilterModelChange={(model) => {
|
|
// Specific search for the key itself to find it fast across the index
|
|
if (model?.items?.length === 1) {
|
|
if (model?.items[0]?.operatorValue === "equals" && model?.items[0]?.columnField === "key") {
|
|
// Run backend search for a specific key
|
|
listOrgCache(orgId, selectedCategory, 0, pageSize, page, model?.items[0]?.value)
|
|
}
|
|
}
|
|
}}
|
|
|
|
getRowHeight={() => {
|
|
return "auto"
|
|
}}
|
|
|
|
hideFooterSelectedRowCount={true}
|
|
hideFooter={true}
|
|
/>
|
|
|
|
<div
|
|
style={{
|
|
position: 'fixed',
|
|
bottom: 10,
|
|
left: 250,
|
|
right: 0,
|
|
height: 52,
|
|
zIndex: 1000,
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
paddingLeft: 16,
|
|
}}
|
|
>
|
|
<div style={{
|
|
width: 1200,
|
|
margin: "auto",
|
|
padding: 10,
|
|
backgroundColor: theme.palette.platformColor,
|
|
borderRadius: 4,
|
|
border: "1px solid rgba(255, 255, 255, 0.5)",
|
|
|
|
display: "flex",
|
|
textAlign: "center",
|
|
}}>
|
|
<Typography variant="body2" style={{ color: theme.palette.text.primary, marginRight: 50, marginTop: 6, marginLeft: 350, }}>
|
|
{page * pageSize + 1} - {Math.min((page + 1) * pageSize, totalAmount)} of {totalAmount}
|
|
</Typography>
|
|
|
|
<Pagination
|
|
count={Number.parseInt(totalAmount/pageSize)}
|
|
page={page+1}
|
|
renderItem={(item) => {
|
|
var disabled = false
|
|
if (item?.type === "page") {
|
|
if (cursors[item.page-1] === undefined) {
|
|
disabled = true
|
|
}
|
|
}
|
|
if (item?.type === "previous") {
|
|
disabled = page === 0
|
|
}
|
|
|
|
if (cachedLoaded === false) {
|
|
disabled = true
|
|
}
|
|
|
|
return (
|
|
<PaginationItem
|
|
{...item}
|
|
style={{
|
|
marginLeft: 4,
|
|
marginRight: 4,
|
|
height: 35,
|
|
width: 35,
|
|
}}
|
|
|
|
disabled={disabled}
|
|
/>
|
|
)
|
|
|
|
}}
|
|
onChange={(e, value) => {
|
|
if (value < 1) {
|
|
return
|
|
}
|
|
|
|
const newPage = value-1
|
|
|
|
listOrgCache(orgId, selectedCategory, 0, pageSize, newPage)
|
|
|
|
setPage(newPage)
|
|
}}
|
|
/>
|
|
|
|
{selectedRows.length > 0 ?
|
|
<Button
|
|
style={{ marginLeft: 50, }}
|
|
onClick={() => {
|
|
setDeleteConfirmTarget({ bulk: true });
|
|
setDeleteConfirmOpen(true);
|
|
}}
|
|
variant={"outlined"}
|
|
color="secondary"
|
|
>
|
|
<DeleteOutlineIcon
|
|
style={{
|
|
color: red,
|
|
marginRight: 10,
|
|
}}
|
|
/>
|
|
Delete {selectedRows.length} Key{ selectedRows.length > 1 ? "s" : "" }
|
|
</Button>
|
|
: null}
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
<TextField
|
|
id="copy_element_shuffle"
|
|
value={to_be_copied}
|
|
style={{ display: "none" }}
|
|
/>
|
|
</div>
|
|
);
|
|
})
|
|
|
|
//export default CacheView;
|
|
export default memo(CacheView);
|