Sync + Added dashboard for stats API

This commit is contained in:
Frikky
2025-10-20 11:37:26 +02:00
parent 9a20dcd426
commit 1bff92172b
38 changed files with 3985 additions and 1432 deletions
+6 -5
View File
@@ -992,16 +992,16 @@ const AppAuthTab = memo((props) => {
<Typography variant='h5' style={{ marginBottom: 8, marginTop: 0, }}>App Authentication</Typography>
<div style={{display: 'flex', flexDirection: 'row', alignItems: 'center', }}>
<Typography variant='body2' color="textSecondary">
Control the authentication options for individual apps.
Control the authentication options for individual apps. These keys are write-only, and cannot be viewed after creation. If you want editable secrets (e.g. for use in code), use <a href="admin?tab=datastore&category=protected" style={{ color: theme.palette.linkColor }}>Protected Keys</a>.
</Typography>
&nbsp;
<a
target="_blank"
rel="noopener noreferrer"
href="/docs/organizations#app_authentication"
style={{ color: theme.palette.linkColor }}
style={{ minWidth: 200, marginleft: 25, color: theme.palette.linkColor }}
>
Learn more about App Authentication
Learn more
</a>
</div>
</div>
@@ -1787,7 +1787,7 @@ const Hits = ({
if (selectedAppData.authentication === undefined || selectedAppData.authentication === null) {
setAuthenticationType({
type: "",
type: "",
})
selectedAppData.authentication = {
@@ -1955,6 +1955,7 @@ const Hits = ({
if (data === undefined || data === null) {
return;
}
const filteredData = data.filter((appAuth) => appAuth?.app?.id === appid);
if (filteredData.length === 0) {
setAppAuthentication([]);
@@ -1965,7 +1966,7 @@ const Hits = ({
}
};
const HandleAppAuthentication = ()=>{
const HandleAppAuthentication = () => {
const url = `${globalUrl}/api/v1/apps/authentication`;
+63 -24
View File
@@ -21,8 +21,9 @@ import AutoFixHighIcon from '@mui/icons-material/AutoFixHigh'
import CreateIcon from '@mui/icons-material/Create'
import { toast } from 'react-toastify'
import YAML from "yaml";
import Dropzone from "./Dropzone.jsx";
const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud }) => {
const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud, startOpenApi = false, prefillOpenApiData = "" }) => {
const [openApiModal, setOpenApiModal] = useState(false)
const [generateAppModal, setGenerateAppModal] = useState(false)
const [openApi, setOpenApi] = useState("")
@@ -35,6 +36,16 @@ const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud }) => {
const navigate = useNavigate()
const upload = useRef()
useEffect(() => {
if (open && (startOpenApi || (prefillOpenApiData && prefillOpenApiData.length > 0))) {
if (prefillOpenApiData && prefillOpenApiData.length > 0) {
setOpenApiData(prefillOpenApiData)
setIsDropzone(true)
}
setOpenApiModal(true)
}
}, [open, startOpenApi, prefillOpenApiData])
// Style for the create options
const AppCreateButton = ({ text, func, icon }) => {
const [hover, setHover] = React.useState(false)
@@ -467,6 +478,7 @@ const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud }) => {
}
}}
>
<Dropzone onDrop={uploadFile} style={{ width: '100%' }}>
<DialogTitle sx={{
display: 'flex',
justifyContent: 'space-between',
@@ -504,12 +516,15 @@ const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud }) => {
<Typography sx={{ color: theme.palette.text.primary, fontSize: '16px' }}>
Paste in the URI for the OpenAPI or find out
</Typography>
<Link style={{
<Link
to="https://shuffler.io/docs/apps#getting-started"
style={{
color: '#ff8544',
textDecoration: 'none',
textDecoration: 'underline',
fontSize: '16px',
fontFamily: theme?.typography?.fontFamily
fontFamily: theme?.typography?.fontFamily,
textUnderlineOffset: "3px",
}}>
How to find URI for openAPI?
</Link>
@@ -568,31 +583,54 @@ const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud }) => {
Must point to a version 2 or 3 OpenAPI specification.
</Typography>
<Typography sx={{ mb: 2, color: 'rgba(255,255,255,0.7)', fontSize: '14px', fontFamily: theme?.typography?.fontFamily }}>
<Typography sx={{ mb: 1.5, mt: 2, color: 'rgba(255,255,255,0.7)', fontSize: '14px', fontFamily: theme?.typography?.fontFamily }}>
Or upload a YAML or JSON specification
</Typography>
<Button
variant="outlined"
<div style={{
width: '100%',
boxSizing: 'border-box',
border: '1px dashed rgba(255,255,255,0.35)',
borderRadius: 4,
padding: 20,
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
backgroundColor: '#1a1a1a',
marginTop: '12px',
cursor: 'pointer'
}}
onClick={() => upload.current.click()}
sx={{
color: '#FF8544',
borderColor: '#FF8544',
px: 5,
py: 1,
'&:hover': {
borderColor: '#FF8544',
color: '#FF8544',
bgcolor: 'rgba(255,133,68,0.1)'
},
textTransform: 'none',
fontSize: '14px',
fontFamily: theme?.typography?.fontFamily,
height: '40px'
}}
>
Upload
</Button>
<div style={{ display: 'flex', flexDirection: 'column' }}>
<Typography sx={{ color: 'rgba(255,255,255,0.9)', fontSize: '15px', fontFamily: theme?.typography?.fontFamily }}>
Drag & drop your OpenAPI (YAML/JSON) anywhere
</Typography>
<Typography sx={{ color: 'rgba(255,255,255,0.6)', fontSize: '13px', mt: 0.5, fontFamily: theme?.typography?.fontFamily }}>
or click to browse files
</Typography>
</div>
<Button
variant="outlined"
onClick={(e) => { e.stopPropagation(); upload.current.click(); }}
sx={{
color: '#FF8544',
borderColor: '#FF8544',
px: 3,
py: 0.75,
'&:hover': {
borderColor: '#FF8544',
color: '#FF8544',
bgcolor: 'rgba(255,133,68,0.1)'
},
textTransform: 'none',
fontSize: '14px',
fontFamily: theme?.typography?.fontFamily,
height: '36px'
}}
>
Upload
</Button>
</div>
<input
hidden
@@ -638,6 +676,7 @@ const AppCreationModal = ({ open, onClose, theme, globalUrl, isCloud }) => {
Continue
</Button>
</DialogActions>
</Dropzone>
</Dialog>
{/* Generate App Modal */}
+6 -2
View File
@@ -33,6 +33,7 @@ import {
ClearRefinements,
connectStateResults
} from "react-instantsearch-dom";
import { useDebouncedCallback } from "../utils/useDebouncedCallback";
import aa from "search-insights";
import { useLocation } from 'react-router-dom';
@@ -160,6 +161,8 @@ const AppGrid = (props) => {
refine(searchQuery.trim());
};
const debouncedRefine = useDebouncedCallback((value) => refine(value), 300);
return (
<form noValidate action="" role="search">
<TextField
@@ -229,9 +232,10 @@ const AppGrid = (props) => {
placeholder="Search more than 2500 Apps"
id="shuffle_search_field"
onChange={(event) => {
setSearchQuery(event.currentTarget.value);
const value = event.currentTarget.value;
setSearchQuery(value);
removeQuery("q");
refine(event.currentTarget.value);
debouncedRefine(value);
}}
onKeyDown={(event) => {
if(event.key === "Enter") {
+197 -145
View File
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useContext, memo, useMemo } from 'react';
import React, { useState, useEffect, useContext, useCallback } from 'react';
import {getTheme} from '../theme.jsx';
import classNames from "classnames";
@@ -73,6 +73,7 @@ const AppStats = (defaultprops) => {
const [resultRows, setResultRows] = useState([])
const [resultLoading, setResultLoading] = useState(true)
const { themeMode, brandColor } = useContext(Context);
const [onpremAppRuns, setOnpremAppRuns] = useState(0)
const theme = getTheme(themeMode, brandColor)
const includedExecutions = selectedOrganization?.sync_features?.app_executions !== undefined ? selectedOrganization?.sync_features?.app_executions?.limit : 0
@@ -83,12 +84,156 @@ const AppStats = (defaultprops) => {
}
}, [])
const handleDataSetting = useCallback((inputdata, grouping) => {
if (inputdata === undefined || inputdata === null) {
return
}
const statKey = syncStats === true ? "onprem_stats" : "daily_statistics"
const dailyStats = inputdata[statKey]
if (dailyStats === undefined || dailyStats === null) {
return
}
var appRuns = {
"key": "App Runs",
"data": []
}
var childorgappRuns = {
"key": "Child Org App Runs",
"data": []
}
var workflowRuns = {
"key": "Workflow Runs (includes subflows)",
"data": []
}
var subflowRuns = {
"key": "Subflow Runs",
"data": []
}
var appcostRuns = {
"key": "Cost of App Runs",
"data": []
}
for (let key in dailyStats) {
// Always skips first one as it has accumulated data in it
if (key === 0) {
continue
}
const item = dailyStats[key]
if (item["date"] === undefined) {
console.log("No date: ", item)
continue
}
// Check if app_executions key in item
if (item["app_executions"] !== undefined && item["app_executions"] !== null) {
appRuns["data"].push({
key: new Date(item["date"]).toISOString(),
data: item["app_executions"]
})
// Add number
appcostRuns["data"].push({
key: new Date(item["date"]).toISOString(),
data: (item["app_executions"] * invocationCost).toFixed(2)
})
}
if (item["child_app_executions"] !== undefined && item["child_app_executions"] !== null) {
childorgappRuns["data"].push({
key: new Date(item["date"]).toISOString(),
data: item["child_app_executions"]
})
}
// Check if workflow_executions key in item
if (item["workflow_executions"] !== undefined && item["workflow_executions"] !== null) {
workflowRuns["data"].push({
key: new Date(item["date"]).toISOString(),
data: item["workflow_executions"]
})
}
if (item["subflow_executions"] !== undefined && item["subflow_executions"] !== null) {
subflowRuns["data"].push({
key: new Date(item["date"]).toISOString(),
data: item["subflow_executions"]
})
}
}
// Only add today's data if endTime is not set or if today falls within the selected date range
const today = new Date()
const todayStartOfDay = new Date(today)
todayStartOfDay.setHours(0, 0, 0, 0)
const shouldAddTodayData = endTime === "" || endTime === undefined || endTime === null ||
(new Date(endTime) >= todayStartOfDay)
if (!syncStats && shouldAddTodayData) {
// Adds data for today
if (inputdata["daily_app_executions"] !== undefined && inputdata["daily_app_executions"] !== null) {
appRuns["data"].push({
key: new Date().toISOString(),
data: inputdata["daily_app_executions"]
})
appcostRuns["data"].push({
key: new Date().toISOString(),
data: (inputdata["daily_app_executions"] * invocationCost).toFixed(2)
})
}
if (inputdata["daily_child_app_executions"] !== undefined && inputdata["daily_app_executions"] !== null) {
childorgappRuns["data"].push({
key: new Date().toISOString(),
data: inputdata["daily_child_app_executions"]
})
}
if (inputdata["daily_workflow_executions"] !== undefined && inputdata["daily_workflow_executions"] !== null) {
workflowRuns["data"].push({
key: new Date().toISOString(),
data: inputdata["daily_workflow_executions"]
})
}
if (inputdata["daily_subflow_executions"] !== undefined && inputdata["daily_subflow_executions"] !== null) {
subflowRuns["data"].push({
key: new Date().toISOString(),
data: inputdata["daily_subflow_executions"]
})
}
}
// Only for parent orgs
if (childorgappRuns["data"].length > 0) {
setChildOrgsAppRuns(childorgappRuns)
}
setSubflowRuns(subflowRuns)
setWorkflowRuns(workflowRuns)
setAppruns(appRuns)
setApprunCosts(appcostRuns)
}, [syncStats, endTime, startTime])
useEffect(() => {
if (statistics && statistics?.org_id?.length > 0) {
handleDataSetting(statistics, "day")
}
}, [statistics])
useEffect(() => {
setStartTime("")
setEndTime("")
}, [currentTab])
const getWorkflowStats = async (workflow, startTime, endTime) => {
if (workflow.id === undefined || workflow.id === null || workflow.id === "") {
@@ -227,7 +372,7 @@ const AppStats = (defaultprops) => {
}
const statKey = syncStats === true ? "onprem_stats" : "daily_statistics"
if (statistics[statKey] === undefined || statistics[statKey] === null) {
if (!syncStats && (statistics[statKey] === undefined || statistics[statKey] === null)) {
setFilteredStatistics(statistics)
setMonthlyAppRunsParent(statistics["monthly_app_executions"] ?? 0)
return
@@ -356,17 +501,55 @@ const AppStats = (defaultprops) => {
workflowexecutions += item["workflow_executions"]
appexecutions += item["app_executions"]
if (currentTab === 0) {
if (currentTab === 0 || currentTab === 3) {
appexecutions += (item["child_app_executions"] ?? 0)
}
estimatedcost += (item["app_executions"] * invocationCost)
}
const today = new Date();
const isCurrentMonthSelected =
(startTime === "" && endTime === "") ||
(
new Date(foundstarttime).getMonth() === today.getMonth() &&
new Date(foundstarttime).getFullYear() === today.getFullYear() &&
new Date(foundendtime).getMonth() === today.getMonth() &&
new Date(foundendtime).getFullYear() === today.getFullYear()
);
if (!syncStats && isCurrentMonthSelected) {
if (statistics["daily_app_executions"] !== undefined && statistics["daily_app_executions"] !== null) {
appexecutions += statistics["daily_app_executions"] + (statistics["daily_child_app_executions"] ?? 0)
}
}
tmpstats["monthly_workflow_executions"] = workflowexecutions
tmpstats["monthly_app_executions"] = appexecutions
if (syncStats) {
setOnpremAppRuns(appexecutions)
}
} else {
const today = new Date();
const isCurrentMonthSelected =
(startTime === "" && endTime === "") ||
(
new Date(foundstarttime).getMonth() === today.getMonth() &&
new Date(foundstarttime).getFullYear() === today.getFullYear() &&
new Date(foundendtime).getMonth() === today.getMonth() &&
new Date(foundendtime).getFullYear() === today.getFullYear()
);
if (!syncStats && isCurrentMonthSelected) {
if (statistics["daily_app_executions"] !== undefined && statistics["daily_app_executions"] !== null) {
appexecutions += statistics["daily_app_executions"] + (statistics["daily_child_app_executions"] ?? 0)
}
}
tmpstats["monthly_app_executions"] = appexecutions
}
// Make estimatedcost have max 2 decimals
if (isCloud) {
// Exclude includedExecutions*month
@@ -380,11 +563,11 @@ const AppStats = (defaultprops) => {
handleDataSetting(tmpstats, "day")
// if we have done monthly reset than only show monthly app runs as current month app run
const currentMonth = new Date().getMonth() + 1
if (!monthlyAppRunsParent && statistics["monthly_app_executions"] > 0 && currentMonth === statistics["last_monthly_reset_month"]) {
if (!syncStats && !monthlyAppRunsParent && statistics["monthly_app_executions"] > 0 && currentMonth === statistics["last_monthly_reset_month"]) {
setMonthlyAppRunsParent(statistics["monthly_app_executions"])
}
if (!monthlyAllSuborgExecutions && statistics["monthly_child_app_executions"]> 0 && currentMonth === statistics["last_monthly_reset_month"]) {
if (!syncStats && !monthlyAllSuborgExecutions && statistics["monthly_child_app_executions"]> 0 && currentMonth === statistics["last_monthly_reset_month"]) {
setMonthlyAllSuborgExecutions(statistics["monthly_child_app_executions"])
}
@@ -397,7 +580,7 @@ const AppStats = (defaultprops) => {
loadWorkflowStats(foundWorkflows, startTime, endTime)
}
}, [statistics, startTime, endTime])
}, [statistics, startTime, endTime, syncStats, currentTab, handleDataSetting])
const handleStartTimeChange = (date) => {
setStartTime(date)
@@ -407,142 +590,7 @@ const AppStats = (defaultprops) => {
setEndTime(date)
}
const handleDataSetting = (inputdata, grouping) => {
if (inputdata === undefined || inputdata === null) {
return
}
const statKey = syncStats === true ? "onprem_stats" : "daily_statistics"
const dailyStats = inputdata[statKey]
if (dailyStats === undefined || dailyStats === null) {
return
}
var appRuns = {
"key": "App Runs",
"data": []
}
var childorgappRuns = {
"key": "Child Org App Runs",
"data": []
}
var workflowRuns = {
"key": "Workflow Runs (includes subflows)",
"data": []
}
var subflowRuns = {
"key": "Subflow Runs",
"data": []
}
var appcostRuns = {
"key": "Cost of App Runs",
"data": []
}
for (let key in dailyStats) {
// Always skips first one as it has accumulated data in it
if (key === 0) {
continue
}
const item = dailyStats[key]
if (item["date"] === undefined) {
console.log("No date: ", item)
continue
}
// Check if app_executions key in item
if (item["app_executions"] !== undefined && item["app_executions"] !== null) {
appRuns["data"].push({
key: new Date(item["date"]),
data: item["app_executions"]
})
// Add number
appcostRuns["data"].push({
key: new Date(item["date"]),
data: (item["app_executions"] * invocationCost).toFixed(2)
})
}
if (item["child_app_executions"] !== undefined && item["child_app_executions"] !== null) {
childorgappRuns["data"].push({
key: new Date(item["date"]),
data: item["child_app_executions"]
})
}
// Check if workflow_executions key in item
if (item["workflow_executions"] !== undefined && item["workflow_executions"] !== null) {
workflowRuns["data"].push({
key: new Date(item["date"]),
data: item["workflow_executions"]
})
}
if (item["subflow_executions"] !== undefined && item["subflow_executions"] !== null) {
subflowRuns["data"].push({
key: new Date(item["date"]),
data: item["subflow_executions"]
})
}
}
// Only add today's data if endTime is not set or if today falls within the selected date range
const today = new Date()
const shouldAddTodayData = endTime === "" || endTime === undefined || endTime === null ||
(new Date(endTime) >= today.setHours(0, 0, 0, 0))
if (shouldAddTodayData) {
// Adds data for today
if (inputdata["daily_app_executions"] !== undefined && inputdata["daily_app_executions"] !== null) {
appRuns["data"].push({
key: new Date(),
data: inputdata["daily_app_executions"]
})
appcostRuns["data"].push({
key: new Date(),
data: (inputdata["daily_app_executions"] * invocationCost).toFixed(2)
})
}
if (inputdata["daily_child_app_executions"] !== undefined && inputdata["daily_app_executions"] !== null) {
childorgappRuns["data"].push({
key: new Date(),
data: inputdata["daily_child_app_executions"]
})
}
if (inputdata["daily_workflow_executions"] !== undefined && inputdata["daily_workflow_executions"] !== null) {
workflowRuns["data"].push({
key: new Date(),
data: inputdata["daily_workflow_executions"]
})
}
if (inputdata["daily_subflow_executions"] !== undefined && inputdata["daily_subflow_executions"] !== null) {
subflowRuns["data"].push({
key: new Date(),
data: inputdata["daily_subflow_executions"]
})
}
}
// Only for parent orgs
if (childorgappRuns["data"].length > 0) {
setChildOrgsAppRuns(childorgappRuns)
}
setSubflowRuns(subflowRuns)
setWorkflowRuns(workflowRuns)
setAppruns(appRuns)
setApprunCosts(appcostRuns)
}
console.log("sync stats: ", syncStats, statistics)
const paperStyle = {
textAlign: "center",
@@ -708,22 +756,26 @@ const AppStats = (defaultprops) => {
</Tooltip>
} */}
{syncStats === true ? null :
{/* {syncStats === true ? null : */}
<Tooltip title={
<Typography variant="body1" style={{padding: 10, }}>
App runs in the selected period
</Typography>
}>
<Box sx={paperStyle}>
{syncStats === true ?
<Typography variant="h4">
{onpremAppRuns}
</Typography>:
<Typography variant="h4">
{filteredStatistics.monthly_app_executions === null || filteredStatistics.monthly_app_executions === undefined ? 0 : filteredStatistics.monthly_app_executions}
</Typography>
</Typography>}
<Typography variant="h6">
App Runs
</Typography>
</Box>
</Tooltip>
}
{/* } */}
{syncStats === true || currentTab === 0 ? null :
<Tooltip title={
+45 -11
View File
@@ -147,6 +147,20 @@ const CacheView = memo((props) => {
const [showSettingsMenu, setShowSettingsMenu] = useState(false);
const [showCollectIngestMenu, setShowCollectIngestMenu] = useState(false);
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 = [
{
@@ -299,7 +313,16 @@ const CacheView = memo((props) => {
useEffect(() => {
getWorkflows()
getApps()
listOrgCache(orgId, selectedCategory, 0, pageSize, page)
var chosenCategory = selectedCategory
const urlParams = new URLSearchParams(window.location.search)
const categoryParam = urlParams.get("category")
if (categoryParam && categoryParam !== undefined && categoryParam !== "default" && categoryParam !== "") {
chosenCategory = categoryParam
setSelectedCategory(categoryParam)
}
listOrgCache(orgId, chosenCategory, 0, pageSize, page)
}, [])
@@ -423,7 +446,6 @@ const CacheView = memo((props) => {
setDatastoreCategories(newcategories)
}
if (responseJson?.category_config !== undefined && responseJson?.category_config !== null) {
if (responseJson?.category_config?.id !== undefined && responseJson?.category_config?.id !== null && responseJson?.category_config?.id !== "") {
@@ -458,7 +480,12 @@ const CacheView = memo((props) => {
}
}
} else {
toast.warn("Failed to load keys. Please try again or contact support@shuffler if this persists.")
//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) => {
@@ -513,8 +540,8 @@ const CacheView = memo((props) => {
category: selectedCategory,
}
if (dataValue?.category !== "" && dataValue?.category !== "default") {
entry.category = dataValue.category.replaceAll(" ", "_");
if (dataValue?.category !== undefined && dataValue?.category !== "" && dataValue?.category !== "default") {
entry.category = dataValue?.category?.replaceAll(" ", "_");
}
@@ -1513,7 +1540,7 @@ const CacheView = memo((props) => {
name={null}
/>
:
<Typography variant="body2" style={{maxHeight: 200, overflow: "hidden", }}>
<Typography variant="body2" style={{maxWidth: 500, maxHeight: 200, overflow: "auto", }}>
{data.value}
</Typography>
}
@@ -1712,7 +1739,7 @@ const CacheView = memo((props) => {
<span>
<IconButton
style={{ padding: "6px" }}
disabled={data.public_authorization === undefined || data.public_authorization === null || data.public_authorization === "" || data.org_id !== selectedOrganization.id ? true : false}
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()
@@ -1911,7 +1938,7 @@ const CacheView = memo((props) => {
{selectedCategory === "protected" ?
<div style={{ color: red, }}>
Protected keys are encrypted, only available to admins, and will be masked when used in workflows. This is a basic protection, and is NOT bulletproof.
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}
@@ -2051,7 +2078,7 @@ const CacheView = memo((props) => {
</Button>
</Tooltip>
:
<Tooltip title={"Add new file category"} style={{}} aria-label={""}>
<Tooltip title={"Add or find category"} style={{}} aria-label={""}>
<Button
style={{
whiteSpace: "nowrap",
@@ -2077,8 +2104,14 @@ const CacheView = memo((props) => {
{renderTextBox && <TextField
onKeyPress={(event)=>{
handleKeyDown(event);
if(event.key === 'Enter' && selectedFileId.length > 0){
// 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)
}
}}
@@ -2096,6 +2129,7 @@ const CacheView = memo((props) => {
paddingTop: 0,
},
}}
id=""
color="primary"
placeholder="Category name"
required
@@ -2416,7 +2450,7 @@ const CacheView = memo((props) => {
</Typography>
<Pagination
count={Number.parseInt(totalAmount/pageSize*100/2)}
count={Number.parseInt(totalAmount/pageSize)}
page={page+1}
renderItem={(item) => {
var disabled = false
+90 -40
View File
@@ -18,14 +18,21 @@ import {
Tooltip,
Autocomplete,
TextField,
Box,
} from '@mui/material';
import {
Rocket as RocketIcon,
FilterAlt as FilterAltIcon,
Add as AddIcon,
Check as CheckIcon,
} from '@mui/icons-material';
import {
green,
red,
} from '../views/AngularWorkflow.jsx'
import algoliasearch from 'algoliasearch/lite';
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
@@ -97,6 +104,7 @@ const CollectIngestModal = (props) => {
const [showAppsearch, setShowAppsearch] = useState(false);
const [algoliaOptions, setAlgoliaOptions] = useState([]);
const [generating, setGenerating] = useState(false);
const appname = type
const ingestedAmount = 20
@@ -107,7 +115,7 @@ const CollectIngestModal = (props) => {
})
var foundMatchingWorkflow = null
if (workflows !== undefined && workflows !== null && workflows.length > 0) {
if (!showAppsearch && workflows !== undefined && workflows !== null && workflows.length > 0) {
const parsedName = type.toLowerCase().replaceAll(" ", "_");
const foundWorkflow = workflows.find((workflow) => {
return workflow?.name?.toLowerCase().replaceAll(" ", "_") === parsedName
@@ -169,12 +177,36 @@ const CollectIngestModal = (props) => {
}
}
const runIngestion = () => {
setGenerating(true)
setTimeout(() => {
setGenerating(false)
}, 5000)
toast.info("Starting ingest for relevant apps")
var newapps = ""
for (var key in selectedApps) {
const app = selectedApps[key]
if (newapps.length > 0) {
newapps += ","
}
newapps += app.name
}
startIngestion(appname, newapps, appCategory, index)
if (webhook === true) {
startIngestion(appname+"_webhook", newapps, appCategory, index)
}
}
return (
//<Grid item xs={hovering ? 12 : 5.9}
<Grid item xs={12}
style={{
minHeight: hovering ? 200 : 200,
maxHeight: hovering ? "auto" : 140,
cursor: "pointer",
position: "relative",
transition: "all 0.3s ease-in-out",
@@ -200,7 +232,7 @@ const CollectIngestModal = (props) => {
<div style={{flex: 1, margin: "auto", marginTop: 50, }}>
<div style={{width: 50+selectedApps?.length*50, margin: "auto", itemAlign: "center", textAlign: "center", display: "flex", }}>
{selectedApps.map((app, index) => {
{generating ? null : selectedApps.map((app, index) => {
// Show image of each one
return (
<div key={index} style={{display: "flex", alignItems: "center", marginLeft: 10, }}>
@@ -214,18 +246,29 @@ const CollectIngestModal = (props) => {
)
})}
<Tooltip title="Select Apps" placement="top">
<IconButton
style={{marginLeft: 10, marginRight: 50, }}
variant="outlined"
color="secondary"
onClick={() => {
setShowAppsearch(!showAppsearch)
}}
>
<AddIcon style={{color: theme.palette.primary.main, }} />
</IconButton>
</Tooltip>
{!generating && appCategory !== undefined && appCategory !== null && appCategory.length > 0 ?
<Tooltip title={showAppsearch ? "Done selecting apps" : "Select Apps"} placement="top">
<IconButton
style={{marginLeft: 10, marginRight: 50, }}
variant="outlined"
color="secondary"
onClick={() => {
if (showAppsearch === true) {
runIngestion()
}
setShowAppsearch(!showAppsearch)
}}
>
{showAppsearch ?
<CheckIcon style={{color: green, }} />
:
<AddIcon style={{color: theme.palette.primary.main, }} />
}
</IconButton>
</Tooltip>
: null}
</div>
{showAppsearch ?
@@ -237,20 +280,42 @@ const CollectIngestModal = (props) => {
value={selectedApps}
onChange={(event, value) => {
console.log("New value: ", value)
setSelectedApps(value)
}}
getOptionLabel={(option) => {
const parsedname = option.name.replaceAll("_", " ")
return (
<div>
<img src={option?.large_image} alt={option.name} style={{ width: 24, height: 24, marginRight: 10, borderRadius: 5, }} />
<Typography variant="body1" style={{ display: "inline-block", verticalAlign: "middle", marginTop: -12, }}>
{parsedname}
</Typography>
</div>
)
return parsedname
//return (
// <div>
// <img src={option?.large_image} alt={option.name} style={{ width: 24, height: 24, marginRight: 10, borderRadius: 5, }} />
// <Typography variant="body1" style={{ display: "inline-block", verticalAlign: "middle", marginTop: -12, }}>
// {parsedname}
// </Typography>
// </div>
//)
}}
renderOption={(props, option, state, ownerState) => {
const { key, ...optionProps } = props;
return (
<Box
key={key}
sx={{
borderRadius: '8px',
margin: '5px',
padding: '8px',
}}
component="li"
{...optionProps}
>
<img src={option?.large_image} alt={option.name} style={{ width: 24, height: 24, marginRight: 10, borderRadius: 5, }} />
{ownerState.getOptionLabel(option)}
</Box>
);
}}
renderInput={(params) => {
return (
@@ -267,24 +332,9 @@ const CollectIngestModal = (props) => {
style={{width: 250, margin: 25, }}
variant={foundMatchingWorkflow !== null ? "outlined" : "contained"}
onClick={() => {
toast.info("Starting ingest for relevant apps")
var newapps = ""
for (var key in selectedApps) {
const app = selectedApps[key]
if (newapps.length > 0) {
newapps += ","
}
newapps += app.name
}
startIngestion(appname, newapps, appCategory, index)
if (webhook === true) {
startIngestion(appname+"_webhook", newapps, appCategory, index)
}
runIngestion()
}}
disabled={generating}
>
{foundMatchingWorkflow !== null ?
"Re-Create Ingestion"
+10 -7
View File
@@ -36,6 +36,7 @@ import {
Avatar,
AvatarGroup,
} from "@mui/material"
import { useDebouncedCallback } from "../utils/useDebouncedCallback";
const searchClient = algoliasearch("JNSS5CFDZZ", "c8f882473ff42d41158430be09ec2b4e")
const CreatorGrid = props => {
@@ -109,6 +110,8 @@ const CreatorGrid = props => {
}
}
const debouncedRefine = useDebouncedCallback((value) => refine(value), 300)
return (
<form noValidate action="" role="search">
<TextField
@@ -134,7 +137,7 @@ const CreatorGrid = props => {
id="shuffle_search_field"
onChange={(event) => {
removeQuery("q")
refine(event.currentTarget.value)
debouncedRefine(event.currentTarget.value)
}}
onKeyDown={(event) => {
if(event.key === "Enter") {
@@ -190,10 +193,10 @@ const CreatorGrid = props => {
null
}
</span>
</div>
<Typography variant="body1" color="textSecondary" style={{marginTop: 10, }}>
<b>{data.apps === undefined || data.apps === null ? 0 : data.apps}</b> apps <span style={{marginLeft: 15, }}/><b>{data.workflows === null || data.workflows === undefined ? 0 : data.workflows}</b> workflows
</Typography>
</div>
<Typography variant="body1" color="textSecondary" style={{marginTop: 10, }}>
<b>{data.apps === undefined || data.apps === null ? 0 : data.apps}</b> apps <span style={{marginLeft: 15, }}/><b>{data.workflows === null || data.workflows === undefined ? 0 : data.workflows}</b> workflows
</Typography>
{data.specialized_apps !== undefined && data.specialized_apps !== null && data.specialized_apps.length > 0 ?
<AvatarGroup max={10} style={{flexDirection: "row", padding: 0, margin: 0, itemAlign: "left", textAlign: "left", marginTop: 3,}}>
{data.specialized_apps.map((app, index) => {
@@ -267,7 +270,7 @@ const CreatorGrid = props => {
autoComplete="email"
margin="normal"
variant="outlined"
onChange={e => setFormMail(e.target.value)}
onChange={e => setFormMail(e.target.value)}
/>
<TextField
required
@@ -285,7 +288,7 @@ const CreatorGrid = props => {
margin="normal"
variant="outlined"
autoComplete="off"
onChange={e => setMessage(e.target.value)}
onChange={e => setMessage(e.target.value)}
/>
</div>
<Button
@@ -0,0 +1,446 @@
import React from "react";
import {
Box,
Button,
Typography,
Stack,
styled,
} from "@mui/material";
import theme from "../theme.jsx";
// Simple icon placeholders; replace with proper assets if desired
const StepIcon = styled("div")(({ completed }) => ({
width: 28,
height: 28,
borderRadius: "50%",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontWeight: 700,
fontSize: 14,
color: completed ? "#0C111D" : "#ffffff",
background: completed ? "#43D17C" : "#2F2F2F",
border: completed ? "1px solid #43D17C" : "1px solid rgba(255,255,255,0.15)",
flexShrink: 0,
}));
// Single continuous rail will be drawn once for the entire steps list
const StepCard = styled(Box)(({ completed, flash }) => ({
display: "flex",
gap: "14px",
minHeight: 60,
backgroundColor: "#212121",
border: `1px solid ${flash ? "#f87171" : completed ? "#43D17C" : "rgba(255, 255, 255, 0.1)"}`,
borderRadius: "12px",
padding: "16px",
width: "100%",
flex: 1,
minWidth: 0,
}));
const PrimaryButton = styled(Button)({
background: "linear-gradient(90deg, #FF8544 0%, #FB47A0 100%)",
color: "#fff",
borderRadius: 6,
textTransform: "none",
fontWeight: 600,
fontSize: 14,
padding: "8px 20px",
"&:hover": {
opacity: 0.95,
background: "linear-gradient(90deg, #FF8544 0%, #FB47A0 100%)",
},
});
const SecondaryButton = styled(Button)({
color: "#FF8544",
borderRadius: 6,
textTransform: "none",
fontWeight: 600,
fontSize: 14,
padding: "8px 12px",
"&:hover": {
backgroundColor: "rgba(255, 255, 255, 0.08)",
},
"&.Mui-disabled": {
color: "rgba(255, 255, 255, 0.5)",
backgroundColor: "rgba(255, 255, 255, 0.06)",
},
});
const StepItem = React.forwardRef(({ step, iconRef }, ref) => (
<Box ref={ref} sx={{ display: "flex", gap: 1.2, alignItems: "flex-start" }}>
<Box ref={iconRef} sx={{ display: "flex", alignItems: "center", justifyContent: "center", width: 40, height: 28, zIndex: 1, mt: 1.5 }}>
<StepIcon completed={step.completed}>{step.index}</StepIcon>
</Box>
<StepCard completed={step.completed} flash={Array.isArray(step.flashKeys) && step.flashKeys.includes(step.key)}>
<Box sx={{ display: "flex", flexDirection: "row", alignItems: "flex-start", justifyContent: "space-between", gap: 1, width: "100%" }}>
<Stack direction="column" spacing={1} sx={{ width: "70%" }}>
<Typography
sx={{
color: "#ffffff",
fontWeight: 600,
fontSize: 16,
fontFamily: theme.typography.fontFamily,
}}
>
{step.title}
</Typography>
{step.description && (
<Typography
sx={{
color: "#c5c5c5",
fontSize: 12,
fontFamily: theme.typography.fontFamily,
}}
>
{step.description}
</Typography>
)}
</Stack>
<Stack direction="row" spacing={1}>
{step.secondaryCta && (
<SecondaryButton onClick={step.secondaryCta.onClick}>
{step.secondaryCta.label}
</SecondaryButton>
)}
{step.primaryCta && (
<PrimaryButton onClick={step.primaryCta.onClick}>
{step.primaryCta.label}
</PrimaryButton>
)}
</Stack>
</Box>
</StepCard>
</Box>
));
const DashboardOnboarding = ({
open,
onClose,
headerTitle = "Get started with your Dashboard",
headerSubtitle = "Follow these steps to unlock insights.",
footer,
globalUrl,
onExplore,
}) => {
// Internal completion state only; handlers are defined separately
const [completed, setCompleted] = React.useState({
docs: false,
apps: false,
workflow: false,
wait: false,
invite: false,
});
const [checkingApps, setCheckingApps] = React.useState(false);
const [checkingWait, setCheckingWait] = React.useState(false);
const [flashKeys, setFlashKeys] = React.useState([]);
const [waitProgress, setWaitProgress] = React.useState(0);
// Load persisted completion state
React.useEffect(() => {
try {
const raw = localStorage.getItem("dashboard_onboarding_completed");
if (!raw) return;
const data = JSON.parse(raw);
if (data && typeof data === "object") {
setCompleted((prev) => ({ ...prev, ...data }));
}
} catch {}
}, []);
// Persist completion state
React.useEffect(() => {
try {
localStorage.setItem("dashboard_onboarding_completed", JSON.stringify(completed));
} catch {}
}, [completed]);
// Handlers
const handleDocsClick = React.useCallback(() => {
window.open('/docs', '_blank');
setCompleted((prev) => ({ ...prev, docs: true }));
}, []);
const handleDiscoverApps = React.useCallback(() => {
window.open('/apps?tab=discover_apps', '_blank');
}, []);
const handleCheckAppsStatus = React.useCallback(async () => {
if (checkingApps) return;
setCheckingApps(true);
try {
const resp = await fetch(`${globalUrl}/api/v1/apps`, {
method: 'GET',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
credentials: 'include',
});
if (resp.status !== 200) return;
const data = await resp.json();
let count = 0;
if (Array.isArray(data)) {
count = data.length;
} else if (data && typeof data === 'object') {
count = Object.keys(data).length;
}
if (count >= 3) {
setCompleted((prev) => ({ ...prev, apps: true }));
}
} catch (_) {
// ignore
} finally {
setCheckingApps(false);
}
}, [checkingApps]);
const handleOpenWorkflow = React.useCallback(() => {
window.open('/workflows/b658f2a0-7316-40d9-97ed-350a54fe3adc', '_blank');
setCompleted((prev) => ({ ...prev, workflow: true }));
}, []);
const handleWaitCheck = React.useCallback(async () => {
if (checkingWait) return;
setCheckingWait(true);
try {
const days = 5;
const url = `${globalUrl}/api/v1/stats/workflow_executions_finished?days=${days}`;
const resp = await fetch(url, {
method: 'GET',
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
credentials: 'include',
});
if (resp.status !== 200) return;
const data = await resp.json();
const entries = Array.isArray(data?.entries) ? data.entries : [];
const now = new Date();
let successDays = 0;
for (let i = 0; i < entries.length; i++) {
const e = entries[i];
const d = new Date(e?.Date || e?.date || e?.time || e?.timestamp);
const diffDays = Math.floor((now - d) / (24 * 60 * 60 * 1000));
const value = Number(e?.Value ?? e?.value ?? 0);
if (!isNaN(diffDays) && diffDays <= 4 && value > 0) {
successDays += 1;
}
}
const simulated = Math.min(5, successDays);
if (simulated < 5) {
setWaitProgress(simulated);
setFlashKeys(["wait"]);
setTimeout(() => setFlashKeys([]), 800);
setCheckingWait(false);
} else {
setCompleted((prev) => ({ ...prev, wait: true }));
setCheckingWait(false);
}
} catch (_) {
// ignore
} finally {
setCheckingWait(false);
}
setCheckingWait(false);
}, [checkingWait, globalUrl]);
const handleOpenUsers = React.useCallback(() => {
window.open('/admin?tab=users', '_blank');
setCompleted((prev) => ({ ...prev, invite: true }));
}, []);
const steps = [
{
index: 1,
key: 'docs',
title: 'Read our docs to understand Shuffle',
description: 'Explore the basics of Shuffle in our documentation. It will help you understand the platform and how to use it.',
primaryCta: { label: 'Read docs', onClick: handleDocsClick },
completed: completed.docs,
},
{
index: 2,
key: 'apps',
title: 'Activate at least 3 apps',
description: 'Go to Discover Apps and enable your first three integrations.',
primaryCta: { label: 'Discover apps', onClick: handleDiscoverApps },
secondaryCta: { label: checkingApps ? 'Checking…' : 'Check status', onClick: handleCheckAppsStatus, disabled: checkingApps },
completed: completed.apps,
},
{
index: 3,
key: 'workflow',
title: 'Save a public workflow and start its scheduler',
description: 'Open the public workflow, save it to your org, and start a daily scheduler.',
primaryCta: { label: 'Open public workflow', onClick: handleOpenWorkflow },
completed: completed.workflow,
},
{
index: 4,
key: 'wait',
title: `Wait for 5 days of runs${completed.wait ? '' : waitProgress > 0 ? ` (${waitProgress}/5)` : ''}`,
description: 'We will show daily stats after 5 runs. Come back to check again.',
primaryCta: { label: checkingWait ? 'Checking…' : 'Check status', onClick: handleWaitCheck, disabled: checkingWait },
completed: completed.wait,
},
{
index: 5,
key: 'invite',
title: 'Invite more team members (optional)',
description: 'Add teammates to collaborate in your org.',
primaryCta: { label: 'Open users page', onClick: handleOpenUsers },
completed: completed.invite,
},
];
const mandatoryKeys = ['docs', 'apps', 'workflow', 'wait'];
const handleFinalDone = React.useCallback(() => {
const missing = mandatoryKeys.filter((k) => !completed[k]);
if (missing.length === 0) {
try { localStorage.setItem("dashboard_onboarding_complete", "true"); } catch {}
if (typeof onExplore === 'function') {
try { onExplore(); } catch {}
}
if (onClose) onClose();
return;
}
setFlashKeys(missing);
setTimeout(() => setFlashKeys([]), 800);
}, [completed, onClose, onExplore]);
if (!open) return null;
return (
<Box sx={{ position: "fixed", inset: 0, zIndex: 2000 }}>
{/* Blur overlay with visible background */}
<Box
onClick={onClose}
sx={{
position: "absolute",
inset: 0,
backdropFilter: "blur(6px)",
background: "rgba(0,0,0,0.05)",
}}
/>
{/* Modal container */}
<Box
sx={{
position: "relative",
zIndex: 2001,
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
p: 2,
}}
>
<Box
sx={{
width: "100%",
maxWidth: 780,
backgroundColor: "#1A1A1A",
borderRadius: "16px",
border: "1px solid rgba(255,255,255,0.08)",
p: 3,
boxShadow: "0 10px 40px rgba(0,0,0,0.5)",
}}
>
{/* Header */}
<Box sx={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start", mb: 2 }}>
<Box>
<Typography
sx={{
color: "#f1f1f1",
fontWeight: 700,
fontSize: 22,
letterSpacing: "-0.2px",
fontFamily: theme.typography.fontFamily,
}}
>
{headerTitle}
</Typography>
<Typography
sx={{
color: "#c5c5c5",
mt: 0.5,
fontSize: 14,
fontFamily: theme.typography.fontFamily,
}}
>
{headerSubtitle}
</Typography>
</Box>
</Box>
{/* Steps list with a single continuous rail */}
<Box sx={{ position: "relative", display: "flex", flexDirection: "column", gap: 3, marginLeft: -1.5, marginTop: 4 }}>
{/* Base grey rail */}
<Box
sx={{
position: "absolute",
left: 20,
top: 30,
bottom: 60,
width: 2,
background: "rgba(255,255,255,0.2)",
borderRadius: 2,
}}
/>
{/* Green segments between consecutive completed steps */}
{steps.map((s, i) => ({ s, i }))
.filter(({ i }) => i < steps.length - 1)
.filter(({ i }) => steps[i].completed && steps[i + 1].completed)
.map(({ i }) => (
<Box
key={`seg-${i}`}
sx={{
position: "absolute",
left: 20,
top: 30 + i * 110, // approximate segment height per step
height: 130, // matches gap+card combined height; tuned visually
width: 2,
background: "#43D17C",
borderRadius: 2,
}}
/>
))}
{steps.map((step, idx) => (
<StepItem key={step.key || idx} step={{...step, flashKeys}} />
))}
</Box>
{/* Footer */}
<Box sx={{ mt: 4, display: 'flex', justifyContent: 'center', gap: 1.5 }}>
{footer}
<Button variant="contained" color="primary" onClick={handleFinalDone}
sx={{
fontSize: 14,
padding: "8px 60px",
}}
>
Explore Now
</Button>
</Box>
</Box>
</Box>
</Box>
);
};
export default DashboardOnboarding;
+1
View File
@@ -143,6 +143,7 @@ const Detection = (props) => {
size="small"
sx={{ mr: 2 }}
value={searchQuery}
disabled
onChange={(e) => setSearchQuery(e.target.value)}
/>
{/* <Button
+231 -20
View File
@@ -16,11 +16,14 @@ import {
import {
OpenInNew as OpenInNewIcon,
FmdGood as FmdGoodIcon,
Check as CheckIcon,
} from "@mui/icons-material"
import { toast } from "react-toastify";
import RunDetectionTest from '../components/RunDetectionTest.jsx';
import theme from '../theme.jsx';
import DetectionRuleCard from "../components/DetectionRuleCard.jsx";
import CollectIngestModal from "../components/CollectIngestModal.jsx";
import {
green,
red,
@@ -30,13 +33,12 @@ import {
import WorkflowValidationTimeline from "../components/WorkflowValidationTimeline.jsx"
const handleDirectoryChange = (folderDisabled, setFolderDisabled, globalUrl, isDetectionActive) => {
if (!isDetectionActive) {
toast.warn("Connect to siem first for global enable/disable to work");
return;
}
//if (!isDetectionActive) {
// toast.warn("Connect first for global enable/disable to work");
// return;
//}
const action = folderDisabled ? "enable_folder" : "disable_folder";
//const url = `${globalUrl}/api/v1/detections/${detectionType}/selected_rules/${action}`;
const url = `${globalUrl}/api/v1/detections/sigma/selected_rules/${action}`;
fetch(url, {
@@ -68,10 +70,117 @@ const DetectionExplorer = (props) => {
const [loading, setLoading] = useState(false);
const [workflow, setWorkflow] = useState({})
const [detectionWorkflowId, setDetectionWorkflowId] = useState("")
const [isDetectionValid, setIsDetectionValid] = useState(false)
const [availableDetection, setAvailableDetection] = React.useState([]);
const [environmentList, setEnvironmentList] = React.useState([])
const [showCollectIngestMenu, setShowCollectIngestMenu] = useState(false);
const [workflows, setWorkflows] = React.useState([])
const [apps, setApps] = React.useState([])
const [pipelines, setPipelines] = React.useState([])
const [ticketWebhook, setTicketWebhook] = React.useState("");
const [detectionWorkflowId, setDetectionWorkflowId] = React.useState("");
const handleGetAllTriggers = () => {
fetch(globalUrl + "/api/v1/triggers", {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for getting all triggers");
}
return response.json();
})
.then((responseJson) => {
//setWebHooks(responseJson.webhooks || []);
//setAllSchedules(responseJson.schedules || []);
setPipelines(responseJson.pipelines || []);
//setShowLoader(false);
})
.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 !== false) {
setWorkflows(responseJson || []);
for (var i = 0; i < responseJson?.length; i++) {
if (responseJson[i].background_processing === true && responseJson[i].name.toLowerCase().includes("ingest tickets") && responseJson[i].triggers !== undefined) {
for (var triggerkey in responseJson[i].triggers) {
if (responseJson[i].triggers[triggerkey].trigger_type === "WEBHOOK") {
setDetectionWorkflowId(responseJson[i].id)
setTicketWebhook(`${globalUrl}/api/v1/hooks/webhook_${responseJson[i].triggers[triggerkey].id}`)
//setNewPipelineValue(`export | sigma /tmp/sigma_rules | to ${globalUrl}/api/v1/hooks/webhook_${responseJson[i].triggers[triggerkey].id}`)
break;
}
}
}
}
} else {
toast.warn("Failed to load workflows. Please try again or contact support@shuffler if this persists.")
}
})
.catch((error) => {
toast(error.toString());
});
}
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 loadUsecases = () => {
const url = `${globalUrl}/api/v1/workflows/usecases`
@@ -133,6 +242,11 @@ const DetectionExplorer = (props) => {
}
const handleConnectClick = () => {
// NEW way to handle it
setShowCollectIngestMenu(true)
return
if (detectionWorkflowId !== "") {
console.log("Already have a workflow ID for this detection")
//toast.info(`Already have a detection workflow for ${detectionInfo?.category}`)
@@ -179,7 +293,7 @@ const DetectionExplorer = (props) => {
}
if (responseJson.workflow_valid !== undefined && responseJson.workflow_valid !== null) {
setIsDetectionValid(responseJson.workflow_valid)
//setIsDetectionValid(responseJson.workflow_valid)
}
} else {
if (responseJson.reason !== undefined && responseJson.reason !== null) {
@@ -238,8 +352,12 @@ const DetectionExplorer = (props) => {
}
useEffect(() => {
getApps()
getWorkflows()
loadUsecases()
loadEnvironments()
handleGetAllTriggers()
}, [])
useEffect(() => {
@@ -251,18 +369,87 @@ const DetectionExplorer = (props) => {
return
}
handleConnectClick()
console.log("Detection info: ", detectionInfo)
//handleConnectClick()
}, [detectionInfo])
const filteredRules = ruleInfo === "default" ? [] : ruleInfo?.filter((rule) =>
rule.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
rule.description.toLowerCase().includes(searchQuery.toLowerCase())
rule?.file_name?.replaceAll(" ", "_")?.toLowerCase().includes(searchQuery) ||
rule?.title?.replaceAll(" ", "_")?.toLowerCase().includes(searchQuery) ||
rule?.description?.replaceAll(" ", "_")?.toLowerCase().includes(searchQuery)
)
const lakeNodes = environmentList !== undefined && environmentList !== null ? environmentList.filter((env) => env?.archived === false && env?.data_lake?.enabled === true).length : 0
const submitPipeline = (pipeline, environment) => {
var pipelineConfig = {
command: pipeline,
name: pipeline,
type: "create",
environment: "",
workflow_id: "",
trigger_id: "",
start_node: "",
}
if (environment !== undefined && environment !== "") {
pipelineConfig.environment = environment
}
const url = `${globalUrl}/api/v1/triggers/pipeline`;
fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify(pipelineConfig),
credentials: "include",
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for stream results :O!");
}
return response.json();
})
.then((responseJson) => {
if (!responseJson.success && pipelineConfig.type !== "delete") {
toast.error("Failed to set pipeline: " + responseJson.reason);
} else {
if (pipelineConfig.type === "create") {
toast.success("Pipeline will be created: " + responseJson.reason)
//setPipelineModalOpen(false)
} else if (pipelineConfig.type === "stop") {
toast.success("Pipeline will be stopped: " + responseJson.reason)
//setPipelineModalOpen(false)
} else {
toast.info("Unknown pipeline type: " + pipelineConfig.type)
}
}
})
.catch((error) => {
console.log("Get pipeline error: ", error.toString());
});
}
return (
<Container>
<CollectIngestModal
globalUrl={globalUrl}
open={showCollectIngestMenu}
setOpen={setShowCollectIngestMenu}
workflows={workflows}
getWorkflows={getWorkflows}
apps={apps}
/>
<Paper
style={{
marginTop: 50,
@@ -316,8 +503,21 @@ const DetectionExplorer = (props) => {
</div>
: */}
<div style={{marginRight: 20, }}>
<RunDetectionTest
globalUrl={globalUrl}
pipelines={pipelines}
workflows={workflows}
ticketWebhook={ticketWebhook}
detectionWorkflowId={detectionWorkflowId}
changePipelineState={undefined}
submitPipelineWrapper={submitPipeline}
/>
</div>
<Button
variant="contained"
variant={detectionWorkflowId === "" ? "contained" : "outlined"}
onClick={() => {
handleConnectClick()
}}
@@ -326,18 +526,26 @@ const DetectionExplorer = (props) => {
// Red = workflow exists, validation is false
// Green = workflow exists, validation is true
// Grey = workflow does not exist
backgroundColor: detectionWorkflowId === "" ? grey : isDetectionValid ? green : red,
}}
>
{loading ? <CircularProgress size={24} /> :
detectionWorkflowId === "" ? `Connect to ${detectionInfo?.category}` :
isDetectionValid ? `Connected to ${detectionInfo?.category}` : `Fix ${detectionInfo?.category} connection`}
{loading ?
<CircularProgress size={24} />
:
detectionWorkflowId !== "" ?
<span>
<CheckIcon style={{color: green, marginRight: 10, top: 5, }} />
Connected
</span>
:
`Connect to ${detectionInfo?.category}`
}
</Button>
{/**/}
{detectionInfo?.category === "SIGMA" || detectionInfo?.category === "SIEM" ?
<Tooltip title={`You have ${lakeNodes} available Data Lake node(s)`}>
<a href="/admin?tab=Locations" style={{textDecoration: "none", color: "inherit", }} target="_blank" rel="noreferrer">
<a href="/admin?tab=locations" style={{textDecoration: "none", color: "inherit", }} target="_blank" rel="noreferrer">
<FmdGoodIcon style={{marginLeft: 15, marginTop: 5, color: lakeNodes > 0 ? green : red}} />
</a>
</Tooltip>
@@ -345,7 +553,7 @@ const DetectionExplorer = (props) => {
</div>
</Box>
{filteredRules?.length > 0 ?
{ruleInfo?.length > 0 ?
<Box
sx={{
display: "flex",
@@ -367,7 +575,9 @@ const DetectionExplorer = (props) => {
size="small"
sx={{ mr: 2 }}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onChange={(e) => {
setSearchQuery(e?.target?.value?.replaceAll(" ", "_")?.toLowerCase())
}}
/>
</Box>
<Box sx={{ display: "flex", alignItems: "center" }}>
@@ -386,7 +596,7 @@ const DetectionExplorer = (props) => {
<Divider />
<Box
sx={{
height: "500px",
minHeight: 500,
width: "100%",
overflowY: "auto",
p: 1,
@@ -410,6 +620,7 @@ const DetectionExplorer = (props) => {
folderDisabled={folderDisabled}
isDetectionActive={isDetectionActive}
ruleDetails={rule}
ruleMapping={ruleMapping}
setRuleMapping={setRuleMapping}
+67 -26
View File
@@ -12,16 +12,18 @@ import {
FormLabel,
} from "@mui/material";
import DashboardBarchart, { LoadStats } from '../components/DashboardBarchart.jsx';
import LineChartWrapper, { LoadStats } from "../components/LineChartWrapper.jsx";
import {
Edit as EditIcon,
Refresh as RefreshIcon,
} from "@mui/icons-material";
import { toast } from "react-toastify";
import ShuffleCodeEditor from "../components/ShuffleCodeEditor1.jsx";
import theme from '../theme.jsx';
const RuleCard = (props) => {
const { ruleName, description, file_id, globalUrl, folderDisabled, isDetectionActive, availableDetection, ruleMapping, setRuleMapping, ruleDetails, key, ...otherProps } = props
const RuleCard = ({ ruleName, description, file_id, globalUrl, folderDisabled, isTenzirActive, availableDetection, ruleMapping, setRuleMapping, ...otherProps }) => {
const [openCodeEditor, setOpenCodeEditor] = React.useState(false);
const [fileData, setFileData] = React.useState("");
const [isEnabled, setIsEnabled] = React.useState(otherProps.is_enabled);
@@ -30,35 +32,33 @@ const RuleCard = ({ ruleName, description, file_id, globalUrl, folderDisabled, i
const [responseValue, setResponseValue] = React.useState("No response action")
const isCloud = ["localhost:3002", "shuffler.io"].includes(window.location.host);
console.log("Rulemapping: ", ruleMapping)
useEffect(() => {
//const url = `${globalUrl}/api/v1/stats/app_executions_test2`
//const resp = LoadStats(globalUrl, ruleName)
//const resp = LoadStats(globalUrl, "app_executions_test2")
const resp = LoadStats(globalUrl, "app_executions_cloud")
resp.then((data) => {
if (data === undefined) {
setFilteredBarchart([])
} else {
setFilteredBarchart(data)
if (key < 10) {
console.log("RuleCard Key: ", key, ruleName, file_id, otherProps)
}
})
if (ruleMapping !== undefined && ruleMapping !== null && ruleMapping.value !== undefined && ruleMapping.value !== null) {
console.log("FIX MAPPING FROM ruleMapping.value: ", ruleMapping)
}
if (ruleDetails?.title === undefined || ruleDetails?.title === null || ruleDetails?.title.length === 0) {
//toast.error("Can't load stats for this rule. Contact support@shuffler.io if this persists.")
return
}
const resp = LoadStats(globalUrl, `detection_rule_${ruleDetails?.title.replaceAll(" ", "_").toLowerCase()}`)
resp.then((data) => {
if (data === undefined) {
setFilteredBarchart([])
} else {
setFilteredBarchart(data)
}
})
}, [])
console.log("Response Value: ", responseValue)
const handleSwitchChange = (event) => {
if (folderDisabled) {
toast.warn("Enable the directory to enable individual rules");
return;
}
if (!isTenzirActive) {
if (!isDetectionActive) {
toast.warn("Connect to the siem first to enable/disable the rule");
return;
}
@@ -96,6 +96,7 @@ const RuleCard = ({ ruleName, description, file_id, globalUrl, folderDisabled, i
});
};
var parsedRulename = ruleName.charAt(0).toUpperCase() + ruleName.slice(1).replaceAll("_", " ")
return (
<Card style={{
borderRadius: theme.palette?.borderRadius,
@@ -116,10 +117,13 @@ const RuleCard = ({ ruleName, description, file_id, globalUrl, folderDisabled, i
color: "white",
}}
>
<Typography variant="h6">{ruleName.replaceAll("_", " ")} ({filteredBarchart === null || filteredBarchart.total === undefined ? 0 : filteredBarchart.total})</Typography>
<Typography variant="h6">
{parsedRulename} {/*({filteredBarchart === null || filteredBarchart.total === undefined ? 0 : filteredBarchart.total})*/}
</Typography>
<div style={{ display: 'flex', alignItems: 'center' }}>
<Select
{/*
<Select
MenuProps={{
disableScrollLock: true,
}}
@@ -148,6 +152,7 @@ const RuleCard = ({ ruleName, description, file_id, globalUrl, folderDisabled, i
color: "white",
height: 40,
borderRadius: theme.palette?.borderRadius,
marginRight: 20,
}}
>
<MenuItem
@@ -178,6 +183,7 @@ const RuleCard = ({ ruleName, description, file_id, globalUrl, folderDisabled, i
)
})}
</Select>
*/}
<Tooltip title="Edit Rule" placement="top">
@@ -204,12 +210,47 @@ const RuleCard = ({ ruleName, description, file_id, globalUrl, folderDisabled, i
minHeight: 40,
maxHeight: 40,
display: "flex",
}}>
{filteredBarchart === null ? null :
<DashboardBarchart
timelineData={filteredBarchart}
/>
<Tooltip title="Refresh stats" placement="top">
<IconButton
onClick={() => {
if (ruleDetails?.title === undefined || ruleDetails?.title === null || ruleDetails?.title.length === 0) {
toast.error("Can't load stats for this rule. Contact support@shuffler.io if this persists.")
return
}
const resp = LoadStats(globalUrl, `detection_rule_${ruleDetails?.title.replaceAll(" ", "_").toLowerCase()}`)
resp.then((data) => {
console.log("DATA: ", data)
if (data === undefined) {
setFilteredBarchart([])
} else {
setFilteredBarchart(data)
}
})
}}
>
<RefreshIcon
color="secondary"
/>
</IconButton>
</Tooltip>
{filteredBarchart === null ? <Typography variant="body2" color="textSecondary" style={{marginTop: 10, marginLeft: 18, }}>No stats yet</Typography> :
<div style={{minWidth: "90%", }}>
<LineChartWrapper
inputname={""}
keys={filteredBarchart}
height={100}
width={"100%"}
border={false}
color={"#808080"}
/>
</div>
}
</div>
{/*
+13 -2
View File
@@ -16,6 +16,7 @@ import {
ListItemText,
} from '@mui/material';
import { Search as SearchIcon } from '@mui/icons-material';
import useDebouncedCallback from '../utils/useDebouncedCallback.js';
const searchClient = algoliasearch("JNSS5CFDZZ", "1e5f29b1550939855de5915eac3bf5f7");
@@ -69,12 +70,22 @@ const DiscordChat = props => {
}
const SearchBox = ({ currentRefinement, refine }) => {
const [inputValue, setInputValue] = useState("");
const debouncedRefine = useDebouncedCallback((value) => refine(value), 300);
useEffect(() => {
setInputValue(currentRefinement || "");
}, [currentRefinement]);
return (
<form noValidate action="" role="search">
<TextField
fullWidth
value={currentRefinement}
onChange={(event) => refine(event.currentTarget.value)}
value={inputValue}
onChange={(event) => {
const value = event.currentTarget.value;
setInputValue(value);
debouncedRefine(value);
}}
onKeyDown={(event) => {
if(event.key === "Enter") {
event.preventDefault();
+4 -1
View File
@@ -26,6 +26,7 @@ import {
ListItemAvatar,
ListItemText,
} from '@mui/material';
import { useDebouncedCallback } from "../utils/useDebouncedCallback";
@@ -97,6 +98,8 @@ const DocsGrid = props => {
}
}
const debouncedRefine = useDebouncedCallback((value) => refine(value), 300)
return (
<form noValidate action="" role="search">
<TextField
@@ -122,7 +125,7 @@ const DocsGrid = props => {
id="shuffle_search_field"
onChange={(event) => {
removeQuery("q")
refine(event.currentTarget.value)
debouncedRefine(event.currentTarget.value)
}}
onKeyDown={(event) => {
if(event.key === "Enter") {
+2 -2
View File
@@ -917,10 +917,10 @@ const EditWorkflow = (props) => {
}}
/>
<Typography variant="h6" style={{ marginBottom: 5 }}>
Generate Workflow from Flowchart
Generate Workflow from Flowchart (beta)
</Typography>
<Typography variant="body2" color="textSecondary" style={{ marginBottom: 10 }}>
Click to upload your flowchart - AI will convert it to a workflow
Click to upload your flowchart - Your LLM will convert it to a workflow
</Typography>
<Typography variant="caption" color="textSecondary">
PNG, JPG, JPEG Max 5MB
+275 -154
View File
@@ -35,6 +35,7 @@ import {
Help as HelpIcon,
ExpandLess as ExpandLessIcon,
ExpandMore as ExpandMoreIcon,
Delete as DeleteIcon,
} from "@mui/icons-material";
import { toast } from 'react-toastify';
import { Context } from '../context/ContextApi.jsx';
@@ -53,7 +54,7 @@ const EnvironmentTab = memo((props) => {
pipelines: false,
proxies: false,
})
const [installationTab, setInstallationTab] = React.useState(0);
const [installationTab, setInstallationTab] = React.useState(1);
const [isExpanded, setIsExpanded] = React.useState(false);
const [listItemExpanded, setListItemExpanded] = React.useState(-1);
const [, setUpdate] = React.useState(0);
@@ -61,6 +62,7 @@ const EnvironmentTab = memo((props) => {
const [selectedEnvironment, setSelectedEnvironment] = React.useState(null);
const [selectedSubOrg, setSelectedSubOrg] = React.useState([]);
const [showLocationActionModal, setShowLocationActionModal] = React.useState(undefined)
const [currentEnvQueue, setCurrentEnvQueue] = React.useState([])
const { themeMode, supportEmail, brandColor } = useContext(Context);
const theme = getTheme(themeMode, brandColor);
@@ -408,6 +410,11 @@ const EnvironmentTab = memo((props) => {
skipPipeline = true
}
var showDetection = false
if (commandController.detection === true) {
showDetection = true
}
var addProxy = false
if (commandController.proxies === true) {
addProxy = true
@@ -422,12 +429,11 @@ const EnvironmentTab = memo((props) => {
-e AUTH="${auth}" \\
-e ENVIRONMENT_NAME="${environment.Name}" \\
-e ORG="${environment.org_id}" \\
-e SHUFFLE_WORKER_IMAGE="ghcr.io/shuffle/shuffle-worker:latest" \\
-e SHUFFLE_SWARM_CONFIG=run \\
-e SHUFFLE_LOGS_DISABLED=true \\
-e BASE_URL="${newUrl}" \\${addProxy ? `
-e HTTPS_PROXY=IP:PORT \\` : ""}${skipPipeline ? `
-e SHUFFLE_SKIP_PIPELINES=true \\` : ""}
-e HTTPS_PROXY=IP:PORT \\` : ""}${skipPipeline ? `
-e SHUFFLE_SKIP_PIPELINES=true \\` : ""}${showDetection ? `
-v /tmp:/tmp \\` : ""}
ghcr.io/shuffle/shuffle-orborus:latest
`)
} else if (installationTab === 2) {
@@ -676,6 +682,76 @@ const EnvironmentTab = memo((props) => {
)
}
const removeEnvQueueItem = (environment, queueItem) => {
const url = `${globalUrl}/api/v1/workflows/queue/confirm`
const headers = {
"Org-Id": environment.Name,
"Org": environment.org_id,
"Authorization": environment.auth,
}
const items = {
"data": [queueItem],
}
fetch(url, {
method: "POST",
headers: headers,
body: JSON.stringify(items),
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for apps :O!");
toast("Failed removing queue item")
return;
}
return response.json();
})
.then((responseJson) => {
if (responseJson?.success !== false) {
toast("Successfully removed queue item")
getEnvQueue(environment)
} else {
toast("Failed removing queue item")
}
})
.catch((error) => {
toast(error.toString());
})
}
const getEnvQueue = (environment) => {
const url = `${globalUrl}/api/v1/workflows/queue`
const headers = {
"Org-Id": environment.Name,
"Org": environment.org_id,
"Authorization": environment.auth,
}
fetch(url, {
method: "POST",
headers: headers,
})
.then((response) => {
if (response.status !== 200) {
console.log("Status not 200 for apps :O!");
}
return response.json();
})
.then((responseJson) => {
if (responseJson?.success !== false && responseJson?.data?.length > 0) {
setCurrentEnvQueue(responseJson.data)
}
})
.catch((error) => {
toast(error.toString());
})
}
const editEnvironmentConfig = (id, selectedSubOrg, cacheKey) => {
const data = {
action: "suborg_distribute",
@@ -881,14 +957,14 @@ const EnvironmentTab = memo((props) => {
<ListItem
style={{
display: "grid",
gridTemplateColumns: "80px 80px 80px 120px 120px 120px 120px 350px 150px",
gridTemplateColumns: "80px 80px 80px 150px 100px 80px 400px 100px",
width: "100%",
minWidth: 800,
paddingBottom: 0,
borderBottom: theme.palette.defaultBorder,
}}
>
{["Type", "Status", "Scale", "Pipeline", "Name", "Type", "Queue", "Actions", "Distribution"].map((header, index) => {
{["Type", "Status", "Pipeline", "Name", "Type", "Queue", "Actions", "Distribution"].map((header, index) => {
return (
<ListItemText
@@ -912,7 +988,6 @@ const EnvironmentTab = memo((props) => {
key={rowIndex}
style={{
display: "grid",
gridTemplateColumns: "80px 80px 80px 120px 120px 120px 120px 350px 150px",
backgroundColor: theme.palette.platformColor,
height: 40,
width: "100%",
@@ -1014,12 +1089,17 @@ const EnvironmentTab = memo((props) => {
key={index}
style={{ cursor: "pointer", backgroundColor: bgColor, marginLeft: 0, borderBottomLeftRadius: environments?.length - 1 === index ? 8 : 0, borderBottomRightRadius: environments?.length - 1 === index ? 8 : 0, display: 'grid', gridTemplateColumns: "80px 80px 80px 120px 120px 120px 120px 405px 125px", }}
onClick={() => {
if (environment.Type === "cloud") {
toast("Cloud environments are not configurable. To see what is possible, create a new environment.")
return
}
setListItemExpanded(listItemExpanded === index ? -1 : index)
if (environment.Type === "cloud") {
toast("Cloud environments are not configurable. To see what is possible, create a new environment.")
return
}
setListItemExpanded(listItemExpanded === index ? -1 : index)
if (listItemExpanded !== index) {
getEnvQueue(environment)
setCurrentEnvQueue([])
}
}}
>
<ListItemText
@@ -1100,7 +1180,7 @@ const EnvironmentTab = memo((props) => {
<br />
<br />
Last checkin: {environment?.checkin !== undefined && environment.checkin !== null && environment?.checkin > 0 ? new Date(environment?.checkin * 1000).toLocaleString() : "Never"} {environment?.Type === "cloud" ? "" : "Timeout: 180 seconds"}
Last checkin: {environment?.checkin !== undefined && environment.checkin !== null && environment?.checkin > 0 ? new Date(environment?.checkin * 1000).toLocaleString() : "Never"}. {environment?.Type === "cloud" ? "" : "Timeout: 180 seconds"}
</Typography>
} placement="top">
<Typography
@@ -1182,42 +1262,14 @@ const EnvironmentTab = memo((props) => {
}
/>
<ListItemText
primary={
selectedOrganization.id !== undefined && environment?.org_id !== selectedOrganization.id ?
"N/A"
:
environment.licensed ? (
<Tooltip title="Scale configured (auto on cloud)" placement="top">
<CheckCircleIcon style={{ color: "#4caf50" }} />
</Tooltip>
) : (
<Tooltip
title="In Verbose mode. Set SHUFFLE_SWARM_CONFIG=run to Scale. This will not be as verbose. Details: https://shuffler.io/docs/configuration#scaling-shuffle"
placement="top"
>
<a
href="/docs/configuration#scaling-shuffle"
target="_blank"
rel="noopener noreferrer"
>
<CancelIcon style={{ color: "#f85a3e" }} />
</a>
</Tooltip>
)
}
style={{
minWidth: 60,
marginLeft: 20,
overflow: "hidden",
whiteSpace: "normal",
wordWrap: "break-word",
padding: 8,
display: "table-cell",
}}
/>
<ListItemText
style={{
marginLeft: 30,
overflow: "hidden",
whiteSpace: "normal",
wordWrap: "break-word",
display: "table-cell",
}}
primary={
environment.Type === "cloud" ?
<Tooltip title={`Make a new environment to set up a Datalake node. Please contact ${supportEmail} if this is something you want to see on Cloud directly.`} placement="top">
@@ -1231,40 +1283,32 @@ const EnvironmentTab = memo((props) => {
rel="noopener noreferrer"
>
<Tooltip title={"Data Lake node enabled. Check /detections/Sigma to learn more"} placement="top">
<CheckCircleIcon style={{ color: "#4caf50" }} />
</Tooltip>
</a>
) : (
<Tooltip
title="Data Lake node disabled. Click to enable."
placement="top"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
window.open("/detections/Sigma", "_blank")
}}
>
<a
href="/detections/Sigma"
target="_blank"
rel="noopener noreferrer"
>
<CancelIcon style={{ color: "#f85a3e" }} />
</a>
</Tooltip>
)
}
style={{
minWidth: 60,
marginLeft: 40,
overflow: "hidden",
whiteSpace: "normal",
wordWrap: "break-word",
display: "table-cell",
}}
/>
<Tooltip title={"Data Lake node enabled. Check /detections/Sigma to learn more"} placement="top">
<CheckCircleIcon style={{ color: "#4caf50" }} />
</Tooltip>
</a>
) : (
<Tooltip
title="Data Lake node disabled. Click to enable."
placement="top"
onClick={(e) => {
e.preventDefault()
e.stopPropagation()
window.open("/detections/Sigma", "_blank")
}}
>
<a
href="/detections/Sigma"
target="_blank"
rel="noopener noreferrer"
>
<CancelIcon style={{ color: "#f85a3e" }} />
</a>
</Tooltip>
)
}
/>
<ListItemText
primary={(
@@ -1274,12 +1318,12 @@ const EnvironmentTab = memo((props) => {
)}
primaryTypographyProps={{
style:{
maxWidth: 150,
whiteSpace: 'nowrap',
overflow: "hidden",
textOverflow: 'ellipsis',
wordWrap: "break-word",
transition: "all 0.3s ease",
textAlign: "center",
}}}
style={{
minWidth: 120,
@@ -1292,7 +1336,7 @@ const EnvironmentTab = memo((props) => {
primary={environment.Type}
primaryTypographyProps={{
style:{
minWidth: 70,
minWidth: 50,
overflow: "hidden",
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
@@ -1302,6 +1346,7 @@ const EnvironmentTab = memo((props) => {
}}}
style={{display: "table-cell",}}
/>
<ListItemText
primaryTypographyProps={{
style:{
@@ -1344,7 +1389,7 @@ const EnvironmentTab = memo((props) => {
}}
color="primary"
>
Make Default
Default
</Button>
<Button
variant={environment.archived ? "contained" : "outlined"}
@@ -1418,34 +1463,38 @@ const EnvironmentTab = memo((props) => {
</ButtonGroup>
{/*
<IconButton disabled={environment.Type === "cloud"} onClick={()=> {setIsExpanded(prev => !prev)}}>
{listItemExpanded === index ? <ExpandLessIcon sx={{color: theme.palette.text.primary}} /> : <ExpandMoreIcon sx={{color: theme.palette.text.primary}}/>}
</IconButton>
*/}
</div>
</ListItemText>
{selectedOrganization.id !== undefined && environment?.org_id !== selectedOrganization.id ?
<ListItemText
primary={
<Tooltip
<ListItemText
primary={
<Tooltip
title="Parent organization controlled environments. You can use, but not modify this environments. Contact an admin of your parent organization if you need changes to this."
placement="top"
>
<Chip
label={"Parent"}
variant="contained"
color="secondary"
/>
</Tooltip>
}
style={{ textAlign: 'center', verticalAlign: 'middle', }}
/>
:
<Tooltip
title={environment.Name === "Cloud" ? "Cloud environments cannot be distributed" : "Distributed to sub-organizations. This means the sub organizations can use this environment, but can not modify it."}
placement="top"
>
<IconButton
sx={{":hover": {backgroundColor: "transparent"}}}
>
<Chip
style={{marginLeft: 200, }}
label={"Parent"}
variant="contained"
color="secondary"
/>
</Tooltip>
}
style={{ textAlign: 'center', verticalAlign: 'middle', }}
/>
:
<Tooltip
title={environment.Name === "Cloud" ? "Cloud environments cannot be distributed" : "Distributed to sub-organizations. This means the sub organizations can use this environment, but can not modify it."}
placement="top"
>
<IconButton
style={{marginLeft: 200, }}
disabled={ environment.Name === "Cloud" || userdata?.active_org?.role !== "admin" || (selectedOrganization.creator_org !== undefined && selectedOrganization.creator_org !== null && selectedOrganization.creator_org !== "" )? true : false}
onClick={(e) => {
e.stopPropagation()
@@ -1489,35 +1538,41 @@ const EnvironmentTab = memo((props) => {
aria-label="disabled tabs example"
variant="scrollable"
scrollButtons="auto"
style={{textAlign: "center", marginTop: 25, }}
style={{
textAlign: "center",
marginTop: 25,
marginBottom: 25,
borderBottom: "1px solid rgba(255,255,255,0.3)",
}}
>
<Tab
value={0}
label=<span style={{color: theme.palette.text.secondary, }}>
<img
src="/icons/docker.svg"
style={{ width: 20, height: 20, marginRight: 10, }}
/> Verbose (default)
</span>
/>
<Tab
value={1}
label=<span style={{color: theme.palette.text.secondary, }}>
<img
src="/icons/docker.svg"
style={{ width: 20, height: 20, marginRight: 10,}}
/> Scale
</span>
label=<span style={{color: theme.palette.text.secondary, textTransform: "none", }}>
<img
src="/icons/docker.svg"
style={{ width: 20, height: 20, marginRight: 10,}}
/> Docker (default)
</span>
/>
<Tab
value={2}
label=<span style={{color: theme.palette.text.secondary, }}>
label=<span style={{color: theme.palette.text.secondary, textTransform: "none", }}>
<img
src="/icons/k8s.svg"
style={{ width: 20, height: 20, marginRight: 10 }}
/> k8s
/> Kubernetes
</span>
/>
<Tab
value={0}
style={{marginLeft: 300, }}
label=<span style={{color: theme.palette.text.secondary, textTransform: "none", }}>
Verbose Mode
</span>
/>
</Tabs>
<Typography variant="body1" color="textSecondary" style={{marginTop: 15, }}>
{installationTab === 2 ?
@@ -1563,6 +1618,7 @@ const EnvironmentTab = memo((props) => {
fontFamily: "monospace",
fontSize: 18,
border: themeMode === "dark" ? "1px solid #555" : "1px solid #ddd",
minHeight: 325,
}}
>
{getOrborusCommand(environment)}
@@ -1588,49 +1644,114 @@ const EnvironmentTab = memo((props) => {
<Divider style={{marginTop: 25, marginBottom: 10, }}/>
<div style={{display: 'flex', alignItems: 'center', }}>
<Typography variant='body2' color="textSecondary">Configure HTTP Proxies:</Typography> <Checkbox
id="shuffle_skip_proxies"
onClick={() => {
if (commandController.proxies === undefined) {
commandController.proxies = true
} else {
commandController.proxies = !commandController.proxies
}
setCommandController(commandController)
setUpdate(Math.random())
}}
/>
<Checkbox
id="shuffle_skip_proxies"
onClick={() => {
if (commandController.proxies === undefined) {
commandController.proxies = true
} else {
commandController.proxies = !commandController.proxies
}
setCommandController(commandController)
setUpdate(Math.random())
}}
/>
<Typography variant='body2' color="textSecondary">Configure HTTP Proxies</Typography>
</div>
<div />
<div style={{display: 'flex', alignItems: 'center', }}>
<Typography variant='body2' color="textSecondary">Disable Pipelines & Data Lake:</Typography> <Checkbox
id="shuffle_skip_pipelines"
onClick={() => {
if (commandController.pipelines === undefined) {
commandController.pipelines = true
} else {
commandController.pipelines = !commandController.pipelines
}
setCommandController(commandController)
setUpdate(Math.random())
}}
/>
<Checkbox
id="shuffle_enable_detection"
onClick={() => {
if (commandController.detection === undefined) {
commandController.detection = true
} else {
commandController.detection = !commandController.detection
}
setCommandController(commandController)
setUpdate(Math.random())
}}
/>
<Typography variant='body2' color="textSecondary">Enable Detection Controller</Typography>
</div>
{/*
<div style={{display: 'flex', alignItems: 'center', }}>
<Checkbox
id="shuffle_skip_pipelines"
onClick={() => {
if (commandController.pipelines === undefined) {
commandController.pipelines = true
} else {
commandController.pipelines = !commandController.pipelines
}
setCommandController(commandController)
setUpdate(Math.random())
}}
/>
<Typography variant='body2' color="textSecondary">Disable Pipelines & Data Lake</Typography>
</div>
*/}
</div>
<Typography variant="body1" color="textSecondary" style={{marginTop: 15, }}>
{installationTab === 2 ? null :
<span>
3. Verify if the node is running. Try to refresh the page a little while after running the command.
3. Verify if the Runtime Location is running. Refresh the page 2 minutes after running the command.
</span>
}
</Typography>
</div>
</div>
</Grid>
</Grid>
</Collapse>
{currentEnvQueue.length === 0 ? null :
<List style={{ minWidth: 700, maxWidth: 700, maxHeight: 300, overflowY: "auto", scrollbarColor: theme.palette.scrollbarColorTransparent, scrollbarWidth: 'thin', }}>
{currentEnvQueue.map((queueItem, queueIndex) => {
return (
<ListItem
style={{
backgroundColor: theme.palette.surfaceColor,
borderBottom: theme.palette.defaultBorder,
maxHeight: 50,
}}
>
<ListItemText style={{minWidth: 50, maxWidth: 50, }}>
{queueItem.priority}
</ListItemText>
<ListItemText style={{minWidth: 125, maxWidth: 150, marginLeft: 25, }}>
{queueItem.type}
</ListItemText>
<ListItemText style={{minWidth: 325, maxWidth: 350, marginLeft: 25, overflow: "auto", }}>
{queueItem.execution_argument}
</ListItemText>
<ListItemText style={{minWidth: 50, maxWidth: 50, marginLeft: 25, overflow: "auto", }}>
<Tooltip title="Remove job from queue">
<IconButton
onClick={()=>{
removeEnvQueueItem(
environment,
queueItem,
)
}}
>
<DeleteIcon style={{
color: red,
}} />
</IconButton>
</Tooltip>
</ListItemText>
</ListItem>
)
})}
</List>
}
</Grid>
</Grid>
</Collapse>
{showCPUAlert === false ? null : (
<ListItem
+155 -91
View File
@@ -1,5 +1,6 @@
import React, { useState, useEffect, useContext, memo } from "react";
import { toast } from 'react-toastify';
import { GetIconInfo, } from "../views/Workflows2.jsx";
import {
IconButton,
@@ -9,24 +10,25 @@ import {
ListItemAvatar,
ListItemSecondaryAction,
Tooltip,
Button,
FormControl,
InputLabel,
TextField,
Divider,
Select,
MenuItem,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Typography,
Skeleton,
Checkbox,
Chip,
Menu,
Pagination,
PaginationItem,
Button,
ButtonGroup,
FormControl,
InputLabel,
TextField,
Divider,
Select,
MenuItem,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Typography,
Skeleton,
Checkbox,
Chip,
Menu,
Pagination,
PaginationItem,
} from "@mui/material";
import { DataGrid } from "@mui/x-data-grid";
@@ -352,8 +354,9 @@ const [filesLoaded, setFilesLoaded] = useState(false);
</Tooltip>
<Tooltip
title={"Delete file"}
style={{marginLeft: isSelectedFiles?5:15, }}
style={{}}
aria-label={"Delete"}
placement="right"
>
<span>
<IconButton
@@ -628,8 +631,8 @@ const [filesLoaded, setFilesLoaded] = useState(false);
return;
}
if (folder === undefined || folder === null || folder.length < 2) {
toast("Please enter a valid folder name")
if (folder === undefined || folder === null || folder.length < 1) {
toast("Please enter a valid folder name. For Root: /")
return
}
@@ -637,6 +640,8 @@ const [filesLoaded, setFilesLoaded] = useState(false);
url: url,
path: folder,
field_3: downloadBranch || "master",
namespace: selectedCategory !== undefined && selectedCategory !== null && selectedCategory !== "default" ? selectedCategory : "",
};
if (field1.length > 0) {
@@ -1312,57 +1317,80 @@ const [filesLoaded, setFilesLoaded] = useState(false);
<Button
color="primary"
variant="contained"
onClick={() => {
upload.click();
}}
style={{ textTransform: 'none',fontSize: 16, borderRadius:isSelectedFiles?4:null, width:isSelectedFiles?143:null, height:isSelectedFiles?35:null, boxShadow: isSelectedFiles?'none':null,}}
>
Upload files
</Button>
{/* <FileCategoryInput
isSet={renderTextBox} /> */}
<input
hidden
type="file"
multiple
ref={(ref) => (upload = ref)}
onChange={(event) => {
//const file = event.target.value
//const fileObject = URL.createObjectURL(actualFile)
//setFile(fileObject)
//const files = event.target.files[0]
uploadFiles(event.target.files);
<ButtonGroup style={{top: -10, position: "relative", }}>
<Button
color="primary"
variant="contained"
onClick={() => {
upload.click();
}}
style={{ textTransform: 'none',fontSize: 16, width:isSelectedFiles?143:null, height:isSelectedFiles?35:null, boxShadow: isSelectedFiles?'none':null,}}
>
Upload files
</Button>
{/* <FileCategoryInput
isSet={renderTextBox} /> */}
<input
hidden
type="file"
multiple
ref={(ref) => (upload = ref)}
onChange={(event) => {
//const file = event.target.value
//const fileObject = URL.createObjectURL(actualFile)
//setFile(fileObject)
//const files = event.target.files[0]
uploadFiles(event.target.files);
}}
/>
<Button
style={{ marginLeft: 16, marginRight: 15, borderRadius:isSelectedFiles?4:null, width:isSelectedFiles?81:null, height:isSelectedFiles?35:null, boxShadow: isSelectedFiles?'none':null, }}
variant="contained"
color="secondary"
onClick={() => getFiles(selectedCategory)}
>
<CachedIcon />
</Button>
}}
/>
<Button
style={{ width:isSelectedFiles?81:null, height:isSelectedFiles?35:null, boxShadow: isSelectedFiles?'none':null, }}
variant="contained"
color="secondary"
onClick={() => getFiles(selectedCategory)}
>
<CachedIcon />
</Button>
</ButtonGroup>
<ButtonGroup style={{marginLeft: 10, }}>
{/* <div style={{height: 35, width: 1, color: "#494949"}}></div> */}
{selectedCategory === "sigma" || selectedCategory === "yara" ?
<Tooltip title={"Open Detection Tab"} style={{}} aria-label={""}>
<a href={`/detections/${selectedCategory}`} target="_blank" rel="noopener noreferrer">
<OpenInNewIcon
color="primary"
style={{
marginLeft: 10,
marginRight: 10,
top: 7,
position: 'relative',
}}
/>
</a>
</Tooltip>
: null}
{fileCategories !== undefined &&
fileCategories !== null &&
fileCategories.length > 1 ? (
<FormControl style={{ minWidth: 150, maxWidth: 150 }}>
<FormControl style={{ minWidth: 175, maxWidth: 175, }}>
<InputLabel id="category-choice" style={{
color: "rgba(255, 255, 255, 0.65)",
}}>
Category
</InputLabel>
<Select
labelId="input-namespace-select-label"
labelId="category-choice"
id="input-namespace-select-id"
style={{
minWidth: 122,
maxWidth: 122,
minWidth: 175,
maxWidth: 175,
height: 35,
float: "right",
position: 'relative',
top: 8
borderRadius: "5px 0px 0px 5px",
overflow: "hidden",
}}
value={selectedCategory}
onChange={(event) => {
@@ -1389,12 +1417,31 @@ const [filesLoaded, setFilesLoaded] = useState(false);
}}
>
{fileCategories.map((data, index) => {
const fixedname = data?.charAt(0)?.toUpperCase() + data?.slice(1)?.replaceAll("_", " ")
const iconDetails = GetIconInfo({
"app_name": fixedname,
"name": fixedname,
})
return (
<MenuItem
key={index}
value={data}
style={{
color: theme.palette.textFieldStyle.color,
display: "flex",
borderBottom: theme.palette.defaultBorder,
}}
>
{data.replaceAll("_", " ")}
<Typography style={{display: "flex", marginTop: 5, }}>
<div style={{marginRight: 10, }}>
{iconDetails?.originalIcon && (
iconDetails?.originalIcon
)}
</div>
{fixedname}
</Typography>
</MenuItem>
);
})}
@@ -1430,36 +1477,51 @@ const [filesLoaded, setFilesLoaded] = useState(false);
</FormControl>
) : null}
<div style={{display: "inline-flex", position:"relative", top: 8}}>
{renderTextBox ?
<Tooltip title={"Close"} style={{}} aria-label={""}>
<Button
style={{ marginLeft: 5, marginRight: 15, height: 35, borderRadius: 4, textTransform: 'none', fontSize: 16, }}
variant="contained"
color="secondary"
onClick={() => {
setRenderTextBox(false);
console.log(" close clicked")
{/*<div style={{display: "inline-flex", position:"relative", top: 8}}>*/}
{renderTextBox ?
<Tooltip title={"Close"} style={{}} aria-label={""}>
<Button
style={{
height: 35,
borderRadius: 4,
textTransform: 'none',
fontSize: 16,
borderRadius: "0px 5px 5px 0px",
marginRight: 10,
}}
>
<ClearIcon/>
</Button>
</Tooltip>
:
<Tooltip title={"Add new file category"} style={{}} aria-label={""}>
<Button
style={{whiteSpace: 'nowrap', textWrap: 'nowrap', marginLeft: 5, marginRight: 15, width: 169, height: 35, borderRadius: 4, textTransform: 'none', fontSize: 16, }}
variant="contained"
color="secondary"
onClick={() => {
setRenderTextBox(true);
color="secondary"
variant="contained"
onClick={() => {
setRenderTextBox(false);
console.log(" close clicked")
}}
>
<AddIcon/>
File Category
</Button>
</Tooltip>
}
>
<ClearIcon/>
</Button>
</Tooltip>
:
<Tooltip title={"Add new file category"} style={{}} aria-label={""}>
<Button
style={{
whiteSpace: "nowrap",
width: fileCategories !== undefined && fileCategories !== null && fileCategories.length > 1 ? 50 : 169,
height: 35,
textTransform: 'none',
fontSize: 16,
}}
variant="outlined"
color="secondary"
onClick={() => {
setRenderTextBox(true);
}}
>
<AddIcon/>
</Button>
</Tooltip>
}
</ButtonGroup>
{renderTextBox && <TextField
onKeyPress={(event)=>{
@@ -1491,7 +1553,8 @@ const [filesLoaded, setFilesLoaded] = useState(false);
margin="dense"
defaultValue={""}
autoFocus
/>}</div>
/>}
<ShuffleCodeEditor
isCloud={isCloud}
expansionModalOpen={openEditor}
@@ -1673,3 +1736,4 @@ const DownloadFileIcon = memo(({ setLoadFileModalOpen, isSelectedFiles }) => {
</Tooltip>
);
});
+4 -2
View File
@@ -751,6 +751,8 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
localStorage.setItem("getting_started_sidebar", "open");
localStorage.removeItem("workflows");
localStorage.removeItem("apps");
localStorage.removeItem("dashboard_onboarding_complete")
localStorage.removeItem("dashboard_onboarding_completed")
fetch(`${globalUrl}/api/v1/orgs/${orgId}/change`, {
mode: "cors",
@@ -955,7 +957,7 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
if (!fetched && org) {
setActiveOrgData(org);
if (!isCloud) {
if (org?.cloud_sync && org?.subscriptions[0]?.name?.toLowerCase().includes("enterprise") && org?.subscriptions[0]?.active) {
if (org?.cloud_sync && org?.subscriptions[0]?.name?.toLowerCase().includes("enterprise") && org?.subscriptions[0]?.active) {
setIsProdStatusOn(true);
} else if (org?.subscriptions[0]?.name?.toLowerCase().includes("enterprise") && org?.subscriptions[0]?.active) {
setIsProdStatusOn(true);
@@ -1224,7 +1226,7 @@ const LeftSideBar = ({ userdata, serverside, globalUrl, notifications, }) => {
<Box sx={{ display: "flex", flexDirection: "row", marginTop: 2.5, width: expandLeftNav ? "100%" : 48, padding: "0px", }}>
<Button
component={Link}
to="/usecases"
to={userdata?.support ? "/new-dashboard" : "/usecases"}
onClick={(event) => {
setOpenautomateTab(true);
setOpenSecurityTab(false);
+93 -91
View File
@@ -688,51 +688,51 @@ const AuthenticationOauth2 = (props) => {
const autoAuthButton =
<Button
fullWidth
variant="contained"
style={{
marginBottom: 20,
marginTop: 20,
flex: 1,
textTransform: "none",
textAlign: "left",
justifyContent: "flex-start",
backgroundColor: "#ffffff",
color: "#2f2f2f",
borderRadius: theme.palette?.borderRadius,
minWidth: 300,
maxWidth: 300,
maxHeight: 50,
overflow: "hidden",
border: `1px solid ${theme.palette.inputColor}`,
}}
color="primary"
disabled={
clientSecret.length > 0 || clientId.length > 0
}
fullWidth
onClick={() => {
// Hardcode some stuff?
// This could prolly be added to the app itself with a "default" client ID
startOauth2Request()
}}
color="primary"
>
{buttonClicked ? (
<CircularProgress style={{ color: "#f86a3e", width: 45, height: 45, margin: "auto", }} />
) : (
<span style={{display: "flex"}}>
<img
alt={selectedAction.app_name}
style={{ margin: 4, minHeight: 30, maxHeight: 30, borderRadius: theme.palette?.borderRadius, }}
src={selectedAction.large_image}
/>
<Typography style={{ margin: 0, marginLeft: 10, marginTop: 5, color: "#2f2f2f",}} variant="body1">
One-click Login
</Typography>
</span>
)}
</Button>
fullWidth
variant="contained"
style={{
marginBottom: 20,
marginTop: 20,
flex: 1,
textTransform: "none",
textAlign: "left",
justifyContent: "flex-start",
backgroundColor: "#ffffff",
color: "#2f2f2f",
borderRadius: theme.palette?.borderRadius,
minWidth: 275,
maxWidth: 275,
maxHeight: 50,
overflow: "hidden",
border: `1px solid ${theme.palette.inputColor}`,
}}
color="primary"
disabled={
clientSecret.length > 0 || clientId.length > 0
}
fullWidth
onClick={() => {
// Hardcode some stuff?
// This could prolly be added to the app itself with a "default" client ID
startOauth2Request()
}}
color="primary"
>
{buttonClicked ? (
<CircularProgress style={{ color: "#f86a3e", width: 45, height: 45, margin: "auto", }} />
) : (
<span style={{display: "flex"}}>
<img
alt={selectedAction.app_name}
style={{ margin: 4, minHeight: 30, maxHeight: 30, borderRadius: theme.palette?.borderRadius, }}
src={selectedAction.large_image}
/>
<Typography style={{ margin: 0, marginLeft: 10, marginTop: 8, color: "#2f2f2f",}} variant="body1">
One-click Login
</Typography>
</span>
)}
</Button>
if (authButtonOnly === true && (authenticationType.redirect_uri !== undefined && authenticationType.redirect_uri !== null && authenticationType.redirect_uri.length > 0) && (authenticationType.token_uri !== undefined && authenticationType.token_uri !== null && authenticationType.token_uri.length > 0)) {
return autoAuthButton
@@ -747,7 +747,8 @@ const AuthenticationOauth2 = (props) => {
</DialogTitle>
<DialogContent>
<span style={{}}>
Oauth2 requires a client ID and secret to authenticate, defined in the remote system. <span>Your redirect URL is <b>{window.location.origin}/set_authentication</b>&nbsp;-&nbsp;</span>
Oauth2 requires a Client ID and Client Secret to authenticate, defined in your apps' remote website. <span>Your redirect URL: <br/><b>{window.location.origin}/set_authentication</b><br/>
</span>
<a
target="_blank"
rel="norefferer"
@@ -760,51 +761,52 @@ const AuthenticationOauth2 = (props) => {
<div />
</span>
{isCloud && registeredApps?.includes(selectedApp?.name?.replaceAll(" ", "_").toLowerCase()) ?
<span>
<span style={{display: "flex"}}>
{autoAuthButton}
{buttonClicked ?
null
:
<Tooltip
color="primary"
title={"Force Admin Consent"}
placement="top"
>
<Button
fullWidth
variant="outlined"
style={{
maxWidth: 50,
marginBottom: 20,
marginTop: 20,
maxHeight: 50,
}}
color="primary"
disabled={
clientSecret.length > 0 || clientId.length > 0
}
fullWidth
onClick={() => {
// Hardcode some stuff?
// This could prolly be added to the app itself with a "default" client ID
//startOauth2Request(true)
startOauth2Request()
}}
color="primary"
>
<SupervisorAccountIcon />
</Button>
</Tooltip>
}
</span>
<Typography style={{textAlign: "center", marginTop: 0, marginBottom: 0, }}>
OR
</Typography>
</span>
: null}
{isCloud && registeredApps?.includes(selectedApp?.name?.replaceAll(" ", "_").toLowerCase()) ?
<span>
<span style={{display: "flex"}}>
{autoAuthButton}
{buttonClicked ?
null
:
<Tooltip
color="primary"
title={"Force Admin Consent"}
placement="top"
>
<Button
fullWidth
variant="outlined"
style={{
maxWidth: 40,
marginBottom: 20,
marginTop: 20,
maxHeight: 50,
marginLeft: 10,
}}
color="secondary"
disabled={
clientSecret.length > 0 || clientId.length > 0
}
fullWidth
onClick={() => {
// Hardcode some stuff?
// This could prolly be added to the app itself with a "default" client ID
//startOauth2Request(true)
startOauth2Request()
}}
color="primary"
>
<SupervisorAccountIcon />
</Button>
</Tooltip>
}
</span>
<Typography style={{textAlign: "center", marginTop: 0, marginBottom: 0, }}>
OR
</Typography>
</span>
: null}
{/*<TextField
style={{backgroundColor: theme.palette.inputColor, borderRadius: theme.palette?.borderRadius,}}
InputProps={{
@@ -499,7 +499,7 @@ const OrgHeaderexpandedNew = (props) => {
</div>
{userdata?.support ? (
<div style={{ alignItems: 'center' }}>
<div style={{ marginRight: '12px', color: theme.palette.text.primary, fontFamily: theme?.typography?.fontFamily }}>Status</div>
<div style={{ marginRight: '12px', color: theme.palette.text.primary, fontFamily: theme?.typography?.fontFamily, marginTop: 2.5 }}>Status</div>
<FormControl style={{ width: 220, height: 35 }}>
<Select
style={{ minWidth: 220, marginTop: 5, maxWidth: 220, height: 35, borderRadius: 4, color: theme.palette.textFieldStyle.color}}
@@ -711,11 +711,12 @@ const OrgHeaderexpandedNew = (props) => {
/>
</span>
</Grid>
{!selectedOrganization || selectedOrganization?.creator_org === undefined || selectedOrganization?.creator_org || null || selectedOrganization?.creator_org?.length > 0 ? null :
<CloudSyncTab
globalUrl={globalUrl}
userdata={userdata}
serverside={false}
/>
/>}
<Grid item xs={12} style={{ marginTop: 20, }}>
<Typography variant="h5" style={{ textAlign: "left", fontWeight: 500, }}>Workflow Backup Repository</Typography>
<Typography variant="body2" style={{ textAlign: "left", marginTop: 8, color: theme.palette.text.secondary, fontSize: 16, fontWeight: 400 }}>
+8 -6
View File
@@ -437,7 +437,8 @@ const ParsedAction = (props) => {
];
const getApp = (appId, setApp) => {
fetch(globalUrl + "/api/v1/apps/" + appId + "/config?openapi=false", {
const url = `${globalUrl}/api/v1/apps/${appId}/config?openapi=false`;
fetch(url, {
headers: {
Accept: "application/json",
},
@@ -447,7 +448,7 @@ const ParsedAction = (props) => {
if (response.status === 200) {
//toast("Successfully GOT app "+appId)
} else {
toast("Failed getting app");
toast.error("Failed getting app. Please try again or contact support@shuffler.io");
}
return response.json();
@@ -1711,6 +1712,7 @@ const ParsedAction = (props) => {
}
const sortByCategoryLabel = (a, b) => {
const aHasCategoryLabel = a.category_label !== undefined && a.category_label !== null && a.category_label.length > 0
const bHasCategoryLabel = b.category_label !== undefined && b.category_label !== null && b.category_label.length > 0
@@ -1739,11 +1741,12 @@ const ParsedAction = (props) => {
})
}
// Gets the most important actions first
const renderedActionOptions = deduplicateByName((
selectedApp.actions === undefined || selectedApp.actions === null ? [] :
selectedApp.actions.filter((a) =>
a.category_label !== undefined && a.category_label !== null && a.category_label.length > 0).concat(sortByKey(selectedApp.actions, "label"))
selectedApp.actions === undefined || selectedApp.actions === null ? [] :
isIntegration ? selectedApp.actions :
selectedApp.actions.filter((a) => a.category_label !== undefined && a.category_label !== null && a.category_label.length > 0).concat(sortByKey(selectedApp.actions, "label"))
).sort(sortByCategoryLabel))
@@ -2981,7 +2984,6 @@ const ParsedAction = (props) => {
dataLPIgnore="true"
autoComplete="off"
id="checkbox-search"
style={{
...theme.palette.textFieldStyle,
+7 -6
View File
@@ -223,7 +223,7 @@ const PartnerDetails = (props) => {
<div style={{ display: "flex" }}>
<div style={{ width: "100%", maxWidth: 434, marginRight: 10 }}>
<Typography variant="text" style={{ color: theme?.palette?.text?.primary, fontFamily: theme?.typography?.fontFamily }}>
Name
Company Name
</Typography>
<Skeleton
variant="rounded"
@@ -267,7 +267,7 @@ const PartnerDetails = (props) => {
/>
</div> */}
<div style={{ alignItems: "center" }}>
<div style={{ marginRight: "12px", color: theme?.palette?.text?.primary, fontFamily: theme?.typography?.fontFamily }}>
<div style={{ marginRight: "12px", color: theme?.palette?.text?.primary, fontFamily: theme?.typography?.fontFamily, marginTop: 2.5}}>
Solutions
</div>
<Skeleton
@@ -399,7 +399,7 @@ const PartnerDetails = (props) => {
variant="text"
style={{ color: theme.palette.text.primary, fontFamily: theme?.typography?.fontFamily }}
>
Name
Company Name
</Typography>
<TextField
required
@@ -419,7 +419,7 @@ const PartnerDetails = (props) => {
cursor: isDisabled ? "not-allowed" : "pointer",
}}
fullWidth={true}
placeholder="Name"
placeholder="Company Name"
type="name"
id="standard-required"
margin="normal"
@@ -544,7 +544,8 @@ const PartnerDetails = (props) => {
style={{
marginRight: "12px",
color: theme.palette.text.primary,
fontFamily: theme?.typography?.fontFamily
fontFamily: theme?.typography?.fontFamily,
marginTop: 2.5,
}}
>
Solutions
@@ -895,7 +896,7 @@ const PartnerDetails = (props) => {
cursor: isDisabled ? "not-allowed" : "pointer",
}}
fullWidth={true}
placeholder="support@shuffler.io"
placeholder="example@company.com"
type="name"
id="standard-required"
margin="normal"
+4 -4
View File
@@ -1362,10 +1362,10 @@ print('"' + encoded + '"')
<div style={{ maxHeight: 1700, overflowY: "auto", width: '100%', scrollbarColor: theme.palette.scrollbarColorTransparent, scrollbarWidth: 'thin' }}>
<div style={{ maxWidth: "calc(100% - 20px)" }}>
<Typography variant="h5" style={{ fontSize: 24, fontWeight: 500, textAlign: "left" }}>
Notification Workflow
Error Workflow
</Typography>
<Typography color="textSecondary" style={{ fontSize: 16, fontWeight: 400, marginTop: 5, }}>
The notification workflow triggers when an error occurs in one of your workflows. Each individual one will only start a workflow once every 2 minutes. <b>You can point child org notifications into the parent org notification by choosing it in the list.</b>
The error workflow triggers when an error occurs in one of your workflows. Each individual one will only start a workflow once every 2 minutes. <b>You can point child org errors to a parent org's error workflow by choosing it in the list.</b>
</Typography>
{modalView}
@@ -1614,12 +1614,12 @@ print('"' + encoded + '"')
</div>
}
<Typography variant="h5" style={{ marginTop: 50, fontSize: 24, display: clickedFromOrgTab ? null : "inline", marginBottom: clickedFromOrgTab ? 8 : null, }}>Notifications ({
<Typography variant="h5" style={{ marginTop: 50, fontSize: 24, display: clickedFromOrgTab ? null : "inline", marginBottom: clickedFromOrgTab ? 8 : null, }}>Errors ({
notifications?.filter((notification) => showRead === true || notification.read === false).length
})</Typography>
<Typography variant="body2" color="textSecondary" style={{ fontSize: 16, marginLeft: clickedFromOrgTab ? null : 25, color: clickedFromOrgTab ? "#9E9E9E" : null, }}>
Notifications help you find potential problems with your workflows and apps.&nbsp;
Error help you find potential problems with your workflows and apps.&nbsp;
<a
target="_blank"
rel="noopener noreferrer"
@@ -0,0 +1,223 @@
import React, { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import {
Button,
ButtonGroup,
CircularProgress,
Tooltip,
} from "@mui/material";
import {
Check as CheckIcon,
OpenInNew as OpenInNewIcon,
} from "@mui/icons-material";
import { green, yellow, red } from '../views/AngularWorkflow.jsx'
const RunDetectionTest = (props) => {
const {
globalUrl,
pipelines,
workflows,
ticketWebhook,
detectionWorkflowId,
changePipelineState,
submitPipelineWrapper,
} = props
const [executions, setExecutions] = React.useState([]);
const [detectionTestRunning, setDetectionTestRunning] = React.useState(false);
const [detectionTestExecutionId, setDetectionTestExecutionId] = React.useState("");
useEffect(() => {
if (detectionWorkflowId !== "") {
handleLoadExecutions(detectionWorkflowId)
}
}, [detectionWorkflowId])
if (workflows === undefined || workflows === null || workflows.length === 0) {
return null
}
const handleLoadExecutions = (workflowId, detectionTestRunning) => {
const url = `${globalUrl}/api/v2/workflows/${workflowId}/executions`
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 getting all executions");
}
return response.json();
})
.then((responseJson) => {
if (responseJson.success !== false && responseJson?.executions?.length > 0) {
if (detectionTestRunning === true) {
console.log("Checking executions in workflow: ", workflowId, responseJson.executions)
for (var executionKey in responseJson.executions) {
const curExec = responseJson.executions[executionKey]
if (curExec.execution_id === detectionTestExecutionId) {
continue
}
// started_at = unix timestamp
// check within the last 60 seconds
const datecomparison = (Date.now() / 1000) - 60
if (curExec.started_at >= datecomparison) {
if (curExec?.execution_argument?.includes("rule") && curExec?.execution_argument?.includes("Test Notepad Event")) {
setDetectionTestRunning(false)
setDetectionTestExecutionId(curExec.execution_id)
}
break;
}
}
} else {
setExecutions(responseJson.executions || [])
}
}
})
.catch((error) => {
toast(error.toString());
})
}
const runDetectionTest = () => {
setDetectionTestRunning(true)
if (ticketWebhook === "") {
setDetectionTestRunning(false)
toast.error("No ticketing webhook found. Please enable the ticketing workflow first.")
return
}
if (detectionWorkflowId === "") {
setDetectionTestRunning(false)
toast.error("No ticketing workflow found. Please enable the ticketing workflow first.")
return
}
if (haveDetectionPipelines() === false) {
setDetectionTestRunning(false)
toast.error("No detection pipelines found. Please deploy the Syslog (TCP) & Sigma pipelines first.")
return
}
// 1. Run a new pipeline which exits.
const detectionTest = `from {message: "<165>1 2025-10-06T12:34:56.789Z myhost.example.com myapp 1234 ID47 [huh eventSource=\\\"App\\\" EventID=\\\"4688\\\" NewProcessName=\\\"notepad.exe\\\" Context=\\\"Testing\\\"] This is a test log message"} | this = message.parse_syslog() | import`
for (var pipelineKey in pipelines) {
const curPipeline = pipelines[pipelineKey]
if (curPipeline.definition === detectionTest && changePipelineState !== undefined) {
changePipelineState(curPipeline, "stop");
}
}
// 1. Submit it to run
// 2. Check executions if they happened recently~
if (submitPipelineWrapper !== undefined) {
submitPipelineWrapper(detectionTest)
}
for (var i = 0; i < 10; i++) {
setTimeout(() => {
handleLoadExecutions(detectionWorkflowId, true)
}, i * 5000)
}
setTimeout(() => {
setDetectionTestRunning(false)
}, 60000)
}
const haveDetectionPipelines = () => {
if (pipelines === undefined) {
toast.warn("No pipelines found. Please create the Syslog (TCP) & Sigma pipelines first.")
return false
}
var foundCorrect = 0
for (var pipelineKey in pipelines) {
const curPipeline = pipelines[pipelineKey]
//if (curPipeline?.definition?.includes("load_tcp") && curPipeline?.definition?.includes("import")) {
// foundCorrect += 1
//}
if (curPipeline?.definition?.includes("sigma") && curPipeline?.definition?.includes("export")) {
foundCorrect += 1
}
}
if (foundCorrect >= 1) {
return true
}
return false
}
return (
<div style={{display: "flex", }}>
<ButtonGroup style={{minWidth: 150, maxWidth: 225,}}>
<Tooltip title={"Run a detection test with Sigma rules on your Tenzir Orborus instance. Requires: TCPC Syslog- & Sigma pipeline"} style={{}} aria-label={"Run detection test"}>
<div>
<Button
style={{minWidth: 150, maxWidth: 150, minHeight: 40, maxHeight: 40, }}
variant="outlined"
color="secondary"
disabled={haveDetectionPipelines() == false || ticketWebhook === "" || detectionWorkflowId === "" || detectionTestRunning}
onClick={() => {
//setPipelineModalOpen(true)
runDetectionTest()
}}
>
{
detectionTestRunning === true ? <CircularProgress size={20} style={{marginRight: 10, }} /> : "Run Detection Test"
}
</Button>
</div>
</Tooltip>
{detectionTestRunning === false && detectionTestExecutionId !== "" && detectionTestExecutionId !== undefined ?
<Tooltip title={`Go to detection test: ${detectionTestExecutionId}`} style={{}} aria-label={"Go to logs"}>
<a href={`/workflows/${detectionWorkflowId}?execution_id=${detectionTestExecutionId}`} target="_blank" rel="noopener noreferrer">
<Button
color="secondary"
style={{
minWidth: 75, maxWidth: 75,
minHeight: 40, maxHeight: 40,
}}
>
<CheckIcon style={{color: green}}/>
</Button>
</a>
</Tooltip>
: null}
</ButtonGroup>
{window?.location?.href?.includes("/detections/") === true ? null :
<Tooltip title={"Open Detection Tab"} style={{}} aria-label={""}>
<a href={`/detections/sigma`} target="_blank" rel="noopener noreferrer">
<OpenInNewIcon
color="secondary"
style={{
marginLeft: 20,
top: 8,
position: 'relative',
}}
/>
</a>
</Tooltip>
}
</div>
)
}
export default RunDetectionTest
+318 -189
View File
@@ -7,6 +7,7 @@ import {
ListItem,
ListItemText,
Button,
ButtonGroup,
Tooltip,
IconButton,
Dialog,
@@ -14,15 +15,21 @@ import {
DialogContent,
DialogActions,
TextField,
Chip,
CircularProgress,
} from '@mui/material';
import {
FileCopy as FileCopyIcon,
OpenInNew as OpenInNewIcon,
Padding,
FileCopy as FileCopyIcon,
OpenInNew as OpenInNewIcon,
Refresh as RefreshIcon,
Delete as DeleteIcon,
Check as CheckIcon,
} from "@mui/icons-material"
import { green, yellow, red } from '../views/AngularWorkflow.jsx'
import { Box, Skeleton, Typography } from '@mui/material';
import { Context } from '../context/ContextApi.jsx';
import RunDetectionTest from '../components/RunDetectionTest.jsx';
const SchedulesTab = memo((props) => {
const {globalUrl, users, } = props;
@@ -30,13 +37,58 @@ const SchedulesTab = memo((props) => {
const [allSchedules, setAllSchedules] = React.useState([]);
const [pipelines, setPipelines] = React.useState([]);
const [showLoader, setShowLoader] = React.useState(true);
const [workflows, setWorkflows] = React.useState([]);
const [pipelineModalOpen, setPipelineModalOpen] = React.useState(false);
const [newPipelineValue, setNewPipelineValue] = React.useState("export | sigma /tmp/sigma_rules | to SHUFFLE_WEBHOOK");
const [newPipelineValue, setNewPipelineValue] = React.useState(`export | sigma "/tmp/sigma_rules" | to "SHUFFLE_WEBHOOK"`);
const [ticketWebhook, setTicketWebhook] = React.useState("");
const [detectionWorkflowId, setDetectionWorkflowId] = React.useState("");
const { themeMode, brandColor } = useContext(Context);
const theme = getTheme(themeMode, brandColor);
const handleGetWorkflows = () => {
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 getting all workflows");
}
return response.json();
})
.then((responseJson) => {
if (responseJson.success !== false) {
setWorkflows(responseJson || []);
for (var i = 0; i < responseJson?.length; i++) {
if (responseJson[i].background_processing === true && responseJson[i].name.toLowerCase().includes("ingest tickets") && responseJson[i].triggers !== undefined) {
for (var triggerkey in responseJson[i].triggers) {
if (responseJson[i].triggers[triggerkey].trigger_type === "WEBHOOK") {
setDetectionWorkflowId(responseJson[i].id)
setTicketWebhook(`${globalUrl}/api/v1/hooks/webhook_${responseJson[i].triggers[triggerkey].id}`)
setNewPipelineValue(`export | sigma /tmp/sigma_rules | to ${globalUrl}/api/v1/hooks/webhook_${responseJson[i].triggers[triggerkey].id}`)
break;
}
}
}
}
}
})
.catch((error) => {
toast(error.toString());
})
}
useEffect(() => {
handleGetWorkflows()
if (allSchedules.length === 0 && webHooks.length === 0 && pipelines.length === 0) {
handleGetAllTriggers()
}
@@ -58,8 +110,11 @@ const SchedulesTab = memo((props) => {
environment: pipeline.environment,
};
if (state === "start") toast("starting the pipeline");
else toast.info("Stopping the pipeline. This may take a few minutes to propagate.")
if (state === "start") {
toast("starting the pipeline")
} else {
toast.info("Stopping a pipeline. This may take a few minutes to propagate.")
}
const url = `${globalUrl}/api/v1/triggers/pipeline`;
fetch(url, {
@@ -144,16 +199,65 @@ const SchedulesTab = memo((props) => {
},
}}
>
<DialogTitle>
<DialogTitle style={{padding: "50px 50px 25px 50px", }}>
<Typography variant='h5' color="textPrimary" >
Run a Tenzir pipeline
</Typography>
<Typography variant="body2" color="textSecondary" style={{marginTop: 10, }}>
Alpha feature. Deploys to the first available Orborus location. <a href="https://docs.tenzir.com/pipelines" target="_blank" rel="noopener noreferrer" style={{ color: theme.palette.primary.main }}>Explore Tenzir Pipelines</a>. The example below exports everything in the Tenzir database, runs Sigma rules on it, and forwards the results to a Shuffle webhook.
Alpha feature. Deploys to the first available Orborus location. <a href="https://docs.tenzir.com/explanations/architecture/pipeline/" target="_blank" rel="noopener noreferrer" style={{ color: theme.palette.primary.main }}>Explore Tenzir Pipelines</a>. The example below exports everything in the Tenzir database, runs Sigma rules on it, and forwards the results to a Shuffle webhook.
</Typography>
</DialogTitle>
<DialogContent>
<div>
<DialogContent style={{padding: "0px 50px 50px 50px", }}>
<div style={{marginTop: 10, }}>
<Chip
onClick={() => {
setNewPipelineValue(`load_tcp "0.0.0.0:1514" { read_syslog } | import`)
}}
label={"Syslog Listener (TCP)"}
variant="outlined"
color="secondary"
style={{
marginRight: 10,
}}
/>
<Chip
onClick={() => {
setNewPipelineValue(`load_udp "0.0.0.0:1514", insert_newlines=true | read_syslog | import`)
}}
label={"Syslog Listener (UDP)"}
variant="outlined"
color="secondary"
style={{
marginRight: 10,
}}
/>
<Chip
onClick={() => {
setNewPipelineValue(`export live=true | sigma "/tmp/sigma_rules" | to "${ticketWebhook !== "" ? ticketWebhook : "SHUFFLE_WEBHOOK"}"`)
}}
label={"Sigma Rules"}
variant="outlined"
color="secondary"
style={{
marginRight: 10,
}}
/>
<Chip
onClick={() => {
setNewPipelineValue(`export live=true | to_opensearch "localhost:9200", action="create", index="shuffle_logs", user="admin", passwd="PASSWORD"`)
}}
label={"Opensearch Ingest"}
variant="outlined"
color="secondary"
style={{
marginRight: 10,
}}
/>
<TextField
color="primary"
style={{ backgroundColor: theme.palette.textFieldStyle.backgroundColor,}}
@@ -163,8 +267,9 @@ const SchedulesTab = memo((props) => {
minRows={4}
required
fullWidth={true}
defaultValue="export | sigma /tmp/sigma_rules | to SHUFFLE_WEBHOOK"
placeholder="export | sigma /tmp/sigma_rules | to SHUFFLE_WEBHOOK"
defaultValue={`export | sigma /tmp/sigma_rules | to ${ticketWebhook !== "" ? ticketWebhook : "SHUFFLE_WEBHOOK"}`}
value={newPipelineValue}
placeholder={`export | sigma /tmp/sigma_rules | to ${ticketWebhook !== "" ? ticketWebhook : "SHUFFLE_WEBHOOK"}`}
id="environment_name"
margin="normal"
variant="outlined"
@@ -174,7 +279,7 @@ const SchedulesTab = memo((props) => {
/>
</div>
</DialogContent>
<DialogActions>
<DialogActions style={{padding: "0px 50px 50px 50px", }}>
<Button
style={{ borderRadius: "2px", fontSize: 16, textTransform: "none", color: theme.palette.primary.main }}
onClick={() => {
@@ -191,7 +296,7 @@ const SchedulesTab = memo((props) => {
}}
color="primary"
>
Submit
Create Pipeline
</Button>
</DialogActions>
</Dialog>
@@ -232,18 +337,18 @@ const SchedulesTab = memo((props) => {
})
.then((responseJson) => {
if (!responseJson.success && pipelineConfig.type !== "delete") {
toast("Failed to set pipeline: " + responseJson.reason);
toast.error("Failed to set pipeline: " + responseJson.reason);
} else {
if (pipelineConfig.type === "create") {
toast("Pipeline will be created: " + responseJson.reason)
toast.success("Pipeline will be created. Page will autorefresh in a bit: " + responseJson.reason)
setPipelineModalOpen(false)
} else if (pipelineConfig.type === "stop") {
toast("Pipeline will be stopped: " + responseJson.reason)
toast.success("Pipeline will be stopped: " + responseJson.reason)
setPipelineModalOpen(false)
} else {
toast("Unknown pipeline type: " + pipelineConfig.type)
toast.info("Unknown pipeline type: " + pipelineConfig.type)
}
}
@@ -274,12 +379,7 @@ const SchedulesTab = memo((props) => {
// Just use this one?
const url =
globalUrl +
"/api/v1/workflows/" +
data["workflow_id"] +
"/schedule/" +
data.id;
const url = `${globalUrl}/api/v1/workflows/${data?.workflow_id}/schedule/${data.id}`;
fetch(url, {
method: "DELETE",
credentials: "include",
@@ -414,7 +514,7 @@ const SchedulesTab = memo((props) => {
//toast(error.toString());
console.log("Get schedule error: ", error.toString());
});
};
}
const startWebHook = (trigger) => {
const hookname = trigger.info.name;
@@ -490,8 +590,197 @@ const SchedulesTab = memo((props) => {
Triggers are Automatic Workflow starters. <b>Status: Schedules ({allSchedules.length}), Webhooks ({webHooks.length}), Pipelines ({pipelines.length})</b>
</Typography>
<div style={{ marginTop: 50, marginBottom: 20 }}>
<Typography variant='h6' color="textPrimary" >Pipelines</Typography>
<Typography variant='body2' color="textSecondary" >
Controls Pipelines on your Orborus Runners, e.g. for Log Ingestion, MQ subscriptions or Sigma Detections.{" "}
<a
target="_blank"
rel="noopener noreferrer"
href="/docs/triggers#pipelines"
style={{ color: theme.palette.primary.main }}
>
Learn more
</a>
</Typography>
<div style={{marginBottom: 10, marginTop: 10, }}/>
<Button
style={{}}
variant="contained"
color="primary"
onClick={() => setPipelineModalOpen(true)}
>
Deploy New Pipeline
</Button>
<Button
style={{marginLeft: 10, }}
variant="outlined"
color="primary"
onClick={() => {
handleGetAllTriggers()
}}
>
<RefreshIcon style={{}}/>
</Button>
</div>
<div
style={{
borderRadius: 4,
marginTop: 24,
border: theme.palette.defaultBorder,
width: "100%",
overflowX: pipelines?.length === 0 ? "hidden" : "auto",
paddingBottom: 0,
}}
>
<List
style={{
borderRadius: 4,
width: '100%',
tableLayout: "auto",
display: "table",
minWidth: pipelines?.length === 0 ? "auto" : 800,
overflowX: "auto",
paddingBottom: 0
}}>
<ListItem style={{width:"100%", borderBottom:theme.palette.defaultBorder, display: "table-row"}}>
{["Status", "Command", "Environment", "Total Runs", "Actions"].map((header, index) => (
<ListItemText
key={index}
primary={header}
style={{
display: "table-cell",
padding: index === 0 ? "0px 8px 8px 15px": "0px 8px 8px 8px",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
borderBottom: theme.palette.defaultBorder,
position: "sticky",
}}
/>
))}
</ListItem>
{showLoader ? (
[...Array(6)].map((_, rowIndex) => {
return (
<ListItem
key={rowIndex}
style={{
display: "table-row",
backgroundColor: theme.palette.platformColor,
}}
>
{Array(5)
.fill()
.map((_, colIndex) => {
return (
<ListItemText
key={colIndex}
style={{
display: "table-cell",
padding: "8px",
}}
>
<Skeleton
variant="text"
animation="wave"
sx={{
backgroundColor: theme.palette.loaderColor,
height: "20px",
borderRadius: "4px",
}}
/>
</ListItemText>
)
})}
</ListItem>
)
}
)
) : (
pipelines?.length === 0 ? (
<div style={{width: "100%", textAlign: "center", }}>
<Typography style={{color: theme.palette.text.primary, padding: 20,width: "100%", fontSize: 16, textAlign: 'center'}}>No pipelines found.</Typography>
</div>
):(
pipelines.map((pipeline, index) => {
var bgColor = themeMode === "dark" ? "#212121" : "#FFFFFF";
if (index % 2 === 0) {
bgColor = themeMode === "dark" ? "#1A1A1A" : "#EAEAEA";
}
return (
<ListItem key={index} style={{ backgroundColor: bgColor, borderRadius: index === pipelines.length - 1 ? 8 : 0, display: 'table-row', }} >
<ListItemText
style={{ minWidth: 75, maxWidth: 75, overflow: "auto", display:'table-cell', padding: "8px 8px 8px 15px" }}
primary={pipeline.state}
/>
<ListItemText
style={{ maxWidth: 350, overflow: "auto", display:'table-cell', padding: "8px 8px 8px 15px" }}
primary={pipeline.definition}
/>
<ListItemText
style={{ display:'table-cell', padding: 8 }}
primary={pipeline.environment}
/>
<ListItemText
style={{ display:'table-cell', }}
primary={pipeline.total_runs}
/>
<ListItemText
style={{ display:'table-cell', }}
primary={(
<Box display="table-cell">
<Tooltip title={"Copy deletion command"} style={{}} aria-label={"Go to logs"}>
<IconButton style={{marginRight: 10, }} onClick={() => {
const copyContent = `curl -XPOST http://localhost:5160/api/v0/pipeline/delete -H "Content-Type: application/json" -d '{"id":"${pipeline.id}"}' -v`
const copyText = navigator?.clipboard?.writeText(copyContent)
if (copyText) {
toast.success("Pipeline copied to clipboard")
} else {
toast.error("Failed to copy pipeline")
}
}}>
<FileCopyIcon />
</IconButton>
</Tooltip>
<Tooltip title={"Delete Pipeline"} style={{}} aria-label={"Go to logs"}>
<IconButton style={{marginRight: 10, }} onClick={() => {
changePipelineState(pipeline, "stop");
}}>
<DeleteIcon style={{color: red, }} />
</IconButton>
</Tooltip>
</Box>
)}
/>
</ListItem>
);
})
)
)}
</List>
<div style={{margin: 25, }}>
<RunDetectionTest
globalUrl={globalUrl}
pipelines={pipelines}
workflows={workflows}
ticketWebhook={ticketWebhook}
detectionWorkflowId={detectionWorkflowId}
changePipelineState={changePipelineState}
submitPipelineWrapper={submitPipelineWrapper}
/>
</div>
</div>
<div>
<Typography variant='h6' color="textPrimary" style={{ marginBottom: 8, marginTop: 0, fontWeight: 500}}>
<Typography variant='h6' color="textPrimary" style={{ marginBottom: 8, marginTop: 50, fontWeight: 500}}>
Schedules
</Typography>
<Typography variant='body2' color="textSecondary">
@@ -903,11 +1192,9 @@ const SchedulesTab = memo((props) => {
style={{
textTransform: 'none',
fontSize: 16,
color:webhook.status === "running" ? '#1a1a1a' : null,
backgroundColor: webhook.status === "running" ? '#ff8544' : null,
width: 150,
}}
color={webhook.status === "running" ? "secondary" : "primary"}
color={"secondary"}
variant={webhook.status === "running" ? "contained" : "outlined"}
disabled={webhook.status === "uninitialized"}
onClick={() => {
@@ -929,166 +1216,7 @@ const SchedulesTab = memo((props) => {
)}
</List>
</div>
<div style={{ marginTop: 50, marginBottom: 20 }}>
<Typography variant='h6' color="textPrimary" >Pipelines</Typography>
<Typography variant='body2' color="textSecondary" >
Controls Pipelines on your Orborus Runners, e.g. for Log Ingestion, MQ subscriptions or Sigma Detections.{" "}
<a
target="_blank"
rel="noopener noreferrer"
href="/docs/triggers#pipelines"
style={{ color: theme.palette.primary.main }}
>
Learn more
</a>
</Typography>
<div style={{marginBottom: 10, marginTop: 10, }}/>
<Button
style={{ borderRadius: 4, textTransform: "capitalize", fontSize: 16, }}
variant="contained"
color="primary"
onClick={() => setPipelineModalOpen(true)}
>
Deploy New Pipeline
</Button>
</div>
<div
style={{
borderRadius: 4,
marginTop: 24,
border: theme.palette.defaultBorder,
width: "100%",
overflowX: pipelines?.length === 0 ? "hidden" : "auto",
paddingBottom: 0,
}}
>
<List
style={{
borderRadius: 4,
width: '100%',
tableLayout: "auto",
display: "table",
minWidth: pipelines?.length === 0 ? "auto" : 800,
overflowX: "auto",
paddingBottom: 0
}}>
<ListItem style={{width:"100%", borderBottom:theme.palette.defaultBorder, display: "table-row"}}>
{["Command", "Environment", "Total Runs", "Actions"].map((header, index) => (
<ListItemText
key={index}
primary={header}
style={{
display: "table-cell",
padding: index === 0 ? "0px 8px 8px 15px": "0px 8px 8px 8px",
whiteSpace: "nowrap",
textOverflow: "ellipsis",
borderBottom: theme.palette.defaultBorder,
position: "sticky",
}}
/>
))}
</ListItem>
{showLoader ? (
[...Array(6)].map((_, rowIndex) => {
return (
<ListItem
key={rowIndex}
style={{
display: "table-row",
backgroundColor: theme.palette.platformColor,
}}
>
{Array(5)
.fill()
.map((_, colIndex) => {
return (
<ListItemText
key={colIndex}
style={{
display: "table-cell",
padding: "8px",
}}
>
<Skeleton
variant="text"
animation="wave"
sx={{
backgroundColor: theme.palette.loaderColor,
height: "20px",
borderRadius: "4px",
}}
/>
</ListItemText>
)
})}
</ListItem>
)
}
)
): (
pipelines?.length === 0 ? (
<div style={{width: "100%", textAlign: "center", }}>
<Typography style={{color: theme.palette.text.primary, padding: 20,width: "100%", fontSize: 16, textAlign: 'center'}}>No pipeline trigger found</Typography>
</div>
):(
pipelines.map((pipeline, index) => {
var bgColor = themeMode === "dark" ? "#212121" : "#FFFFFF";
if (index % 2 === 0) {
bgColor = themeMode === "dark" ? "#1A1A1A" : "#EAEAEA";
}
return (
<ListItem key={index} style={{ backgroundColor: bgColor, borderRadius: index === pipelines.length - 1 ? 8 : 0, display: 'table-row', }} >
<ListItemText
style={{ display:'table-cell', padding: "8px 8px 8px 15px" }}
primary={pipeline.definition}
/>
<ListItemText
style={{ display:'table-cell', padding: 8 }}
primary={pipeline.environment}
/>
<ListItemText
style={{ display:'table-cell', }}
primary={pipeline.total_runs}
/>
<ListItemText
style={{ display:'table-cell', }}
primary={(
<Box display="table-cell">
<Button
style={{
textTransform: 'none',
fontSize: 16,
}}
variant={"outlined"}
disabled={pipeline.status === "uninitialized"}
onClick={() => {
changePipelineState(pipeline, "stop");
/*
if (pipeline.status === "running") {
changePipelineState(pipeline, "stop");
} else changePipelineState(pipeline, "start");
*/
}}
>
Stop Pipeline
</Button>
</Box>
)}
/>
</ListItem>
);
})
)
)}
</List>
</div>
</div>
</div>
</div>
@@ -1096,3 +1224,4 @@ const SchedulesTab = memo((props) => {
});
export default SchedulesTab;
+34 -1
View File
@@ -147,6 +147,8 @@ const CodeEditor = (props) => {
// Auto-indent JSON-like content (with safety hehe)
const autoIndentContent = React.useCallback((content) => {
return content
// Safety checks :)
if (!content || typeof content !== 'string' || content.trim().length === 0) {
return content;
@@ -173,6 +175,7 @@ const CodeEditor = (props) => {
}
}, []);
const [localcodedata, setlocalcodedata] = React.useState(codedata === undefined || codedata === null || codedata.length === 0 ? "" : codedata);
// const {codelang, setcodelang} = props
@@ -1832,6 +1835,7 @@ const CodeEditor = (props) => {
display: 'flex',
}}
>
<div style={{ display: "flex" }}>
<DialogTitle
style={{
@@ -1842,6 +1846,35 @@ const CodeEditor = (props) => {
File Editor ({localcodedata.length})
</DialogTitle>
</div>
<IconButton
style={{
position: "absolute",
height: 50,
width: 50,
right: 25,
top: 90,
zIndex: 5000,
}}
disabled={localcodedata === undefined || localcodedata === null || localcodedata.length === 0}
onClick={() => {
const indentedText = IndentJsonLikeString(localcodedata, 2)
if (indentedText !== undefined && indentedText !== null) {
setlocalcodedata(indentedText)
} else {
toast.warn("Could not indent the text. Please check the input format.", { autoClose: 5000 })
}
}}
color="secondary"
>
<Tooltip
title={"Indent Text"}
placement="top"
>
<FormatIndentIncreaseIcon />
</Tooltip>
</IconButton>
</div>
:
<div
@@ -2270,7 +2303,7 @@ const CodeEditor = (props) => {
width: 50,
marginLeft: 100,
}}
disabled={editorData === undefined || editorData.example === undefined || editorData.example === null || editorData.example.length === 0}
disabled={localcodedata === undefined || localcodedata === null || localcodedata.length === 0}
onClick={() => {
const indentedText = IndentJsonLikeString(localcodedata, 2)
if (indentedText !== undefined && indentedText !== null) {
+12 -2
View File
@@ -20,6 +20,7 @@ import {
Zoom,
Chip,
} from '@mui/material';
import { useDebouncedCallback } from "../utils/useDebouncedCallback";
import WorkflowPaper from "../components/WorkflowPaper.jsx"
import WorkflowPaperNew from "../components/WorkflowPaperNew.jsx"
@@ -172,6 +173,7 @@ const AppGrid = props => {
// value={currentRefinement}
const SearchBox = ({currentRefinement, refine, isSearchStalled} ) => {
var defaultSearch = ""
const [inputValue, setInputValue] = useState("")
useEffect(() => {
if (window !== undefined && window.location !== undefined && window.location.search !== undefined && window.location.search !== null) {
const urlSearchParams = new URLSearchParams(window.location.search)
@@ -185,6 +187,12 @@ const AppGrid = props => {
}
}, [])
useEffect(() => {
setInputValue(currentRefinement || defaultSearch || "")
}, [currentRefinement])
const debouncedRefine = useDebouncedCallback((value) => refine(value), 300)
if (localMessage !== inputsearch && inputsearch !== undefined && inputsearch !== null && inputsearch.length > 0) {
//setLocalMessage(inputsearch)
refine(inputsearch)
@@ -217,12 +225,14 @@ const AppGrid = props => {
autoComplete='off'
type="search"
color="primary"
value={currentRefinement}
value={inputValue}
placeholder="Find Workflows..."
id="shuffle_search_field"
onChange={(event) => {
removeQuery("q")
refine(event.currentTarget.value)
const value = event.currentTarget.value
setInputValue(value)
debouncedRefine(value)
}}
onKeyDown={(event) => {
if(event.key === "Enter") {