Sync + Added dashboard for stats API
This commit is contained in:
@@ -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>
|
||||
|
||||
<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`;
|
||||
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -143,6 +143,7 @@ const Detection = (props) => {
|
||||
size="small"
|
||||
sx={{ mr: 2 }}
|
||||
value={searchQuery}
|
||||
disabled
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
/>
|
||||
{/* <Button
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
{/*
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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> - </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 }}>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
Error help you find potential problems with your workflows and apps.
|
||||
<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
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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") {
|
||||
|
||||
Reference in New Issue
Block a user