dashboard upates

This commit is contained in:
its-a-feature
2026-05-07 08:45:32 -05:00
parent f9f1677a1d
commit cea59d06c2
3 changed files with 1229 additions and 373 deletions
@@ -5,9 +5,13 @@ import Typography from '@mui/material/Typography';
import {useNavigate} from 'react-router-dom';
import {getStringSize} from "../Callbacks/ResponseDisplayTable";
import {getSkewedNow} from "../../utilities/Time";
import MenuItem from '@mui/material/MenuItem';
import Button from '@mui/material/Button';
import TextField from '@mui/material/TextField';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import ToggleButton from '@mui/material/ToggleButton';
import ToggleButtonGroup from '@mui/material/ToggleButtonGroup';
import Popover from '@mui/material/Popover';
import {CallbackDataCard, DashboardEmptyCard, GaugeCard, LineTimeMultiChartCard, PieChartCard, TableDataCard} from './DashboardComponents';
import {MythicAgentSVGIcon} from "../../MythicComponents/MythicAgentSVGIcon";
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
@@ -41,7 +45,6 @@ import {GetMythicSetting, useSetMythicSetting} from "../../MythicComponents/Myth
import FormatListBulletedAddIcon from '@mui/icons-material/FormatListBulletedAdd';
import AddchartIcon from '@mui/icons-material/Addchart';
import PlaylistRemoveIcon from '@mui/icons-material/PlaylistRemove';
import {MythicSelectFromListDialog} from "../../MythicComponents/MythicSelectFromListDialog";
import {PreviewFileMediaDialog} from "../../MythicComponents/PreviewFileMedia";
import {faPhotoVideo} from '@fortawesome/free-solid-svg-icons';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
@@ -60,6 +63,7 @@ import {ImageWithAuth} from "../../utilities/ImageWithAuth";
import {MythicPageHeader, MythicPageHeaderChip} from "../../MythicComponents/MythicPageHeader";
import {MythicStatusChip} from "../../MythicComponents/MythicStatusChip";
import {MythicLoadingState} from "../../MythicComponents/MythicStateDisplay";
import TuneIcon from '@mui/icons-material/Tune';
const LeadDashboardQuery = gql`
${taskingDataFragment}
@@ -236,6 +240,74 @@ const errorColors = [
];
const dashboardOptions = ["operator", "lead", "custom"];
const dashboardElementGridSpans = {
"Activity Timeline": 2,
"Credentials": 2,
"Downloads": 2,
"Eventing Status": 2,
"My Operations": 2,
"Payloads": 2,
"Proxy Usage": 2,
"Recent Screenshots": 2,
"Screenshots": 2,
"Tasking": 2,
"Top 10 Active Hosts": 2,
"Top 10 Host Contexts": 2,
"Top 10 User Contexts": 2,
};
const getDashboardRowTemplate = (dashboardRow = []) => {
const columnCount = dashboardRow.reduce((total, dashboardElement) => {
return total + (dashboardElementGridSpans[dashboardElement] || 1);
}, 0);
return `repeat(${Math.max(1, columnCount)}, minmax(0, 1fr))`;
};
const ActiveCallbackRecentHoursControl = ({recentHours, onChangeRecent}) => {
const [anchorEl, setAnchorEl] = React.useState(null);
const open = Boolean(anchorEl);
const onOpen = (event) => {
event.stopPropagation();
setAnchorEl(event.currentTarget);
};
const onClose = () => {
setAnchorEl(null);
};
const onChange = (name, value, error) => {
const nextValue = Math.max(1, Number(value) || 1);
onChangeRecent(name, nextValue, error);
};
return (
<>
<MythicStyledTooltip title={`Recent check-in window: ${recentHours} ${recentHours === 1 ? "hour" : "hours"}`}>
<IconButton className="mythic-dashboard-icon-button mythic-dashboard-icon-button-hover-info" onClick={onOpen} size="small">
<TuneIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<Popover
anchorEl={anchorEl}
anchorOrigin={{vertical: "bottom", horizontal: "right"}}
onClose={onClose}
open={open}
transformOrigin={{vertical: "top", horizontal: "right"}}
>
<div className="mythic-dashboard-widget-settings-popover">
<div className="mythic-dashboard-widget-settings-title">Recent callback window</div>
<div className="mythic-dashboard-widget-settings-copy">
Count callbacks that checked in within this many hours.
</div>
<MythicTextField
type="number"
value={recentHours}
name="Recent hours"
onChange={onChange}
showLabel={true}
marginBottom="0px"
width={9}
/>
</div>
</Popover>
</>
);
};
const ActiveCallbacksDashboardElement = ({me, data, editing, removeElement}) => {
const [recentHours, setRecentHours] = React.useState(1);
const navigate = useNavigate();
@@ -261,41 +333,30 @@ const ActiveCallbacksDashboardElement = ({me, data, editing, removeElement}) =>
}
return prev;
}, 0);
setActive({"active": newActive, "recent": recent, "total": data?.callback?.length});
setActive({"active": newActive || 0, "recent": recent, "total": data?.callback?.length || 0});
}, [data, recentHours]);
const onChangeRecent = (name, value, error) => {
setRecentHours(value);
}
const activeRatio = active.total > 0 ? active.active / active.total : 0;
const activeStatus = active.total === 0 ? "neutral" : activeRatio > 0.5 ? "success" : active.active > 0 ? "warning" : "danger";
const activeStatusLabel = active.total === 0 ? "No callbacks" : active.active > 0 ? "Live callbacks" : "None active";
return (
<CallbackDataCard data={active}
<CallbackDataCard
mainTitle={
<div className="mythic-dashboard-metric-title-row">
{"Active Callbacks"}
<MythicTextField type={"number"} value={recentHours} name={"recent hours"} onChange={onChangeRecent}
showLabel={true} inline={true} marginBottom={"0px"}
variant={"standard"}
width={5} />
</div>}
secondTitle={
<div style={{display: "flex", flexDirection: "row"}}>
{"Recent Checkins <" + recentHours + "hrs"}
</div>
"Active Callbacks"
}
mainElement={
<>
<Typography className="mythic-dashboard-metric-value" variant={"h1"} onClick={() => navigate("/new/callbacks")}>
{active.active}
</Typography>
<Typography className="mythic-dashboard-metric-total" variant={"h5"}>
/ {active.total}
</Typography>
</>
}
secondaryElement={
<Typography className="mythic-dashboard-metric-secondary" variant={"h2"}>
{active.recent}
</Typography>
actions={
<ActiveCallbackRecentHoursControl recentHours={recentHours} onChangeRecent={onChangeRecent} />
}
primaryValue={active.active}
totalValue={active.total}
primaryLabel="Active callbacks"
secondaryValue={active.recent}
secondaryLabel={`Recent check-ins under ${recentHours} ${recentHours === 1 ? "hour" : "hours"}`}
statusLabel={activeStatusLabel}
statusLevel={activeStatus}
onClick={() => navigate("/new/callbacks")}
editing={editing}
removeElement={removeElement}
/>
@@ -453,10 +514,10 @@ const ProxyUsageDashboardElement = ({me, data, editing, removeElement}) => {
callbackPortActiveArrayOptions.push({
label: key,
value: <>
<Typography>
<Typography className="mythic-dashboard-table-secondary">
{"Tx: " + getStringSize({cellData: {"plaintext": String(value.sent)}})}
</Typography>
<Typography>
<Typography className="mythic-dashboard-table-secondary">
{"Rx: " + getStringSize({cellData: {"plaintext": String(value.received)}})}
</Typography>
</>,
@@ -481,8 +542,8 @@ const ProxyUsageDashboardElement = ({me, data, editing, removeElement}) => {
<TableBody >
{callbackPorts.map( (d, index) => (
<TableRow hover key={d.label + index} onClick={() => {handlePortClick(d)}} style={{cursor: "pointer"}}>
<MythicTableCell>{d.label}</MythicTableCell>
<MythicTableCell>{d.value}</MythicTableCell>
<MythicTableCell className="mythic-dashboard-table-cell-primary">{d.label}</MythicTableCell>
<MythicTableCell className="mythic-dashboard-table-cell-tight">{d.value}</MythicTableCell>
</TableRow>
)
)}
@@ -572,8 +633,8 @@ const Top10UserContextsDashboardElement = ({me, data, editing, removeElement}) =
<TableBody >
{taskedUser.map( (d, index) => (
<TableRow hover key={d.label + index} onClick={() => {handleUserContextClick(d)}} style={{cursor: "pointer"}}>
<MythicTableCell>{d.label}</MythicTableCell>
<MythicTableCell>{d.value}</MythicTableCell>
<MythicTableCell className="mythic-dashboard-table-cell-primary">{d.label}</MythicTableCell>
<MythicTableCell className="mythic-dashboard-table-cell-count">{d.value}</MythicTableCell>
</TableRow>
)
)}
@@ -659,8 +720,8 @@ const Top10HostContextsDashboardElement = ({me, data, editing, removeElement}) =
<TableBody >
{taskedHosts.map( (d, index) => (
<TableRow hover key={d.label + index} onClick={() => {handleHostContextClick(d)}} style={{cursor: "pointer"}}>
<MythicTableCell>{d.label}</MythicTableCell>
<MythicTableCell>{d.value}</MythicTableCell>
<MythicTableCell className="mythic-dashboard-table-cell-primary">{d.label}</MythicTableCell>
<MythicTableCell className="mythic-dashboard-table-cell-count">{d.value}</MythicTableCell>
</TableRow>
)
)}
@@ -687,29 +748,32 @@ const Top10RecentPayloadsDashboardElement = ({me, data, editing, removeElement})
return {
payload: p,
id: p.uuid,
label: <div style={{display: "flex", alignItems: "center"}}>
label: <div className="mythic-dashboard-table-identity">
<MythicStyledTooltip title={p.payloadtype.name} tooltipStyle={{marginRight: "5px"}}>
<MythicAgentSVGIcon payload_type={p.payloadtype.name} style={{width: "20px", height: "20px"}} />
</MythicStyledTooltip>
{b64DecodeUnicode(p.filemetum.filename_text)}
<span className="mythic-dashboard-table-primary-text">{b64DecodeUnicode(p.filemetum.filename_text)}</span>
</div>,
value: <>
value: <div className="mythic-dashboard-table-actions-inline">
<PayloadsTableRowBuildStatus {...p} />
<MythicStyledTooltip title={"View Payload Configuration"} tooltipStyle={{marginRight: "5px"}}>
<InfoIconOutline color={"info"} onClick={(e)=> clickDetail(e, p)} />
<InfoIconOutline className="mythic-dashboard-table-icon-action mythic-dashboard-table-icon-action-info" onClick={(e)=> clickDetail(e, p)} />
</MythicStyledTooltip>
</>
</div>
}
}) || [];
setPayloads(newPayloadData);
}, [data]);
if( data.payload && payloads.length === 0){
return (
<DashboardEmptyCard
<TableDataCard
title={"Recently Created Payloads"}
editing={editing}
removeElement={removeElement}
title={"No Payloads Created"}
action={
empty={true}
emptyTitle="No payloads created"
emptyDescription="Build a payload to start populating this view."
emptyAction={
<Button
className="mythic-dashboard-primary-button"
onClick={() => navigate("/new/createpayload")}
@@ -719,9 +783,7 @@ const Top10RecentPayloadsDashboardElement = ({me, data, editing, removeElement})
Create Your First Payload
</Button>
}
>
Build a payload to start populating this view.
</DashboardEmptyCard>
/>
)
}
return (
@@ -739,8 +801,8 @@ const Top10RecentPayloadsDashboardElement = ({me, data, editing, removeElement})
<TableBody >
{payloads.map( (d, index) => (
<TableRow hover key={d.label + index} >
<MythicTableCell>{d.label}</MythicTableCell>
<MythicTableCell>{d.value}</MythicTableCell>
<MythicTableCell className="mythic-dashboard-table-cell-primary">{d.label}</MythicTableCell>
<MythicTableCell className="mythic-dashboard-table-cell-actions">{d.value}</MythicTableCell>
</TableRow>
)
)}
@@ -789,17 +851,17 @@ const Top10RecentWorkflowsDashboardElement = ({me, data, editing, removeElement}
return {
entry: p,
id: p.id,
label: <>
<Typography>
label: <div className="mythic-dashboard-table-stack">
<Typography className="mythic-dashboard-table-primary-text">
{p.eventgroup.name}
</Typography>
</>,
value: <>
</div>,
value: <div className="mythic-dashboard-table-actions-inline">
<GetStatusSymbol data={p} />
<MythicStyledTooltip title={"Open Graph in Modal"}>
<OpenInNewTwoToneIcon color={"secondary"} onClick={(e) => clickDetail(e, p)} />
<OpenInNewTwoToneIcon className="mythic-dashboard-table-icon-action" onClick={(e) => clickDetail(e, p)} />
</MythicStyledTooltip>
</>
</div>
}
}) || [];
setWorkflows(newPayloadData);
@@ -807,11 +869,14 @@ const Top10RecentWorkflowsDashboardElement = ({me, data, editing, removeElement}
if( data.eventgroupinstance && workflows.length === 0){
return (
<>
<DashboardEmptyCard
<TableDataCard
title={"Workflow Executions"}
editing={editing}
removeElement={removeElement}
title={"No Workflows Created"}
action={
empty={true}
emptyTitle="No workflows created"
emptyDescription="Create a workflow to see recent eventing executions here."
emptyAction={
<Button
className="mythic-dashboard-primary-button"
onClick={() => setOpenNewWorkflowModal(true)}
@@ -821,9 +886,7 @@ const Top10RecentWorkflowsDashboardElement = ({me, data, editing, removeElement}
Create a Workflow
</Button>
}
>
Create a workflow to see recent eventing executions here.
</DashboardEmptyCard>
/>
{openNewWorkflowModal &&
<MythicDialog fullWidth={true} maxWidth="xl" open={openNewWorkflowModal}
onClose={(e) => {
@@ -854,8 +917,8 @@ const Top10RecentWorkflowsDashboardElement = ({me, data, editing, removeElement}
<TableBody >
{workflows.map( (d, index) => (
<TableRow hover key={d.label + index} onClick={() => {handleHostContextClick(d)}} style={{cursor: "pointer"}}>
<MythicTableCell>{d.label}</MythicTableCell>
<MythicTableCell>{d.value}</MythicTableCell>
<MythicTableCell className="mythic-dashboard-table-cell-primary">{d.label}</MythicTableCell>
<MythicTableCell className="mythic-dashboard-table-cell-actions">{d.value}</MythicTableCell>
</TableRow>
)
)}
@@ -1066,15 +1129,15 @@ const MyOperationsDashboardElement = ({me, data, reloadDashboard, editing, remov
<TableBody >
{operations.map( (d) => (
<TableRow hover key={d.id} onClick={() => {handleHostContextClick(d)}} style={{cursor: "pointer"}}>
<MythicTableCell>
<MythicTableCell className="mythic-dashboard-table-cell-primary">
<div className="mythic-status-stack">
<span>{d.label}</span>
<span className="mythic-dashboard-table-primary-text">{d.label}</span>
{d.operation.complete &&
<MythicStatusChip label="Completed" status="completed" />
}
</div>
</MythicTableCell>
<MythicTableCell>
<MythicTableCell className="mythic-dashboard-table-cell-actions">
<Button className="mythic-dashboard-table-action" size="small" onClick={()=>{setOpenUpdateNotifications(true);}} startIcon={<EditIcon/>}
disabled={me?.user?.current_operation_id !== d.operation.id}
variant="outlined">Edit</Button>
@@ -1085,7 +1148,7 @@ const MyOperationsDashboardElement = ({me, data, reloadDashboard, editing, remov
/>
}
</MythicTableCell>
<MythicTableCell>
<MythicTableCell className="mythic-dashboard-table-cell-actions">
<Button className="mythic-dashboard-table-action" size="small" onClick={()=>{setOpenUpdateOperators(true);}}
disabled={me?.user?.current_operation_id !== d.operation.id}
startIcon={<AssignmentIndIcon/>} variant="outlined">Edit</Button>
@@ -1096,7 +1159,7 @@ const MyOperationsDashboardElement = ({me, data, reloadDashboard, editing, remov
/>
}
</MythicTableCell>
<MythicTableCell>
<MythicTableCell className="mythic-dashboard-table-cell-actions">
{d.id === me.user.current_operation_id ? (
<MythicStatusChip label="Current" status="active" />
) : (
@@ -1421,17 +1484,17 @@ const Top10RecentFileDownloadsDashboardElement = ({me, data, editing, removeElem
return {
filemeta: newFile,
id: p.id,
label: <div style={{display: "flex", alignItems: "center"}}>
{newFile.host}
label: <div className="mythic-dashboard-table-identity">
<span className="mythic-dashboard-table-primary-text">{newFile.host}</span>
</div>,
value: <div style={{display: "flex", alignItems: "center"}}>
value: <div className="mythic-dashboard-table-file-row">
<MythicStyledTooltip title={"Preview Media"}>
<FontAwesomeIcon icon={faPhotoVideo} style={{height: "20px", position: "relative", cursor: "pointer", display: "inline-block", marginRight: "10px"}}
<FontAwesomeIcon className="mythic-dashboard-table-icon-action mythic-dashboard-table-icon-action-info" icon={faPhotoVideo}
onClick={(e) => onPreviewMedia(e, newFile)} />
</MythicStyledTooltip>
<Link style={{wordBreak: "break-all"}} color="textPrimary" underline="always" href={"/direct/download/" + newFile.agent_file_id}>{newFile.filename_text}</Link>
<Link className="mythic-dashboard-table-link" color="textPrimary" underline="always" href={"/direct/download/" + newFile.agent_file_id}>{newFile.filename_text}</Link>
{!newFile.complete &&
<Typography color="secondary" >({newFile.chunks_received} / <b>{newFile.total_chunks}</b>) Chunks</Typography>
<Typography className="mythic-dashboard-table-secondary" color="secondary" >({newFile.chunks_received} / <b>{newFile.total_chunks}</b>) Chunks</Typography>
}
</div>,
}
@@ -1452,9 +1515,9 @@ const Top10RecentFileDownloadsDashboardElement = ({me, data, editing, removeElem
tableBody={
<TableBody >
{files.map( (d, index) => (
<TableRow hover key={d.label + index} >
<MythicTableCell>{d.label}</MythicTableCell>
<MythicTableCell>{d.value}</MythicTableCell>
<TableRow hover key={d.label + index} >
<MythicTableCell className="mythic-dashboard-table-cell-primary">{d.label}</MythicTableCell>
<MythicTableCell className="mythic-dashboard-table-cell-primary">{d.value}</MythicTableCell>
</TableRow>
)
)}
@@ -1547,6 +1610,7 @@ const Top10RecentScreenshotsDashboardElement = ({me, data, editing, removeElemen
empty={files.length === 0}
emptyTitle="No screenshots"
emptyDescription="Recent screenshots will appear here once screenshot files are created."
summary={false}
/>
{ openScreenshot.open &&
<MythicDialog fullWidth={true} maxWidth="xl" open={openScreenshot.open}
@@ -1584,14 +1648,16 @@ const Top10RecentTasksDashboardElement = ({me, data, editing, removeElement}) =>
tableBody={
<TableBody>
<TableRow>
<MythicTableCell>
{tasks.map( (task) => (
task.is_interactive_task ? (
<TaskDisplayInteractiveSearch key={"taskinteractdisplay" + task.id} me={me} task={task} responsesSurrounding={5} />
) : (
<TaskDisplay key={"taskinteractdisplay" + task.id} me={me} task={task} command_id={task.command == null ? 0 : task.command.id} />
)
))}
<MythicTableCell className="mythic-dashboard-tasking-cell">
<div className="mythic-dashboard-tasking-list">
{tasks.map( (task) => (
task.is_interactive_task ? (
<TaskDisplayInteractiveSearch key={"taskinteractdisplay" + task.id} me={me} task={task} responsesSurrounding={5} />
) : (
<TaskDisplay key={"taskinteractdisplay" + task.id} me={me} task={task} command_id={task.command == null ? 0 : task.command.id} />
)
))}
</div>
</MythicTableCell>
</TableRow>
</TableBody>
@@ -1605,6 +1671,8 @@ const Top10RecentTasksDashboardElement = ({me, data, editing, removeElement}) =>
empty={tasks.length === 0}
emptyTitle="No recent tasking"
emptyDescription="Recent tasks will appear here once operators issue tasking."
summary={false}
tableClassName="mythic-dashboard-tasking-table"
/>
</>
@@ -1640,20 +1708,20 @@ const Top10RecentCredentialsDashboardElement = ({me, data, editing, removeElemen
<TableBody>
{credentials.map(c => (
<TableRow key={c.id} hover>
<MythicStyledTableCell style={{ whiteSpace: "pre-line",wordBreak: "break-all", display: "flex", flexDirection: "row"}}>
<MythicStyledTableCell className="mythic-dashboard-table-cell-wrap mythic-dashboard-table-cell-account">
<MythicStyledTooltip title={"Copy Credential to clipboard"}>
<IconButton className="mythic-dashboard-icon-button" onClick={() => onCopyToClipboard(c.credential_text)} size="small">
<FontAwesomeIcon icon={faCopy}/>
</IconButton>
</MythicStyledTooltip>
<span>
<Typography variant="body2" style={{wordBreak: "break-all"}}><b>Account: </b>{c.account}</Typography>
<Typography variant="body2" style={{wordBreak: "break-all"}}><b>Realm: </b>{c.realm}</Typography>
<Typography variant="body2" style={{wordBreak: "break-all"}}><b>Type: </b>{c.type}</Typography>
<span className="mythic-dashboard-table-stack">
<Typography className="mythic-dashboard-table-primary-text" variant="body2"><b>Account: </b>{c.account}</Typography>
<Typography className="mythic-dashboard-table-secondary" variant="body2"><b>Realm: </b>{c.realm}</Typography>
<Typography className="mythic-dashboard-table-secondary" variant="body2"><b>Type: </b>{c.type}</Typography>
</span>
</MythicStyledTableCell>
<MythicTableCell>
<Typography variant="body2" style={{whiteSpace: "pre-line",wordBreak: "break-all",}}>{c.comment}</Typography>
<MythicTableCell className="mythic-dashboard-table-cell-wrap">
<Typography className="mythic-dashboard-table-comment" variant="body2">{c.comment}</Typography>
</MythicTableCell>
</TableRow>
))}
@@ -1782,6 +1850,98 @@ const DashboardElementOptions = [
"Credentials"
].sort();
const DashboardElementDetails = {
"Active Callbacks": {category: "Metric", summary: "Active, total, and recent callback check-ins."},
"Activity Timeline": {category: "Trend", summary: "Task and callback activity over time."},
"Credentials": {category: "Table", summary: "Recently captured credentials."},
"Downloads": {category: "Table", summary: "Recently downloaded files."},
"Eventing Status": {category: "Table", summary: "Recent eventing workflow executions."},
"Health - Installed Services": {category: "Metric", summary: "Installed service health."},
"My Operations": {category: "Table", summary: "Operations, operators, and current status."},
"Operator Activity": {category: "Chart", summary: "Task volume by operator."},
"Payloads": {category: "Table", summary: "Recently created payloads."},
"Proxy Usage": {category: "Table", summary: "Top proxy traffic."},
"Screenshots": {category: "Media", summary: "Recent screenshots preview."},
"Task Completion Status": {category: "Chart", summary: "Task success, error, and processing states."},
"Tasking": {category: "Table", summary: "Recently issued tasks."},
"Top 10 Active Hosts": {category: "Table", summary: "Most active hosts."},
"Top 10 Failed Commands": {category: "Chart", summary: "Commands with the most errors."},
"Top 10 Host Contexts": {category: "Table", summary: "Hosts with the most tasking."},
"Top 10 Issued Commands": {category: "Chart", summary: "Most frequently issued commands."},
"Top 10 TagTypes": {category: "Chart", summary: "Most used tag types."},
"Top 10 Task Artifacts": {category: "Chart", summary: "Most common task artifacts."},
"Top 10 User Contexts": {category: "Table", summary: "User contexts with the most tasking."},
};
const DashboardWidgetSelectDialog = ({onClose, onSubmit, rowIndex}) => {
const [selected, setSelected] = React.useState(DashboardElementOptions[0] || "");
const submitSelection = (option = selected) => {
if(option === ""){
return;
}
onSubmit({id: option});
onClose();
};
const handleKeyDown = (event, option) => {
if(event.key === "Enter"){
event.preventDefault();
submitSelection(option);
} else if(event.key === " "){
event.preventDefault();
setSelected(option);
}
};
return (
<>
<DialogTitle>Add dashboard widget</DialogTitle>
<DialogContent dividers={true} className="mythic-dashboard-widget-dialog">
<div className="mythic-dashboard-widget-dialog-header">
<div>
<div className="mythic-dashboard-widget-dialog-title">Row {rowIndex + 1}</div>
<div className="mythic-dashboard-widget-dialog-subtitle">{selected || "No widget selected"}</div>
</div>
{selected !== "" &&
<span className="mythic-dashboard-widget-category">
{DashboardElementDetails[selected]?.category || "Widget"}
</span>
}
</div>
<div className="mythic-dashboard-widget-grid">
{DashboardElementOptions.map((option) => {
const details = DashboardElementDetails[option] || {category: "Widget", summary: "Dashboard widget."};
return (
<div
aria-pressed={selected === option}
className={`mythic-dashboard-widget-option ${selected === option ? "mythic-dashboard-widget-option-selected" : ""}`}
key={"dashboard-widget-option-" + option}
onClick={() => setSelected(option)}
onDoubleClick={() => submitSelection(option)}
onKeyDown={(event) => handleKeyDown(event, option)}
role="button"
tabIndex={0}
>
<div className="mythic-dashboard-widget-option-top">
<span className="mythic-dashboard-widget-option-title">{option}</span>
<span className="mythic-dashboard-widget-category">{details.category}</span>
</div>
<div className="mythic-dashboard-widget-option-summary">{details.summary}</div>
</div>
);
})}
</div>
</DialogContent>
<DialogActions className="mythic-dialog-actions">
<Button onClick={onClose} variant="outlined">
Close
</Button>
<Button className="mythic-dialog-button-primary" disabled={selected === ""} onClick={() => submitSelection()} variant="contained">
Add widget
</Button>
</DialogActions>
</>
);
};
const CustomDashboard = ({me, setLoading, loading, editing}) => {
const initialDashboardView = GetMythicSetting({
setting_name: "customDashboardElements",
@@ -1919,24 +2079,22 @@ const CustomDashboard = ({me, setLoading, loading, editing}) => {
<div className="mythic-dashboard-custom">
{dashboards.map((d, i) => (
<div key={"dashboardRow" + i}
className="mythic-dashboard-row">
className={`mythic-dashboard-row ${editing ? "mythic-dashboard-row-editing" : ""}`.trim()}
style={{"--mythic-dashboard-row-template": getDashboardRowTemplate(d)}}>
{editing &&
<div className="mythic-dashboard-edit-rail">
<MythicStyledTooltip title={"Add New Chart To This Row"}>
<IconButton className="mythic-dashboard-icon-button" onClick={() => setOpenAddElement({open: true, row: i})} size="small">
<AddchartIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Add Row After This Row"}>
<IconButton className="mythic-dashboard-icon-button" onClick={() => addDashboardRow(i)} size="small">
<FormatListBulletedAddIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<MythicStyledTooltip title={"Remove This Row"}>
<IconButton className="mythic-dashboard-icon-button mythic-dashboard-icon-button-hover-danger" onClick={() => removeDashboardRow(i)} size="small">
<PlaylistRemoveIcon fontSize="small" />
</IconButton>
</MythicStyledTooltip>
<div className="mythic-dashboard-edit-toolbar">
<span className="mythic-dashboard-edit-toolbar-title">Row {i + 1}</span>
<div className="mythic-dashboard-edit-toolbar-actions">
<Button className="mythic-dashboard-table-action mythic-table-row-action-hover-info" onClick={() => setOpenAddElement({open: true, row: i})} size="small" startIcon={<AddchartIcon fontSize="small" />}>
Add widget
</Button>
<Button className="mythic-dashboard-table-action" onClick={() => addDashboardRow(i)} size="small" startIcon={<FormatListBulletedAddIcon fontSize="small" />}>
Add row
</Button>
<Button className="mythic-dashboard-table-action mythic-dashboard-table-action-hover-danger" onClick={() => removeDashboardRow(i)} size="small" startIcon={<PlaylistRemoveIcon fontSize="small" />}>
Remove row
</Button>
</div>
</div>
}
{d.map((e,j) => (
@@ -1948,14 +2106,11 @@ const CustomDashboard = ({me, setLoading, loading, editing}) => {
{openAddElement.open &&
<MythicDialog fullWidth={true} maxWidth="md" open={openAddElement.open}
onClose={()=>{setOpenAddElement({open: false, row: 0});}}
innerDialog={<MythicSelectFromListDialog onClose={()=>{setOpenAddElement({open: false, row: 0})}}
onSubmit={addDashboardElement}
options={DashboardElementOptions.map(e => {return{id: e}})}
title={"Select Dashboard Element to Add"}
action={"select"}
identifier={"id"}
display={"id"}
dontCloseOnSubmit={false} />}
innerDialog={<DashboardWidgetSelectDialog
onClose={()=>{setOpenAddElement({open: false, row: 0})}}
onSubmit={addDashboardElement}
rowIndex={openAddElement.row}
/>}
/>
}
</div>
@@ -1972,9 +2127,12 @@ export function CallbacksCard({me}) {
});
const [dashboard, setDashboard] = React.useState(initialDashboardView);
const [updateSetting] = useSetMythicSetting();
const onChangeDashboardOption = (event) => {
setDashboard(event.target.value);
updateSetting({setting_name: "dashboard", value: event.target.value});
const onChangeDashboardOption = (event, value) => {
if(value === null || value === dashboard){
return;
}
setDashboard(value);
updateSetting({setting_name: "dashboard", value: value});
setLoading(true);
}
return (
@@ -1994,51 +2152,31 @@ export function CallbacksCard({me}) {
}
actions={
<>
<TextField
sx={{
width: "11rem",
"& .MuiInputBase-root": {
backgroundColor: "rgba(255, 255, 255, 0.08)",
color: "inherit",
minHeight: 30,
},
"& .MuiInputLabel-root": {
color: "currentColor",
},
"& .MuiOutlinedInput-notchedOutline": {
borderColor: "rgba(255, 255, 255, 0.24)",
},
"& .MuiSelect-select": {
py: "4px",
},
"& .MuiSvgIcon-root": {
color: "inherit",
},
}}
size={"small"}
select={true}
label={"Dashboard Perspective"}
value={dashboard}
onChange={onChangeDashboardOption}
>
{
dashboardOptions.map((opt, i) => (
<MenuItem key={"searchopt" + opt} value={opt}>{opt}</MenuItem>
))
}
</TextField>
<MythicStyledTooltip title={"Analyze Operation Data Again"} tooltipStyle={{display: "inline-block"}}>
<IconButton onClick={() => setLoading(true)}>
<ReplayIcon />
</IconButton>
</MythicStyledTooltip>
{dashboard === "custom" &&
<MythicStyledTooltip title={editing ? "Stop Editing Dashboard Contents":"Edit Dashboard Contents"}>
<IconButton onClick={() => setEditing(!editing)}>
<EditIcon />
</IconButton>
<ToggleButtonGroup
className="mythic-dashboard-perspective-toggle"
exclusive
onChange={onChangeDashboardOption}
size="small"
value={dashboard}
>
{dashboardOptions.map((opt) => (
<ToggleButton key={"dashboard-perspective-" + opt} value={opt}>
{opt}
</ToggleButton>
))}
</ToggleButtonGroup>
<MythicStyledTooltip title={"Analyze Operation Data Again"} tooltipStyle={{display: "inline-block"}}>
<Button className="mythic-table-row-action-hover-info" onClick={() => setLoading(true)} size="small" startIcon={<ReplayIcon fontSize="small" />} variant="outlined">
Refresh
</Button>
</MythicStyledTooltip>
}
{dashboard === "custom" &&
<MythicStyledTooltip title={editing ? "Stop Editing Dashboard Contents":"Edit Dashboard Contents"}>
<Button className={editing ? "mythic-table-row-action-hover-success" : "mythic-table-row-action-hover-info"} onClick={() => setEditing(!editing)} size="small" startIcon={<EditIcon fontSize="small" />} variant="outlined">
{editing ? "Done editing" : "Edit layout"}
</Button>
</MythicStyledTooltip>
}
</>
}
/>
@@ -46,12 +46,13 @@ const DashboardCard = ({
className = "",
editing,
removeElement,
size = "standard",
title,
width = "100%",
}) => {
return (
<Paper
className={`mythic-dashboard-card ${className}`.trim()}
className={`mythic-dashboard-card mythic-dashboard-card-${size} ${className}`.trim()}
elevation={0}
style={{width}}
>
@@ -107,25 +108,27 @@ export const DashboardEmptyCard = ({action, children, editing, removeElement, ti
);
const DashboardNoDataState = ({
action,
title = "No data yet",
description = "This dashboard element will populate when matching operation activity exists.",
}) => (
<MythicEmptyState
action={action}
compact
title={title}
description={description}
minHeight={132}
sx={{p: 0}}
minHeight={0}
sx={{flex: "1 1 auto", height: "100%", minHeight: 0, p: 0}}
/>
);
export const PieChartCard = ({
data, width = "100%", additionalStyles, innerElement,
margin = {
left: 10,
right: 10,
top: 10,
bottom: 10,
left: 8,
right: 8,
top: 8,
bottom: 8,
}, colors,
onClick, title = "", editing, removeElement, customizeElement
}) => {
@@ -142,75 +145,85 @@ export const PieChartCard = ({
<>
{customizeElement}
<MythicStyledTooltip title={showLegend ? "Hide Legend" : "Show Legend"}>
<IconButton className="mythic-dashboard-icon-button" onClick={toggleLegend} size="small">
<IconButton className="mythic-dashboard-icon-button mythic-dashboard-icon-button-hover-info" onClick={toggleLegend} size="small">
{showLegend ? <VisibilityIcon fontSize="small" /> : <VisibilityOffIcon fontSize="small" />}
</IconButton>
</MythicStyledTooltip>
</>
}
bodyClassName="mythic-dashboard-chart-body"
editing={editing}
removeElement={removeElement}
title={title}
width={width}
>
{hasChartData ? (
<PieChart
skipAnimation={true}
series={[
{
// item has id, label, value, data
//arcLabel: (item) => `${item.label}`,
//arcLabelMinAngle: 35,
//arcLabelRadius: "60%",
data: chartData,
highlightScope: {fade: 'global', highlighted: 'item'},
faded: {innerRadius: 0, additionalRadius: -10, color: 'gray'},
paddingAngle: 1,
cornerRadius: 4,
innerRadius: 0,
...additionalStyles
},
]}
height={200}
margin={margin}
sx={{
[`& .${pieArcLabelClasses.root}`]: {
fill: 'white',
fontWeight: 'bold',
},
}}
colors={colors || getDashboardColors(theme)}
onItemClick={onClick}
hideLegend={!showLegend}
slotProps={{
legend: {
direction: "vertical", // "horizontal"
sx: {
gap: "4px", // itemGap (distance between legend items)
// CSS class
['.MuiChartsLegend-series']: {
gap: '8px', // markGap (distance between legend dot and text)
},
[`.MuiChartsLegend-mark`]: {
height: 12, // size of the legend dot
width: 12,
},
<div className="mythic-dashboard-chart-canvas mythic-dashboard-chart-canvas-pie">
<PieChart
skipAnimation={true}
series={[
{
data: chartData,
highlightScope: {fade: 'global', highlighted: 'item'},
faded: {innerRadius: 0, additionalRadius: -10, color: 'gray'},
paddingAngle: 1,
cornerRadius: 4,
innerRadius: 0,
...additionalStyles
},
]}
height={210}
margin={margin}
sx={{
[`& .${pieArcLabelClasses.root}`]: {
fill: 'white',
fontWeight: 'bold',
},
}}
colors={colors || getDashboardColors(theme)}
onItemClick={onClick}
hideLegend={!showLegend}
slotProps={{
legend: {
direction: "vertical",
sx: {
gap: "5px",
['.MuiChartsLegend-series']: {
gap: '8px',
},
[`.MuiChartsLegend-mark`]: {
height: 10,
rx: 2,
width: 10,
},
[`.MuiChartsLegend-label`]: {
fontSize: 11,
fontWeight: 650,
},
}
}
}
}}>
{innerElement}
</PieChart>
}}>
{innerElement}
</PieChart>
</div>
) : (
<DashboardNoDataState />
<div className="mythic-dashboard-chart-canvas mythic-dashboard-chart-canvas-empty">
<DashboardNoDataState />
</div>
)}
</DashboardCard>
);
}
export const GaugeCard = ({data, width = "100%", title = "", editing, removeElement, customizeElement }) => {
const theme = useTheme();
const online = data?.online || 0;
const total = data?.total || 0;
const percentOnline = total > 0 ? Math.round((online / total) * 100) : 0;
const statusLevel = total === 0 ? "neutral" : percentOnline > 85 ? "success" : percentOnline > 50 ? "warning" : "danger";
const statusLabel = total === 0 ? "No services" : percentOnline > 85 ? "Healthy" : percentOnline > 50 ? "Degraded" : "Attention";
const getFillColor = () => {
if(data['total'] === 0){return theme.palette.text.disabled}
let ratio = data['online'] / data['total'];
if(total === 0){return theme.palette.text.disabled}
let ratio = online / total;
if( ratio > 0.85){
return theme.palette.success.main;
}else if(ratio > 0.5){
@@ -222,55 +235,105 @@ export const GaugeCard = ({data, width = "100%", title = "", editing, removeElem
return (
<DashboardCard
actions={customizeElement}
bodyClassName="mythic-dashboard-card-body-centered"
bodyClassName="mythic-dashboard-kpi-body"
editing={editing}
removeElement={removeElement}
size="metric"
title={title}
width={width}
>
<Gauge
height={200}
width={200}
skipAnimation={true}
valueMax={data['total'] > 0 ? data['total'] : 100}
value={data['online']}
innerRadius={"70%"}
cornerRadius="20%"
text={({ value, valueMax }) => `${value} / ${valueMax}`}
sx={() => ({
[`& .MuiGauge-valueText > text > tspan`]: {
fontSize: 30,
},
[`.MuiGauge-valueArc`]: {
fill: getFillColor(),
},
})}
>
</Gauge>
<div className="mythic-dashboard-service-kpi">
<div className="mythic-dashboard-kpi-main">
<div className="mythic-dashboard-kpi-status-row">
<span className={`mythic-dashboard-kpi-chip mythic-dashboard-kpi-chip-${statusLevel}`}>
{statusLabel}
</span>
<span className="mythic-dashboard-kpi-percent">{percentOnline}%</span>
</div>
<div className="mythic-dashboard-kpi-value-row">
<span className="mythic-dashboard-kpi-value">{online}</span>
<span className="mythic-dashboard-kpi-total">/ {total}</span>
</div>
<div className="mythic-dashboard-kpi-label">
Services online
</div>
</div>
<div className="mythic-dashboard-kpi-gauge">
<Gauge
height={112}
width={112}
skipAnimation={true}
valueMax={total > 0 ? total : 100}
value={online}
innerRadius={"72%"}
cornerRadius="20%"
text={() => `${percentOnline}%`}
sx={() => ({
[`& .MuiGauge-valueText > text > tspan`]: {
fontSize: 18,
fontWeight: 850,
},
[`.MuiGauge-valueArc`]: {
fill: getFillColor(),
},
})}
>
</Gauge>
</div>
</div>
</DashboardCard>
);
}
export const CallbackDataCard = ({mainTitle, secondTitle, mainElement, secondaryElement, width="100%",
editing, removeElement}) => {
export const CallbackDataCard = ({mainTitle, primaryValue, totalValue, primaryLabel, secondaryValue, secondaryLabel,
statusLabel = "Tracking", statusLevel = "info", onClick, width="100%",
actions, editing, removeElement}) => {
const handleKeyDown = (event) => {
if(!onClick){
return;
}
if(event.key === "Enter" || event.key === " "){
event.preventDefault();
onClick();
}
};
return (
<DashboardCard
bodyClassName="mythic-dashboard-metric-body"
actions={actions}
bodyClassName="mythic-dashboard-kpi-body"
editing={editing}
removeElement={removeElement}
size="metric"
title={mainTitle}
width={width}
>
<div className="mythic-dashboard-metric-content">
<MythicStyledTooltip title={"Go to Active Callbacks"}>
<div className="mythic-dashboard-metric-link">
{mainElement}
<div className="mythic-dashboard-metric-label">
{secondTitle}
</div>
{secondaryElement}
<MythicStyledTooltip title={"Go to Active Callbacks"}>
<div
className="mythic-dashboard-callback-kpi"
onClick={onClick}
onKeyDown={handleKeyDown}
role={onClick ? "button" : undefined}
tabIndex={onClick ? 0 : undefined}
>
<div className="mythic-dashboard-kpi-main">
<div className="mythic-dashboard-kpi-status-row">
<span className={`mythic-dashboard-kpi-chip mythic-dashboard-kpi-chip-${statusLevel}`}>
{statusLabel}
</span>
</div>
</MythicStyledTooltip>
<div className="mythic-dashboard-kpi-value-row">
<span className="mythic-dashboard-kpi-value">{primaryValue}</span>
<span className="mythic-dashboard-kpi-total">/ {totalValue}</span>
</div>
<div className="mythic-dashboard-kpi-label">
{primaryLabel}
</div>
</div>
<div className="mythic-dashboard-kpi-secondary-panel">
<span className="mythic-dashboard-kpi-secondary-value">{secondaryValue}</span>
<span className="mythic-dashboard-kpi-secondary-label">{secondaryLabel}</span>
</div>
</div>
</MythicStyledTooltip>
</DashboardCard>
)
}
@@ -285,21 +348,32 @@ export const TableDataCard = ({
empty = false,
emptyTitle,
emptyDescription,
emptyAction,
summary = true,
tableClassName = "",
}) => {
const tableClasses = [
"mythic-dashboard-table",
summary ? "mythic-dashboard-summary-table" : "",
tableClassName,
].filter(Boolean).join(" ");
return (
<DashboardCard
actions={customizeElement}
bodyClassName="mythic-dashboard-table-body"
editing={editing}
removeElement={removeElement}
size="table"
title={title}
width={width}
>
{empty ? (
<DashboardNoDataState title={emptyTitle} description={emptyDescription} />
<TableContainer className="mythic-dashboard-table-container mythic-dashboard-empty-container mythicElement">
<DashboardNoDataState title={emptyTitle} description={emptyDescription} action={emptyAction} />
</TableContainer>
) : (
<TableContainer className="mythic-dashboard-table-container mythicElement">
<Table className="mythic-dashboard-table" stickyHeader size="small">
<Table className={tableClasses} stickyHeader size="small">
{tableHead}
{tableBody}
</Table>
@@ -338,57 +412,57 @@ export const LineTimeChartCard = ({data, additionalStyles}) => {
}
};
return (
<DashboardCard title="Tasks Issued per Day">
<LineChart
xAxis={[
{
dataKey: 'x',
//valueFormatter: (v) => (new Date(v)).toISOString().substr(0, 10),
scaleType: "time",
min: data[value[0]]?.x || 0,
max: data[value[1]]?.x || 0,
id: 'bottomAxis',
labelStyle: {
fontSize: 10,
},
tickLabelStyle: {
angle: 25,
textAnchor: 'start',
fontSize: 5,
},
<DashboardCard bodyClassName="mythic-dashboard-chart-body" title="Tasks Issued per Day" size="wide">
<div className="mythic-dashboard-chart-canvas mythic-dashboard-chart-canvas-line">
<LineChart
xAxis={[
{
dataKey: 'x',
scaleType: "time",
min: data[value[0]]?.x || 0,
max: data[value[1]]?.x || 0,
id: 'bottomAxis',
labelStyle: {
fontSize: 10,
},
tickLabelStyle: {
angle: 0,
fontSize: 10,
},
},
]}
series={[
{
dataKey: 'y',
label: "mythic_admin",
showMark: ({index}) => index % 2 === 0,
//color: "#4e79a7"
}
]}
sx={{
[`.${axisClasses.left} .${axisClasses.label}`]: {
transform: 'translate(-25px, 0)',
},
[`.${axisClasses.right} .${axisClasses.label}`]: {
transform: 'translate(30px, 0)',
},
}}
margin={{ top: 10 }}
dataset={data}
height={200}
{...additionalStyles}
></LineChart>
<Slider
value={value}
onChange={handleChange}
valueLabelDisplay="auto"
min={range[0]}
max={range[1]}
className="mythic-dashboard-slider"
sx={{ width: "80%" }}
/>
},
]}
series={[
{
dataKey: 'y',
label: "mythic_admin",
showMark: ({index}) => index % 2 === 0,
}
]}
sx={{
[`.${axisClasses.left} .${axisClasses.label}`]: {
transform: 'translate(-25px, 0)',
},
[`.${axisClasses.right} .${axisClasses.label}`]: {
transform: 'translate(30px, 0)',
},
}}
margin={{ top: 18, right: 20, bottom: 28, left: 42 }}
dataset={data}
height={186}
{...additionalStyles}
></LineChart>
</div>
<div className="mythic-dashboard-chart-slider-row">
<Slider
value={value}
onChange={handleChange}
valueLabelDisplay="auto"
min={range[0]}
max={range[1]}
className="mythic-dashboard-slider"
/>
</div>
</DashboardCard>
)
@@ -450,73 +524,79 @@ export const LineTimeMultiChartCard = ({data, additionalStyles, colors, view_utc
<>
{customizeElement}
<MythicStyledTooltip title={showLegend ? "Hide Legend" : "Show Legend"}>
<IconButton className="mythic-dashboard-icon-button" onClick={toggleLegend} size="small">
<IconButton className="mythic-dashboard-icon-button mythic-dashboard-icon-button-hover-info" onClick={toggleLegend} size="small">
{showLegend ? <VisibilityIcon fontSize="small" /> : <VisibilityOffIcon fontSize="small" />}
</IconButton>
</MythicStyledTooltip>
</>
}
bodyClassName="mythic-dashboard-chart-body"
editing={editing}
removeElement={removeElement}
size="wide"
title={`Activity per Day ${view_utc_time ? "(UTC)" : `(${Intl?.DateTimeFormat()?.resolvedOptions()?.timeZone})`}`}
>
{hasChartData ? (
<>
<LineChart
colors={colors || getDashboardColors(theme)}
hideLegend={!showLegend}
xAxis={[
{
data: data.x,
scaleType: "time",
min: data?.x?.[value[0]] || 0,
max: data?.x?.[value[1]] || 0,
id: 'bottomAxis',
tickMinStep: 86400000,
labelStyle: {
<div className="mythic-dashboard-chart-canvas mythic-dashboard-chart-canvas-line">
<LineChart
colors={colors || getDashboardColors(theme)}
hideLegend={!showLegend}
xAxis={[
{
data: data.x,
scaleType: "time",
min: data?.x?.[value[0]] || 0,
max: data?.x?.[value[1]] || 0,
id: 'bottomAxis',
tickMinStep: 86400000,
labelStyle: {
fontSize: 10,
},
tickLabelStyle: {
angle: 0,
fontSize: 10,
},
},
]}
yAxis={[
{id: "taskAxis", scaleType: "linear", label: "Tasks Issued"},
{id: "callbackAxis", scaleType: "linear", label: "Active Callbacks", position: "right"}
]}
series={data.y}
sx={{
[`.${axisClasses.left} .${axisClasses.label}`]: {
fontSize: 10,
},
tickLabelStyle: {
angle: 25,
textAnchor: 'start',
fontSize: 5,
[`.${axisClasses.right} .${axisClasses.label}`]: {
fontSize: 10,
},
},
]}
yAxis={[
{id: "taskAxis", scaleType: "linear", label: "Tasks Issued"},
{id: "callbackAxis", scaleType: "linear", label: "Active Callbacks", position: "right"}
]}
series={data.y}
sx={{
[`.${axisClasses.left} .${axisClasses.label}`]: {
//transform: 'translate(-25px, 0)',
},
[`.${axisClasses.right} .${axisClasses.label}`]: {
//transform: 'translate(30px, 0)',
},
}}
margin={{ }}
height={200}
{...additionalStyles}
></LineChart>
<Slider
value={value}
onChange={handleChange}
size={"small"}
valueLabelDisplay={"auto"}
valueLabelFormat={sliderVal => sliderDate(sliderVal, view_utc_time)}
min={range[0]}
max={range[1]}
className="mythic-dashboard-slider"
sx={{ width: "80%" }}
/>
}}
margin={{ top: 18, right: 46, bottom: 30, left: 48 }}
height={186}
{...additionalStyles}
></LineChart>
</div>
<div className="mythic-dashboard-chart-slider-row">
<Slider
value={value}
onChange={handleChange}
size={"small"}
valueLabelDisplay={"auto"}
valueLabelFormat={sliderVal => sliderDate(sliderVal, view_utc_time)}
min={range[0]}
max={range[1]}
className="mythic-dashboard-slider"
/>
</div>
</>
) : (
<DashboardNoDataState
title="No activity yet"
description="Task and callback activity will appear here once the operation has timeline data."
/>
<div className="mythic-dashboard-chart-canvas mythic-dashboard-chart-canvas-empty">
<DashboardNoDataState
title="No activity yet"
description="Task and callback activity will appear here once the operation has timeline data."
/>
</div>
)}
</DashboardCard>
+654 -16
View File
@@ -3337,14 +3337,18 @@ tspan {
}
.mythic-dashboard-row {
align-items: stretch;
display: flex;
display: grid;
flex: 0 0 auto;
flex-wrap: wrap;
gap: 0.75rem;
grid-auto-flow: dense;
grid-template-columns: var(--mythic-dashboard-row-template, repeat(auto-fit, minmax(min(100%, 19rem), 1fr)));
min-width: 0;
padding: 0 0.75rem;
width: 100%;
}
.mythic-dashboard-row-editing {
align-items: start;
}
.mythic-dashboard-card {
background-color: ${(props) => props.theme.surfaces?.raised || props.theme.palette.background.paper};
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
@@ -3353,23 +3357,59 @@ tspan {
display: flex;
flex: 1 1 18rem;
flex-direction: column;
grid-column: span 1;
height: var(--mythic-dashboard-card-height, 18rem);
max-height: var(--mythic-dashboard-card-height, 18rem);
min-height: var(--mythic-dashboard-card-height, 18rem);
min-width: min(100%, 18rem);
min-width: 0;
overflow: hidden;
position: relative;
transition: border-color 160ms ease, box-shadow 160ms ease, transform 160ms ease;
width: 100%;
}
.mythic-dashboard-card:hover {
border-color: ${(props) => alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.34 : 0.24)};
box-shadow: ${(props) => props.theme.palette.mode === "dark" ? `inset 0 1px 0 ${alpha(props.theme.palette.common.white, 0.055)}, 0 8px 18px ${alpha(props.theme.palette.common.black, 0.18)}` : `0 8px 18px ${alpha(props.theme.palette.common.black, 0.08)}`} !important;
}
.mythic-dashboard-card-wide {
grid-column: span 2;
}
.mythic-dashboard-card-table {
grid-column: span 2;
max-width: min(100%, 58rem);
}
.mythic-dashboard-row > .mythic-dashboard-card-table:only-child {
max-width: min(100%, 64rem);
}
.mythic-dashboard-card-metric {
grid-column: span 1;
}
.mythic-dashboard-card-header {
align-items: center;
background-color: ${(props) => props.theme.surfaces?.muted || props.theme.palette.background.default};
border-bottom: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
background-image: ${(props) => {
const headerTextColor = props.theme.pageHeaderText?.main || props.theme.palette.text.primary;
return `linear-gradient(90deg, ${alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.16 : 0.09)} 0%, ${alpha(headerTextColor, props.theme.palette.mode === "dark" ? 0.05 : 0.035)} 100%)`;
}};
border-bottom: 1px solid ${(props) => alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.34 : 0.2)};
display: flex;
flex: 0 0 auto;
gap: 0.65rem;
justify-content: space-between;
min-height: 42px;
min-width: 0;
padding: 0.55rem 0.65rem;
overflow: hidden;
padding: 0.55rem 0.65rem 0.55rem 0.85rem;
position: relative;
}
.mythic-dashboard-card-header::before {
background-color: ${(props) => props.theme.palette.primary.main};
bottom: 0;
content: "";
left: 0;
position: absolute;
top: 0;
width: 4px;
}
.mythic-dashboard-card-title {
color: ${(props) => props.theme.palette.text.primary};
@@ -3423,11 +3463,23 @@ tspan {
.mythic-dashboard-icon-button-hover-danger:hover {
color: ${(props) => props.theme.palette.error.main} !important;
}
.mythic-dashboard-icon-button-hover-info:hover {
background-color: ${(props) => props.theme.palette.info.main + "2b"} !important;
border-color: ${(props) => props.theme.palette.info.main + "88"} !important;
color: ${(props) => props.theme.palette.info.main} !important;
}
.mythic-dashboard-icon-button.Mui-disabled {
background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)"} !important;
border-color: ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important;
color: ${(props) => props.theme.palette.text.disabled} !important;
}
.mythic-dashboard-perspective-toggle {
flex: 0 1 auto;
min-width: 0;
}
.mythic-dashboard-perspective-toggle .MuiToggleButton-root {
min-width: 5.6rem !important;
}
.mythic-dashboard-primary-button,
.mythic-dashboard-table-action {
background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.035)"} !important;
@@ -3452,34 +3504,244 @@ tspan {
.mythic-dashboard-table-action:hover {
background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.06)"} !important;
}
.mythic-dashboard-table-action-info {
background-color: ${(props) => props.theme.palette.info.main + "1c"} !important;
border-color: ${(props) => props.theme.palette.info.main + "66"} !important;
color: ${(props) => props.theme.palette.info.main} !important;
}
.mythic-dashboard-table-action-info:hover {
background-color: ${(props) => props.theme.palette.info.main + "2b"} !important;
border-color: ${(props) => props.theme.palette.info.main + "88"} !important;
.mythic-dashboard-table-action-hover-danger:hover {
background-color: ${(props) => props.theme.palette.error.main + "2b"} !important;
border-color: ${(props) => props.theme.palette.error.main + "88"} !important;
color: ${(props) => props.theme.palette.error.main} !important;
}
.mythic-dashboard-table-action.Mui-disabled {
background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)"} !important;
border-color: ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important;
color: ${(props) => props.theme.palette.text.disabled} !important;
}
.mythic-dashboard-chart-body {
gap: 0.35rem;
padding: 0.5rem 0.55rem 0.55rem;
}
.mythic-dashboard-chart-canvas {
background-color: ${(props) => props.theme.palette.mode === "dark" ? alpha(props.theme.palette.common.white, 0.028) : alpha(props.theme.palette.common.black, 0.014)};
background-image: ${(props) => `linear-gradient(135deg, ${alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.1 : 0.055)} 0%, ${alpha(props.theme.palette.background.paper, 0)} 62%)`};
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
display: flex;
flex: 1 1 auto;
min-height: 0;
min-width: 0;
overflow: hidden;
width: 100%;
}
.mythic-dashboard-chart-canvas-pie {
align-items: center;
justify-content: center;
padding: 0.25rem;
}
.mythic-dashboard-chart-canvas-line {
align-items: stretch;
justify-content: center;
padding: 0.2rem 0.3rem 0;
}
.mythic-dashboard-chart-canvas-empty {
align-items: stretch;
justify-content: stretch;
padding: 0.75rem;
}
.mythic-dashboard-chart-canvas .MuiChartsLegend-root {
color: ${(props) => props.theme.palette.text.secondary};
}
.mythic-dashboard-chart-canvas .MuiChartsAxis-line,
.mythic-dashboard-chart-canvas .MuiChartsAxis-tick {
stroke: ${(props) => props.theme.table?.border || props.theme.borderColor};
}
.mythic-dashboard-chart-canvas .MuiChartsAxis-tickLabel,
.mythic-dashboard-chart-canvas .MuiChartsAxis-label {
fill: ${(props) => props.theme.palette.text.secondary} !important;
}
.mythic-dashboard-chart-slider-row {
align-items: center;
background-color: ${(props) => props.theme.palette.mode === "dark" ? alpha(props.theme.palette.common.white, 0.03) : alpha(props.theme.palette.common.black, 0.018)};
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
display: flex;
flex: 0 0 auto;
justify-content: center;
min-height: 2rem;
padding: 0.15rem 0.8rem;
}
.mythic-dashboard-table-body {
padding: 0;
}
.mythic-dashboard-empty-container {
display: flex;
overflow: hidden;
}
.mythic-dashboard-table-container {
background-color: ${(props) => props.theme.palette.mode === "dark" ? alpha(props.theme.palette.common.white, 0.018) : alpha(props.theme.palette.common.black, 0.01)};
flex: 1 1 auto;
height: 100%;
max-height: 100%;
min-height: 0;
min-width: 0;
overflow: auto;
scrollbar-gutter: stable;
width: 100%;
}
.mythic-dashboard-table {
max-width: 100%;
min-width: 100%;
overflow: auto;
width: 100%;
}
.mythic-dashboard-summary-table {
table-layout: fixed;
}
.mythic-dashboard-card-table .MuiTableCell-root {
vertical-align: middle;
}
.mythic-dashboard-summary-table .MuiTableCell-root {
font-size: 0.75rem;
height: 2.15rem;
line-height: 1.25;
overflow: hidden;
padding-bottom: 0.35rem !important;
padding-top: 0.35rem !important;
text-overflow: ellipsis;
white-space: nowrap;
}
.mythic-dashboard-summary-table .MuiTableRow-hover:hover .MuiTableCell-root {
background-color: ${(props) => props.theme.palette.mode === "dark" ? alpha(props.theme.palette.common.white, 0.035) : alpha(props.theme.palette.common.black, 0.025)};
}
.mythic-dashboard-table-cell-primary {
min-width: 0;
}
.mythic-dashboard-table-cell-tight,
.mythic-dashboard-table-cell-count,
.mythic-dashboard-table-cell-actions {
text-align: right;
white-space: nowrap !important;
width: 1%;
}
.mythic-dashboard-table-cell-count {
color: ${(props) => props.theme.palette.text.primary};
font-weight: 850;
}
.mythic-dashboard-table-cell-actions {
min-width: 6.25rem;
}
.mythic-dashboard-table-cell-wrap {
overflow: visible !important;
overflow-wrap: anywhere;
white-space: normal !important;
}
.mythic-dashboard-table-cell-account {
align-items: flex-start;
display: flex !important;
flex-direction: row;
gap: 0.45rem;
}
.mythic-dashboard-table-cell-wrap .MuiTypography-root,
.mythic-dashboard-table-comment {
overflow-wrap: anywhere;
white-space: normal !important;
}
.mythic-dashboard-table-identity,
.mythic-dashboard-table-file-row,
.mythic-dashboard-table-actions-inline {
align-items: center;
display: flex;
gap: 0.45rem;
min-width: 0;
}
.mythic-dashboard-table-actions-inline {
justify-content: flex-end;
}
.mythic-dashboard-table-file-row {
max-width: 100%;
}
.mythic-dashboard-table-stack {
display: flex;
flex-direction: column;
gap: 0.12rem;
min-width: 0;
}
.mythic-dashboard-table-primary-text,
.mythic-dashboard-table-link,
.mythic-dashboard-summary-table .MuiTypography-root {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mythic-dashboard-table-primary-text,
.mythic-dashboard-table-link {
display: inline-block;
max-width: 100%;
}
.mythic-dashboard-table-secondary {
color: ${(props) => props.theme.palette.text.secondary};
font-size: 0.7rem !important;
font-weight: 650;
line-height: 1.2 !important;
}
.mythic-dashboard-table-cell-wrap .MuiTypography-root,
.mythic-dashboard-table-cell-wrap .mythic-dashboard-table-primary-text,
.mythic-dashboard-table-cell-wrap .mythic-dashboard-table-secondary {
overflow: visible;
text-overflow: clip;
white-space: normal !important;
}
.mythic-dashboard-table-icon-action {
color: ${(props) => props.theme.palette.text.secondary};
cursor: pointer;
display: inline-block;
flex: 0 0 auto;
height: 1.1rem;
width: 1.1rem;
}
.mythic-dashboard-table-icon-action:hover,
.mythic-dashboard-table-icon-action-info:hover {
color: ${(props) => props.theme.palette.info.main};
}
.mythic-dashboard-tasking-table {
table-layout: fixed;
}
.mythic-dashboard-tasking-cell {
max-width: 0;
min-width: 0;
overflow: hidden;
width: 100%;
}
.mythic-dashboard-tasking-list {
display: flex;
flex-direction: column;
max-width: 100%;
min-width: 0;
overflow: hidden;
width: 100%;
}
.mythic-dashboard-tasking-list .TaskDisplay-root,
.mythic-dashboard-tasking-list .TaskDisplayAccordion-root,
.mythic-dashboard-tasking-list .TaskDisplayAccordion-content,
.mythic-dashboard-tasking-list .TaskDisplayAccordionDetails-root {
max-width: 100%;
min-width: 0;
}
.mythic-dashboard-tasking-list .MuiAccordion-root,
.mythic-dashboard-tasking-list .MuiGrid-root,
.mythic-dashboard-tasking-list .mythic-response-table,
.mythic-dashboard-tasking-list .mythic-response-table-grid {
max-width: 100%;
min-width: 0;
width: 100%;
}
.mythic-dashboard-tasking-list .MuiCollapse-root,
.mythic-dashboard-tasking-list .MuiCollapse-wrapper,
.mythic-dashboard-tasking-list .MuiCollapse-wrapperInner {
max-width: 100%;
min-width: 0;
overflow: hidden;
}
.mythic-dashboard-card-table .MuiButton-root {
white-space: nowrap;
}
.mythic-dashboard-screenshot-row {
align-items: center;
@@ -3557,6 +3819,182 @@ tspan {
.mythic-dashboard-metric-title-row .MuiFormControl-root {
margin: 0 !important;
}
.mythic-dashboard-kpi-body {
padding: 0.55rem 0.65rem 0.65rem;
}
.mythic-dashboard-widget-settings-popover {
background-color: ${(props) => props.theme.surfaces?.raised || props.theme.palette.background.paper};
border: 1px solid ${(props) => props.theme.table?.border || props.theme.borderColor};
color: ${(props) => props.theme.palette.text.primary};
display: flex;
flex-direction: column;
gap: 0.55rem;
max-width: 17rem;
min-width: 14rem;
padding: 0.8rem;
}
.mythic-dashboard-widget-settings-title {
color: ${(props) => props.theme.palette.text.primary};
font-size: 0.82rem;
font-weight: 850;
line-height: 1.2;
}
.mythic-dashboard-widget-settings-copy {
color: ${(props) => props.theme.palette.text.secondary};
font-size: 0.72rem;
font-weight: 650;
line-height: 1.35;
}
.mythic-dashboard-callback-kpi,
.mythic-dashboard-service-kpi {
background-color: ${(props) => props.theme.palette.mode === "dark" ? alpha(props.theme.palette.common.white, 0.035) : alpha(props.theme.palette.common.black, 0.018)};
background-image: ${(props) => `linear-gradient(135deg, ${alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.13 : 0.075)} 0%, ${alpha(props.theme.palette.background.paper, 0)} 58%)`};
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
display: flex;
flex: 1 1 auto;
gap: 0.75rem;
min-height: 0;
min-width: 0;
overflow: hidden;
padding: 0.8rem;
}
.mythic-dashboard-callback-kpi {
cursor: pointer;
flex-direction: column;
justify-content: space-between;
}
.mythic-dashboard-callback-kpi:hover,
.mythic-dashboard-callback-kpi:focus-visible {
border-color: ${(props) => alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.52 : 0.34)};
outline: none;
}
.mythic-dashboard-service-kpi {
align-items: center;
flex-direction: row;
justify-content: space-between;
}
.mythic-dashboard-kpi-main {
display: flex;
flex: 1 1 auto;
flex-direction: column;
justify-content: center;
min-height: 0;
min-width: 0;
}
.mythic-dashboard-kpi-status-row {
align-items: center;
display: flex;
gap: 0.45rem;
justify-content: space-between;
min-width: 0;
}
.mythic-dashboard-kpi-chip {
align-items: center;
border: 1px solid transparent;
border-radius: 999px;
display: inline-flex;
flex: 0 0 auto;
font-size: 0.66rem;
font-weight: 850;
line-height: 1;
max-width: 100%;
overflow: hidden;
padding: 0.25rem 0.45rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.mythic-dashboard-kpi-chip-info {
background-color: ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.22 : 0.12)};
border-color: ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.52 : 0.34)};
color: ${(props) => props.theme.palette.info.main};
}
.mythic-dashboard-kpi-chip-success {
background-color: ${(props) => alpha(props.theme.palette.success.main, props.theme.palette.mode === "dark" ? 0.22 : 0.12)};
border-color: ${(props) => alpha(props.theme.palette.success.main, props.theme.palette.mode === "dark" ? 0.52 : 0.34)};
color: ${(props) => props.theme.palette.success.main};
}
.mythic-dashboard-kpi-chip-warning {
background-color: ${(props) => alpha(props.theme.palette.warning.main, props.theme.palette.mode === "dark" ? 0.24 : 0.14)};
border-color: ${(props) => alpha(props.theme.palette.warning.main, props.theme.palette.mode === "dark" ? 0.52 : 0.34)};
color: ${(props) => props.theme.palette.warning.main};
}
.mythic-dashboard-kpi-chip-danger {
background-color: ${(props) => alpha(props.theme.palette.error.main, props.theme.palette.mode === "dark" ? 0.22 : 0.12)};
border-color: ${(props) => alpha(props.theme.palette.error.main, props.theme.palette.mode === "dark" ? 0.52 : 0.34)};
color: ${(props) => props.theme.palette.error.main};
}
.mythic-dashboard-kpi-chip-neutral {
background-color: ${(props) => props.theme.palette.mode === "dark" ? alpha(props.theme.palette.common.white, 0.06) : alpha(props.theme.palette.common.black, 0.035)};
border-color: ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
color: ${(props) => props.theme.palette.text.secondary};
}
.mythic-dashboard-kpi-percent {
color: ${(props) => props.theme.palette.text.secondary};
flex: 0 0 auto;
font-size: 0.74rem;
font-weight: 850;
line-height: 1;
}
.mythic-dashboard-kpi-value-row {
align-items: baseline;
display: flex;
gap: 0.35rem;
min-width: 0;
}
.mythic-dashboard-kpi-value {
color: ${(props) => props.theme.palette.text.primary};
font-size: 3.35rem;
font-weight: 880;
letter-spacing: 0;
line-height: 0.98;
}
.mythic-dashboard-kpi-total {
color: ${(props) => props.theme.palette.text.secondary};
font-size: 1.25rem;
font-weight: 760;
line-height: 1;
}
.mythic-dashboard-kpi-label {
color: ${(props) => props.theme.palette.text.secondary};
font-size: 0.78rem;
font-weight: 750;
letter-spacing: 0;
line-height: 1.25;
margin-top: 0.25rem;
}
.mythic-dashboard-kpi-secondary-panel {
align-items: center;
background-color: ${(props) => props.theme.palette.mode === "dark" ? alpha(props.theme.palette.common.white, 0.045) : alpha(props.theme.palette.common.black, 0.025)};
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
display: flex;
gap: 0.5rem;
min-width: 0;
padding: 0.55rem 0.65rem;
}
.mythic-dashboard-kpi-secondary-value {
color: ${(props) => props.theme.palette.text.primary};
flex: 0 0 auto;
font-size: 1.65rem;
font-weight: 880;
line-height: 1;
}
.mythic-dashboard-kpi-secondary-label {
color: ${(props) => props.theme.palette.text.secondary};
font-size: 0.72rem;
font-weight: 700;
line-height: 1.25;
min-width: 0;
overflow-wrap: anywhere;
}
.mythic-dashboard-kpi-gauge {
align-items: center;
display: flex;
flex: 0 0 7rem;
justify-content: center;
min-width: 7rem;
}
.mythic-dashboard-metric-content,
.mythic-dashboard-metric-link {
display: flex;
@@ -3596,7 +4034,15 @@ tspan {
}
.mythic-dashboard-slider {
align-self: center;
margin-top: 0.65rem;
margin: 0;
width: min(100%, 32rem);
}
.mythic-dashboard-slider .MuiSlider-rail {
opacity: 0.28;
}
.mythic-dashboard-slider .MuiSlider-thumb {
height: 12px;
width: 12px;
}
.mythic-dashboard-edit-rail {
align-items: center;
@@ -3610,6 +4056,172 @@ tspan {
justify-content: center;
padding: 0.45rem;
}
.mythic-dashboard-edit-toolbar {
align-items: center;
background-color: ${(props) => {
const headerSurface = props.theme.pageHeader?.main || props.theme.surfaces?.muted || props.theme.palette.background.paper;
return props.theme.palette.mode === "dark" ? headerSurface : alpha(props.theme.palette.primary.main, 0.16);
}} !important;
background-image: ${(props) => `linear-gradient(90deg, ${alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.44 : 0.32)} 0%, ${alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.22 : 0.22)} 42%, ${alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.08 : 0.12)} 100%)`} !important;
border: 1px solid ${(props) => alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.55 : 0.38)};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
box-shadow: inset 0 1px 0 ${(props) => alpha(props.theme.pageHeaderText?.main || props.theme.palette.text.primary, 0.22)}, 0 2px 6px ${(props) => alpha(props.theme.palette.common.black, props.theme.palette.mode === "dark" ? 0.24 : 0.1)};
color: ${(props) => props.theme.pageHeaderText?.main || props.theme.palette.text.primary};
display: flex;
flex-wrap: wrap;
gap: 0.55rem;
grid-column: 1 / -1;
justify-content: space-between;
min-width: 0;
overflow: hidden;
padding: 0.52rem 0.65rem 0.52rem 1rem;
position: relative;
}
.mythic-dashboard-edit-toolbar::before {
background-color: ${(props) => props.theme.palette.primary.main};
bottom: 0;
box-shadow: 0 0 0 1px ${(props) => alpha(props.theme.pageHeaderText?.main || props.theme.palette.text.primary, 0.2)};
content: "";
left: 0;
position: absolute;
top: 0;
width: 6px;
}
.mythic-dashboard-edit-toolbar-title {
color: inherit;
font-size: 0.78rem;
font-weight: 850;
letter-spacing: 0;
line-height: 1.2;
white-space: nowrap;
}
.mythic-dashboard-edit-toolbar-actions {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
justify-content: flex-end;
min-width: 0;
}
.mythic-dashboard-edit-toolbar .MuiButton-root.mythic-dashboard-table-action {
background-color: ${(props) => alpha(props.theme.pageHeaderText?.main || props.theme.palette.text.primary, 0.1)} !important;
border-color: ${(props) => alpha(props.theme.pageHeaderText?.main || props.theme.palette.text.primary, 0.24)} !important;
color: ${(props) => props.theme.pageHeaderText?.main || props.theme.palette.text.primary} !important;
}
.mythic-dashboard-edit-toolbar .MuiButton-root.mythic-dashboard-table-action:hover {
background-color: ${(props) => alpha(props.theme.pageHeaderText?.main || props.theme.palette.text.primary, 0.16)} !important;
border-color: ${(props) => alpha(props.theme.pageHeaderText?.main || props.theme.palette.text.primary, 0.42)} !important;
}
.mythic-dashboard-edit-toolbar .MuiButton-root.mythic-table-row-action-hover-info:hover {
background-color: ${(props) => props.theme.palette.info.main + "2b"} !important;
border-color: ${(props) => props.theme.palette.info.main + "88"} !important;
color: ${(props) => props.theme.palette.info.main} !important;
}
.mythic-dashboard-edit-toolbar .MuiButton-root.mythic-dashboard-table-action-hover-danger:hover {
background-color: ${(props) => props.theme.palette.error.main + "2b"} !important;
border-color: ${(props) => props.theme.palette.error.main + "88"} !important;
color: ${(props) => props.theme.palette.error.main} !important;
}
.mythic-dashboard-widget-dialog {
background-color: ${(props) => props.theme.surfaces?.muted || props.theme.palette.background.default};
min-height: 28rem;
}
.mythic-dashboard-widget-dialog-header {
align-items: center;
background-color: ${(props) => props.theme.surfaces?.raised || props.theme.palette.background.paper};
background-image: ${(props) => {
const headerTextColor = props.theme.palette.text.primary;
return `linear-gradient(90deg, ${alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.12 : 0.07)} 0%, ${alpha(headerTextColor, props.theme.palette.mode === "dark" ? 0.045 : 0.035)} 100%)`;
}};
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
display: flex;
gap: 0.75rem;
justify-content: space-between;
margin-bottom: 0.75rem;
min-width: 0;
padding: 0.7rem 0.8rem;
}
.mythic-dashboard-widget-dialog-title {
color: ${(props) => props.theme.palette.text.primary};
font-size: 0.9rem;
font-weight: 850;
line-height: 1.2;
}
.mythic-dashboard-widget-dialog-subtitle {
color: ${(props) => props.theme.palette.text.secondary};
font-size: 0.74rem;
font-weight: 650;
line-height: 1.25;
margin-top: 0.18rem;
}
.mythic-dashboard-widget-grid {
display: grid;
gap: 0.65rem;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
min-width: 0;
}
.mythic-dashboard-widget-option {
background-color: ${(props) => props.theme.surfaces?.raised || props.theme.palette.background.paper};
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
box-shadow: ${(props) => props.theme.palette.mode === "dark" ? "inset 0 1px 0 rgba(255,255,255,0.04)" : "0 1px 2px rgba(15,23,42,0.05)"};
cursor: pointer;
display: flex;
flex-direction: column;
gap: 0.5rem;
min-height: 6.4rem;
min-width: 0;
padding: 0.7rem;
transition: background-color 140ms ease, border-color 140ms ease, box-shadow 140ms ease, transform 140ms ease;
}
.mythic-dashboard-widget-option:hover,
.mythic-dashboard-widget-option:focus-visible {
background-color: ${(props) => props.theme.palette.primary.main + "10"};
border-color: ${(props) => alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.5 : 0.34)};
box-shadow: ${(props) => props.theme.palette.mode === "dark" ? `inset 0 1px 0 ${alpha(props.theme.palette.common.white, 0.055)}, 0 8px 18px ${alpha(props.theme.palette.common.black, 0.18)}` : `0 8px 18px ${alpha(props.theme.palette.common.black, 0.08)}`};
outline: none;
}
.mythic-dashboard-widget-option-selected {
background-image: ${(props) => `linear-gradient(135deg, ${alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.18 : 0.1)} 0%, ${alpha(props.theme.palette.background.paper, 0)} 60%)`};
border-color: ${(props) => alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.68 : 0.48)};
}
.mythic-dashboard-widget-option-top {
align-items: flex-start;
display: flex;
gap: 0.5rem;
justify-content: space-between;
min-width: 0;
}
.mythic-dashboard-widget-option-title {
color: ${(props) => props.theme.palette.text.primary};
font-size: 0.84rem;
font-weight: 820;
line-height: 1.2;
min-width: 0;
overflow-wrap: anywhere;
}
.mythic-dashboard-widget-option-summary {
color: ${(props) => props.theme.palette.text.secondary};
font-size: 0.72rem;
font-weight: 600;
line-height: 1.35;
overflow-wrap: anywhere;
}
.mythic-dashboard-widget-category {
align-items: center;
background-color: ${(props) => props.theme.palette.primary.main + "18"};
border: 1px solid ${(props) => props.theme.palette.primary.main + "45"};
border-radius: 999px;
color: ${(props) => props.theme.palette.primary.main};
display: inline-flex;
flex: 0 0 auto;
font-size: 0.64rem;
font-weight: 850;
line-height: 1;
padding: 0.24rem 0.42rem;
white-space: nowrap;
}
.mythic-dashboard-loading-overlay {
align-items: center;
background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(0,0,0,0.45)" : "rgba(255,255,255,0.55)"};
@@ -3629,6 +4241,31 @@ tspan {
font-weight: 750;
padding: 0.75rem 0.9rem;
}
@media (max-width: 900px) {
.mythic-dashboard-row {
grid-template-columns: 1fr;
}
.mythic-dashboard-card,
.mythic-dashboard-card-wide,
.mythic-dashboard-card-table {
grid-column: span 1;
max-width: 100%;
}
.mythic-dashboard-perspective-toggle {
width: 100%;
}
.mythic-dashboard-perspective-toggle .MuiToggleButton-root {
flex: 1 1 0;
min-width: 0 !important;
}
.mythic-dashboard-edit-toolbar {
align-items: stretch;
flex-direction: column;
}
.mythic-dashboard-edit-toolbar-actions {
justify-content: flex-start;
}
}
.mythic-form-code-editor {
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
@@ -5543,10 +6180,11 @@ tspan {
}
.mythic-eventing-flow-node-action {
align-items: center;
background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.04)" : "rgba(0,0,0,0.035)"};
background-color: ${(props) => props.theme.palette.info.main + "16"};
border-color: ${(props) => props.theme.palette.info.main + "45"};
color: ${(props) => props.theme.palette.info.main};
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
color: ${(props) => props.theme.palette.text.secondary};
display: inline-flex;
font-size: 0.66rem;
font-weight: 750;