updated browser script table page

This commit is contained in:
its-a-feature
2026-05-05 18:14:27 -05:00
parent 45e8910faa
commit 9b053e272e
13 changed files with 795 additions and 254 deletions
@@ -211,16 +211,21 @@ export const MythicPageHeader = ({
export const MythicPageHeaderChip = ({status, sx = {}, ...props}) => {
const theme = useTheme();
const headerTextColor = theme.pageHeaderText?.main || theme.palette.text.primary;
const statusColor = status ? theme.palette[status]?.main : null;
const normalizedStatus = status === "active" || status === "enabled" ? "success" :
status === "inactive" || status === "disabled" ? "warning" :
status === "neutral" ? "neutral" : status;
const statusColor = normalizedStatus && normalizedStatus !== "neutral" ? theme.palette[normalizedStatus]?.main : null;
const chipColor = statusColor || alpha(headerTextColor, 0.88);
const neutralBackground = theme.palette.mode === "dark" ? alpha(theme.palette.common.white, 0.06) : alpha(theme.palette.common.black, 0.035);
const neutralBorder = theme.table?.borderSoft || alpha(headerTextColor, 0.2);
return (
<Chip
size="small"
variant="outlined"
{...props}
sx={{
backgroundColor: statusColor ? alpha(statusColor, theme.palette.mode === "dark" ? 0.22 : 0.13) : alpha(headerTextColor, 0.08),
borderColor: statusColor ? alpha(statusColor, theme.palette.mode === "dark" ? 0.55 : 0.38) : alpha(headerTextColor, 0.2),
backgroundColor: statusColor ? alpha(statusColor, theme.palette.mode === "dark" ? 0.22 : 0.13) : (normalizedStatus === "neutral" ? neutralBackground : alpha(headerTextColor, 0.08)),
borderColor: statusColor ? alpha(statusColor, theme.palette.mode === "dark" ? 0.55 : 0.38) : (normalizedStatus === "neutral" ? neutralBorder : alpha(headerTextColor, 0.2)),
color: chipColor,
fontSize: "0.72rem",
fontWeight: 750,
@@ -0,0 +1,15 @@
import React from 'react';
import Box from '@mui/material/Box';
export function MythicStateChip({children, className = "", compact = false, label, state = "neutral", ...props}) {
const chipLabel = label ?? children;
return (
<Box
component="span"
className={`mythic-state-chip mythic-state-chip-${state}${compact ? " mythic-state-chip-compact" : ""}${className ? ` ${className}` : ""}`}
{...props}
>
{chipLabel}
</Box>
);
}
@@ -137,7 +137,7 @@ export function BrowserScripts({me}){
}, [])
return (
<MythicPageBody>
<Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute"}} invisible={false}>
<Backdrop open={backdropOpen} style={{zIndex: 1300, position: "absolute"}} invisible={false}>
<MythicLoadingState compact title="Loading browser scripts" description="Fetching scripts for this operator." sx={{color: "inherit"}} />
</Backdrop>
<BrowserScriptsTable
@@ -1,7 +1,6 @@
import React from 'react';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
@@ -10,16 +9,52 @@ import AddCircleIcon from '@mui/icons-material/AddCircle';
import { MythicDialog } from '../../MythicComponents/MythicDialog';
import {EditScriptDialog} from './EditScriptDialog';
import {MythicPageHeader, MythicPageHeaderChip} from "../../MythicComponents/MythicPageHeader";
import {MythicToolbarButton} from "../../MythicComponents/MythicTableToolbar";
import {
MythicSearchField,
MythicTableToolbar,
MythicTableToolbarGroup,
MythicToolbarButton,
MythicToolbarMenuItem,
MythicToolbarSelect
} from "../../MythicComponents/MythicTableToolbar";
import {MythicTableEmptyState} from "../../MythicComponents/MythicStateDisplay";
import MythicStyledTableCell from "../../MythicComponents/MythicTableCell";
export function BrowserScriptsTable(props){
const [openNewScriptDialog, setOpenNewScriptDialog] = React.useState(false);
const [search, setSearch] = React.useState("");
const [statusFilter, setStatusFilter] = React.useState("all");
const normalizedSearch = search.trim().toLowerCase();
const visibleScripts = React.useMemo(() => {
return props.browserscripts.filter((script) => {
const matchesStatus = statusFilter === "all" ||
(statusFilter === "active" && script.active) ||
(statusFilter === "disabled" && !script.active) ||
(statusFilter === "modified" && script.user_modified) ||
(statusFilter === "default" && !script.user_modified);
if(!matchesStatus){
return false;
}
if(normalizedSearch === ""){
return true;
}
return [
script.payloadtype?.name,
script.command?.cmd,
script.author,
].some((value) => (value || "").toLowerCase().includes(normalizedSearch));
});
}, [normalizedSearch, props.browserscripts, statusFilter]);
const scriptCountLabel = props.browserscripts.length === 1 ? "1 script" : `${props.browserscripts.length} scripts`;
const visibleCountLabel = visibleScripts.length === 1 ? "1 shown" : `${visibleScripts.length} shown`;
const activeCount = props.browserscripts.filter((script) => script.active).length;
const activeCountLabel = activeCount === 1 ? "1 active" : `${activeCount} active`;
const modifiedCount = props.browserscripts.filter((script) => script.user_modified).length;
const hasFilter = normalizedSearch !== "" || statusFilter !== "all";
const onChangeSearch = (name, value) => {
setSearch(value);
}
return (
<>
<MythicPageHeader
@@ -28,6 +63,7 @@ export function BrowserScriptsTable(props){
meta={
<>
<MythicPageHeaderChip label={scriptCountLabel} />
{hasFilter && <MythicPageHeaderChip label={visibleCountLabel} />}
<MythicPageHeaderChip label={activeCountLabel} />
{modifiedCount > 0 && <MythicPageHeaderChip label={`${modifiedCount} modified`} />}
</>
@@ -45,28 +81,50 @@ export function BrowserScriptsTable(props){
<EditScriptDialog me={props.me} onClose={()=>{setOpenNewScriptDialog(false);}} title="Create New Browser Script" new={true} onSubmitEdit={props.onSubmitNew} />
} />
}
<TableContainer className="mythicElement" style={{height: "100%", flexGrow: 1}}>
<Table stickyHeader={true} size="small" style={{"tableLayout": "fixed", "maxWidth": "100%", "overflow": "scroll"}}>
<MythicTableToolbar variant="search">
<MythicTableToolbarGroup grow>
<MythicSearchField
name="Search browser scripts"
placeholder="Search payload, command, or author..."
value={search}
onChange={onChangeSearch}
/>
</MythicTableToolbarGroup>
<MythicTableToolbarGroup>
<MythicToolbarSelect
value={statusFilter}
onChange={(event) => setStatusFilter(event.target.value)}
>
<MythicToolbarMenuItem value="all">All scripts</MythicToolbarMenuItem>
<MythicToolbarMenuItem value="active">Active</MythicToolbarMenuItem>
<MythicToolbarMenuItem value="disabled">Disabled</MythicToolbarMenuItem>
<MythicToolbarMenuItem value="modified">User modified</MythicToolbarMenuItem>
<MythicToolbarMenuItem value="default">Container default</MythicToolbarMenuItem>
</MythicToolbarSelect>
</MythicTableToolbarGroup>
</MythicTableToolbar>
<TableContainer className="mythicElement" style={{flexGrow: 1, minHeight: 0, overflow: "auto"}}>
<Table className="mythic-browser-scripts-table" stickyHeader={true} size="small">
<TableHead>
<TableRow>
<TableCell style={{width: "3rem"}}>Edit</TableCell>
<TableCell style={{width: "5rem"}}>Active</TableCell>
<TableCell style={{width: "5rem"}}>Payload</TableCell>
<TableCell style={{width: "20rem"}}>Command</TableCell>
<TableCell style={{width: "12rem"}}> Author</TableCell>
<TableCell style={{textAlign: "left"}}>User Modified?</TableCell>
<MythicStyledTableCell style={{width: "24rem"}}>Script</MythicStyledTableCell>
<MythicStyledTableCell style={{width: "10rem"}}>Author</MythicStyledTableCell>
<MythicStyledTableCell style={{width: "9.5rem"}}>Active</MythicStyledTableCell>
<MythicStyledTableCell style={{width: "12rem"}}>Source</MythicStyledTableCell>
<MythicStyledTableCell style={{width: "4rem", textAlign: "center"}}>Actions</MythicStyledTableCell>
<MythicStyledTableCell className="mythic-browser-script-spacer-cell" />
</TableRow>
</TableHead>
<TableBody>
{props.browserscripts.length === 0 &&
{visibleScripts.length === 0 &&
<MythicTableEmptyState
colSpan={6}
compact
title="No browser scripts"
description="Create or import browser scripts to customize task output rendering."
title={props.browserscripts.length === 0 ? "No browser scripts" : "No browser scripts match"}
description={props.browserscripts.length === 0 ? "Create or import browser scripts to customize task output rendering." : "Try changing the search text or status filter."}
/>
}
{props.browserscripts.map( (op) => (
{visibleScripts.map( (op) => (
<BrowserScriptsTableRow
me={props.me}
operation_id={props.operation_id} onToggleActive={props.onToggleActive}
@@ -1,6 +1,6 @@
import React from 'react';
import { Switch} from '@mui/material';
import TableCell from '@mui/material/TableCell';
import Box from '@mui/material/Box';
import { Switch } from '@mui/material';
import TableRow from '@mui/material/TableRow';
import { MythicDialog } from '../../MythicComponents/MythicDialog';
import {EditScriptDialog} from './EditScriptDialog';
@@ -8,6 +8,8 @@ import EditIcon from '@mui/icons-material/Edit';
import IconButton from '@mui/material/IconButton';
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
import {MythicAgentSVGIcon} from "../../MythicComponents/MythicAgentSVGIcon";
import MythicStyledTableCell from "../../MythicComponents/MythicTableCell";
import {MythicStateChip} from "../../MythicComponents/MythicStateChip";
export function BrowserScriptsTableRow(props){
const [openEdit, setOpenEdit] = React.useState(false);
@@ -22,29 +24,45 @@ export function BrowserScriptsTableRow(props){
}
return (
<React.Fragment>
<TableRow key={"payload" + props.id} hover>
<TableCell >
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small" onClick={()=>{setOpenEdit(true);}}>
<EditIcon fontSize="small" />
</IconButton>
</TableCell>
<TableCell >
<Switch
checked={props.active}
onChange={onToggleActive}
color="info"
inputProps={{ 'aria-label': 'checkbox', "track": "white" }}
name="Active"
/>
</TableCell>
<TableCell>
<MythicStyledTooltip title={props.payloadtype.name}>
<MythicAgentSVGIcon payload_type={props.payloadtype.name} style={{width: "35px", height: "35px"}} />
</MythicStyledTooltip>
</TableCell>
<TableCell>{props.command.cmd}</TableCell>
<TableCell>{props.author}</TableCell>
<TableCell>{props.user_modified ? "User Modified" : "" } </TableCell>
<TableRow className={props.active ? "" : "mythic-browser-script-row-disabled"} key={"payload" + props.id} hover>
<MythicStyledTableCell>
<Box className="mythic-browser-script-script-cell">
<MythicStyledTooltip title={props.payloadtype.name}>
<Box className="mythic-browser-script-payload-icon">
<MythicAgentSVGIcon payload_type={props.payloadtype.name} style={{width: "32px", height: "32px"}} />
</Box>
</MythicStyledTooltip>
<Box className="mythic-browser-script-script-copy">
<Box className="mythic-browser-script-command-name">{props.command.cmd}</Box>
<Box className="mythic-browser-script-payload-name">{props.payloadtype.name}</Box>
</Box>
</Box>
</MythicStyledTableCell>
<MythicStyledTableCell>{props.author}</MythicStyledTableCell>
<MythicStyledTableCell>
<Box className="mythic-browser-script-active-cell">
<Switch
checked={props.active}
onChange={onToggleActive}
color="success"
inputProps={{ 'aria-label': 'Toggle browser script active state', "track": "white" }}
name="Active"
size="small"
/>
<MythicStateChip label={props.active ? "Active" : "Disabled"} state={props.active ? "active" : "disabled"} />
</Box>
</MythicStyledTableCell>
<MythicStyledTableCell>
<MythicStateChip label={props.user_modified ? "User modified" : "Container default"} state={props.user_modified ? "warning" : "neutral"} />
</MythicStyledTableCell>
<MythicStyledTableCell style={{textAlign: "center"}}>
<Box className="mythic-table-row-actions mythic-table-row-actions-nowrap mythic-browser-script-actions">
<IconButton className="mythic-table-row-icon-action mythic-table-row-icon-action-hover-info" size="small" onClick={()=>{setOpenEdit(true);}}>
<EditIcon fontSize="small" />
</IconButton>
</Box>
</MythicStyledTableCell>
<MythicStyledTableCell className="mythic-browser-script-spacer-cell" />
{openEdit &&
<MythicDialog fullWidth={true} maxWidth="xl" open={openEdit}
@@ -1,8 +1,10 @@
import React, {useEffect, useRef} from 'react';
import Button from '@mui/material/Button';
import DialogActions from '@mui/material/DialogActions';
import Box from '@mui/material/Box';
import Chip from '@mui/material/Chip';
import Collapse from '@mui/material/Collapse';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import IconButton from '@mui/material/IconButton';
import AceEditor from 'react-ace';
import "ace-builds/src-noconflict/mode-javascript";
import "ace-builds/src-noconflict/theme-github";
@@ -14,11 +16,21 @@ import MenuItem from '@mui/material/MenuItem';
import FormControl from '@mui/material/FormControl';
import Select from '@mui/material/Select';
import InputLabel from '@mui/material/InputLabel';
import Input from '@mui/material/Input';
import Tooltip from '@mui/material/Tooltip';
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import Split from 'react-split';
import {TaskDisplay} from "../Callbacks/TaskDisplay";
import {taskingDataFragment} from '../Callbacks/CallbackMutations'
import {
MythicDialogBody,
MythicDialogButton,
MythicDialogFooter,
MythicFormField,
MythicFormGrid,
MythicFormNote
} from "../../MythicComponents/MythicDialogLayout";
import {MythicEmptyState, MythicLoadingState} from "../../MythicComponents/MythicStateDisplay";
const getCommandsAndPayloadTypesQuery = gql`
@@ -42,6 +54,38 @@ query getAvailableTasks($command_id: Int!){
}
`;
const BROWSER_SCRIPT_WORKBENCH_SPLIT_KEY = "browserScriptWorkbenchSplitSizes";
const BROWSER_SCRIPT_TOP_SPLIT_KEY = "browserScriptTopSplitSizes";
const defaultWorkbenchSplitSizes = [48, 52];
const defaultTopSplitSizes = [68, 32];
const getStoredSplitSizes = (storageKey, fallback) => {
try {
if(typeof window === "undefined"){
return fallback;
}
const storedSizes = window.localStorage.getItem(storageKey);
if(!storedSizes){
return fallback;
}
const parsedSizes = JSON.parse(storedSizes);
if(!Array.isArray(parsedSizes) || parsedSizes.length !== fallback.length || !parsedSizes.every(Number.isFinite)){
return fallback;
}
return parsedSizes;
} catch (error) {
return fallback;
}
}
const storeSplitSizes = (storageKey, sizes) => {
try {
window.localStorage.setItem(storageKey, JSON.stringify(sizes));
} catch (error) {
// Best effort only; layout still works without persisted sizes.
}
}
export function EditScriptDialog(props) {
const theme = useTheme();
const [script, setScript] = React.useState("");
@@ -49,12 +93,19 @@ export function EditScriptDialog(props) {
const [selectedCommand, setSelectedCommand] = React.useState('');
const [payloadTypeCmdOptions, setPayloadTypeCmdOptions] = React.useState([]);
const [commandOptions, setCommandOptions] = React.useState([]);
const inputPTRef = useRef(null);
const inputCMDRef = useRef(null);
const inputTaskRef = useRef(null);
const [availableTasks, setAvailableTasks] = React.useState([]);
const [selectedTask, setSelectedTask] = React.useState('');
useQuery(getCommandsAndPayloadTypesQuery, {
const [workbenchSplitSizes, setWorkbenchSplitSizes] = React.useState(() => getStoredSplitSizes(BROWSER_SCRIPT_WORKBENCH_SPLIT_KEY, defaultWorkbenchSplitSizes));
const [topSplitSizes, setTopSplitSizes] = React.useState(() => getStoredSplitSizes(BROWSER_SCRIPT_TOP_SPLIT_KEY, defaultTopSplitSizes));
const [targetOpen, setTargetOpen] = React.useState(() => props.new === true);
const selectedPayloadTypeOption = React.useMemo(() => {
return payloadTypeCmdOptions.find((payloadType) => payloadType.id === selectedPayloadType);
}, [payloadTypeCmdOptions, selectedPayloadType]);
const selectedCommandOption = React.useMemo(() => {
return commandOptions.find((command) => command.id === selectedCommand);
}, [commandOptions, selectedCommand]);
const taskSelectorValue = selectedTask || "";
const {loading: targetLoading} = useQuery(getCommandsAndPayloadTypesQuery, {
onCompleted: data => {
setPayloadTypeCmdOptions(data.payloadtype);
if(props.payload_type_id !== undefined){
@@ -66,11 +117,8 @@ export function EditScriptDialog(props) {
}
if(props.command_id !== undefined){
setSelectedCommand(props.command_id);
for(let i = 0; i < data.payloadtype.length; i++){
if(props.payload_type_id === data.payloadtype[i].id){
setCommandOptions(data.payloadtype[i].commands)
}
}
const payloadType = data.payloadtype.find((payloadType) => payloadType.id === props.payload_type_id);
setCommandOptions(payloadType?.commands || []);
}else{
if(data.payloadtype.length > 0){
if(data.payloadtype[0].commands.length > 0){
@@ -84,11 +132,13 @@ export function EditScriptDialog(props) {
}
});
const [getAvailableTasks] = useLazyQuery(getExistingTasksForCommand, {
const [getAvailableTasks, {loading: tasksLoading}] = useLazyQuery(getExistingTasksForCommand, {
onCompleted: data => {
setAvailableTasks(data.task);
if(data.task.length > 0){
setSelectedTask(data.task[0]);
}else{
setSelectedTask('');
}
},
onError: data => {
@@ -102,9 +152,12 @@ export function EditScriptDialog(props) {
useEffect( () => {
if(selectedCommand !== ""){
getAvailableTasks({variables: {command_id: selectedCommand}})
}else{
setAvailableTasks([]);
setSelectedTask('');
}
}, [selectedCommand]);
}, [selectedCommand, getAvailableTasks]);
useEffect( () => {
if(props.script !== undefined){
try{
@@ -137,9 +190,12 @@ export function EditScriptDialog(props) {
}
const onChangeSelectedPayloadType = (event) => {
setSelectedPayloadType(event.target.value);
const cmds = payloadTypeCmdOptions.filter( (p) => p.id === event.target.value);
setCommandOptions(cmds[0].commands);
setSelectedCommand(cmds[0].commands[0].id);
const selectedPayload = payloadTypeCmdOptions.find( (p) => p.id === event.target.value);
const cmds = selectedPayload?.commands || [];
setCommandOptions(cmds);
setSelectedCommand(cmds[0]?.id || '');
setAvailableTasks([]);
setSelectedTask('');
}
const onChangeTask = (event) => {
setSelectedTask(event.target.value);
@@ -148,34 +204,48 @@ export function EditScriptDialog(props) {
setSelectedCommand(event.target.value);
}
const onLoad = (editor) => {
// Your editor options comes here
editor.on('change', (arg, activeEditor) => {
editorRef.current = activeEditor;
editor.removeEventListener('change');
editorRef.current.resize();
});
editorRef.current = editor;
requestAnimationFrame(() => editor.resize());
}
const onOutputLoad = (editor) => {
// Your editor options comes here
editor.on('change', (arg, activeEditor) => {
outputRef.current = activeEditor;
editor.removeEventListener('change');
outputRef.current.resize();
});
outputRef.current = editor;
requestAnimationFrame(() => editor.resize());
}
React.useEffect( () => {
var logBackup = console.log;
console.log = function(msg) {
logStreamRef.current += "\n" + msg;
logBackup.apply(msg);
setLogOutput(logStreamRef.current)
const logBackup = console.log;
const formatConsoleArg = (arg) => {
if(typeof arg === "string"){
return arg;
}
try {
return JSON.stringify(arg, null, 2);
} catch (error) {
return String(arg);
}
};
console.log = function(...args) {
const message = args.map(formatConsoleArg).join(" ");
logStreamRef.current += "\n" + message;
setLogOutput(logStreamRef.current);
logBackup.apply(console, args);
};
return () => {
console.log = logBackup;
};
}, []);
const onDragging = () => {
React.useEffect(() => {
requestAnimationFrame(() => {
if(editorRef.current){
editorRef.current.resize();
}
if(outputRef.current){
outputRef.current.resize();
}
});
}, [selectedTask, tasksLoading]);
const resizeEditors = () => {
if(editorRef.current){
editorRef.current.resize();
}
@@ -183,128 +253,227 @@ export function EditScriptDialog(props) {
outputRef.current.resize();
}
}
const onWorkbenchDragEnd = (sizes) => {
setWorkbenchSplitSizes(sizes);
storeSplitSizes(BROWSER_SCRIPT_WORKBENCH_SPLIT_KEY, sizes);
resizeEditors();
}
const onTopDragEnd = (sizes) => {
setTopSplitSizes(sizes);
storeSplitSizes(BROWSER_SCRIPT_TOP_SPLIT_KEY, sizes);
resizeEditors();
}
const onToggleTargetOpen = () => {
setTargetOpen((current) => !current);
}
const selectedTaskLabel = (task) => {
return `${task.command_name} / ${task.display_id} / ${task.display_params}`;
}
return (
<React.Fragment>
<DialogTitle >
{props.title ? props.title : "Edit " + props.author + "'s BrowserScript Code"}
</DialogTitle>
<DialogContent dividers={true} style={{height: `calc(100vh)`, display: "flex", flexDirection: "column", width: "100%"}}>
<div style={{display: "flex"}}>
<FormControl style={{width: "50%"}}>
<InputLabel ref={inputPTRef}>Payload Type</InputLabel>
<Select
labelId="demo-dialog-select-label"
id="demo-dialog-select"
value={selectedPayloadType}
onChange={onChangeSelectedPayloadType}
input={<Input style={{width: "100%"}}/>}
<DialogTitle>
<Box className="mythic-dialog-title-row">
<Box component="span">{props.title ? props.title : "Edit " + props.author + "'s BrowserScript Code"}</Box>
</Box>
</DialogTitle>
<DialogContent className="mythic-browser-script-dialog-content" dividers={true}>
<MythicDialogBody className="mythic-browser-script-dialog-body">
<Box className={`mythic-browser-script-target-panel ${targetOpen ? "mythic-browser-script-target-panel-open" : ""}`}>
<Box className="mythic-browser-script-target-summary">
<Box className="mythic-browser-script-target-copy">
<Box component="span" className="mythic-browser-script-target-label">Script Target</Box>
<Box className="mythic-browser-script-target-chips">
<Chip size="small" label={selectedPayloadTypeOption?.name || (targetLoading ? "Loading payload types" : "No payload type")} />
<Chip size="small" label={selectedCommandOption?.cmd || "No command"} />
</Box>
</Box>
<Tooltip title={targetOpen ? "Collapse target settings" : "Edit target settings"}>
<IconButton
aria-expanded={targetOpen}
aria-label={targetOpen ? "Collapse target settings" : "Edit target settings"}
className="mythic-browser-script-target-toggle"
onClick={onToggleTargetOpen}
size="small"
>
{targetOpen ? <KeyboardArrowUpIcon fontSize="small" /> : <KeyboardArrowDownIcon fontSize="small" />}
</IconButton>
</Tooltip>
</Box>
<Collapse
in={targetOpen}
onEntered={resizeEditors}
onExited={resizeEditors}
timeout="auto"
unmountOnExit
>
{payloadTypeCmdOptions.map( (opt) => (
<MenuItem value={opt.id} key={"payloadtype" + opt.id}>{opt.name}</MenuItem>
) )}
</Select>
</FormControl>
<FormControl style={{width: "50%", paddingBottom: "10px"}}>
<InputLabel ref={inputCMDRef}>Command</InputLabel>
<Select
labelId="demo-dialog-select-label"
id="demo-dialog-select"
value={selectedCommand}
onChange={onChangeSelectedCommand}
input={<Input style={{width: "100%"}}/>}
<Box className="mythic-browser-script-target-details">
<MythicFormGrid minWidth="16rem">
<MythicFormField label="Payload Type">
<FormControl fullWidth size="small">
<InputLabel id="browser-script-payload-type-label">Payload Type</InputLabel>
<Select
label="Payload Type"
labelId="browser-script-payload-type-label"
value={selectedPayloadType}
onChange={onChangeSelectedPayloadType}
disabled={targetLoading}
>
{payloadTypeCmdOptions.map( (opt) => (
<MenuItem value={opt.id} key={"payloadtype" + opt.id}>{opt.name}</MenuItem>
) )}
</Select>
</FormControl>
</MythicFormField>
<MythicFormField label="Command">
<FormControl fullWidth size="small">
<InputLabel id="browser-script-command-label">Command</InputLabel>
<Select
label="Command"
labelId="browser-script-command-label"
value={selectedCommand}
onChange={onChangeSelectedCommand}
disabled={targetLoading || commandOptions.length === 0}
>
{commandOptions.map( (opt) => (
<MenuItem value={opt.id} key={"command" + opt.id}>{opt.cmd}</MenuItem>
) )}
</Select>
</FormControl>
</MythicFormField>
</MythicFormGrid>
<MythicFormNote>
Use Save for Testing to update the script without closing the editor. Select an already executed matching task below, then collapse and re-expand that task if you need it to reload the updated renderer.
</MythicFormNote>
</Box>
</Collapse>
</Box>
<Split
className="mythic-browser-script-workbench"
direction="vertical"
gutterSize={8}
minSize={[210, 300]}
onDrag={resizeEditors}
onDragEnd={onWorkbenchDragEnd}
sizes={workbenchSplitSizes}
>
<Split
className="mythic-browser-script-top-split"
gutterSize={8}
minSize={[320, 220]}
onDrag={resizeEditors}
onDragEnd={onTopDragEnd}
sizes={topSplitSizes}
>
{commandOptions.map( (opt) => (
<MenuItem value={opt.id} key={"command" + opt.id}>{opt.cmd}</MenuItem>
) )}
</Select>
</FormControl>
</div>
<p>
<b>To test locally</b>: Make your changes in the top left code box. Any <b>console.log</b> entries will appear in the box to the right when executed.
Click <b>Save for Testing</b> to finalize your changes. Make sure to select an already executed task that matches this command from the bottom.
You will need to collapse and re-expand the task to pull in your updated changes.
</p>
<div style={{display: "flex", width: "100%", flexGrow: 1}}>
<Split direction="vertical" style={{height: "100%", width: "100%"}} sizes={[30, 70]} onDrag={onDragging} >
<Split style={{display: "flex"}} sizes={[70, 30]} onDrag={onDragging} >
<div className="bg-gray-light">
<AceEditor
mode="javascript"
theme={theme.palette.mode === 'dark' ? 'monokai' : 'github'}
width="100%"
onLoad={onLoad}
height="100%"
value={script}
focus={true}
onChange={onChange}
setOptions={{
useWorker: false
}}
/>
<div className="mythic-browser-script-pane mythic-browser-script-editor-pane">
<div className="mythic-browser-script-pane-header">
<span>Script Code</span>
<span>JavaScript</span>
</div>
<div className="mythic-browser-script-editor-frame">
<AceEditor
mode="javascript"
theme={theme.palette.mode === 'dark' ? 'monokai' : 'github'}
width="100%"
onLoad={onLoad}
height="100%"
value={script}
focus={true}
onChange={onChange}
setOptions={{
displayIndentGuides: true,
fontSize: 13,
showPrintMargin: false,
tabSize: 4,
useWorker: false,
}}
/>
</div>
</div>
<div className="bg-gray-light" >
<AceEditor
mode="javascript"
theme={theme.palette.mode === 'dark' ? 'monokai' : 'github'}
width="100%"
onLoad={onOutputLoad}
height="100%"
value={logOutput}
focus={false}
onChange={onChange}
setOptions={{
useWorker: false
}}
/>
<div className="mythic-browser-script-pane mythic-browser-script-console-pane">
<div className="mythic-browser-script-pane-header">
<span>Console Output</span>
<span>console.log</span>
</div>
<div className="mythic-browser-script-editor-frame">
<AceEditor
mode="javascript"
theme={theme.palette.mode === 'dark' ? 'monokai' : 'github'}
width="100%"
onLoad={onOutputLoad}
height="100%"
value={logOutput}
focus={false}
readOnly
setOptions={{
fontSize: 12,
highlightActiveLine: false,
showGutter: false,
showPrintMargin: false,
tabSize: 4,
useWorker: false,
}}
/>
</div>
</div>
</Split>
<div className="bg-gray-light" >
<FormControl style={{width: "100%"}}>
<InputLabel ref={inputTaskRef} style={{paddingTop: "10px"}}>Test Script With Task</InputLabel>
<Select
labelId="demo-dialog-select-label"
id="demo-dialog-select"
value={selectedTask}
onChange={onChangeTask}
input={<Input style={{width: "100%"}}/>}
>
{availableTasks.map( (opt) => (
<MenuItem value={opt} key={"task" + opt.id}>{opt.command_name + " / " + opt.display_id + " / " + opt.display_params}</MenuItem>
) )}
</Select>
</FormControl>
{selectedTask !== "" &&
<TaskDisplay me={props.me} task={selectedTask} command_id={selectedTask.command == null ? 0 : selectedTask.command.id} />
}
<div className="mythic-browser-script-pane mythic-browser-script-preview-pane">
<div className="mythic-browser-script-pane-header">
<span>Test Preview</span>
<span>{availableTasks.length === 1 ? "1 task" : `${availableTasks.length} tasks`}</span>
</div>
<div className="mythic-browser-script-preview-controls">
<FormControl fullWidth size="small">
<InputLabel id="browser-script-test-task-label">Test Script With Task</InputLabel>
<Select
label="Test Script With Task"
labelId="browser-script-test-task-label"
value={taskSelectorValue}
onChange={onChangeTask}
disabled={tasksLoading || availableTasks.length === 0}
onOpen={resizeEditors}
renderValue={(task) => task ? selectedTaskLabel(task) : ""}
>
{availableTasks.map( (opt) => (
<MenuItem value={opt} key={"task" + opt.id}>{selectedTaskLabel(opt)}</MenuItem>
) )}
</Select>
</FormControl>
</div>
<div className="mythic-browser-script-preview-frame">
{tasksLoading ? (
<MythicLoadingState compact title="Loading test tasks" description="Fetching previous tasks for this command." />
) : selectedTask !== "" ? (
<TaskDisplay me={props.me} task={selectedTask} command_id={selectedTask.command == null ? 0 : selectedTask.command.id} />
) : (
<MythicEmptyState compact title="No matching tasks" description="Run this command at least once to preview this browser script against task output." />
)}
</div>
</div>
</Split>
</div>
</MythicDialogBody>
</DialogContent>
<DialogActions>
<Button onClick={props.onClose} variant="contained" color="primary">
<MythicDialogFooter>
<MythicDialogButton onClick={props.onClose}>
Close
</Button>
</MythicDialogButton>
{props.new ? (
<Button onClick={onSubmit} variant="contained" color="success">
<MythicDialogButton disabled={selectedCommand === "" || selectedPayloadType === ""} intent="primary" onClick={onSubmit}>
Create
</Button>
</MythicDialogButton>
) : (
<React.Fragment>
<Button onClick={onRevert} variant="contained" color="warning">
<MythicDialogButton intent="warning" onClick={onRevert}>
Revert
</Button>
<Button onClick={onTest} variant="contained" color="info">
</MythicDialogButton>
<MythicDialogButton className="mythic-table-row-action-hover-info" intent="info" disabled={selectedCommand === "" || selectedPayloadType === ""} onClick={onTest}>
Save For Testing
</Button>
<Button onClick={onSubmit} variant="contained" color="success">
</MythicDialogButton>
<MythicDialogButton disabled={selectedCommand === "" || selectedPayloadType === ""} intent="primary" onClick={onSubmit}>
Save and Exit
</Button>
</MythicDialogButton>
</React.Fragment>
)}
</DialogActions>
</MythicDialogFooter>
</React.Fragment>
);
}
@@ -160,8 +160,7 @@ export function EventGroupTable({selectedEventGroup, me, showInstances, showGrap
<MythicPageHeaderChip
icon={selectedEventGroup.active ? <NotificationsActiveTwoToneIcon /> : <NotificationsOffTwoToneIcon />}
label={selectedEventGroup.active ? "Enabled" : "Disabled"}
status={selectedEventGroup.active ? "success" : undefined}
className={selectedEventGroup.active ? "" : "mythic-eventing-header-chip-disabled"}
status={selectedEventGroup.active ? "enabled" : "disabled"}
/>
<MythicPageHeaderChip label={selectedEventGroup.trigger} />
<MythicPageHeaderChip label={selectedEventGroup.run_as || "unknown"} />
@@ -21,6 +21,7 @@ import CategoryIcon from '@mui/icons-material/Category';
import {MythicPageBody} from "../../MythicComponents/MythicPageBody";
import {MythicPageHeader, MythicPageHeaderChip} from "../../MythicComponents/MythicPageHeader";
import {MythicToolbarButton, MythicToolbarToggle} from "../../MythicComponents/MythicTableToolbar";
import {MythicStateChip} from "../../MythicComponents/MythicStateChip";
const get_eventgroups = gql`
query GetEventGroups {
@@ -180,6 +181,20 @@ const getEventGroupStatus = (eventGroup) => {
}
return {key: "runnable", label: "Runnable", rank: 0};
};
const getEventGroupStateChipState = (statusKey) => {
switch(statusKey){
case "runnable":
return "enabled";
case "needs_approval":
return "warning";
case "disabled":
return "disabled";
case "deleted":
return "error";
default:
return "neutral";
}
}
export function Eventing({me}){
const [openTestModal, setOpenTestModal] = React.useState(false);
const [openCreateEventingStepper, setOpenCreateEventingStepper] = React.useState(false);
@@ -428,7 +443,7 @@ export function Eventing({me}){
<div className="mythic-eventing-list-item-content">
<div className="mythic-eventing-list-item-main">
<span className="mythic-eventing-list-item-name">All workflow runs</span>
<span className="mythic-eventing-row-chip mythic-eventing-row-chip-all">{visibleEventGroups.length}</span>
<MythicStateChip compact label={visibleEventGroups.length} state="info" />
</div>
<div className="mythic-eventing-list-item-meta">Review instances across all event groups</div>
</div>
@@ -446,7 +461,7 @@ export function Eventing({me}){
<div className="mythic-eventing-list-item-content">
<div className="mythic-eventing-list-item-main">
<span className={`mythic-eventing-list-item-name ${eventGroup.deleted ? "mythic-eventing-list-item-name-deleted" : ""}`.trim()}>{eventGroup.name}</span>
<span className={`mythic-eventing-row-chip mythic-eventing-row-chip-${status.key}`}>{status.label}</span>
<MythicStateChip compact label={status.label} state={getEventGroupStateChipState(status.key)} />
</div>
<div className="mythic-eventing-list-item-meta">
<span>{eventGroup.trigger || "No trigger"}</span>
@@ -1,10 +1,11 @@
import React from 'react';
import {IconButton, Typography, Link} from '@mui/material';
import {Box, IconButton, Typography, Link} from '@mui/material';
import TableRow from '@mui/material/TableRow';
import DeleteIcon from '@mui/icons-material/Delete';
import Switch from '@mui/material/Switch';
import MythicStyledTableCell from "../../MythicComponents/MythicTableCell";
import {toLocalTime} from "../../utilities/Time";
import {MythicStateChip} from "../../MythicComponents/MythicStateChip";
export function APITokenRow(props){
return (
@@ -18,14 +19,21 @@ export function APITokenRow(props){
)}
</MythicStyledTableCell>
<MythicStyledTableCell>
<Switch
color={ "info"}
disabled={props.deleted}
checked={props.active}
onChange={() => {props.onToggleActive(props.id, !props.active)}}
inputProps={{ 'aria-label': 'primary checkbox' }}
name="active"
/>
<Box className="mythic-state-toggle-cell">
<Switch
color="success"
disabled={props.deleted}
checked={props.active}
onChange={() => {props.onToggleActive(props.id, !props.active)}}
inputProps={{ 'aria-label': 'Toggle API token active state' }}
name="active"
size="small"
/>
<MythicStateChip
label={props.deleted ? "Deleted" : props.active ? "Active" : "Disabled"}
state={props.deleted ? "error" : props.active ? "active" : "disabled"}
/>
</Box>
</MythicStyledTableCell>
<MythicStyledTableCell>
<Typography>
@@ -19,7 +19,7 @@ export function SettingsOperatorDeleteDialog(props) {
{props.deleted ?
"This restores an operator and allows them to log in again."
:
"This deletes an operator and hides them from this view by default. If you want a temporary solution, mark the operator as inactive."}
"This deletes an operator and hides them from this view by default. If you want a temporary solution, mark the operator as disabled."}
</DialogContentText>
<DialogContentText>
Are you sure you want to {props.deleted ? "restore" : "delete" } operator "{props.username}"?
@@ -36,4 +36,3 @@ export function SettingsOperatorDeleteDialog(props) {
</React.Fragment>
);
}
@@ -149,7 +149,7 @@ export function SettingsOperatorTable(props){
<TableCell style={{width: "4rem"}}>Login</TableCell>
<TableCell style={{width: "6rem"}}>Use UTC</TableCell>
<TableCell style={{width: "10rem"}}>Preferences</TableCell>
<TableCell style={{width: "6rem"}}>Active</TableCell>
<TableCell style={{width: "9rem"}}>Active</TableCell>
<TableCell >Login Info</TableCell>
<TableCell >Email</TableCell>
<TableCell style={{width: "6rem"}}>Admin</TableCell>
@@ -48,6 +48,7 @@ import {
MythicDialogSection,
MythicFormNote
} from "../../MythicComponents/MythicDialogLayout";
import {MythicStateChip} from "../../MythicComponents/MythicStateChip";
const createAPITokenMutation = gql`
mutation createAPITokenMutation($operator_id: Int, $name: String, $scopes: [String!]){
@@ -351,14 +352,18 @@ export function SettingsOperatorTableRow(props){
}
</MythicStyledTableCell>
<MythicStyledTableCell>
<Switch
color={ isMe || !props.userIsAdmin ? "secondary" : "info"}
checked={props.active}
disabled={isMe || !props.userIsAdmin}
onChange={onActiveChanged}
inputProps={{ 'aria-label': 'primary checkbox' }}
name="active"
/>
<Box className="mythic-state-toggle-cell">
<Switch
color="success"
checked={props.active}
disabled={isMe || !props.userIsAdmin}
onChange={onActiveChanged}
inputProps={{ 'aria-label': 'Toggle operator active state' }}
name="active"
size="small"
/>
<MythicStateChip label={props.active ? "Active" : "Disabled"} state={props.active ? "active" : "disabled"} />
</Box>
</MythicStyledTableCell>
<MythicStyledTableCell>
<b>Last Login: </b>{toLocalTime(props.last_login, me?.user?.view_utc_time )}<br/>
@@ -515,7 +520,7 @@ const APITokens = ({apiTokens, error, loading, onDeleteAPIToken, onToggleActive,
<TableHead>
<TableRow>
<MythicStyledTableCell style={{width: "3rem"}} />
<MythicStyledTableCell style={{width: "5rem"}}>Active</MythicStyledTableCell>
<MythicStyledTableCell style={{width: "9rem"}}>Active</MythicStyledTableCell>
<MythicStyledTableCell>Created By</MythicStyledTableCell>
<MythicStyledTableCell>Scopes</MythicStyledTableCell>
<MythicStyledTableCell style={{width: "9rem"}}>Type</MythicStyledTableCell>
+295 -45
View File
@@ -1346,6 +1346,249 @@ tspan {
font-size: 0.78rem;
line-height: 1.45;
}
.mythic-browser-script-dialog-content {
display: flex;
flex-direction: column;
height: min(74vh, calc(100vh - 9rem));
min-height: min(38rem, calc(100vh - 9rem));
overflow: hidden;
}
.mythic-browser-script-dialog-body {
flex: 1 1 auto;
min-height: 0;
overflow: hidden;
}
.mythic-browser-script-dialog-body > .mythic-browser-script-target-panel {
flex: 0 0 auto;
}
.mythic-browser-script-target-panel {
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" ? "none" : "0 8px 18px rgba(15, 23, 42, 0.04)"};
overflow: hidden;
}
.mythic-browser-script-target-summary {
align-items: center;
display: flex;
gap: 0.65rem;
justify-content: space-between;
min-height: 42px;
padding: 0.42rem 0.5rem 0.42rem 0.7rem;
}
.mythic-browser-script-target-panel-open .mythic-browser-script-target-summary {
border-bottom: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
}
.mythic-browser-script-target-copy {
align-items: center;
display: flex;
gap: 0.65rem;
min-width: 0;
}
.mythic-browser-script-target-label {
color: ${(props) => props.theme.palette.text.secondary};
flex: 0 0 auto;
font-size: 0.74rem;
font-weight: 800;
line-height: 1;
}
.mythic-browser-script-target-chips {
align-items: center;
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
min-width: 0;
}
.mythic-browser-script-target-chips .MuiChip-root {
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: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
color: ${(props) => props.theme.palette.text.primary};
font-size: 0.72rem;
font-weight: 750;
max-width: 18rem;
}
.mythic-browser-script-target-chips .MuiChip-label {
overflow: hidden;
text-overflow: ellipsis;
}
.mythic-browser-script-target-toggle {
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important;
border-radius: ${(props) => props.theme.shape.borderRadius}px !important;
color: ${(props) => props.theme.palette.text.secondary} !important;
}
.mythic-browser-script-target-toggle:hover {
background-color: ${(props) => alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.22 : 0.1)} !important;
border-color: ${(props) => alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.45 : 0.32)} !important;
color: ${(props) => props.theme.palette.primary.main} !important;
}
.mythic-browser-script-target-details {
background-color: ${(props) => props.theme.surfaces?.muted || props.theme.palette.background.paper};
display: flex;
flex-direction: column;
gap: 0.55rem;
padding: 0.65rem;
}
.mythic-browser-script-workbench {
display: flex;
flex-direction: column;
flex: 1 1 auto;
min-height: 0;
min-width: 0;
width: 100%;
}
.mythic-browser-script-top-split {
display: flex;
min-height: 0;
min-width: 0;
width: 100%;
}
.mythic-browser-script-workbench > .gutter.gutter-vertical,
.mythic-browser-script-top-split > .gutter.gutter-horizontal {
background-color: ${(props) => props.theme.palette.mode === "dark" ? alpha(props.theme.palette.common.white, 0.1) : alpha(props.theme.palette.common.black, 0.08)};
border-radius: 999px;
flex: 0 0 auto;
}
.mythic-browser-script-workbench > .gutter.gutter-vertical:hover,
.mythic-browser-script-top-split > .gutter.gutter-horizontal:hover {
background-color: ${(props) => alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.5 : 0.32)};
}
.mythic-browser-script-pane {
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;
display: flex;
flex-direction: column;
min-height: 0;
min-width: 0;
overflow: hidden;
}
.mythic-browser-script-editor-pane {
height: 100%;
}
.mythic-browser-script-console-pane {
height: 100%;
}
.mythic-browser-script-preview-pane {
height: 100%;
}
.mythic-browser-script-pane-header {
align-items: center;
background-color: ${(props) => props.theme.surfaces?.muted || props.theme.palette.background.paper};
border-bottom: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
color: ${(props) => props.theme.palette.text.primary};
display: flex;
flex: 0 0 auto;
font-size: 0.78rem;
font-weight: 800;
justify-content: space-between;
letter-spacing: 0;
min-height: 34px;
padding: 0.4rem 0.65rem;
}
.mythic-browser-script-pane-header span:last-child {
color: ${(props) => props.theme.palette.text.secondary};
font-size: 0.68rem;
font-weight: 750;
}
.mythic-browser-script-editor-frame {
flex: 1 1 auto;
min-height: 0;
min-width: 0;
}
.mythic-browser-script-editor-frame .ace_editor {
background-color: ${(props) => props.theme.palette.mode === "dark" ? alpha(props.theme.palette.common.black, 0.18) : alpha(props.theme.palette.common.white, 0.62)};
font-family: ${(props) => props.theme.typography.fontFamilyMono || "monospace"} !important;
}
.mythic-browser-script-preview-controls {
background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.025)" : "rgba(0,0,0,0.012)"};
border-bottom: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
flex: 0 0 auto;
padding: 0.65rem;
}
.mythic-browser-script-preview-frame {
flex: 1 1 auto;
min-height: 0;
min-width: 0;
overflow: auto;
padding: 0.55rem;
}
.mythic-browser-scripts-table {
max-width: 100%;
min-width: 60rem;
table-layout: fixed;
width: 100%;
}
.mythic-browser-script-spacer-cell {
min-width: 0;
padding-left: 0 !important;
padding-right: 0 !important;
width: auto;
}
.mythic-browser-script-script-cell {
align-items: center;
display: flex;
gap: 0.6rem;
min-width: 0;
}
.mythic-browser-script-payload-icon {
align-items: center;
background-color: ${(props) => props.theme.palette.mode === "dark" ? alpha(props.theme.palette.common.white, 0.055) : alpha(props.theme.palette.common.black, 0.028)};
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: ${(props) => props.theme.shape.borderRadius}px;
display: inline-flex;
flex: 0 0 auto;
height: 40px;
justify-content: center;
width: 40px;
}
.mythic-browser-script-script-copy {
display: flex;
flex-direction: column;
gap: 0.12rem;
min-width: 0;
}
.mythic-browser-script-command-name {
color: ${(props) => props.theme.palette.text.primary};
font-size: 0.86rem;
font-weight: 750;
line-height: 1.25;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mythic-browser-script-payload-name {
color: ${(props) => props.theme.palette.text.secondary};
font-size: 0.74rem;
font-weight: 650;
line-height: 1.25;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.mythic-browser-script-active-cell {
align-items: center;
display: flex;
gap: 0.4rem;
min-width: 0;
}
.mythic-browser-script-actions {
justify-content: center;
}
@media screen and (max-width: 1050px) {
.mythic-browser-script-dialog-content {
height: calc(100vh - 8rem);
}
.mythic-browser-script-dialog-body {
overflow: auto;
}
.mythic-browser-script-workbench {
flex: 0 0 auto;
min-height: 42rem;
}
.mythic-browser-script-top-split {
min-width: 44rem;
}
}
.mythic-single-task-list {
display: flex;
flex-direction: column;
@@ -2196,6 +2439,58 @@ tspan {
.mythic-table-row-actions-nowrap {
flex-wrap: nowrap;
}
.mythic-state-toggle-cell {
align-items: center;
display: flex;
gap: 0.4rem;
min-width: 0;
}
.mythic-state-chip {
align-items: center;
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
border-radius: 999px;
display: inline-flex;
flex: 0 0 auto;
font-size: 0.72rem;
font-weight: 800;
line-height: 1;
min-height: 24px;
padding: 0.25rem 0.55rem;
white-space: nowrap;
}
.mythic-state-chip-compact {
font-size: 0.66rem;
min-height: 1.25rem;
padding: 0 0.35rem;
}
.mythic-state-chip-active,
.mythic-state-chip-enabled {
background-color: ${(props) => alpha(props.theme.palette.success.main, props.theme.palette.mode === "dark" ? 0.18 : 0.1)};
border-color: ${(props) => alpha(props.theme.palette.success.main, props.theme.palette.mode === "dark" ? 0.48 : 0.32)};
color: ${(props) => props.theme.palette.success.main};
}
.mythic-state-chip-inactive,
.mythic-state-chip-disabled,
.mythic-state-chip-warning {
background-color: ${(props) => alpha(props.theme.palette.warning.main, props.theme.palette.mode === "dark" ? 0.18 : 0.1)};
border-color: ${(props) => alpha(props.theme.palette.warning.main, props.theme.palette.mode === "dark" ? 0.48 : 0.32)};
color: ${(props) => props.theme.palette.warning.main};
}
.mythic-state-chip-neutral {
background-color: ${(props) => props.theme.palette.mode === "dark" ? alpha(props.theme.palette.common.white, 0.045) : alpha(props.theme.palette.common.black, 0.028)};
border-color: ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
color: ${(props) => props.theme.palette.text.secondary};
}
.mythic-state-chip-error {
background-color: ${(props) => alpha(props.theme.palette.error.main, props.theme.palette.mode === "dark" ? 0.18 : 0.1)};
border-color: ${(props) => alpha(props.theme.palette.error.main, props.theme.palette.mode === "dark" ? 0.48 : 0.32)};
color: ${(props) => props.theme.palette.error.main};
}
.mythic-state-chip-info {
background-color: ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.18 : 0.1)};
border-color: ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.48 : 0.32)};
color: ${(props) => props.theme.palette.info.main};
}
.mythic-payload-progress-cell {
align-items: center;
display: flex;
@@ -2556,46 +2851,6 @@ tspan {
margin-top: 0.22rem;
min-width: 0;
}
.mythic-eventing-row-chip {
align-items: center;
border: 1px solid transparent;
border-radius: ${(props) => props.theme.shape.borderRadius}px;
display: inline-flex;
flex: 0 0 auto;
font-size: 0.66rem;
font-weight: 850;
line-height: 1;
min-height: 1.25rem;
padding: 0 0.35rem;
}
.mythic-eventing-row-chip-all {
background-color: ${(props) => props.theme.palette.info.main + "16"};
border-color: ${(props) => props.theme.palette.info.main + "40"};
color: ${(props) => props.theme.palette.info.main};
}
.mythic-eventing-row-chip-runnable {
background-color: ${(props) => props.theme.palette.success.main + "18"};
border-color: ${(props) => props.theme.palette.success.main + "42"};
color: ${(props) => props.theme.palette.success.main};
}
.mythic-eventing-row-chip-needs_approval {
background-color: ${(props) => props.theme.palette.warning.main + "22"};
border-color: ${(props) => props.theme.palette.warning.main + "66"};
color: ${(props) => props.theme.palette.warning.main};
}
.mythic-eventing-row-chip-disabled {
background-color: ${(props) => props.theme.palette.action.disabledBackground};
border-color: ${(props) => props.theme.palette.action.disabled};
color: ${(props) => props.theme.palette.text.secondary};
}
.mythic-eventing-header-chip-disabled {
background-color: ${(props) => props.theme.palette.action.disabledBackground} !important;
border-color: ${(props) => props.theme.palette.action.disabled} !important;
color: ${(props) => props.theme.palette.text.secondary} !important;
}
.mythic-eventing-header-chip-disabled .MuiChip-icon {
color: ${(props) => props.theme.palette.text.secondary} !important;
}
.mythic-eventing-runas-chip {
align-items: center;
background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.04)"};
@@ -2609,11 +2864,6 @@ tspan {
min-height: 1.2rem;
padding: 0 0.35rem;
}
.mythic-eventing-row-chip-deleted {
background-color: ${(props) => props.theme.palette.error.main + "16"};
border-color: ${(props) => props.theme.palette.error.main + "40"};
color: ${(props) => props.theme.palette.error.main};
}
.mythic-eventing-list-empty {
color: ${(props) => props.theme.palette.text.secondary};
font-size: 0.78rem;