mirror of
https://github.com/its-a-feature/Mythic
synced 2026-06-08 14:55:38 +00:00
updating operator settings and virtualized tables
This commit is contained in:
@@ -18,6 +18,61 @@ export const taskTimestampDisplayFieldOptions = [
|
||||
display: "When Agent Picked up Task",
|
||||
}
|
||||
]
|
||||
export const taskingDisplayFieldOptions = [
|
||||
{
|
||||
name: "timestamp",
|
||||
display: "Timestamp",
|
||||
description: "Show the configured task timestamp."
|
||||
},
|
||||
{
|
||||
name: "task",
|
||||
display: "Task number",
|
||||
description: "Show the T-number link for each task."
|
||||
},
|
||||
{
|
||||
name: "username",
|
||||
display: "Username",
|
||||
description: "Show the operator that issued the task."
|
||||
},
|
||||
{
|
||||
name: "callback",
|
||||
display: "Callback number",
|
||||
description: "Show the C-number link for the task callback."
|
||||
},
|
||||
{
|
||||
name: "host",
|
||||
display: "Host",
|
||||
description: "Show the callback host."
|
||||
},
|
||||
{
|
||||
name: "ip",
|
||||
display: "IP address",
|
||||
description: "Show the callback primary IP."
|
||||
},
|
||||
{
|
||||
name: "groups",
|
||||
display: "Callback groups",
|
||||
description: "Show the callback's tree groups."
|
||||
},
|
||||
{
|
||||
name: "payload_type",
|
||||
display: "Payload type",
|
||||
description: "Show the task payload type."
|
||||
},
|
||||
];
|
||||
export const defaultTaskingDisplayFields = ["timestamp", "task", "username", "callback", "payload_type"];
|
||||
export const normalizeTaskingDisplayFields = (fields) => {
|
||||
if(!Array.isArray(fields)){
|
||||
return [...defaultTaskingDisplayFields];
|
||||
}
|
||||
const validFieldNames = taskingDisplayFieldOptions.map((option) => option.name);
|
||||
return fields.reduce( (prev, fieldName) => {
|
||||
if(validFieldNames.includes(fieldName) && !prev.includes(fieldName)){
|
||||
return [...prev, fieldName];
|
||||
}
|
||||
return prev;
|
||||
}, []);
|
||||
}
|
||||
export const taskingContextFieldsOptions = ["impersonation_context", "cwd", "user", "host", "ip", "pid", "process_short_name", "extra_info", "architecture"].sort();
|
||||
export const defaultShortcuts = [
|
||||
"ActiveCallbacks", "Payloads", "PayloadTypesAndC2",
|
||||
@@ -29,11 +84,8 @@ export const operatorSettingDefaults = {
|
||||
navBarOpen: false,
|
||||
fontFamily: 'Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif',
|
||||
showMedia: true,
|
||||
hideUsernames: false,
|
||||
showIP: false,
|
||||
showHostname: false,
|
||||
showOPSECBypassUsername: false,
|
||||
showCallbackGroups: false,
|
||||
taskingDisplayFields: defaultTaskingDisplayFields,
|
||||
useDisplayParamsForCLIHistory: true,
|
||||
interactType: "interactSplit",
|
||||
taskTimestampDisplayField: "timestamp",
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
import React from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import TableCell from '@mui/material/TableCell';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableContainer from '@mui/material/TableContainer';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import { Divider, Input, MenuItem, Select } from '@mui/material';
|
||||
import MythicTextField from './MythicTextField';
|
||||
import FormControl from '@mui/material/FormControl';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import Select from '@mui/material/Select';
|
||||
import TextField from '@mui/material/TextField';
|
||||
import {gql } from '@apollo/client';
|
||||
import {snackActions} from '../utilities/Snackbar';
|
||||
import {useMutation } from '@apollo/client';
|
||||
import {
|
||||
MythicDialogBody,
|
||||
MythicDialogButton,
|
||||
MythicDialogFooter,
|
||||
MythicDialogGrid,
|
||||
MythicDialogSection,
|
||||
MythicForm,
|
||||
MythicFormField,
|
||||
MythicFormNote
|
||||
} from './MythicDialogLayout';
|
||||
|
||||
const submitFeedbackMutation = gql`
|
||||
mutation submitFeedback($webhookType: String!, $webhookData: jsonb!){
|
||||
@@ -25,15 +29,15 @@ const submitFeedbackMutation = gql`
|
||||
`;
|
||||
export function MythicFeedbackDialog(props) {
|
||||
const [message, setMessage] = React.useState("");
|
||||
const [taskID, setTaskID] = React.useState(0);
|
||||
const [taskID, setTaskID] = React.useState("");
|
||||
const messageTypeOptions = [
|
||||
{display: "Bug", value: "bug"},
|
||||
{display: "Feature Request", value: "feature_request"},
|
||||
{display: "Confusing UI", value: "confusing_ui"},
|
||||
{display: "Detection", value: "detection"},
|
||||
{display: "Bug", value: "bug", description: "Something is broken or behaving unexpectedly."},
|
||||
{display: "Feature Request", value: "feature_request", description: "A workflow, capability, or quality-of-life improvement."},
|
||||
{display: "Confusing UI", value: "confusing_ui", description: "A part of the interface is unclear or hard to use."},
|
||||
{display: "Detection", value: "detection", description: "Detection, telemetry, or visibility feedback."},
|
||||
];
|
||||
const [messageType, setMessageType] = React.useState("bug");
|
||||
const [submitFeedback] = useMutation(submitFeedbackMutation, {
|
||||
const [submitFeedback, {loading}] = useMutation(submitFeedbackMutation, {
|
||||
update: (cache, {data}) => {
|
||||
if(data.sendExternalWebhook.status === "success"){
|
||||
snackActions.success("Submitted Feedback!");
|
||||
@@ -48,90 +52,111 @@ export function MythicFeedbackDialog(props) {
|
||||
}
|
||||
});
|
||||
const handleSubmit = () => {
|
||||
let webhookData = {};
|
||||
if(taskID > 0){
|
||||
webhookData["task_display_id"] = String(taskID);
|
||||
const trimmedMessage = message.trim();
|
||||
if(trimmedMessage.length === 0){
|
||||
snackActions.warning("Please include feedback before submitting.");
|
||||
return;
|
||||
}
|
||||
webhookData["message"] = message;
|
||||
let webhookData = {};
|
||||
if(Number(taskID) > 0){
|
||||
webhookData["task_display_id"] = String(Number(taskID));
|
||||
}
|
||||
webhookData["message"] = trimmedMessage;
|
||||
webhookData["feedback_type"] = messageType;
|
||||
submitFeedback({variables: {webhookType: "new_feedback", webhookData: webhookData}});
|
||||
}
|
||||
const handleMessageTypeChange = (evt) => {
|
||||
setMessageType(evt.target.value);
|
||||
}
|
||||
const handleTaskIDChange = (name, value, error) => {
|
||||
setTaskID(value);
|
||||
const handleTaskIDChange = (evt) => {
|
||||
setTaskID(evt.target.value);
|
||||
}
|
||||
const handleMessageChange = (name, value, error) => {
|
||||
setMessage(value);
|
||||
const handleMessageChange = (evt) => {
|
||||
setMessage(evt.target.value);
|
||||
}
|
||||
const handleOnEnter = (event) => {
|
||||
if( event.shiftKey){
|
||||
handleSubmit();
|
||||
}
|
||||
const onSubmitForm = (event) => {
|
||||
event.preventDefault();
|
||||
handleSubmit();
|
||||
}
|
||||
const selectedMessageType = messageTypeOptions.find((opt) => opt.value === messageType);
|
||||
const canSubmit = message.trim().length > 0 && !loading;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle >{props.title}</DialogTitle>
|
||||
<Divider></Divider>
|
||||
<DialogContent style={{padding: "10px"}}>
|
||||
Send a feedback report to the slack webhook configured for the current operation.
|
||||
This provides a way to easily capture a bug, feedback requests, or comments without breaking operator flow too much. <br/>
|
||||
Shift+Enter will auto-submit the form.
|
||||
<TableContainer className="mythicElement">
|
||||
<Table size="small" style={{ "maxWidth": "100%", "overflow": "scroll"}}>
|
||||
<TableBody style={{whiteSpace: "pre"}}>
|
||||
<TableRow hover >
|
||||
<TableCell style={{width: "5rem"}}>
|
||||
Feedback Type
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Select
|
||||
labelId="demo-dialog-select-label"
|
||||
id="demo-dialog-select"
|
||||
value={messageType}
|
||||
onChange={handleMessageTypeChange}
|
||||
input={<Input style={{width: "100%"}}/>}
|
||||
>
|
||||
{messageTypeOptions.map( (opt) => (
|
||||
<MenuItem value={opt.value} key={opt.value}>{opt.display}</MenuItem>
|
||||
) )}
|
||||
</Select>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow hover >
|
||||
<TableCell style={{width: "5rem"}}>
|
||||
Task (if applicable)
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<MythicTextField value={taskID} type={"number"}
|
||||
onChange={handleTaskIDChange} display="inline-block" name={"taskid"} showLabel={false}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<TableCell>
|
||||
Feedback
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<MythicTextField value={message} multiline={true} onEnter={handleOnEnter}
|
||||
onChange={handleMessageChange} display="inline-block" name={"taskid"} showLabel={false}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
<DialogTitle>{props.title || "Submit Feedback"}</DialogTitle>
|
||||
<DialogContent dividers={true}>
|
||||
<MythicDialogBody>
|
||||
<MythicDialogSection
|
||||
title="Feedback Destination"
|
||||
description="Send this report to the feedback webhook configured for the current operation."
|
||||
>
|
||||
<MythicFormNote>
|
||||
Use this for bugs, workflow friction, feature ideas, detection notes, or anything operators should be able to capture without leaving Mythic.
|
||||
</MythicFormNote>
|
||||
</MythicDialogSection>
|
||||
<MythicDialogSection
|
||||
title="Report Details"
|
||||
description="Capture the category, optional task context, and the feedback details."
|
||||
>
|
||||
<MythicForm onSubmit={onSubmitForm}>
|
||||
<MythicDialogGrid minWidth="14rem">
|
||||
<MythicFormField label="Feedback Type" description={selectedMessageType?.description} required>
|
||||
<FormControl fullWidth size="small">
|
||||
<Select
|
||||
labelId="feedback-type-select-label"
|
||||
id="feedback-type-select"
|
||||
value={messageType}
|
||||
onChange={handleMessageTypeChange}
|
||||
>
|
||||
{messageTypeOptions.map( (opt) => (
|
||||
<MenuItem value={opt.value} key={opt.value}>{opt.display}</MenuItem>
|
||||
) )}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</MythicFormField>
|
||||
<MythicFormField
|
||||
label="Task"
|
||||
description="Optional task display ID for feedback tied to specific output or tasking."
|
||||
>
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
type="number"
|
||||
value={taskID}
|
||||
onChange={handleTaskIDChange}
|
||||
placeholder="Optional"
|
||||
inputProps={{min: 0}}
|
||||
/>
|
||||
</MythicFormField>
|
||||
</MythicDialogGrid>
|
||||
<MythicFormField
|
||||
label="Feedback"
|
||||
description="Include the context an operator or maintainer would need to understand the issue."
|
||||
required
|
||||
>
|
||||
<TextField
|
||||
autoFocus
|
||||
fullWidth
|
||||
multiline
|
||||
minRows={6}
|
||||
maxRows={10}
|
||||
value={message}
|
||||
onChange={handleMessageChange}
|
||||
placeholder="What happened, what did you expect, and what would make this easier?"
|
||||
/>
|
||||
</MythicFormField>
|
||||
</MythicForm>
|
||||
</MythicDialogSection>
|
||||
</MythicDialogBody>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={props.onClose} variant="contained" color="primary">
|
||||
<MythicDialogFooter>
|
||||
<MythicDialogButton onClick={props.onClose}>
|
||||
Close
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} variant="contained" color="success">
|
||||
Submit Feedback
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</MythicDialogButton>
|
||||
<MythicDialogButton onClick={handleSubmit} intent="primary" disabled={!canSubmit}>
|
||||
{loading ? "Submitting..." : "Submit Feedback"}
|
||||
</MythicDialogButton>
|
||||
</MythicDialogFooter>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -435,33 +435,35 @@ const FileBrowserVirtualTreePreMemo = ({
|
||||
}
|
||||
}, [selectedFolderData, flattenedNodes]);
|
||||
return flattenedNodes.length > 0 ? (
|
||||
<StyledAutoSizer>
|
||||
{(AutoSizerProps) => (
|
||||
<List
|
||||
itemData={flattenedNodes}
|
||||
layout="vertical"
|
||||
height={AutoSizerProps.height}
|
||||
width={AutoSizerProps.width}
|
||||
itemCount={flattenedNodes.length}
|
||||
itemKey={itemKey}
|
||||
itemSize={24}
|
||||
ref={gridRef}
|
||||
>
|
||||
{(ListProps) => (
|
||||
<VirtualTreeRow
|
||||
{...ListProps}
|
||||
tabInfo={tabInfo}
|
||||
selectedFolderData={selectedFolderData}
|
||||
treeRootData={treeRootData}
|
||||
onSelectNode={onSelectNode}
|
||||
onExpandNode={onExpandNode}
|
||||
onCollapseNode={onCollapseNode}
|
||||
onContextMenu={onContextMenu}
|
||||
/>
|
||||
)}
|
||||
</List>
|
||||
)}
|
||||
</StyledAutoSizer>
|
||||
<div style={{height: "100%", width: "100%", minHeight: 0, minWidth: 0, overflow: "hidden"}}>
|
||||
<StyledAutoSizer style={{height: "100%", width: "100%"}}>
|
||||
{(AutoSizerProps) => (
|
||||
<List
|
||||
itemData={flattenedNodes}
|
||||
layout="vertical"
|
||||
height={AutoSizerProps.height}
|
||||
width={AutoSizerProps.width}
|
||||
itemCount={flattenedNodes.length}
|
||||
itemKey={itemKey}
|
||||
itemSize={24}
|
||||
ref={gridRef}
|
||||
>
|
||||
{(ListProps) => (
|
||||
<VirtualTreeRow
|
||||
{...ListProps}
|
||||
tabInfo={tabInfo}
|
||||
selectedFolderData={selectedFolderData}
|
||||
treeRootData={treeRootData}
|
||||
onSelectNode={onSelectNode}
|
||||
onExpandNode={onExpandNode}
|
||||
onCollapseNode={onCollapseNode}
|
||||
onContextMenu={onContextMenu}
|
||||
/>
|
||||
)}
|
||||
</List>
|
||||
)}
|
||||
</StyledAutoSizer>
|
||||
</div>
|
||||
) : null;
|
||||
};
|
||||
export const FileBrowserVirtualTree = React.memo(FileBrowserVirtualTreePreMemo);
|
||||
|
||||
@@ -13,13 +13,22 @@ const CellPreMemo = ({ style, rowIndex, columnIndex, data }) => {
|
||||
const cellStyle = item?.props?.cellData?.cellStyle || {};
|
||||
const rowStyle = data.items[rowIndex][columnIndex]?.props?.rowData?.rowStyle || {};
|
||||
const contextMenuLocationRef = React.useRef({x: 0, y: 0});
|
||||
const ownsContextRowRef = React.useRef(false);
|
||||
const updateRowClass = useCallback((className, shouldAdd) => {
|
||||
const cells = document.getElementsByClassName(rowClassName);
|
||||
if(cells.length > 0){
|
||||
for(const cell of cells){
|
||||
cell.classList.toggle(className, shouldAdd);
|
||||
}
|
||||
}
|
||||
}, [rowClassName]);
|
||||
const handleDoubleClick = useCallback(
|
||||
(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
data.onDoubleClickRow(e, rowIndex - 1, data.items[rowIndex][columnIndex]?.props?.rowData); // minus 1 to account for header row
|
||||
},
|
||||
[data, rowIndex]
|
||||
[columnIndex, data, rowIndex]
|
||||
);
|
||||
const handleClick = (event) => {
|
||||
event.preventDefault();
|
||||
@@ -29,22 +38,23 @@ const CellPreMemo = ({ style, rowIndex, columnIndex, data }) => {
|
||||
}
|
||||
};
|
||||
const selectedClass = data.items[rowIndex][columnIndex]?.props?.rowData?.selected ? "selectedCallback" : "";
|
||||
const onMouseEnter = () => {
|
||||
const cells = document.getElementsByClassName(rowClassName);
|
||||
if(cells.length > 0){
|
||||
for(const cell of cells){
|
||||
cell.classList.add(classes.hoveredRow);
|
||||
}
|
||||
const rowFirstCellClass = columnIndex === 0 ? classes.rowFirstCell : "";
|
||||
const rowLastCellClass = columnIndex === data.items[rowIndex].length - 1 ? classes.rowLastCell : "";
|
||||
const rowInteractiveClass = data.onRowClick ? classes.rowInteractive : "";
|
||||
const setHoveredRow = useCallback((enabled) => {
|
||||
updateRowClass(classes.hoveredRow, enabled);
|
||||
}, [updateRowClass]);
|
||||
const setContextRow = useCallback((enabled) => {
|
||||
updateRowClass(classes.contextRow, enabled);
|
||||
}, [updateRowClass]);
|
||||
const onMouseEnter = useCallback(() => {
|
||||
setHoveredRow(true);
|
||||
}, [setHoveredRow]);
|
||||
const onMouseLeave = useCallback(() => {
|
||||
if(!ownsContextRowRef.current){
|
||||
setHoveredRow(false);
|
||||
}
|
||||
}
|
||||
const onMouseLeave = () => {
|
||||
const cells = document.getElementsByClassName(rowClassName);
|
||||
if(cells.length > 0){
|
||||
for(const cell of cells){
|
||||
cell.classList.remove(classes.hoveredRow);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [setHoveredRow]);
|
||||
const handleMenuItemClick = (event, clickOption) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
@@ -52,10 +62,24 @@ const CellPreMemo = ({ style, rowIndex, columnIndex, data }) => {
|
||||
setOpenContextMenu(false);
|
||||
};
|
||||
React.useEffect( () => {
|
||||
if(!openContextMenu){
|
||||
onMouseLeave();
|
||||
if(openContextMenu){
|
||||
ownsContextRowRef.current = true;
|
||||
setContextRow(true);
|
||||
setHoveredRow(true);
|
||||
} else if(ownsContextRowRef.current){
|
||||
ownsContextRowRef.current = false;
|
||||
setContextRow(false);
|
||||
setHoveredRow(false);
|
||||
}
|
||||
}, [openContextMenu]);
|
||||
}, [openContextMenu, setContextRow, setHoveredRow]);
|
||||
React.useEffect( () => {
|
||||
return () => {
|
||||
if(ownsContextRowRef.current){
|
||||
setContextRow(false);
|
||||
setHoveredRow(false);
|
||||
}
|
||||
};
|
||||
}, [setContextRow, setHoveredRow]);
|
||||
const handleContextClick = useCallback(
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
@@ -83,7 +107,7 @@ const CellPreMemo = ({ style, rowIndex, columnIndex, data }) => {
|
||||
);
|
||||
return (
|
||||
<div style={{...style, ...cellStyle, ...rowStyle}}
|
||||
className={`${classes.cell} ${rowClassName} ${rowHighlight} ${selectedClass}`}
|
||||
className={`${classes.cell} ${rowClassName} ${rowHighlight} ${selectedClass} ${rowFirstCellClass} ${rowLastCellClass} ${rowInteractiveClass}`}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
onClick={handleClick}
|
||||
onMouseEnter={onMouseEnter}
|
||||
|
||||
@@ -3,8 +3,9 @@ import { useCallback } from 'react';
|
||||
import useSingleAndDoubleClick from '../../utilities/useSingleAndDoubleClick';
|
||||
import {classes} from './styles';
|
||||
import React from 'react';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import {faFilter} from '@fortawesome/free-solid-svg-icons';
|
||||
import FilterAltOutlinedIcon from '@mui/icons-material/FilterAltOutlined';
|
||||
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
||||
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
|
||||
import {ContextMenu} from "./Cell";
|
||||
|
||||
const HeaderCell = ({
|
||||
@@ -21,6 +22,9 @@ const HeaderCell = ({
|
||||
const dropdownAnchorRef = React.useRef(null);
|
||||
const item = data.items[rowIndex][columnIndex];
|
||||
const isFiltered = item?.filtered || false;
|
||||
const isSorted = sortIndicatorIndex === columnIndex;
|
||||
const HeaderSortIcon = sortDirection === 'ASC' ? KeyboardArrowUpIcon : KeyboardArrowDownIcon;
|
||||
const headerLabel = item?.[headerNameKey] || "";
|
||||
const contextMenuLocationRef = React.useRef({x: 0, y: 0});
|
||||
const handleClick = useCallback(
|
||||
(e) => {
|
||||
@@ -59,38 +63,69 @@ const HeaderCell = ({
|
||||
[]
|
||||
);
|
||||
const [openContextMenu, setOpenContextMenu] = React.useState(false);
|
||||
const menuContext = React.useMemo(() => {
|
||||
return {columnIndex, rowIndex, data: data.items[rowIndex][columnIndex]?.props?.rowData || {}};
|
||||
}, [columnIndex, data.items, rowIndex]);
|
||||
const resolvedContextMenuOptions = React.useMemo(() => {
|
||||
const resolveOption = (option) => {
|
||||
const resolvedOption = {
|
||||
...option,
|
||||
disabled: typeof option.disabled === "function" ? option.disabled(menuContext) : option.disabled,
|
||||
};
|
||||
if(option.menuItems){
|
||||
resolvedOption.menuItems = option.menuItems.map(resolveOption);
|
||||
}
|
||||
return resolvedOption;
|
||||
};
|
||||
return contextMenuOptions.map(resolveOption);
|
||||
}, [contextMenuOptions, menuContext]);
|
||||
const handleContextClick = useCallback(
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
if(item.disableFilterMenu){
|
||||
return;
|
||||
}
|
||||
if(contextMenuOptions && contextMenuOptions.length > 0){
|
||||
if(resolvedContextMenuOptions && resolvedContextMenuOptions.length > 0){
|
||||
contextMenuLocationRef.current.x = event.clientX;
|
||||
contextMenuLocationRef.current.y = event.clientY;
|
||||
setOpenContextMenu(true);
|
||||
}
|
||||
},
|
||||
[contextMenuOptions, columnIndex] // eslint-disable-line react-hooks/exhaustive-deps
|
||||
[resolvedContextMenuOptions]
|
||||
);
|
||||
const handleMenuItemClick = (event, clickOption) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
clickOption({event, columnIndex, rowIndex, data: data.items[rowIndex][columnIndex]?.props?.rowData || {}});
|
||||
clickOption({event, ...menuContext});
|
||||
setOpenContextMenu(false);
|
||||
};
|
||||
|
||||
const handleClicks = useSingleAndDoubleClick(handleClick, handleDoubleClick);
|
||||
const className = `${classes.headerCell} ${item.disableSort ? classes.headerCellNoSort : ""}`;
|
||||
|
||||
return (
|
||||
<div style={style} className={classes.headerCell} onClick={handleClicks} onContextMenu={handleContextClick} ref={dropdownAnchorRef}>
|
||||
<Box display='flex' alignItems='center' justifyContent='space-between' width='100%'>
|
||||
<Typography className={classes.cellInner} variant='body1'>
|
||||
{item[headerNameKey].toUpperCase()}
|
||||
<div
|
||||
style={style}
|
||||
className={className}
|
||||
onClick={handleClicks}
|
||||
onContextMenu={handleContextClick}
|
||||
ref={dropdownAnchorRef}
|
||||
title={typeof headerLabel === "string" ? headerLabel : undefined}
|
||||
>
|
||||
<Box className={classes.headerContent}>
|
||||
<Typography className={`${classes.cellInner} ${classes.headerLabel}`} component='div' variant='body1'>
|
||||
{typeof headerLabel === "string" ? headerLabel : headerLabel}
|
||||
</Typography>
|
||||
{isFiltered && <FontAwesomeIcon icon={faFilter} />}
|
||||
{sortIndicatorIndex === columnIndex && (sortDirection === 'ASC' ? <div>↑</div> : <div>↓</div>)}
|
||||
<ContextMenu dropdownAnchorRef={dropdownAnchorRef} contextMenuOptions={contextMenuOptions}
|
||||
<Box className={classes.headerActions}>
|
||||
{isFiltered &&
|
||||
<span className={`${classes.headerIndicator} ${classes.headerFilterIcon}`} title="Filtered">
|
||||
<FilterAltOutlinedIcon fontSize="inherit" />
|
||||
</span>
|
||||
}
|
||||
{isSorted &&
|
||||
<span className={`${classes.headerIndicator} ${classes.headerSortIcon}`} title={`Sorted ${sortDirection === 'ASC' ? 'ascending' : 'descending'}`}>
|
||||
<HeaderSortIcon fontSize="inherit" />
|
||||
</span>
|
||||
}
|
||||
</Box>
|
||||
<ContextMenu dropdownAnchorRef={dropdownAnchorRef} contextMenuOptions={resolvedContextMenuOptions}
|
||||
disableFilterMenu={item?.disableFilterMenu} openContextMenu={openContextMenu}
|
||||
contextMenuLocationRef={contextMenuLocationRef}
|
||||
setOpenContextMenu={setOpenContextMenu} handleMenuItemClick={handleMenuItemClick}
|
||||
@@ -100,7 +135,7 @@ const HeaderCell = ({
|
||||
<div
|
||||
role="separator"
|
||||
aria-orientation="vertical"
|
||||
aria-label={`Resize ${item[headerNameKey]} column`}
|
||||
aria-label={`Resize ${typeof headerLabel === "string" ? headerLabel : "column"} column`}
|
||||
className={`${classes.headerResizeHandle} ${isResizing ? classes.headerResizeHandleActive : ""}`}
|
||||
onPointerDown={handleResizePointerDown}
|
||||
onDoubleClick={handleResizeDoubleClick}
|
||||
|
||||
+293
-146
@@ -7,12 +7,183 @@ import HeaderCell from './HeaderCell';
|
||||
import Cell from './Cell';
|
||||
import {classes} from './styles';
|
||||
import {GetMythicSetting, useSetMythicSetting} from "../MythicSavedUserSetting";
|
||||
import FitScreenIcon from '@mui/icons-material/FitScreen';
|
||||
import RestartAltIcon from '@mui/icons-material/RestartAlt';
|
||||
import ViewColumnIcon from '@mui/icons-material/ViewColumn';
|
||||
|
||||
const HeaderCellContext = createContext({});
|
||||
|
||||
const MIN_COLUMN_WIDTH = 100;
|
||||
const MIN_FLEX_COLUMN_WIDTH = 150;
|
||||
const AUTOSIZE_HORIZONTAL_PADDING = 44;
|
||||
const AUTOSIZE_HEADER_EXTRA_WIDTH = 28;
|
||||
const headerMenuIconStyle = {fontSize: "1rem", marginRight: "8px"};
|
||||
|
||||
let autosizeCanvas;
|
||||
|
||||
const getNumericWidth = (value) => {
|
||||
const width = Number(value);
|
||||
return Number.isFinite(width) ? width : undefined;
|
||||
};
|
||||
const getColumnMinWidth = (column) => {
|
||||
const configuredMinWidth = getNumericWidth(column?.minWidth);
|
||||
return Math.max(configuredMinWidth || 0, column?.fillWidth ? MIN_FLEX_COLUMN_WIDTH : MIN_COLUMN_WIDTH);
|
||||
};
|
||||
const getColumnMaxWidth = (column) => {
|
||||
const configuredMaxWidth = getNumericWidth(column?.maxWidth);
|
||||
return configuredMaxWidth && configuredMaxWidth > 0 ? configuredMaxWidth : Infinity;
|
||||
};
|
||||
const clampColumnWidth = (column, width) => {
|
||||
const minWidth = getColumnMinWidth(column);
|
||||
const maxWidth = getColumnMaxWidth(column);
|
||||
const numericWidth = getNumericWidth(width) || minWidth;
|
||||
return Math.floor(Math.min(maxWidth, Math.max(numericWidth, minWidth)));
|
||||
};
|
||||
const getPreferredColumnWidth = (column) => {
|
||||
return column?.width === undefined ? getColumnMinWidth(column) : column.width;
|
||||
};
|
||||
const distributeFillWidth = ({columns, columnWidths, totalWidth}) => {
|
||||
const availableWidth = Math.max(0, Math.floor(totalWidth || 0));
|
||||
const currentWidth = columnWidths.reduce((a, b) => a + b, 0);
|
||||
const widthDifference = availableWidth - currentWidth;
|
||||
if(widthDifference <= 0 || columns.length === 0){
|
||||
return columnWidths;
|
||||
}
|
||||
const fillColumnIndexes = columns.reduce((previous, column, index) => {
|
||||
return column.fillWidth ? [...previous, index] : previous;
|
||||
}, []);
|
||||
const targetIndexes = fillColumnIndexes.length > 0 ? fillColumnIndexes : [columns.length - 1];
|
||||
let remainingWidth = widthDifference;
|
||||
const updatedColumnWidths = [...columnWidths];
|
||||
for(let i = 0; i < targetIndexes.length; i++){
|
||||
const columnIndex = targetIndexes[i];
|
||||
const share = i === targetIndexes.length - 1 ? remainingWidth : Math.floor(widthDifference / targetIndexes.length);
|
||||
const currentColumnWidth = updatedColumnWidths[columnIndex];
|
||||
const nextColumnWidth = clampColumnWidth(columns[columnIndex], currentColumnWidth + share);
|
||||
updatedColumnWidths[columnIndex] = nextColumnWidth;
|
||||
remainingWidth -= nextColumnWidth - currentColumnWidth;
|
||||
}
|
||||
return updatedColumnWidths;
|
||||
};
|
||||
const getInitialColumnWidths = ({columns, savedWidths = [], totalWidth = 0}) => {
|
||||
const hasSavedWidths = savedWidths.length === columns.length;
|
||||
const baseColumnWidths = columns.map((column, index) => {
|
||||
return clampColumnWidth(column, hasSavedWidths ? savedWidths[index] : getPreferredColumnWidth(column));
|
||||
});
|
||||
if(hasSavedWidths){
|
||||
return baseColumnWidths;
|
||||
}
|
||||
return distributeFillWidth({columns, columnWidths: baseColumnWidths, totalWidth});
|
||||
};
|
||||
const areColumnWidthsEqual = (first = [], second = []) => {
|
||||
if(first.length !== second.length){
|
||||
return false;
|
||||
}
|
||||
return first.every((width, index) => Math.floor(width || MIN_COLUMN_WIDTH) === Math.floor(second[index] || MIN_COLUMN_WIDTH));
|
||||
};
|
||||
const normalizeColumnWidths = (columnWidths = []) => {
|
||||
return columnWidths.map((columnWidth) => Math.floor(getNumericWidth(columnWidth) || MIN_COLUMN_WIDTH));
|
||||
};
|
||||
const getMeasureContext = () => {
|
||||
if(typeof document === "undefined"){
|
||||
return null;
|
||||
}
|
||||
if(autosizeCanvas === undefined){
|
||||
autosizeCanvas = document.createElement("canvas");
|
||||
}
|
||||
const context = autosizeCanvas.getContext("2d");
|
||||
if(context === null){
|
||||
return null;
|
||||
}
|
||||
const referenceElement = document.querySelector(`.${classes.cell}`) || document.body;
|
||||
const computedStyle = window.getComputedStyle(referenceElement);
|
||||
context.font = [
|
||||
computedStyle.fontStyle,
|
||||
computedStyle.fontVariant,
|
||||
computedStyle.fontWeight,
|
||||
computedStyle.fontSize,
|
||||
computedStyle.fontFamily,
|
||||
].join(" ");
|
||||
return context;
|
||||
};
|
||||
const getDisplayText = (value) => {
|
||||
if(value === undefined || value === null){
|
||||
return "";
|
||||
}
|
||||
if(typeof value === "string"){
|
||||
const trimmedValue = value.trim();
|
||||
if(trimmedValue.startsWith("[")){
|
||||
try{
|
||||
const parsedValue = JSON.parse(trimmedValue);
|
||||
if(Array.isArray(parsedValue) && parsedValue.length > 0){
|
||||
return getDisplayText(parsedValue[0]);
|
||||
}
|
||||
}catch(error){
|
||||
// Keep the original string when it is not a serialized array.
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
if(typeof value === "number" || typeof value === "boolean"){
|
||||
return String(value);
|
||||
}
|
||||
if(Array.isArray(value)){
|
||||
return value.map((entry) => getDisplayText(entry)).filter(Boolean).join(", ");
|
||||
}
|
||||
if(React.isValidElement(value)){
|
||||
return getDisplayText(value.props?.cellData || value.props?.children);
|
||||
}
|
||||
if(typeof value === "object"){
|
||||
if(value.plaintext !== undefined){
|
||||
return getDisplayText(value.plaintext);
|
||||
}
|
||||
if(value.name !== undefined){
|
||||
return getDisplayText(value.name);
|
||||
}
|
||||
if(value.button?.name !== undefined){
|
||||
return getDisplayText(value.button.name);
|
||||
}
|
||||
if(value.display !== undefined){
|
||||
return getDisplayText(value.display);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
return "";
|
||||
};
|
||||
const getCellAutosizeText = ({column, cell}) => {
|
||||
const cellProps = cell?.props || {};
|
||||
const columnKey = column?.key || column?.plaintext;
|
||||
const rowData = cellProps.rowData || {};
|
||||
const cellDataText = getDisplayText(cellProps.cellData);
|
||||
if(cellDataText !== ""){
|
||||
return cellDataText;
|
||||
}
|
||||
if(columnKey !== undefined){
|
||||
return getDisplayText(rowData?.[columnKey]);
|
||||
}
|
||||
return getDisplayText(cell);
|
||||
};
|
||||
const measureAutosizeText = (context, text) => {
|
||||
const normalizedText = getDisplayText(text);
|
||||
if(normalizedText === ""){
|
||||
return 0;
|
||||
}
|
||||
const lines = normalizedText.split(/\r?\n/);
|
||||
return lines.reduce((longestLineWidth, line) => {
|
||||
const textWidth = context ? context.measureText(line).width : line.length * 8;
|
||||
return Math.max(longestLineWidth, textWidth);
|
||||
}, 0);
|
||||
};
|
||||
const getAutosizedColumnWidth = ({column, columnIndex, headerNameKey, items}) => {
|
||||
const measureContext = getMeasureContext();
|
||||
const headerText = getDisplayText(column?.[headerNameKey] || column?.name || column?.plaintext);
|
||||
const headerWidth = measureAutosizeText(measureContext, headerText.toUpperCase()) + AUTOSIZE_HEADER_EXTRA_WIDTH;
|
||||
const widestCellWidth = items.reduce((widestWidth, itemRow) => {
|
||||
const cellText = getCellAutosizeText({column, cell: itemRow[columnIndex]});
|
||||
return Math.max(widestWidth, measureAutosizeText(measureContext, cellText));
|
||||
}, 0);
|
||||
return clampColumnWidth(column, Math.max(headerWidth, widestCellWidth) + AUTOSIZE_HORIZONTAL_PADDING);
|
||||
};
|
||||
|
||||
const CellRendererPreMemo = ({ style, rowIndex, columnIndex, data }) => {
|
||||
return rowIndex === 0 ? null : <Cell style={style} rowIndex={rowIndex} columnIndex={columnIndex} data={data} />;
|
||||
@@ -20,12 +191,16 @@ const CellRendererPreMemo = ({ style, rowIndex, columnIndex, data }) => {
|
||||
const CellRenderer = React.memo(CellRendererPreMemo);
|
||||
const innerElementType = React.forwardRef(({ children, style }, ref) => {
|
||||
const HeaderCellData = useContext(HeaderCellContext);
|
||||
const resizingColumnIndex = HeaderCellData.resizingColumnIndex;
|
||||
const resizeGuideLeft = resizingColumnIndex >= 0 ?
|
||||
HeaderCellData.columnWidths.slice(0, resizingColumnIndex + 1).reduce((a, b) => a + b, 0) :
|
||||
-1;
|
||||
const onHeaderDoubleClick = React.useCallback( (e, columnIndex) => {
|
||||
if (HeaderCellData.columns[columnIndex].disableAutosize) return;
|
||||
HeaderCellData.autosizeColumn({columnIndex});
|
||||
}, [HeaderCellData.columns, HeaderCellData.autosizeColumn]);
|
||||
return (
|
||||
<div ref={ref} style={style}>
|
||||
<div ref={ref} style={{...style, position: "relative"}}>
|
||||
{/* always render header cells */}
|
||||
<div
|
||||
className={classes.headerCellRow}
|
||||
@@ -63,6 +238,15 @@ const innerElementType = React.forwardRef(({ children, style }, ref) => {
|
||||
</div>
|
||||
{/* render other cells as usual */}
|
||||
{children}
|
||||
{resizingColumnIndex >= 0 &&
|
||||
<div
|
||||
className={classes.resizeGuide}
|
||||
style={{
|
||||
left: resizeGuideLeft,
|
||||
height: style.height,
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
@@ -93,14 +277,11 @@ const ResizableGridWrapper = ({
|
||||
const { width: scrollbarWidth } = useScrollbarSize();
|
||||
const localColumnsRef = React.useRef(columns);
|
||||
const initialSavedWidths = name === undefined ? [] : GetMythicSetting({setting_name: `${name}_column_widths`, default_value: {}, output: "json-array"});
|
||||
const [columnWidths, setColumnWidths] = useState(columns.map((column, index) => {
|
||||
if(initialSavedWidths.length > 0){
|
||||
return initialSavedWidths[index];
|
||||
}
|
||||
if(column.fillWidth){
|
||||
return Math.max(column?.width || 0, MIN_FLEX_COLUMN_WIDTH);
|
||||
}
|
||||
return Math.max(column?.width || 0, MIN_COLUMN_WIDTH);
|
||||
const savedColumnWidths = Array.isArray(initialSavedWidths) ? initialSavedWidths : [];
|
||||
const [columnWidths, setColumnWidths] = useState(() => getInitialColumnWidths({
|
||||
columns,
|
||||
savedWidths: savedColumnWidths,
|
||||
totalWidth: AutoSizerProps.width - scrollbarWidth,
|
||||
}));
|
||||
const gridUUID = React.useMemo( () => GetShortRandomString(), []);
|
||||
const gridRef = useRef(null);
|
||||
@@ -112,9 +293,14 @@ const ResizableGridWrapper = ({
|
||||
const [resizingColumnIndex, setResizingColumnIndex] = useState(-1);
|
||||
const [updateSetting] = useSetMythicSetting();
|
||||
const setColumnWidthsAndRef = useCallback( (newColumnWidths, resetColumnIndex=0) => {
|
||||
const normalizedColumnWidths = normalizeColumnWidths(newColumnWidths);
|
||||
if(areColumnWidthsEqual(columnWidthsRef.current, normalizedColumnWidths)){
|
||||
return false;
|
||||
}
|
||||
lastResetColumnIndexRef.current = resetColumnIndex;
|
||||
columnWidthsRef.current = newColumnWidths;
|
||||
setColumnWidths(newColumnWidths);
|
||||
columnWidthsRef.current = normalizedColumnWidths;
|
||||
setColumnWidths(normalizedColumnWidths);
|
||||
return true;
|
||||
}, []);
|
||||
const getColumnWidth = useCallback(
|
||||
(index) => {
|
||||
@@ -132,33 +318,17 @@ const ResizableGridWrapper = ({
|
||||
[rowHeight, headerRowHeight]
|
||||
);
|
||||
useEffect(() => {
|
||||
if(initialSavedWidths.length > 0 && localColumnsRef.current.length === columns.length){
|
||||
if(savedColumnWidths.length === columns.length && localColumnsRef.current.length === columns.length){
|
||||
return;
|
||||
}
|
||||
localColumnsRef.current = columns;
|
||||
const totalWidth = AutoSizerProps.width - scrollbarWidth;
|
||||
const updatedColumnWidths = columns.map((column, index) => {
|
||||
return column.width || MIN_COLUMN_WIDTH
|
||||
const updatedColumnWidths = getInitialColumnWidths({
|
||||
columns,
|
||||
savedWidths: [],
|
||||
totalWidth: AutoSizerProps.width - scrollbarWidth,
|
||||
});
|
||||
const totalWidthDiff = totalWidth - updatedColumnWidths.reduce((a, b) => a + b, 0);
|
||||
if (totalWidthDiff !== 0) {
|
||||
let updatedWidthIndexs = [];
|
||||
for(let i = 0; i < columns.length; i++){
|
||||
// check if any of the columns have the `fillWidth` property to true
|
||||
if(columns[i]["fillWidth"]){
|
||||
updatedWidthIndexs.push(i);
|
||||
}
|
||||
}
|
||||
if(updatedWidthIndexs.length === 0){
|
||||
updatedWidthIndexs.push(columns.length - 1);
|
||||
}
|
||||
for(let i = 0; i < updatedWidthIndexs.length; i++){
|
||||
updatedColumnWidths[updatedWidthIndexs[i]] += Math.max(totalWidthDiff / updatedWidthIndexs.length, MIN_FLEX_COLUMN_WIDTH);
|
||||
}
|
||||
//updatedColumnWidths[updatedWidthIndex] += totalWidthDiff;
|
||||
}
|
||||
setColumnWidthsAndRef(updatedColumnWidths);
|
||||
if(name !== undefined){
|
||||
const didUpdateWidths = setColumnWidthsAndRef(updatedColumnWidths);
|
||||
if(didUpdateWidths && name !== undefined){
|
||||
updateSetting({setting_name: `${name}_column_widths`, value: updatedColumnWidths});
|
||||
}
|
||||
|
||||
@@ -197,7 +367,7 @@ const ResizableGridWrapper = ({
|
||||
return;
|
||||
}
|
||||
const resizeDelta = currentResize.latestClientX - currentResize.startClientX;
|
||||
const nextColumnWidth = Math.floor(Math.max(currentResize.startWidth + resizeDelta, MIN_COLUMN_WIDTH));
|
||||
const nextColumnWidth = clampColumnWidth(columns[currentResize.columnIndex], currentResize.startWidth + resizeDelta);
|
||||
const updatedColumnWidths = currentResize.initialWidths.map( (columnWidth, index) => {
|
||||
return currentResize.columnIndex === index ? nextColumnWidth : columnWidth;
|
||||
});
|
||||
@@ -223,13 +393,13 @@ const ResizableGridWrapper = ({
|
||||
resizeFrameRef.current = null;
|
||||
}
|
||||
const resizeDelta = clientX - currentResize.startClientX;
|
||||
const nextColumnWidth = Math.floor(Math.max(currentResize.startWidth + resizeDelta, MIN_COLUMN_WIDTH));
|
||||
const nextColumnWidth = clampColumnWidth(columns[currentResize.columnIndex], currentResize.startWidth + resizeDelta);
|
||||
const updatedColumnWidths = currentResize.initialWidths.map( (columnWidth, index) => {
|
||||
return currentResize.columnIndex === index ? nextColumnWidth : columnWidth;
|
||||
});
|
||||
activeResizeRef.current = null;
|
||||
setColumnWidthsAndRef(updatedColumnWidths, currentResize.columnIndex);
|
||||
if(name !== undefined){
|
||||
const didUpdateWidths = setColumnWidthsAndRef(updatedColumnWidths, currentResize.columnIndex);
|
||||
if(didUpdateWidths && name !== undefined){
|
||||
updateSetting({setting_name: `${name}_column_widths`, value: updatedColumnWidths});
|
||||
}
|
||||
setResizingColumnIndex(-1);
|
||||
@@ -270,100 +440,74 @@ const ResizableGridWrapper = ({
|
||||
};
|
||||
}, []);
|
||||
const autosizeColumn = useCallback( ({columnIndex}) => {
|
||||
if(columns[columnIndex].disableDoubleClick){
|
||||
const column = columns[columnIndex];
|
||||
if(column?.disableDoubleClick || column?.disableAutosize){
|
||||
return
|
||||
}
|
||||
const longestElementInColumn = Math.max(...items.map((itemRow) => {
|
||||
if(columns[columnIndex].key === undefined){
|
||||
if(columns[columnIndex].plaintext){
|
||||
columns[columnIndex].key = columns[columnIndex].plaintext;
|
||||
} else {
|
||||
return 30;
|
||||
}
|
||||
}
|
||||
if(columns[columnIndex].key !== undefined){
|
||||
if(columns[columnIndex].key.includes("time")){
|
||||
return 30;
|
||||
}
|
||||
if(columns[columnIndex].key === "mythictree_groups"){
|
||||
return itemRow[columnIndex]?.props?.cellData.length;
|
||||
}
|
||||
if(columns[columnIndex].type === "size"){
|
||||
if(itemRow[columnIndex]?.props?.cellData !== undefined){
|
||||
if(typeof itemRow[columnIndex]?.props?.cellData === 'number'){
|
||||
return String(itemRow[columnIndex]?.props?.cellData)?.length;
|
||||
}
|
||||
return String(itemRow[columnIndex]?.props?.cellData?.plaintext)?.length;
|
||||
}
|
||||
return itemRow[columnIndex].length;
|
||||
}
|
||||
try{
|
||||
items = JSON.parse(itemRow[columnIndex]?.props?.rowData?.[columns[columnIndex].key]);
|
||||
if(Array.isArray(items) && items.length > 0){
|
||||
return String(items[0]).length;
|
||||
}
|
||||
}catch(error){
|
||||
//console.log(itemRow[columnIndex]?.props?.rowData?.[columns[columnIndex].key])
|
||||
}
|
||||
if(typeof itemRow[columnIndex]?.props?.cellData === 'string' ){
|
||||
return itemRow[columnIndex]?.props?.cellData.length;
|
||||
}
|
||||
|
||||
let data = itemRow[columnIndex]?.props?.rowData?.[columns[columnIndex].key];
|
||||
if(columns[columnIndex].inMetadata){
|
||||
return itemRow[columnIndex]?.props?.cellData.length;
|
||||
}
|
||||
if(data === undefined){
|
||||
return MIN_COLUMN_WIDTH;
|
||||
}
|
||||
if(data.plaintext){
|
||||
return String(data.plaintext)?.length;
|
||||
} else if(data?.button?.name) {
|
||||
return String(data?.button?.name)?.length ;
|
||||
} else {
|
||||
return MIN_COLUMN_WIDTH;
|
||||
}
|
||||
//return String(itemRow[columnIndex]?.props?.rowData?.[columns[columnIndex].key]).length || -1;
|
||||
} else if(typeof(itemRow[columnIndex]?.props?.cellData) === "string") {
|
||||
try {
|
||||
items = JSON.parse(itemRow[columnIndex]?.props?.cellData);
|
||||
if (Array.isArray(items) && items.length > 0) {
|
||||
return String(items[0]).length;
|
||||
}
|
||||
} catch (error) {
|
||||
return itemRow[columnIndex]?.props?.cellData.length;
|
||||
}
|
||||
} else if(Array.isArray(itemRow[columnIndex]?.props?.cellData)){
|
||||
return itemRow[columnIndex]?.props?.cellData.join(", ").length;
|
||||
}else {
|
||||
return itemRow[columnIndex]?.props?.cellData?.length || -1;
|
||||
}
|
||||
|
||||
}));
|
||||
const updatedColumnWidths = columnWidths.map((columnWidth, index) => {
|
||||
const currentColumnWidths = columnWidthsRef.current;
|
||||
const updatedColumnWidths = currentColumnWidths.map((columnWidth, index) => {
|
||||
if (columnIndex === index) {
|
||||
if(isNaN(longestElementInColumn)){
|
||||
return MIN_COLUMN_WIDTH;
|
||||
}
|
||||
return Math.floor(Math.max(longestElementInColumn * 10 + 40, MIN_COLUMN_WIDTH));
|
||||
return getAutosizedColumnWidth({column, columnIndex, headerNameKey, items});
|
||||
}
|
||||
return Math.floor(columnWidth);
|
||||
});
|
||||
//console.log(updatedColumnWidths, columnWidths, longestElementInColumn);
|
||||
let updatedValues = false;
|
||||
for(let i = 0; i < updatedColumnWidths.length; i++){
|
||||
if(updatedColumnWidths[i] !== columnWidths[i]){
|
||||
updatedValues = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(updatedValues){
|
||||
setColumnWidthsAndRef(updatedColumnWidths, columnIndex);
|
||||
const didUpdateWidths = setColumnWidthsAndRef(updatedColumnWidths, columnIndex);
|
||||
if(didUpdateWidths){
|
||||
if(name !== undefined){
|
||||
updateSetting({setting_name: `${name}_column_widths`, value: updatedColumnWidths});
|
||||
}
|
||||
}
|
||||
}, [columnWidths, columns, items, name, setColumnWidthsAndRef, updateSetting]);
|
||||
}, [columns, headerNameKey, items, name, setColumnWidthsAndRef, updateSetting]);
|
||||
const autosizeAllColumns = useCallback( () => {
|
||||
const currentColumnWidths = columnWidthsRef.current;
|
||||
const updatedColumnWidths = currentColumnWidths.map((columnWidth, columnIndex) => {
|
||||
const column = columns[columnIndex];
|
||||
if(column?.disableDoubleClick || column?.disableAutosize){
|
||||
return Math.floor(columnWidth);
|
||||
}
|
||||
return getAutosizedColumnWidth({column, columnIndex, headerNameKey, items});
|
||||
});
|
||||
const didUpdateWidths = setColumnWidthsAndRef(updatedColumnWidths, 0);
|
||||
if(didUpdateWidths && name !== undefined){
|
||||
updateSetting({setting_name: `${name}_column_widths`, value: updatedColumnWidths});
|
||||
}
|
||||
}, [columns, headerNameKey, items, name, setColumnWidthsAndRef, updateSetting]);
|
||||
const resetColumnWidths = useCallback( () => {
|
||||
const updatedColumnWidths = getInitialColumnWidths({
|
||||
columns,
|
||||
savedWidths: [],
|
||||
totalWidth: AutoSizerProps.width - scrollbarWidth,
|
||||
});
|
||||
const didUpdateWidths = setColumnWidthsAndRef(updatedColumnWidths, 0);
|
||||
if(didUpdateWidths && name !== undefined){
|
||||
updateSetting({setting_name: `${name}_column_widths`, value: updatedColumnWidths});
|
||||
}
|
||||
}, [AutoSizerProps.width, columns, name, scrollbarWidth, setColumnWidthsAndRef, updateSetting]);
|
||||
const headerContextMenuOptions = React.useMemo( () => {
|
||||
const sizingOptions = [
|
||||
{
|
||||
name: "Autosize Column",
|
||||
type: "item",
|
||||
icon: <FitScreenIcon style={headerMenuIconStyle} />,
|
||||
disabled: ({columnIndex}) => columns[columnIndex]?.disableDoubleClick || columns[columnIndex]?.disableAutosize,
|
||||
click: ({columnIndex}) => autosizeColumn({columnIndex}),
|
||||
},
|
||||
{
|
||||
name: "Autosize All Columns",
|
||||
type: "item",
|
||||
icon: <ViewColumnIcon style={headerMenuIconStyle} />,
|
||||
disabled: columns.every((column) => column?.disableDoubleClick || column?.disableAutosize),
|
||||
click: () => autosizeAllColumns(),
|
||||
},
|
||||
{
|
||||
name: "Reset Column Widths",
|
||||
type: "item",
|
||||
icon: <RestartAltIcon style={headerMenuIconStyle} />,
|
||||
click: () => resetColumnWidths(),
|
||||
},
|
||||
];
|
||||
return [...sizingOptions, ...(contextMenuOptions || [])];
|
||||
}, [autosizeAllColumns, autosizeColumn, columns, contextMenuOptions, resetColumnWidths]);
|
||||
|
||||
useEffect( () => {
|
||||
if(callbackTableGridRef){
|
||||
@@ -378,7 +522,7 @@ const ResizableGridWrapper = ({
|
||||
"headerNameKey": headerNameKey,
|
||||
"onClickHeader": onClickHeader,
|
||||
"autosizeColumn": autosizeColumn,
|
||||
"contextMenuOptions": contextMenuOptions,
|
||||
"contextMenuOptions": headerContextMenuOptions,
|
||||
"sortIndicatorIndex": sortIndicatorIndex,
|
||||
"sortDirection": sortDirection,
|
||||
"getColumnWidth": getColumnWidth,
|
||||
@@ -390,6 +534,7 @@ const ResizableGridWrapper = ({
|
||||
<>
|
||||
<HeaderCellContext.Provider value={headerCellData}>
|
||||
<VariableSizeGrid
|
||||
className={classes.grid}
|
||||
height={Math.max(1, AutoSizerProps.height)}
|
||||
width={AutoSizerProps.width}
|
||||
columnCount={columns.length}
|
||||
@@ -432,29 +577,31 @@ const MythicResizableGrid = ({
|
||||
name,
|
||||
}) => {
|
||||
return (
|
||||
<AutoSizer style={{height: "100%"}}>
|
||||
{(AutoSizerProps) => (
|
||||
<ResizableGridWrapper
|
||||
name={name}
|
||||
columns={columns}
|
||||
callbackTableGridRef={callbackTableGridRef}
|
||||
headerNameKey={headerNameKey}
|
||||
sortIndicatorIndex={sortIndicatorIndex}
|
||||
sortDirection={sortDirection}
|
||||
items={items}
|
||||
widthMeasureKey={widthMeasureKey}
|
||||
rowHeight={rowHeight}
|
||||
headerRowHeight={headerRowHeight}
|
||||
onClickHeader={onClickHeader}
|
||||
onDoubleClickRow={onDoubleClickRow}
|
||||
contextMenuOptions={contextMenuOptions}
|
||||
rowContextMenuOptions={rowContextMenuOptions}
|
||||
onRowContextMenuClick={onRowContextMenuClick}
|
||||
onRowClick={onRowClick}
|
||||
{...AutoSizerProps}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
<div className={classes.root}>
|
||||
<AutoSizer style={{height: "100%", width: "100%"}}>
|
||||
{(AutoSizerProps) => (
|
||||
<ResizableGridWrapper
|
||||
name={name}
|
||||
columns={columns}
|
||||
callbackTableGridRef={callbackTableGridRef}
|
||||
headerNameKey={headerNameKey}
|
||||
sortIndicatorIndex={sortIndicatorIndex}
|
||||
sortDirection={sortDirection}
|
||||
items={items}
|
||||
widthMeasureKey={widthMeasureKey}
|
||||
rowHeight={rowHeight}
|
||||
headerRowHeight={headerRowHeight}
|
||||
onClickHeader={onClickHeader}
|
||||
onDoubleClickRow={onDoubleClickRow}
|
||||
contextMenuOptions={contextMenuOptions}
|
||||
rowContextMenuOptions={rowContextMenuOptions}
|
||||
onRowContextMenuClick={onRowContextMenuClick}
|
||||
onRowClick={onRowClick}
|
||||
{...AutoSizerProps}
|
||||
/>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,10 +1,24 @@
|
||||
const PREFIX = 'MythicResizableGrid';
|
||||
export const classes = {
|
||||
root: `${PREFIX}-root`,
|
||||
grid: `${PREFIX}-grid`,
|
||||
headerCellRow: `${PREFIX}-headerCellRow`,
|
||||
headerCell: `${PREFIX}-headerCell`,
|
||||
headerCellNoSort: `${PREFIX}-headerCellNoSort`,
|
||||
headerContent: `${PREFIX}-headerContent`,
|
||||
headerLabel: `${PREFIX}-headerLabel`,
|
||||
headerActions: `${PREFIX}-headerActions`,
|
||||
headerIndicator: `${PREFIX}-headerIndicator`,
|
||||
headerFilterIcon: `${PREFIX}-headerFilterIcon`,
|
||||
headerSortIcon: `${PREFIX}-headerSortIcon`,
|
||||
headerResizeHandle: `${PREFIX}-headerResizeHandle`,
|
||||
headerResizeHandleActive: `${PREFIX}-headerResizeHandleActive`,
|
||||
resizeGuide: `${PREFIX}-resizeGuide`,
|
||||
hoveredRow: `${PREFIX}-hoveredRow`,
|
||||
contextRow: `${PREFIX}-contextRow`,
|
||||
rowFirstCell: `${PREFIX}-rowFirstCell`,
|
||||
rowLastCell: `${PREFIX}-rowLastCell`,
|
||||
rowInteractive: `${PREFIX}-rowInteractive`,
|
||||
cell: `${PREFIX}-cell`,
|
||||
cellInner: `${PREFIX}-cellInner`,
|
||||
}
|
||||
|
||||
@@ -19,10 +19,7 @@ export const GetComputedFontSize = () => {
|
||||
}
|
||||
/*
|
||||
setting_name options:
|
||||
hideUsernames
|
||||
showIP
|
||||
showHostname
|
||||
showCallbackGroups
|
||||
taskingDisplayFields
|
||||
showMedia
|
||||
interactType
|
||||
|
||||
@@ -80,4 +77,4 @@ export function useSetMythicSetting() {
|
||||
updateSetting({variables: {preferences: operatorSettingDefaults}});
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,13 +8,9 @@ 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 TableCell from '@mui/material/TableCell';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableContainer from '@mui/material/TableContainer';
|
||||
import Paper from '@mui/material/Paper';
|
||||
import MythicStyledTableCell from "./MythicTableCell";
|
||||
import Box from '@mui/material/Box';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
export function MythicSelectFromListDialog(props) {
|
||||
const [options, setOptions] = React.useState([]);
|
||||
@@ -78,10 +74,17 @@ export function MythicSelectFromListDialog(props) {
|
||||
|
||||
export function MythicSelectFromRawListDialog(props) {
|
||||
const [options, setOptions] = React.useState([]);
|
||||
const actionText = props.action || "Select";
|
||||
const handleSubmit = (selected) => {
|
||||
props.onSubmit(selected);
|
||||
props.onClose();
|
||||
}
|
||||
const handleKeyDown = (event, selected) => {
|
||||
if(event.key === "Enter" || event.key === " "){
|
||||
event.preventDefault();
|
||||
handleSubmit(selected);
|
||||
}
|
||||
}
|
||||
useEffect( () => {
|
||||
//expects options to be an array of dictionaries with a "display" field for what gets presented to the user
|
||||
const opts = [...props.options];
|
||||
@@ -90,22 +93,33 @@ export function MythicSelectFromRawListDialog(props) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle >{props.title}</DialogTitle>
|
||||
<div style={{height: "100%", display: "flex"}}>
|
||||
<TableContainer component={Paper} className="mythicElement" style={{flexGrow: 1, overflowY: "auto"}}>
|
||||
<Table size="small" style={{ "maxWidth": "100%", "overflow": "scroll"}}>
|
||||
<TableBody style={{whiteSpace: "pre"}}>
|
||||
{options.map( (choice, i) => (
|
||||
<TableRow hover key={choice + i}>
|
||||
<MythicStyledTableCell style={{width: "5rem"}}>
|
||||
<Button onClick={() => handleSubmit(choice)} variant="contained" color="primary">Select</Button>
|
||||
</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>{choice}</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</div>
|
||||
<DialogContent dividers={true} className="mythic-raw-select-dialog-content">
|
||||
{options.length === 0 ? (
|
||||
<Box className="mythic-raw-select-empty">
|
||||
<Typography variant="body2">No options available</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<Stack className="mythic-raw-select-list">
|
||||
{options.map( (choice, i) => (
|
||||
<Box
|
||||
key={String(choice) + i}
|
||||
className="mythic-raw-select-row"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => handleSubmit(choice)}
|
||||
onKeyDown={(event) => handleKeyDown(event, choice)}
|
||||
>
|
||||
<Typography className="mythic-raw-select-value" title={String(choice)}>
|
||||
{String(choice)}
|
||||
</Typography>
|
||||
<Button className="mythic-dialog-button-info mythic-raw-select-action" variant="outlined" size="small" tabIndex={-1}>
|
||||
{actionText}
|
||||
</Button>
|
||||
</Box>
|
||||
))}
|
||||
</Stack>
|
||||
)}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={props.onClose} variant="contained" color="primary">
|
||||
Close
|
||||
|
||||
@@ -192,6 +192,9 @@ export const taskingDataFragment = gql`
|
||||
cmd
|
||||
supported_ui_features
|
||||
id
|
||||
payloadtype {
|
||||
name
|
||||
}
|
||||
}
|
||||
command_name
|
||||
response_count
|
||||
|
||||
@@ -114,7 +114,7 @@ export const ipCompare = (a, b) => {
|
||||
}
|
||||
}
|
||||
const callbackTableInitialColumns = [
|
||||
{key: "id", type: 'number', name: "Interact", width: 150, disableDoubleClick: true},
|
||||
{key: "id", type: 'number', name: "Interact", width: 175, disableDoubleClick: true},
|
||||
{key: "mythictree_groups", type: 'array', name: "Groups", width: 150},
|
||||
{key: "ip", type: 'ip', name: "IP", width: 150},
|
||||
{key: "external_ip",type: 'string', name: "External IP", width: 150},
|
||||
@@ -1014,7 +1014,7 @@ function CallbacksTablePreMemo(props){
|
||||
sortIndicatorIndex={sortColumn}
|
||||
sortDirection={sortData.sortDirection}
|
||||
items={sortedData}
|
||||
rowHeight={GetComputedFontSize() + 7}
|
||||
rowHeight={GetComputedFontSize() + 10}
|
||||
onClickHeader={onClickHeader}
|
||||
onDoubleClickRow={onRowDoubleClick}
|
||||
contextMenuOptions={contextMenuOptions}
|
||||
@@ -1213,4 +1213,4 @@ function CallbacksTablePreMemo(props){
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export const CallbacksTable = React.memo(CallbacksTablePreMemo);
|
||||
export const CallbacksTable = React.memo(CallbacksTablePreMemo);
|
||||
|
||||
@@ -34,7 +34,6 @@ import {faSocks} from '@fortawesome/free-solid-svg-icons';
|
||||
import {TagsDisplay, ViewEditTags} from "../../MythicComponents/MythicTag";
|
||||
|
||||
export const CallbacksTableIDCell = React.memo(({rowData, callbackDropdown}) =>{
|
||||
const theme = useTheme();
|
||||
const dropdownAnchorRef = React.useRef(null);
|
||||
const onOpenTab = useContext(OnOpenTabContext);
|
||||
const interactType = GetMythicSetting({setting_name: "interactType", default_value: "interact"})
|
||||
@@ -104,41 +103,53 @@ export const CallbacksTableIDCell = React.memo(({rowData, callbackDropdown}) =>{
|
||||
const localOnOpenTab = () => {
|
||||
onOpenTab({tabType: interactType, tabID: rowDataStatic.id + interactType, callbackID: rowDataStatic.id, displayID: rowDataStatic.display_id});
|
||||
}
|
||||
let defaultInteractIcon = <KeyboardIcon style={{paddingRight: "5px"}}/>;
|
||||
let defaultInteractIcon = <KeyboardIcon fontSize="small" />;
|
||||
if(interactType === "interactSplit"){
|
||||
defaultInteractIcon = <VerticalSplitIcon style={{paddingRight: "5px"}}/>;
|
||||
defaultInteractIcon = <VerticalSplitIcon fontSize="small" />;
|
||||
} else if(interactType === "interactConsole"){
|
||||
defaultInteractIcon = <TerminalIcon style={{paddingRight: "5px"}}/>;
|
||||
defaultInteractIcon = <TerminalIcon fontSize="small" />;
|
||||
}
|
||||
const highIntegrity = rowDataStatic.integrity_level > 3;
|
||||
const lockOwner = rowDataStatic.locked_operator?.username || "unknown operator";
|
||||
|
||||
return (
|
||||
<div id={`callbacksTableID${rowDataStatic.id}`} style={{display: "inline-flex", alignItems: "flex-start"}}>
|
||||
<IconButton style={{padding: 0, margin: 0}} color={rowDataStatic.integrity_level > 2 ? "error" : ""}
|
||||
onClick={(evt) => {evt.stopPropagation();localOnOpenTab()}}
|
||||
>
|
||||
{rowDataStatic.locked ? (<LockIcon style={{marginRight: "10px"}} />):(defaultInteractIcon)}
|
||||
</IconButton>
|
||||
{rowDataStatic.display_id}
|
||||
<div id={`callbacksTableID${rowDataStatic.id}`} className="mythic-callback-interactCell">
|
||||
<MythicStyledTooltip title={`Open ${interactType === "interactSplit" ? "split" : interactType === "interactConsole" ? "console" : "tasking"} view${highIntegrity ? " - high integrity callback" : ""}`}>
|
||||
<IconButton
|
||||
className={`mythic-callback-iconButton mythic-callback-interactButton ${highIntegrity ? "mythic-callback-interactButtonHighIntegrity" : ""}`}
|
||||
onClick={(evt) => {evt.stopPropagation();localOnOpenTab()}}
|
||||
>
|
||||
{defaultInteractIcon}
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<span className="mythic-callback-displayId">{rowDataStatic.display_id}</span>
|
||||
<IconButton
|
||||
style={{margin: 0, padding: 0}}
|
||||
color={rowDataStatic.integrity_level > 2 ? "error" : ""}
|
||||
className={`mythic-callback-iconButton mythic-callback-menuButton ${highIntegrity ? "mythic-callback-menuButtonHighIntegrity" : ""}`}
|
||||
aria-haspopup="menu"
|
||||
onClick={handleDropdownToggle}
|
||||
ref={dropdownAnchorRef}
|
||||
>
|
||||
<ArrowDropDownIcon/>
|
||||
<ArrowDropDownIcon fontSize="small" />
|
||||
</IconButton>
|
||||
{rowDataStatic.locked &&
|
||||
<MythicStyledTooltip title={`Locked by ${lockOwner}`}>
|
||||
<span className="mythic-callback-lockBadge">
|
||||
<LockIcon fontSize="inherit" />
|
||||
<span>Locked</span>
|
||||
</span>
|
||||
</MythicStyledTooltip>
|
||||
}
|
||||
{rowDataStatic.trigger_on_checkin_after_time > 0 &&
|
||||
<MythicStyledTooltip title={`Alert on callback after no checkin for ${rowDataStatic.trigger_on_checkin_after_time} minutes`}>
|
||||
<NotificationsActiveTwoToneIcon color={"success"}/>
|
||||
<NotificationsActiveTwoToneIcon className="mythic-callback-statusIcon mythic-callback-statusIconSuccess" fontSize="small" />
|
||||
</MythicStyledTooltip>
|
||||
}
|
||||
{rowDataStatic.callbackports.length > 0 &&
|
||||
<MythicStyledTooltip title={proxyMessage()}>
|
||||
<FontAwesomeIcon icon={faSocks} size="lg" style={{color: theme.palette.success.main}}/>
|
||||
<FontAwesomeIcon icon={faSocks} className="mythic-callback-statusIcon mythic-callback-statusIconSuccess" />
|
||||
</MythicStyledTooltip>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
areEqual)
|
||||
@@ -483,4 +494,4 @@ export const CallbacksTableTagsCell = React.memo(({rowData, cellData}) => {
|
||||
<TagsDisplay tags={cellData} expand={false} />
|
||||
</div>
|
||||
)
|
||||
}, areEqual)
|
||||
}, areEqual)
|
||||
|
||||
+71
-65
@@ -681,7 +681,7 @@ export const CallbacksTabsCustomFileBasedBrowserPanel = ({ index, value, tabInfo
|
||||
return (
|
||||
<MythicTabPanel index={index} value={value}>
|
||||
<Split direction="horizontal" style={{width: "100%", height: "100%", display: "flex", overflow: "hidden"}} minSize={[0,0]} sizes={[30, 70]} >
|
||||
<div className="bg-gray-base" style={{display: "inline-flex"}}>
|
||||
<div className="bg-gray-base" style={{display: "inline-flex", height: "100%", minHeight: 0, minWidth: 0, overflow: "hidden"}}>
|
||||
<Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute"}} invisible={true}>
|
||||
<CircularProgress color="inherit" />
|
||||
</Backdrop>
|
||||
@@ -701,8 +701,8 @@ export const CallbacksTabsCustomFileBasedBrowserPanel = ({ index, value, tabInfo
|
||||
/>
|
||||
|
||||
</div>
|
||||
<div className="bg-gray-light" style={{display: "inline-flex"}}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', flexGrow: 1, overflow: "hidden" }}>
|
||||
<div className="bg-gray-light" style={{display: "inline-flex", height: "100%", minHeight: 0, minWidth: 0, overflow: "hidden"}}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', flexGrow: 1, minHeight: 0, minWidth: 0, overflow: "hidden" }}>
|
||||
<div style={{ flexGrow: 0 }}>
|
||||
<FileBrowserTableTop
|
||||
tabInfo={tabInfo}
|
||||
@@ -720,7 +720,7 @@ export const CallbacksTabsCustomFileBasedBrowserPanel = ({ index, value, tabInfo
|
||||
treeConfig={treeConfig}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ flexGrow: 1 }}>
|
||||
<div style={{ flex: "1 1 auto", minHeight: 0, minWidth: 0, overflow: "hidden" }}>
|
||||
<Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute"}} invisible={true}>
|
||||
<CircularProgress color="inherit" />
|
||||
</Backdrop>
|
||||
@@ -987,7 +987,7 @@ const FileBrowserTableTop = ({
|
||||
autoTaskLsOnEmptyDirectoriesRef.current = autoTaskLsOnEmptyDirectories;
|
||||
}, [autoTaskLsOnEmptyDirectories]);
|
||||
return (
|
||||
<Grid container spacing={0} style={{ paddingTop: '5px' }}>
|
||||
<Grid container spacing={0} className="mythic-file-browser-tableTop">
|
||||
<Grid size={12}>
|
||||
{openEditHostDialog && (
|
||||
<MythicDialog
|
||||
@@ -1017,62 +1017,69 @@ const FileBrowserTableTop = ({
|
||||
name={<>
|
||||
{placeHolder}
|
||||
<MythicStyledTooltip title={`Edit the supplied Host field`}>
|
||||
<IconButton style={{ padding: '3px' }}
|
||||
onClick={() => {setOpenEditHostDialog(true);}}
|
||||
size="large">
|
||||
<EditIcon color='info' />
|
||||
<IconButton
|
||||
className="mythic-file-browser-iconButton mythic-file-browser-labelButton mythic-file-browser-hoverInfo"
|
||||
onClick={() => {setOpenEditHostDialog(true);}}
|
||||
size="small">
|
||||
<EditIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
{placeHolderGroups}
|
||||
</>}
|
||||
marginTop="0"
|
||||
marginBottom="0"
|
||||
InputProps={{
|
||||
className: "mythic-file-browser-pathInput",
|
||||
endAdornment: (
|
||||
<React.Fragment>
|
||||
<div className="mythic-file-browser-toolbarGroup mythic-file-browser-toolbarGroupEnd">
|
||||
<MythicStyledTooltip title={`Task current callback (${tabInfo["displayID"]}) to list contents`}>
|
||||
<IconButton style={{ padding: '3px' }}
|
||||
onClick={onLocalListFilesButton}
|
||||
size="large">
|
||||
<RefreshIcon color='info' />
|
||||
<IconButton
|
||||
className="mythic-file-browser-iconButton mythic-file-browser-hoverInfo"
|
||||
onClick={onLocalListFilesButton}
|
||||
size="small">
|
||||
<RefreshIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
{autoTaskLsOnEmptyDirectories ? (
|
||||
<MythicStyledTooltip title={"Currently tasking listing on empty directories, click to toggle off"} >
|
||||
<IconButton style={{padding: "3px"}}
|
||||
onClick={onToggleAutoTaskLsOnEmptyDirectories}
|
||||
disabled={!treeConfig.show_current_path}
|
||||
size={"large"}>
|
||||
<PlaylistAddIcon color={"success"} ></PlaylistAddIcon>
|
||||
<IconButton
|
||||
className="mythic-file-browser-iconButton mythic-file-browser-activeSuccess mythic-file-browser-hoverSuccess"
|
||||
onClick={onToggleAutoTaskLsOnEmptyDirectories}
|
||||
disabled={!treeConfig.show_current_path}
|
||||
size="small">
|
||||
<PlaylistAddIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
) : (
|
||||
<MythicStyledTooltip title={"Currently not tasking listing on empty directories, click to toggle on"} >
|
||||
<IconButton style={{padding: "3px"}}
|
||||
disabled={!treeConfig.show_current_path}
|
||||
onClick={onToggleAutoTaskLsOnEmptyDirectories}
|
||||
size={"large"}>
|
||||
<PlaylistRemoveIcon color={"secondary"} ></PlaylistRemoveIcon>
|
||||
<IconButton
|
||||
className="mythic-file-browser-iconButton mythic-file-browser-hoverWarning"
|
||||
disabled={!treeConfig.show_current_path}
|
||||
onClick={onToggleAutoTaskLsOnEmptyDirectories}
|
||||
size="small">
|
||||
<PlaylistRemoveIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
)}
|
||||
<MythicStyledTooltip title={showDeletedFiles ? 'Hide Deleted Entries' : 'Show Deleted Entries'}>
|
||||
<IconButton
|
||||
style={{ padding: '3px' }}
|
||||
className={`mythic-file-browser-iconButton mythic-file-browser-hoverWarning ${showDeletedFiles ? "mythic-file-browser-activeWarning" : ""}`}
|
||||
onClick={onLocalToggleShowDeletedFiles}
|
||||
size="large">
|
||||
size="small">
|
||||
{showDeletedFiles ? (
|
||||
<VisibilityIcon color="success" />
|
||||
<VisibilityIcon fontSize="small" />
|
||||
) : (
|
||||
<VisibilityOffIcon color={"secondary"} />
|
||||
<VisibilityOffIcon fontSize="small" />
|
||||
)}
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={`Export Current Path and Children`}>
|
||||
<IconButton style={{ padding: '3px' }}
|
||||
disabled={treeConfig.export_function === ""}
|
||||
onClick={onLocalExportButton}
|
||||
color="info"
|
||||
size="large">
|
||||
<IosShareIcon/>
|
||||
<IconButton
|
||||
className="mythic-file-browser-iconButton mythic-file-browser-hoverInfo"
|
||||
disabled={treeConfig.export_function === ""}
|
||||
onClick={onLocalExportButton}
|
||||
size="small">
|
||||
<IosShareIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
{treeConfig.extra_table_inputs?.length > 0 &&
|
||||
@@ -1081,12 +1088,13 @@ const FileBrowserTableTop = ({
|
||||
showExtraInputs ?
|
||||
`Hide Extra Browser Inputs ${extraDataSet ? "( Extra Data Currently Set )" : ""}` :
|
||||
`Show Extra Browser Inputs ${extraDataSet ? "( Extra Data Currently Set )" : ""}`}>
|
||||
<IconButton style={{ padding: '3px' }}
|
||||
onClick={() => {setShowExtraInputs(!showExtraInputs)}}
|
||||
disableFocusRipple={true}
|
||||
disableRipple={true}
|
||||
size="large">
|
||||
<SettingsInputComponentRoundedIcon color={extraDataRequired ? "error" : extraDataSet ? "warning" : "info"} />
|
||||
<IconButton
|
||||
className={`mythic-file-browser-iconButton mythic-file-browser-iconButtonCompound ${extraDataRequired ? "mythic-file-browser-activeError mythic-file-browser-hoverError" : extraDataSet ? "mythic-file-browser-activeWarning mythic-file-browser-hoverWarning" : "mythic-file-browser-hoverInfo"}`}
|
||||
onClick={() => {setShowExtraInputs(!showExtraInputs)}}
|
||||
disableFocusRipple={true}
|
||||
disableRipple={true}
|
||||
size="small">
|
||||
<SettingsInputComponentRoundedIcon fontSize="small" />
|
||||
{showExtraInputs ? (
|
||||
<KeyboardArrowDownRoundedIcon style={{rotate: "180deg"}} />
|
||||
) : (
|
||||
@@ -1095,47 +1103,45 @@ const FileBrowserTableTop = ({
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
}
|
||||
</React.Fragment>
|
||||
</div>
|
||||
),
|
||||
startAdornment: (
|
||||
<div style={{display: "inline-flex",
|
||||
alignItems: "center",
|
||||
borderRight: "1px solid grey",
|
||||
marginRight: "10px",
|
||||
padding: "0 5px 0 0"}}>
|
||||
<div className="mythic-file-browser-toolbarGroup mythic-file-browser-toolbarGroupStart">
|
||||
{tokenOptions.length > 0 &&
|
||||
<CallbacksTabsTaskingInputTokenSelect width={"100%"}
|
||||
options={tokenOptions} changeSelectedToken={changeSelectedToken}/>
|
||||
<div className="mythic-file-browser-tokenSelect">
|
||||
<CallbacksTabsTaskingInputTokenSelect width={"100%"}
|
||||
options={tokenOptions} changeSelectedToken={changeSelectedToken}/>
|
||||
</div>
|
||||
}
|
||||
<MythicStyledTooltip title={`Move back to previous listing`}>
|
||||
<IconButton style={{ padding: '3px' }}
|
||||
disabled={historyIndex >= history.length -1 }
|
||||
onClick={moveIndexToPreviousListing}
|
||||
color='info'
|
||||
size="large">
|
||||
<ArrowBackIcon />
|
||||
<IconButton
|
||||
className="mythic-file-browser-iconButton mythic-file-browser-hoverInfo"
|
||||
disabled={historyIndex >= history.length -1 }
|
||||
onClick={moveIndexToPreviousListing}
|
||||
size="small">
|
||||
<ArrowBackIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={`Move to next listing`}>
|
||||
<IconButton style={{ padding: '3px' }}
|
||||
disabled={historyIndex <= 0}
|
||||
onClick={moveIndexToNextListing}
|
||||
size="large"
|
||||
color='info'>
|
||||
<ArrowForwardIcon />
|
||||
<IconButton
|
||||
className="mythic-file-browser-iconButton mythic-file-browser-hoverInfo"
|
||||
disabled={historyIndex <= 0}
|
||||
onClick={moveIndexToNextListing}
|
||||
size="small">
|
||||
<ArrowForwardIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Move up a directory"} >
|
||||
<IconButton style={{padding: "0 0 0 0"}}
|
||||
onClick={onLocalMoveUpDirectoryButton}
|
||||
disabled={!selectedFolderData?.parent_path_text || selectedFolderData?.parent_path_text?.length === 0 || selectedFolderData.root || fullPath === ""}
|
||||
<IconButton
|
||||
className="mythic-file-browser-iconButton mythic-file-browser-hoverInfo"
|
||||
onClick={onLocalMoveUpDirectoryButton}
|
||||
disabled={!selectedFolderData?.parent_path_text || selectedFolderData?.parent_path_text?.length === 0 || selectedFolderData.root || fullPath === ""}
|
||||
>
|
||||
<KeyboardReturnIcon style={{rotate: "90deg"}} ></KeyboardReturnIcon>
|
||||
<KeyboardReturnIcon fontSize="small" style={{rotate: "90deg"}} />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
|
||||
</div>),
|
||||
style: { },
|
||||
}}
|
||||
/>
|
||||
{showExtraInputs &&
|
||||
|
||||
@@ -683,7 +683,7 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me, setNe
|
||||
return (
|
||||
<MythicTabPanel index={index} value={value}>
|
||||
<Split direction="horizontal" style={{width: "100%", height: "100%", display: "flex", overflow: "hidden"}} sizes={[30, 70]} >
|
||||
<div className="bg-gray-base" style={{display: "inline-flex"}}>
|
||||
<div className="bg-gray-base" style={{display: "inline-flex", height: "100%", minHeight: 0, minWidth: 0, overflow: "hidden"}}>
|
||||
<Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute"}} invisible={true}>
|
||||
<CircularProgress color="inherit" />
|
||||
</Backdrop>
|
||||
@@ -702,8 +702,8 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me, setNe
|
||||
/>
|
||||
|
||||
</div>
|
||||
<div className="bg-gray-light" style={{display: "inline-flex"}}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', flexGrow: 1, overflow: "hidden" }}>
|
||||
<div className="bg-gray-light" style={{display: "inline-flex", height: "100%", minHeight: 0, minWidth: 0, overflow: "hidden"}}>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', flexGrow: 1, minHeight: 0, minWidth: 0, overflow: "hidden" }}>
|
||||
<div style={{ flexGrow: 0 }}>
|
||||
<FileBrowserTableTop
|
||||
tabInfo={tabInfo}
|
||||
@@ -720,7 +720,7 @@ export const CallbacksTabsFileBrowserPanel = ({ index, value, tabInfo, me, setNe
|
||||
baseUIFeature={baseUIFeature}
|
||||
/>
|
||||
</div>
|
||||
<div style={{ flexGrow: 1 }}>
|
||||
<div style={{ flex: "1 1 auto", minHeight: 0, minWidth: 0, overflow: "hidden" }}>
|
||||
<Backdrop open={backdropOpen} style={{zIndex: 2, position: "absolute"}} invisible={true}>
|
||||
<CircularProgress color="inherit" />
|
||||
</Backdrop>
|
||||
@@ -917,7 +917,7 @@ const FileBrowserTableTop = ({
|
||||
autoTaskLsOnEmptyDirectoriesRef.current = autoTaskLsOnEmptyDirectories;
|
||||
}, [autoTaskLsOnEmptyDirectories]);
|
||||
return (
|
||||
<Grid container spacing={0} style={{ paddingTop: '10px' }}>
|
||||
<Grid container spacing={0} className="mythic-file-browser-tableTop">
|
||||
<Grid size={12}>
|
||||
<MythicTextField
|
||||
placeholder={selectedFolderData.host}
|
||||
@@ -925,93 +925,98 @@ const FileBrowserTableTop = ({
|
||||
onEnter={goToDirectory}
|
||||
onChange={onChangePath}
|
||||
name={placeHolder}
|
||||
marginTop="0"
|
||||
marginBottom="0"
|
||||
InputProps={{
|
||||
className: "mythic-file-browser-pathInput",
|
||||
endAdornment: (
|
||||
<React.Fragment>
|
||||
<div className="mythic-file-browser-toolbarGroup mythic-file-browser-toolbarGroupEnd">
|
||||
<MythicStyledTooltip title={`Task current callback (${tabInfo["displayID"]}) to list contents`}>
|
||||
<IconButton style={{ padding: '3px' }}
|
||||
onClick={onLocalListFilesButton}
|
||||
size="large">
|
||||
<RefreshIcon color='info' />
|
||||
<IconButton
|
||||
className="mythic-file-browser-iconButton mythic-file-browser-hoverInfo"
|
||||
onClick={onLocalListFilesButton}
|
||||
size="small">
|
||||
<RefreshIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={`Upload file to folder via current callback (${tabInfo["displayID"]})`}>
|
||||
<IconButton style={{ padding: '3px' }}
|
||||
onClick={onLocalUploadFileButton}
|
||||
size="large">
|
||||
<CloudUploadIcon color="info" />
|
||||
<IconButton
|
||||
className="mythic-file-browser-iconButton mythic-file-browser-hoverInfo"
|
||||
onClick={onLocalUploadFileButton}
|
||||
size="small">
|
||||
<CloudUploadIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
{autoTaskLsOnEmptyDirectories ? (
|
||||
<MythicStyledTooltip title={"Currently tasking listing on empty directories, click to toggle off"} >
|
||||
<IconButton style={{padding: "3px"}}
|
||||
onClick={onToggleAutoTaskLsOnEmptyDirectories}
|
||||
size={"large"}>
|
||||
<PlaylistAddIcon color={"success"} ></PlaylistAddIcon>
|
||||
<IconButton
|
||||
className="mythic-file-browser-iconButton mythic-file-browser-activeSuccess mythic-file-browser-hoverSuccess"
|
||||
onClick={onToggleAutoTaskLsOnEmptyDirectories}
|
||||
size="small">
|
||||
<PlaylistAddIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
) : (
|
||||
<MythicStyledTooltip title={"Currently not tasking listing on empty directories, click to toggle on"} >
|
||||
<IconButton style={{padding: "3px"}}
|
||||
onClick={onToggleAutoTaskLsOnEmptyDirectories}
|
||||
size={"large"}>
|
||||
<PlaylistRemoveIcon color={"secondary"} ></PlaylistRemoveIcon>
|
||||
<IconButton
|
||||
className="mythic-file-browser-iconButton mythic-file-browser-hoverWarning"
|
||||
onClick={onToggleAutoTaskLsOnEmptyDirectories}
|
||||
size="small">
|
||||
<PlaylistRemoveIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
)}
|
||||
<MythicStyledTooltip title={showDeletedFiles ? 'Hide Deleted Files' : 'Show Deleted Files'}>
|
||||
<IconButton
|
||||
style={{ padding: '3px' }}
|
||||
className={`mythic-file-browser-iconButton mythic-file-browser-hoverWarning ${showDeletedFiles ? "mythic-file-browser-activeWarning" : ""}`}
|
||||
onClick={onLocalToggleShowDeletedFiles}
|
||||
size="large">
|
||||
size="small">
|
||||
{showDeletedFiles ? (
|
||||
<VisibilityIcon color="success" />
|
||||
<VisibilityIcon fontSize="small" />
|
||||
) : (
|
||||
<VisibilityOffIcon color="error" />
|
||||
<VisibilityOffIcon fontSize="small" />
|
||||
)}
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
</React.Fragment>
|
||||
</div>
|
||||
),
|
||||
startAdornment: (
|
||||
<div style={{display: "inline-flex",
|
||||
alignItems: "center",
|
||||
borderRight: "1px solid grey",
|
||||
marginRight: "10px",
|
||||
padding: "0 5px 0 0"}}>
|
||||
<div className="mythic-file-browser-toolbarGroup mythic-file-browser-toolbarGroupStart">
|
||||
{tokenOptions.length > 0 &&
|
||||
<CallbacksTabsTaskingInputTokenSelect width={"100%"}
|
||||
options={tokenOptions} changeSelectedToken={changeSelectedToken}/>
|
||||
<div className="mythic-file-browser-tokenSelect">
|
||||
<CallbacksTabsTaskingInputTokenSelect width={"100%"}
|
||||
options={tokenOptions} changeSelectedToken={changeSelectedToken}/>
|
||||
</div>
|
||||
}
|
||||
<MythicStyledTooltip title={`Move back to previous listing`}>
|
||||
<IconButton style={{ padding: '3px' }}
|
||||
disabled={historyIndex >= history.length -1 }
|
||||
onClick={moveIndexToPreviousListing}
|
||||
color='info'
|
||||
size="large">
|
||||
<ArrowBackIcon />
|
||||
<IconButton
|
||||
className="mythic-file-browser-iconButton mythic-file-browser-hoverInfo"
|
||||
disabled={historyIndex >= history.length -1 }
|
||||
onClick={moveIndexToPreviousListing}
|
||||
size="small">
|
||||
<ArrowBackIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={`Move to next listing`}>
|
||||
<IconButton style={{ padding: '3px' }}
|
||||
disabled={historyIndex <= 0}
|
||||
onClick={moveIndexToNextListing}
|
||||
size="large"
|
||||
color='info'>
|
||||
<ArrowForwardIcon />
|
||||
<IconButton
|
||||
className="mythic-file-browser-iconButton mythic-file-browser-hoverInfo"
|
||||
disabled={historyIndex <= 0}
|
||||
onClick={moveIndexToNextListing}
|
||||
size="small">
|
||||
<ArrowForwardIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Move up a directory"} >
|
||||
<IconButton style={{padding: "0 0 0 0"}}
|
||||
onClick={onLocalMoveUpDirectoryButton}
|
||||
disabled={!selectedFolderData?.parent_path_text || selectedFolderData?.parent_path_text?.length === 0 || selectedFolderData.root || fullPath === ""}
|
||||
<IconButton
|
||||
className="mythic-file-browser-iconButton mythic-file-browser-hoverInfo"
|
||||
onClick={onLocalMoveUpDirectoryButton}
|
||||
disabled={!selectedFolderData?.parent_path_text || selectedFolderData?.parent_path_text?.length === 0 || selectedFolderData.root || fullPath === ""}
|
||||
>
|
||||
<KeyboardReturnIcon style={{rotate: "90deg"}} ></KeyboardReturnIcon>
|
||||
<KeyboardReturnIcon fontSize="small" style={{rotate: "90deg"}} />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
|
||||
</div>),
|
||||
style: { },
|
||||
}}
|
||||
/>
|
||||
</Grid>
|
||||
|
||||
@@ -418,10 +418,10 @@ export const CallbacksTabsTaskingSplitPanel = ({tabInfo, index, value, onCloseTa
|
||||
return (
|
||||
<MythicTabPanel index={index} value={value} >
|
||||
<Split direction="horizontal" minSize={[0,0]} sizes={[30, 70]}
|
||||
style={{width: "100%", height: "100%", overflow: "auto", display: "flex", justifyContent: "flex-end"}} >
|
||||
<div className="bg-gray-base" style={{display: "inline-flex", flexDirection: "column"}} >
|
||||
style={{width: "100%", height: "100%", minWidth: 0, overflow: "hidden", display: "flex"}} >
|
||||
<div className="bg-gray-base" style={{display: "inline-flex", flexDirection: "column", minWidth: 0, overflow: "hidden"}} >
|
||||
|
||||
<div style={{overflowY: "auto", flexGrow: 1}} id={`taskingPanelSplit${tabInfo.callbackID}`}>
|
||||
<div style={{overflowY: "auto", overflowX: "hidden", flexGrow: 1, minWidth: 0}} id={`taskingPanelSplit${tabInfo.callbackID}`}>
|
||||
|
||||
{!fetchedAllTasks &&
|
||||
<MythicStyledTooltip title="Fetch Older Tasks">
|
||||
@@ -450,7 +450,7 @@ export const CallbacksTabsTaskingSplitPanel = ({tabInfo, index, value, onCloseTa
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className="bg-gray-light" style={{display: "inline-flex", flexDirection: "column", height: "100%", width: "100%"}}>
|
||||
<div className="bg-gray-light" style={{display: "inline-flex", flexDirection: "column", height: "100%", minWidth: 0, overflow: "hidden", width: "100%"}}>
|
||||
{selectedTask.id > 0 && <TaskDisplayFlat key={"taskinteractdisplaysplit" + selectedTask.id} me={me} task={selectedTask}
|
||||
command_id={selectedTask.command == null ? 0 : selectedTask.command.id}
|
||||
filterOptions={filterOptions}
|
||||
@@ -483,7 +483,7 @@ export const CallbacksTabsTaskingSplitPanel = ({tabInfo, index, value, onCloseTa
|
||||
}
|
||||
const CallbacksTabsTaskingSplitTable = ({selectedTask, me}) => {
|
||||
return (
|
||||
<div style={{width: "100%", height: "100%", overflowY: "auto", display: "flex", flexDirection: "column"}} >
|
||||
<div style={{width: "100%", flex: "1 1 auto", minHeight: 0, minWidth: 0, overflow: "hidden", display: "flex", flexDirection: "column"}} >
|
||||
{selectedTask.id > 0 &&
|
||||
<TaskDisplayContainerFlat key={selectedTask.id} me={me} task={selectedTask} />
|
||||
}
|
||||
|
||||
@@ -364,7 +364,7 @@ const NonInteractiveResponseDisplay = (props) => {
|
||||
{props.searchOutput &&
|
||||
<SearchBar onSubmitSearch={onSubmitSearch} />
|
||||
}
|
||||
<div style={{overflowY: "auto", flexGrow: 1, width: "100%", height: props.expand ? "100%": undefined, display: "flex", flexDirection: "column"}} ref={props.responseRef}>
|
||||
<div style={{overflowY: "auto", overflowX: "hidden", flexGrow: 1, minWidth: 0, width: "100%", height: props.expand ? "100%": undefined, display: "flex", flexDirection: "column"}} ref={props.responseRef}>
|
||||
<ResponseDisplayComponent rawResponses={rawResponses} viewBrowserScript={props.viewBrowserScript}
|
||||
output={output} command_id={props.command_id} displayType={"accordion"}
|
||||
task={props.task} search={search} expand={props.expand}/>
|
||||
@@ -430,7 +430,7 @@ export const NonInteractiveResponseDisplayConsole = (props) => {
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<div style={{overflowY: "auto", width: "100%", height: props.expand ? "100%" : undefined}}
|
||||
<div style={{overflowY: "auto", overflowX: "hidden", minWidth: 0, width: "100%", height: props.expand ? "100%" : undefined}}
|
||||
ref={props.responseRef}>
|
||||
<ResponseDisplayComponent rawResponses={rawResponses} viewBrowserScript={props.viewBrowserScript}
|
||||
output={output} command_id={props.command_id} displayType={"console"}
|
||||
|
||||
@@ -808,10 +808,10 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
|
||||
return expand ? {flexGrow: 1,
|
||||
minHeight: gridData.length > 0 ? Math.min(maxHeight, dataHeight) : headerHeight,
|
||||
width: "100%", position: "relative"} :
|
||||
{height: dataHeight, position: "relative"}
|
||||
{height: dataHeight, position: "relative", width: "100%"}
|
||||
}, [expand, dataHeight, gridData]);
|
||||
return (
|
||||
<div style={{height: "100%", display: "flex", flexDirection: "column", position: "relative", width: "100%"}}>
|
||||
<div className="mythic-response-table" style={{height: "100%", position: "relative"}}>
|
||||
{table?.title && (
|
||||
<MythicSectionHeader
|
||||
dense
|
||||
@@ -820,7 +820,7 @@ export const ResponseDisplayTable = ({table, callback_id, expand, task}) =>{
|
||||
/>
|
||||
)}
|
||||
|
||||
<div style={tableStyle}>
|
||||
<div className="mythic-response-table-grid" style={tableStyle}>
|
||||
<MythicResizableGrid
|
||||
columns={columns}
|
||||
sortIndicatorIndex={sortColumn}
|
||||
|
||||
@@ -14,11 +14,11 @@ import {gql, useSubscription } from '@apollo/client';
|
||||
import {TaskDisplayContainer, TaskDisplayContainerConsole} from './TaskDisplayContainer';
|
||||
import {TagsDisplay} from '../../MythicComponents/MythicTag';
|
||||
import {taskingDataFragment} from './CallbackMutations';
|
||||
import {GetMythicSetting} from "../../MythicComponents/MythicSavedUserSetting";
|
||||
import {GetMythicSetting, useGetMythicSetting} from "../../MythicComponents/MythicSavedUserSetting";
|
||||
import PlayCircleFilledTwoToneIcon from '@mui/icons-material/PlayCircleFilledTwoTone';
|
||||
import CropRotateTwoToneIcon from '@mui/icons-material/CropRotateTwoTone';
|
||||
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
|
||||
import {operatorSettingDefaults} from "../../../cache";
|
||||
import {normalizeTaskingDisplayFields, operatorSettingDefaults} from "../../../cache";
|
||||
import {TaskFromUIButton} from "./TaskFromUIButton";
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||
import {faSkullCrossbones} from '@fortawesome/free-solid-svg-icons';
|
||||
@@ -406,6 +406,66 @@ const getCallbackPrimaryIP = (task) => {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
const useTaskingDisplayFields = () => {
|
||||
const taskingDisplayFields = useGetMythicSetting({setting_name: "taskingDisplayFields", default_value: operatorSettingDefaults.taskingDisplayFields});
|
||||
return React.useMemo(() => normalizeTaskingDisplayFields(taskingDisplayFields), [taskingDisplayFields]);
|
||||
}
|
||||
const TaskingMetadataField = ({fieldName, task, displayTimestamp, me, compactTimestamp=false}) => {
|
||||
const ipValue = getCallbackPrimaryIP(task);
|
||||
const callbackGroups = task.callback.mythictree_groups?.join(', ') || "";
|
||||
switch(fieldName){
|
||||
case "timestamp":
|
||||
return (
|
||||
<TaskMetaItem title={"Task timestamp"} icon={<AccessTimeIcon />}>
|
||||
{compactTimestamp ? toLocalTimeShort(displayTimestamp, me?.user?.view_utc_time || false) : toLocalTime(displayTimestamp, me?.user?.view_utc_time || false)}
|
||||
</TaskMetaItem>
|
||||
);
|
||||
case "task":
|
||||
return (
|
||||
<TaskMetaItem title={"View Task in separate page"} icon={<NumbersIcon />}>
|
||||
<Link underline="hover" target="_blank" href={"/new/task/" + task.display_id}>T-{task.display_id}</Link>
|
||||
</TaskMetaItem>
|
||||
);
|
||||
case "username":
|
||||
return (
|
||||
<TaskMetaItem title={"Operator"} icon={<PersonOutlineIcon />}>
|
||||
{task.operator.username}
|
||||
</TaskMetaItem>
|
||||
);
|
||||
case "callback":
|
||||
return (
|
||||
<TaskMetaItem title={"View Callback in separate page"}>
|
||||
<Link underline="hover" target="_blank" href={"/new/callbacks/" + task.callback.display_id}>C-{task.callback.display_id}</Link>
|
||||
</TaskMetaItem>
|
||||
);
|
||||
case "host":
|
||||
return (
|
||||
<TaskMetaItem title={"Host"} icon={<ComputerIcon />}>
|
||||
{task.callback.host}
|
||||
</TaskMetaItem>
|
||||
);
|
||||
case "ip":
|
||||
return ipValue !== "" ? (
|
||||
<TaskMetaItem title={"IP address"} icon={<PublicIcon />}>
|
||||
{ipValue}
|
||||
</TaskMetaItem>
|
||||
) : null;
|
||||
case "groups":
|
||||
return callbackGroups !== "" ? (
|
||||
<TaskMetaItem title={"Callback groups"}>
|
||||
{callbackGroups}
|
||||
</TaskMetaItem>
|
||||
) : null;
|
||||
case "payload_type":
|
||||
return task?.command?.payloadtype?.name ? (
|
||||
<TaskMetaItem title={"Payload type"}>
|
||||
{task.command.payloadtype.name}
|
||||
</TaskMetaItem>
|
||||
) : null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const TaskMetaItem = ({children, icon, title, style}) => {
|
||||
const item = (
|
||||
<span className={classes.taskMetaItem} style={style}>
|
||||
@@ -450,18 +510,13 @@ const ColoredTaskDisplay = ({task, theme, children, expanded}) => {
|
||||
}
|
||||
export const ColoredTaskLabel = ({task, theme, me, taskDivID, onClick, displayChildren, toggleDisplayChildren, expanded, compact=false }) => {
|
||||
const [displayComment, setDisplayComment] = React.useState(false);
|
||||
const initialHideUsernameValue = GetMythicSetting({setting_name: "hideUsernames", default_value: operatorSettingDefaults.hideUsernames});
|
||||
const initialShowIPValue = GetMythicSetting({setting_name: "showIP", default_value: operatorSettingDefaults.showIP});
|
||||
const ipValue = getCallbackPrimaryIP(task);
|
||||
const initialShowHostnameValue = GetMythicSetting({setting_name: "showHostname", default_value: operatorSettingDefaults.showHostname});
|
||||
const initialShowCallbackGroupsValue = GetMythicSetting({setting_name: "showCallbackGroups", default_value: operatorSettingDefaults.showCallbackGroups});
|
||||
const taskingDisplayFields = useTaskingDisplayFields();
|
||||
const initialTaskTimestampDisplayField = GetMythicSetting({setting_name: "taskTimestampDisplayField", default_value: operatorSettingDefaults.taskTimestampDisplayField});
|
||||
const initialShowOPSECBypassUsername = GetMythicSetting({setting_name: "showOPSECBypassUsername", default_value: operatorSettingDefaults.showOPSECBypassUsername});
|
||||
const displayTimestamp = task[initialTaskTimestampDisplayField] ? task[initialTaskTimestampDisplayField] : task.timestamp;
|
||||
const [openKillTaskButton, setOpenKillTaskButton] = React.useState({open: false});
|
||||
const command = task?.command?.cmd || task.command_name;
|
||||
const commandLine = command + " " + task.display_params;
|
||||
const callbackGroups = task.callback.mythictree_groups?.join(', ') || "";
|
||||
const opsecBypassUsers = [
|
||||
task?.opsec_pre_bypass_user?.username,
|
||||
task?.opsec_post_bypass_user?.username,
|
||||
@@ -499,40 +554,15 @@ export const ColoredTaskLabel = ({task, theme, me, taskDivID, onClick, displayCh
|
||||
</div>
|
||||
)}
|
||||
<div className={classes.taskMetaRow} onClick={preventPropagation}>
|
||||
<TaskMetaItem title={"Task timestamp"} icon={<AccessTimeIcon />}>
|
||||
{toLocalTime(displayTimestamp, me?.user?.view_utc_time || false)}
|
||||
</TaskMetaItem>
|
||||
<TaskMetaItem title={"View Task in separate page"} icon={<NumbersIcon />}>
|
||||
<Link underline="hover" target="_blank" href={"/new/task/" + task.display_id}>T-{task.display_id}</Link>
|
||||
</TaskMetaItem>
|
||||
{!initialHideUsernameValue &&
|
||||
<TaskMetaItem title={"Operator"} icon={<PersonOutlineIcon />}>
|
||||
{task.operator.username}
|
||||
</TaskMetaItem>
|
||||
}
|
||||
<TaskMetaItem title={"View Callback in separate page"}>
|
||||
<Link underline="hover" target="_blank" href={"/new/callbacks/" + task.callback.display_id}>C-{task.callback.display_id}</Link>
|
||||
</TaskMetaItem>
|
||||
{initialShowHostnameValue &&
|
||||
<TaskMetaItem title={"Host"} icon={<ComputerIcon />}>
|
||||
{task.callback.host}
|
||||
</TaskMetaItem>
|
||||
}
|
||||
{initialShowIPValue && ipValue !== "" &&
|
||||
<TaskMetaItem title={"IP address"} icon={<PublicIcon />}>
|
||||
{ipValue}
|
||||
</TaskMetaItem>
|
||||
}
|
||||
{initialShowCallbackGroupsValue && callbackGroups !== "" &&
|
||||
<TaskMetaItem title={"Callback groups"}>
|
||||
{callbackGroups}
|
||||
</TaskMetaItem>
|
||||
}
|
||||
{task?.command?.payloadtype?.name &&
|
||||
<TaskMetaItem title={"Payload type"}>
|
||||
{task.command.payloadtype.name}
|
||||
</TaskMetaItem>
|
||||
}
|
||||
{taskingDisplayFields.map((fieldName) => (
|
||||
<TaskingMetadataField
|
||||
displayTimestamp={displayTimestamp}
|
||||
fieldName={fieldName}
|
||||
key={fieldName}
|
||||
me={me}
|
||||
task={task}
|
||||
/>
|
||||
))}
|
||||
{initialShowOPSECBypassUsername && opsecBypassUsers !== "" &&
|
||||
<TaskMetaItem title={"The specified usernames approved OPSEC bypasses for this task"} icon={<LockOpenIcon />}>
|
||||
{opsecBypassUsers}
|
||||
@@ -1155,13 +1185,10 @@ const TaskRowConsole = ({task, filterOptions, me, newlyIssuedTasks, indentLevel}
|
||||
)
|
||||
}
|
||||
export const ColoredTaskLabelConsole = ({task, theme, me, taskDivID, onClick, displayChildren, toggleDisplayChildren, expanded }) => {
|
||||
const initialHideUsernameValue = GetMythicSetting({setting_name: "hideUsernames", default_value: operatorSettingDefaults.hideUsernames});
|
||||
const taskingDisplayFields = useTaskingDisplayFields();
|
||||
const initialTaskTimestampDisplayField = GetMythicSetting({setting_name: "taskTimestampDisplayField", default_value: operatorSettingDefaults.taskTimestampDisplayField});
|
||||
const displayTimestamp = task[initialTaskTimestampDisplayField] ? task[initialTaskTimestampDisplayField] : task.timestamp;
|
||||
const initialShowHostnameValue = GetMythicSetting({setting_name: "showHostname", default_value: operatorSettingDefaults.showHostname});
|
||||
const initialShowIPValue = GetMythicSetting({setting_name: "showIP", default_value: operatorSettingDefaults.showIP});
|
||||
const [openKillTaskButton, setOpenKillTaskButton] = React.useState({open: false});
|
||||
const ipValue = getCallbackPrimaryIP(task);
|
||||
const command = task?.command?.cmd || task.command_name;
|
||||
const commandLine = command + " " + task.display_params;
|
||||
const preventPropagation = (e) => {
|
||||
@@ -1178,27 +1205,16 @@ export const ColoredTaskLabelConsole = ({task, theme, me, taskDivID, onClick, di
|
||||
<ColoredTaskDisplayConsole task={task} theme={theme} expanded={expanded}>
|
||||
<div id={taskDivID} className={classes.taskHeaderBody}>
|
||||
<div className={classes.taskMetaRow} onClick={preventPropagation}>
|
||||
<TaskMetaItem title={"Task timestamp"} icon={<AccessTimeIcon />}>
|
||||
{toLocalTimeShort(displayTimestamp, me?.user?.view_utc_time || false)}
|
||||
</TaskMetaItem>
|
||||
{!initialHideUsernameValue &&
|
||||
<TaskMetaItem title={"Operator"} icon={<PersonOutlineIcon />}>
|
||||
{task.operator.username}
|
||||
</TaskMetaItem>
|
||||
}
|
||||
<TaskMetaItem title={"View Task in separate page"} icon={<NumbersIcon />}>
|
||||
<Link underline="hover" target="_blank" href={"/new/task/" + task.display_id}>T-{task.display_id}</Link>
|
||||
</TaskMetaItem>
|
||||
{initialShowHostnameValue &&
|
||||
<TaskMetaItem title={"Host"} icon={<ComputerIcon />}>
|
||||
{task.callback.host}
|
||||
</TaskMetaItem>
|
||||
}
|
||||
{initialShowIPValue && ipValue !== "" &&
|
||||
<TaskMetaItem title={"IP address"} icon={<PublicIcon />}>
|
||||
{ipValue}
|
||||
</TaskMetaItem>
|
||||
}
|
||||
{taskingDisplayFields.map((fieldName) => (
|
||||
<TaskingMetadataField
|
||||
compactTimestamp
|
||||
displayTimestamp={displayTimestamp}
|
||||
fieldName={fieldName}
|
||||
key={fieldName}
|
||||
me={me}
|
||||
task={task}
|
||||
/>
|
||||
))}
|
||||
{(task.opsec_pre_blocked && !task.opsec_pre_bypassed) &&
|
||||
<TaskMetaItem title={"OPSEC blocked before tasking"} icon={<WarningAmberIcon />}>
|
||||
OPSEC pre
|
||||
|
||||
@@ -138,7 +138,8 @@ export const TaskDisplayContainerFlat = ({task, me}) => {
|
||||
}, [searchOutput]);
|
||||
|
||||
return (
|
||||
<div style={{ height: "100%", position: "relative", display: "flex", overflowY: "auto", justifyContent: "flex-end" }}>
|
||||
<div style={{height: "100%", minWidth: 0, overflow: "hidden", position: "relative", display: "flex", width: "100%"}}>
|
||||
<div style={{display: "flex", flex: "1 1 auto", height: "100%", minWidth: 0, overflow: "hidden"}}>
|
||||
<ResponseDisplay
|
||||
key={task.id}
|
||||
task={task}
|
||||
@@ -150,6 +151,8 @@ export const TaskDisplayContainerFlat = ({task, me}) => {
|
||||
expand={true}
|
||||
responseRef={responseRef}
|
||||
/>
|
||||
</div>
|
||||
<div style={{flex: "0 0 30px", height: "100%", minWidth: "30px", overflow: "hidden", zIndex: 2}}>
|
||||
<SideDisplayGeneric toggleViewBrowserScript={toggleViewBrowserScript}
|
||||
toggleSelectAllOutput={toggleSelectAllOutput}
|
||||
toggleOpenSearch={toggleOpenSearch}
|
||||
@@ -159,6 +162,7 @@ export const TaskDisplayContainerFlat = ({task, me}) => {
|
||||
style={{position: "absolute", bottom: "15px", zIndex: 2}}
|
||||
fabStyle={{ }}
|
||||
viewAllOutput={selectAllOutput}/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
)
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import React from 'react';
|
||||
import Button from '@mui/material/Button';
|
||||
import DialogActions from '@mui/material/DialogActions';
|
||||
import DialogContent from '@mui/material/DialogContent';
|
||||
import DialogTitle from '@mui/material/DialogTitle';
|
||||
import MythicTextField from '../../MythicComponents/MythicTextField';
|
||||
import TableRow from '@mui/material/TableRow';
|
||||
import Switch from '@mui/material/Switch';
|
||||
import Box from '@mui/material/Box';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import Link from '@mui/material/Link';
|
||||
@@ -15,11 +17,11 @@ import {GetMythicSetting, useSetMythicSetting} from "../../MythicComponents/Myth
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import Select from '@mui/material/Select';
|
||||
import Input from '@mui/material/Input';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import MythicStyledTableCell from "../../MythicComponents/MythicTableCell";
|
||||
import {operatorSettingDefaults, taskingContextFieldsOptions, taskTimestampDisplayFieldOptions} from "../../../cache";
|
||||
import {normalizeTaskingDisplayFields, operatorSettingDefaults, taskingContextFieldsOptions, taskingDisplayFieldOptions, taskTimestampDisplayFieldOptions} from "../../../cache";
|
||||
import CloudDownloadIcon from '@mui/icons-material/CloudDownload';
|
||||
import CloudUploadIcon from '@mui/icons-material/CloudUpload';
|
||||
import DragHandleIcon from '@mui/icons-material/DragHandle';
|
||||
import {MythicStyledTooltip} from "../../MythicComponents/MythicStyledTooltip";
|
||||
import {snackActions} from "../../utilities/Snackbar";
|
||||
import {userSettingsQuery} from "../../App";
|
||||
@@ -28,6 +30,21 @@ import {useLazyQuery } from '@apollo/client';
|
||||
import PhoneCallbackIcon from '@mui/icons-material/PhoneCallback';
|
||||
import ColorLensIcon from '@mui/icons-material/ColorLens';
|
||||
import {MythicColorSwatchInput} from "../../MythicComponents/MythicColorInput";
|
||||
import VisibilityOffIcon from '@mui/icons-material/VisibilityOff';
|
||||
import VisibilityIcon from '@mui/icons-material/Visibility';
|
||||
import {MythicDialog} from "../../MythicComponents/MythicDialog";
|
||||
import {
|
||||
MythicDialogBody,
|
||||
MythicDialogButton,
|
||||
MythicDialogFooter,
|
||||
MythicDialogSection,
|
||||
} from "../../MythicComponents/MythicDialogLayout";
|
||||
import {reorder} from "../../MythicComponents/MythicDraggableList";
|
||||
import {
|
||||
Draggable,
|
||||
DragDropContext,
|
||||
Droppable,
|
||||
} from "@hello-pangea/dnd";
|
||||
|
||||
const interactTypeOptions = [
|
||||
{value: "interact", display: "Accordions"},
|
||||
@@ -39,6 +56,200 @@ const commonFontFamilies = [
|
||||
"-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,\"Helvetica Neue\",Arial,sans-serif,\"Apple Color Emoji\",\"Segoe UI Emoji\",\"Segoe UI Symbol\"",
|
||||
"Monaco"
|
||||
];
|
||||
const getTaskingDisplayOption = (fieldName) => {
|
||||
return taskingDisplayFieldOptions.find((option) => option.name === fieldName);
|
||||
}
|
||||
const getTaskingDisplayLayoutItems = (visibleFields) => {
|
||||
const visibleSet = new Set(visibleFields);
|
||||
const orderedVisibleItems = visibleFields.reduce((prev, fieldName) => {
|
||||
const option = getTaskingDisplayOption(fieldName);
|
||||
if(option){
|
||||
return [...prev, {...option, visible: true}];
|
||||
}
|
||||
return prev;
|
||||
}, []);
|
||||
const hiddenItems = taskingDisplayFieldOptions.reduce((prev, option) => {
|
||||
if(visibleSet.has(option.name)){
|
||||
return prev;
|
||||
}
|
||||
return [...prev, {...option, visible: false}];
|
||||
}, []);
|
||||
return [...orderedVisibleItems, ...hiddenItems];
|
||||
}
|
||||
const TaskingMetadataSummary = ({value, onChange}) => {
|
||||
const [openLayoutDialog, setOpenLayoutDialog] = React.useState(false);
|
||||
const selectedOptions = value.map(getTaskingDisplayOption).filter(Boolean);
|
||||
const hiddenCount = taskingDisplayFieldOptions.length - selectedOptions.length;
|
||||
const onSubmitLayout = (items) => {
|
||||
onChange(normalizeTaskingDisplayFields(items.reduce((prev, item) => {
|
||||
if(item.visible){
|
||||
return [...prev, item.name];
|
||||
}
|
||||
return prev;
|
||||
}, [])));
|
||||
setOpenLayoutDialog(false);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Box className="mythic-tasking-visibility-panel mythic-tasking-visibility-summary-panel">
|
||||
<Box>
|
||||
<Typography component="div" className="mythic-tasking-visibility-title">
|
||||
Tasking metadata
|
||||
</Typography>
|
||||
<Typography component="div" className="mythic-tasking-visibility-description">
|
||||
Selected chips appear in this order above task commands.
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box className="mythic-tasking-visibility-summary-actions">
|
||||
<Typography component="div" className="mythic-tasking-visibility-count">
|
||||
{selectedOptions.length} shown{hiddenCount > 0 ? `, ${hiddenCount} hidden` : ""}
|
||||
</Typography>
|
||||
<Button
|
||||
className="mythic-dialog-title-action mythic-tasking-visibility-manage-button"
|
||||
onClick={() => setOpenLayoutDialog(true)}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
>
|
||||
Manage
|
||||
</Button>
|
||||
</Box>
|
||||
<Box className="mythic-tasking-visibility-chip-row">
|
||||
{selectedOptions.length > 0 ? (
|
||||
selectedOptions.map((option, index) => (
|
||||
<span className="mythic-tasking-visibility-chip" key={option.name}>
|
||||
<span className="mythic-tasking-visibility-chip-index">{index + 1}</span>
|
||||
{option.display}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<Typography component="div" className="mythic-tasking-visibility-empty">
|
||||
No tasking metadata selected.
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
{openLayoutDialog &&
|
||||
<MythicDialog
|
||||
open={openLayoutDialog}
|
||||
onClose={() => setOpenLayoutDialog(false)}
|
||||
maxWidth="sm"
|
||||
innerDialog={
|
||||
<TaskingMetadataLayoutDialog
|
||||
initialItems={getTaskingDisplayLayoutItems(value)}
|
||||
onClose={() => setOpenLayoutDialog(false)}
|
||||
onReset={() => onSubmitLayout(getTaskingDisplayLayoutItems(operatorSettingDefaults.taskingDisplayFields))}
|
||||
onSubmit={onSubmitLayout}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
)
|
||||
}
|
||||
const TaskingMetadataLayoutDialog = ({initialItems, onClose, onReset, onSubmit}) => {
|
||||
const [items, setItems] = React.useState(initialItems);
|
||||
const onDragEnd = ({destination, source}) => {
|
||||
if(!destination){
|
||||
return;
|
||||
}
|
||||
setItems(reorder(items, source.index, destination.index));
|
||||
}
|
||||
const onToggleVisibility = (index) => {
|
||||
setItems(items.map((item, itemIndex) => {
|
||||
if(itemIndex === index){
|
||||
return {...item, visible: !item.visible};
|
||||
}
|
||||
return item;
|
||||
}));
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<DialogTitle id="form-dialog-title">Tasking Metadata Layout</DialogTitle>
|
||||
<DialogContent dividers={true} sx={{p: 0}}>
|
||||
<MythicDialogBody sx={{height: "min(70vh, 38rem)", p: 1}}>
|
||||
<MythicDialogSection
|
||||
title="Metadata Chips"
|
||||
description="Drag to set display order. Toggle visibility to choose what appears in tasking."
|
||||
sx={{display: "flex", flexDirection: "column", flex: "1 1 auto", minHeight: 0}}
|
||||
>
|
||||
<TaskingMetadataDraggableList
|
||||
items={items}
|
||||
onDragEnd={onDragEnd}
|
||||
onToggleVisibility={onToggleVisibility}
|
||||
/>
|
||||
</MythicDialogSection>
|
||||
</MythicDialogBody>
|
||||
</DialogContent>
|
||||
<MythicDialogFooter>
|
||||
<MythicDialogButton onClick={onClose}>
|
||||
Cancel
|
||||
</MythicDialogButton>
|
||||
<MythicDialogButton onClick={onReset} intent="warning">
|
||||
Reset
|
||||
</MythicDialogButton>
|
||||
<MythicDialogButton onClick={() => onSubmit(items)} intent="primary">
|
||||
Save
|
||||
</MythicDialogButton>
|
||||
</MythicDialogFooter>
|
||||
</>
|
||||
)
|
||||
}
|
||||
const TaskingMetadataDraggableList = ({items, onDragEnd, onToggleVisibility}) => {
|
||||
return (
|
||||
<DragDropContext onDragEnd={onDragEnd}>
|
||||
<Droppable droppableId="tasking-metadata-layout-list">
|
||||
{(provided) => (
|
||||
<div className="mythic-reorder-list" ref={provided.innerRef} {...provided.droppableProps}>
|
||||
{items.map((item, index) => (
|
||||
<TaskingMetadataDraggableListItem
|
||||
item={item}
|
||||
index={index}
|
||||
key={item.name}
|
||||
onToggleVisibility={onToggleVisibility}
|
||||
/>
|
||||
))}
|
||||
{provided.placeholder}
|
||||
</div>
|
||||
)}
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
)
|
||||
}
|
||||
const TaskingMetadataDraggableListItem = ({item, index, onToggleVisibility}) => {
|
||||
return (
|
||||
<Draggable draggableId={item.name} index={index}>
|
||||
{(provided, snapshot) => (
|
||||
<div
|
||||
ref={provided.innerRef}
|
||||
className={`mythic-reorder-row mythic-tasking-metadata-row${snapshot.isDragging ? " mythic-reorder-row-dragging" : ""}${item.visible ? "" : " mythic-reorder-row-disabled"}`}
|
||||
{...provided.draggableProps}
|
||||
>
|
||||
<span className="mythic-reorder-drag-handle" {...provided.dragHandleProps}>
|
||||
<DragHandleIcon fontSize="small" />
|
||||
</span>
|
||||
<div className="mythic-reorder-row-main">
|
||||
<span className="mythic-reorder-row-title">{item.display}</span>
|
||||
<span className="mythic-reorder-row-description">{item.description}</span>
|
||||
</div>
|
||||
<div className="mythic-reorder-row-actions">
|
||||
<IconButton
|
||||
aria-label={item.visible ? `Hide ${item.display}` : `Show ${item.display}`}
|
||||
className={`mythic-table-row-icon-action ${item.visible ? "mythic-table-row-icon-action-hover-danger" : "mythic-table-row-icon-action-hover-info"}`}
|
||||
size="small"
|
||||
onClick={() => onToggleVisibility(index)}
|
||||
>
|
||||
{item.visible ? (
|
||||
<VisibilityIcon fontSize="small" />
|
||||
) : (
|
||||
<VisibilityOffIcon fontSize="small" />
|
||||
)}
|
||||
</IconButton>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Draggable>
|
||||
)
|
||||
}
|
||||
const isValidColor = (color) =>{
|
||||
if(typeof color !== "string"){
|
||||
return false;
|
||||
@@ -502,17 +713,8 @@ export function SettingsOperatorUIConfigDialog(props) {
|
||||
const initialShowMediaValue = GetMythicSetting({setting_name: "showMedia", default_value: operatorSettingDefaults.showMedia});
|
||||
const [showMedia, setShowMedia] = React.useState(initialShowMediaValue);
|
||||
|
||||
const initialHideUsernameValue = GetMythicSetting({setting_name: "hideUsernames", default_value: operatorSettingDefaults.hideUsernames});
|
||||
const [hideUsernames, setHideUsernames] = React.useState(initialHideUsernameValue);
|
||||
|
||||
const initialShowIPValue = GetMythicSetting({setting_name: "showIP", default_value: operatorSettingDefaults.showIP});
|
||||
const [showIP, setShowIP] = React.useState(initialShowIPValue);
|
||||
|
||||
const initialShowHostnameValue = GetMythicSetting({setting_name: "showHostname", default_value: operatorSettingDefaults.showHostname});
|
||||
const [showHostname, setShowHostname] = React.useState(initialShowHostnameValue);
|
||||
|
||||
const initialShowCallbackGroupsValue = GetMythicSetting({setting_name: "showCallbackGroups", default_value: operatorSettingDefaults.showCallbackGroups});
|
||||
const [showCallbackGroups, setShowCallbackGroups] = React.useState(initialShowCallbackGroupsValue);
|
||||
const initialTaskingDisplayFields = normalizeTaskingDisplayFields(GetMythicSetting({setting_name: "taskingDisplayFields", default_value: operatorSettingDefaults.taskingDisplayFields}));
|
||||
const [taskingDisplayFields, setTaskingDisplayFields] = React.useState(initialTaskingDisplayFields);
|
||||
|
||||
const initialUseDisplayParamsForCLIHistory = GetMythicSetting({setting_name: "useDisplayParamsForCLIHistory", default_value: operatorSettingDefaults.useDisplayParamsForCLIHistory});
|
||||
const [useDisplayParamsForCLIHistory, setUseDisplayParamsForCLIHistory] = React.useState(initialUseDisplayParamsForCLIHistory);
|
||||
@@ -652,12 +854,6 @@ export function SettingsOperatorUIConfigDialog(props) {
|
||||
const onChangeFontFamily = (name, value, error) => {
|
||||
setFontFamily(value);
|
||||
}
|
||||
const onHideUsernamesChanged = (evt) => {
|
||||
setHideUsernames(!hideUsernames);
|
||||
}
|
||||
const onShowIPChanged = (evt) => {
|
||||
setShowIP(!showIP);
|
||||
}
|
||||
const onHideBrowserTaskingChanged = (evt) => {
|
||||
setHideBrowserTasking(!hideBrowserTasking);
|
||||
}
|
||||
@@ -667,12 +863,6 @@ export function SettingsOperatorUIConfigDialog(props) {
|
||||
const onHideTaskingContextChanged = (evt) => {
|
||||
setHideTaskingContext(!hideTaskingContext);
|
||||
}
|
||||
const onShowHostnameChanged = (evt) => {
|
||||
setShowHostname(!showHostname);
|
||||
}
|
||||
const onShowCallbackGroupsChanged = (evt) => {
|
||||
setShowCallbackGroups(!showCallbackGroups);
|
||||
}
|
||||
const onShowMediaChanged = (evt) => {
|
||||
setShowMedia(!showMedia);
|
||||
}
|
||||
@@ -703,10 +893,7 @@ export function SettingsOperatorUIConfigDialog(props) {
|
||||
}))
|
||||
}
|
||||
updateSettings({settings: {
|
||||
hideUsernames,
|
||||
showIP,
|
||||
showHostname,
|
||||
showCallbackGroups,
|
||||
taskingDisplayFields: normalizeTaskingDisplayFields(taskingDisplayFields),
|
||||
fontSize: parseInt(fontSize),
|
||||
fontFamily,
|
||||
showMedia,
|
||||
@@ -730,10 +917,7 @@ export function SettingsOperatorUIConfigDialog(props) {
|
||||
const setDefaults = () => {
|
||||
setFontSize(operatorSettingDefaults.fontSize);
|
||||
setFontFamily(operatorSettingDefaults.fontFamily);
|
||||
setHideUsernames(operatorSettingDefaults.hideUsernames);
|
||||
setShowIP(operatorSettingDefaults.showIP);
|
||||
setShowHostname(operatorSettingDefaults.showHostname);
|
||||
setShowCallbackGroups(operatorSettingDefaults.showCallbackGroups);
|
||||
setTaskingDisplayFields(operatorSettingDefaults.taskingDisplayFields);
|
||||
setShowMedia(operatorSettingDefaults.showMedia);
|
||||
setInteractType(operatorSettingDefaults.interactType);
|
||||
setUseDisplayParamsForCLIHistory(operatorSettingDefaults.useDisplayParamsForCLIHistory);
|
||||
@@ -762,10 +946,7 @@ export function SettingsOperatorUIConfigDialog(props) {
|
||||
try{
|
||||
let jsonData = JSON.parse(String(contents));
|
||||
let currentSettings = {
|
||||
hideUsernames,
|
||||
showIP,
|
||||
showHostname,
|
||||
showCallbackGroups,
|
||||
taskingDisplayFields,
|
||||
fontSize: parseInt(fontSize),
|
||||
fontFamily,
|
||||
showMedia,
|
||||
@@ -778,7 +959,9 @@ export function SettingsOperatorUIConfigDialog(props) {
|
||||
showOPSECBypassUsername,
|
||||
palette: palette
|
||||
}
|
||||
updateSettings({settings: {...currentSettings, ...jsonData}});
|
||||
const mergedSettings = {...currentSettings, ...jsonData};
|
||||
mergedSettings.taskingDisplayFields = normalizeTaskingDisplayFields(mergedSettings.taskingDisplayFields);
|
||||
updateSettings({settings: mergedSettings});
|
||||
snackActions.info("Updating settings");
|
||||
props.onClose();
|
||||
}catch(error){
|
||||
@@ -818,29 +1001,54 @@ export function SettingsOperatorUIConfigDialog(props) {
|
||||
return (
|
||||
<React.Fragment>
|
||||
<DialogTitle id="form-dialog-title">
|
||||
Configure UI Settings
|
||||
<div style={{float: "right", display: "flex", alignItems: "center"}}>
|
||||
<MythicStyledTooltip title={"Export Preferences"} tooltipStyle={{float: "right"}}>
|
||||
<IconButton onClick={getCurrentPreferences}>
|
||||
<CloudDownloadIcon />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Export Only Color Preferences"} tooltipStyle={{float: "right"}}>
|
||||
<IconButton onClick={getCurrentColorPreferences}>
|
||||
<CloudDownloadIcon fontSize={"medium"} />
|
||||
<ColorLensIcon color={"success"} fontSize={"small"} style={{marginLeft: "-8px", marginTop: "7px"}} />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Import Preferences"} tooltipStyle={{float: "right"}}>
|
||||
<IconButton onClick={()=>fileInputRef.current.click()} >
|
||||
<CloudUploadIcon color={"success"}/>
|
||||
<input ref={fileInputRef} onChange={onFileChange} type="file" hidden />
|
||||
</IconButton>
|
||||
</MythicStyledTooltip>
|
||||
</div>
|
||||
<Typography variant={"body2"}>
|
||||
Community themes are located on <Link target={"_blank"} href={"https://github.com/MythicMeta/CommunityThemes"}>GitHub</Link>
|
||||
</Typography>
|
||||
<Box className="mythic-dialog-title-row mythic-ui-settings-title-row">
|
||||
<Box className="mythic-ui-settings-title-copy">
|
||||
<Typography component="div" className="mythic-ui-settings-title">
|
||||
Configure UI Settings
|
||||
</Typography>
|
||||
<Typography variant={"body2"} className="mythic-ui-settings-subtitle">
|
||||
Community themes are located on <Link target={"_blank"} href={"https://github.com/MythicMeta/CommunityThemes"}>GitHub</Link>
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box className="mythic-ui-settings-title-actions">
|
||||
<MythicStyledTooltip title={"Copy all preferences as JSON"}>
|
||||
<Button
|
||||
className="mythic-dialog-title-action mythic-ui-settings-title-button mythic-ui-settings-title-button-info"
|
||||
onClick={getCurrentPreferences}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
startIcon={<CloudDownloadIcon fontSize="small" />}
|
||||
>
|
||||
Export
|
||||
</Button>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Copy only color preferences as JSON"}>
|
||||
<Button
|
||||
className="mythic-dialog-title-action mythic-ui-settings-title-button mythic-ui-settings-title-button-info"
|
||||
onClick={getCurrentColorPreferences}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
startIcon={
|
||||
<ColorLensIcon fontSize="small" />
|
||||
}
|
||||
>
|
||||
Export Colors
|
||||
</Button>
|
||||
</MythicStyledTooltip>
|
||||
<MythicStyledTooltip title={"Import preferences from a JSON file"}>
|
||||
<Button
|
||||
className="mythic-dialog-title-action mythic-ui-settings-title-button mythic-ui-settings-title-button-success"
|
||||
onClick={()=>fileInputRef.current.click()}
|
||||
size="small"
|
||||
variant="outlined"
|
||||
startIcon={<CloudUploadIcon fontSize="small" />}
|
||||
>
|
||||
Import
|
||||
<input ref={fileInputRef} onChange={onFileChange} type="file" hidden />
|
||||
</Button>
|
||||
</MythicStyledTooltip>
|
||||
</Box>
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
<TableContainer className="mythicElement" style={{paddingLeft: "10px", paddingRight: "10px"}}>
|
||||
<Table size="small" style={{ "maxWidth": "100%", "overflow": "scroll"}}>
|
||||
@@ -862,52 +1070,9 @@ export function SettingsOperatorUIConfigDialog(props) {
|
||||
</Select>
|
||||
</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<MythicStyledTableCell>Hide Usernames In Tasking</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<Switch
|
||||
checked={hideUsernames}
|
||||
onChange={onHideUsernamesChanged}
|
||||
color="info"
|
||||
inputProps={{ 'aria-label': 'info checkbox' }}
|
||||
name="hide_usernames"
|
||||
/>
|
||||
</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<MythicStyledTableCell>Show Callback IP In Tasking</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<Switch
|
||||
checked={showIP}
|
||||
onChange={onShowIPChanged}
|
||||
color="info"
|
||||
inputProps={{ 'aria-label': 'info checkbox' }}
|
||||
name="show_ip"
|
||||
/>
|
||||
</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<MythicStyledTableCell>Show Callback Hostname In Tasking</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<Switch
|
||||
checked={showHostname}
|
||||
onChange={onShowHostnameChanged}
|
||||
color="info"
|
||||
inputProps={{ 'aria-label': 'info checkbox' }}
|
||||
name="show_hostname"
|
||||
/>
|
||||
</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
<MythicStyledTableCell>Show Callback Groups In Tasking</MythicStyledTableCell>
|
||||
<MythicStyledTableCell>
|
||||
<Switch
|
||||
checked={showCallbackGroups}
|
||||
onChange={onShowCallbackGroupsChanged}
|
||||
color="info"
|
||||
inputProps={{ 'aria-label': 'info checkbox' }}
|
||||
name="show_callback_groups"
|
||||
/>
|
||||
<TableRow>
|
||||
<MythicStyledTableCell colSpan={2} style={{paddingTop: "16px", paddingBottom: "16px"}}>
|
||||
<TaskingMetadataSummary value={taskingDisplayFields} onChange={setTaskingDisplayFields} />
|
||||
</MythicStyledTableCell>
|
||||
</TableRow>
|
||||
<TableRow hover>
|
||||
|
||||
@@ -1,25 +1,35 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
|
||||
const useSingleAndDoubleClick = (onSingleClick, onDoubleClick, delay = 250) => {
|
||||
const [click, setClick] = useState(0);
|
||||
const onSingleClickRef = useRef(onSingleClick);
|
||||
const onDoubleClickRef = useRef(onDoubleClick);
|
||||
|
||||
useEffect(() => {
|
||||
onSingleClickRef.current = onSingleClick;
|
||||
onDoubleClickRef.current = onDoubleClick;
|
||||
}, [onSingleClick, onDoubleClick]);
|
||||
|
||||
useEffect(() => {
|
||||
if(click === 0){
|
||||
return;
|
||||
}
|
||||
const timer = setTimeout(() => {
|
||||
// simple click
|
||||
if (click === 1) onSingleClick();
|
||||
if (click === 1) onSingleClickRef.current();
|
||||
setClick(0);
|
||||
}, delay);
|
||||
|
||||
// the duration between this click and the previous one
|
||||
// is less than the value of delay = double-click
|
||||
if (click === 2){
|
||||
onDoubleClick();
|
||||
setClick(0);
|
||||
onDoubleClickRef.current();
|
||||
}
|
||||
return () => clearTimeout(timer);
|
||||
}, [delay, onSingleClick, onDoubleClick, click]);
|
||||
}, [delay, click]);
|
||||
|
||||
return () => setClick((prev) => prev + 1);
|
||||
return useCallback(() => setClick((prev) => prev + 1), []);
|
||||
};
|
||||
|
||||
export default useSingleAndDoubleClick;
|
||||
|
||||
@@ -227,7 +227,133 @@ tspan {
|
||||
.selectedTask {
|
||||
background-color: ${(props) => props.theme.selectedCallbackColor + "DD"} !important;
|
||||
}
|
||||
.mythic-file-browser-tableTop {
|
||||
background-color: ${(props) => alpha(props.theme.palette.background.paper, props.theme.palette.mode === "dark" ? 0.96 : 0.98)};
|
||||
border-bottom: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
|
||||
padding: 0.45rem 0.55rem 0.4rem;
|
||||
}
|
||||
.mythic-file-browser-pathInput.MuiInputBase-root {
|
||||
background-color: ${(props) => props.theme.palette.background.paper};
|
||||
}
|
||||
.mythic-file-browser-pathInput.MuiInputBase-root.MuiOutlinedInput-adornedStart {
|
||||
padding-left: 0.22rem;
|
||||
}
|
||||
.mythic-file-browser-pathInput.MuiInputBase-root.MuiOutlinedInput-adornedEnd {
|
||||
padding-right: 0.22rem;
|
||||
}
|
||||
.mythic-file-browser-pathInput .MuiInputBase-input {
|
||||
min-width: 8rem;
|
||||
padding-bottom: 10px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
.mythic-file-browser-toolbarGroup {
|
||||
align-items: center;
|
||||
background-color: ${(props) => alpha(props.theme.palette.text.primary, props.theme.palette.mode === "dark" ? 0.045 : 0.035)};
|
||||
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;
|
||||
gap: 0.15rem;
|
||||
min-height: 32px;
|
||||
min-width: 0;
|
||||
padding: 0.12rem;
|
||||
}
|
||||
.mythic-file-browser-toolbarGroupStart {
|
||||
margin-right: 0.45rem;
|
||||
}
|
||||
.mythic-file-browser-toolbarGroupEnd {
|
||||
margin-left: 0.45rem;
|
||||
}
|
||||
.mythic-file-browser-tokenSelect {
|
||||
max-width: 12rem;
|
||||
min-width: 8.5rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
.mythic-file-browser-tokenSelect .MuiInputBase-root,
|
||||
.mythic-file-browser-tokenSelect .MuiSelect-select {
|
||||
font-size: 0.78rem;
|
||||
min-height: 0;
|
||||
}
|
||||
.mythic-file-browser-iconButton.MuiIconButton-root {
|
||||
border-radius: ${(props) => props.theme.shape.borderRadius}px;
|
||||
color: ${(props) => props.theme.palette.text.secondary};
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
transition: background-color 120ms ease, color 120ms ease, opacity 120ms ease;
|
||||
width: 28px;
|
||||
}
|
||||
.mythic-file-browser-iconButton.MuiIconButton-root:hover {
|
||||
background-color: ${(props) => alpha(props.theme.palette.text.primary, props.theme.palette.mode === "dark" ? 0.09 : 0.075)};
|
||||
color: ${(props) => props.theme.palette.text.primary};
|
||||
}
|
||||
.mythic-file-browser-iconButton.MuiIconButton-root.Mui-disabled {
|
||||
opacity: 0.42;
|
||||
}
|
||||
.mythic-file-browser-iconButton.mythic-file-browser-hoverInfo:hover {
|
||||
background-color: ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.18 : 0.1)};
|
||||
color: ${(props) => props.theme.palette.info.main};
|
||||
}
|
||||
.mythic-file-browser-iconButton.mythic-file-browser-hoverSuccess:hover {
|
||||
background-color: ${(props) => alpha(props.theme.palette.success.main, props.theme.palette.mode === "dark" ? 0.18 : 0.1)};
|
||||
color: ${(props) => props.theme.palette.success.main};
|
||||
}
|
||||
.mythic-file-browser-iconButton.mythic-file-browser-hoverWarning:hover {
|
||||
background-color: ${(props) => alpha(props.theme.palette.warning.main, props.theme.palette.mode === "dark" ? 0.2 : 0.12)};
|
||||
color: ${(props) => props.theme.palette.warning.main};
|
||||
}
|
||||
.mythic-file-browser-iconButton.mythic-file-browser-hoverError:hover {
|
||||
background-color: ${(props) => alpha(props.theme.palette.error.main, props.theme.palette.mode === "dark" ? 0.18 : 0.1)};
|
||||
color: ${(props) => props.theme.palette.error.main};
|
||||
}
|
||||
.mythic-file-browser-iconButton.mythic-file-browser-activeSuccess {
|
||||
background-color: ${(props) => alpha(props.theme.palette.success.main, props.theme.palette.mode === "dark" ? 0.2 : 0.12)};
|
||||
color: ${(props) => props.theme.palette.success.main};
|
||||
}
|
||||
.mythic-file-browser-iconButton.mythic-file-browser-activeWarning {
|
||||
background-color: ${(props) => alpha(props.theme.palette.warning.main, props.theme.palette.mode === "dark" ? 0.22 : 0.14)};
|
||||
color: ${(props) => props.theme.palette.warning.main};
|
||||
}
|
||||
.mythic-file-browser-iconButton.mythic-file-browser-activeError {
|
||||
background-color: ${(props) => alpha(props.theme.palette.error.main, props.theme.palette.mode === "dark" ? 0.2 : 0.12)};
|
||||
color: ${(props) => props.theme.palette.error.main};
|
||||
}
|
||||
.mythic-file-browser-iconButtonCompound.MuiIconButton-root {
|
||||
gap: 0;
|
||||
padding: 0 0.15rem;
|
||||
width: auto;
|
||||
}
|
||||
.mythic-file-browser-iconButtonCompound .MuiSvgIcon-root + .MuiSvgIcon-root {
|
||||
font-size: 1rem;
|
||||
margin-left: -0.15rem;
|
||||
}
|
||||
.mythic-file-browser-labelButton.MuiIconButton-root {
|
||||
height: 20px;
|
||||
margin: 0 0.1rem;
|
||||
width: 20px;
|
||||
}
|
||||
.mythic-file-browser-labelButton .MuiSvgIcon-root {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
.MythicResizableGrid-root {
|
||||
background-color: ${(props) => props.theme.palette.background.paper};
|
||||
border: 1px solid ${(props) => props.theme.table?.border || 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.035)"};
|
||||
flex: 1 1 auto;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
.MythicResizableGrid-grid {
|
||||
background-color: ${(props) => props.theme.palette.background.paper};
|
||||
outline: none;
|
||||
}
|
||||
.MythicResizableGrid-headerCellRow {
|
||||
background-color: ${(props) => props.theme.tableHeader};
|
||||
box-shadow: inset 0 -1px 0 ${(props) => props.theme.table?.border || props.theme.borderColor};
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
position: sticky;
|
||||
@@ -237,89 +363,296 @@ tspan {
|
||||
z-index: 10;
|
||||
}
|
||||
.MythicResizableGrid-headerCell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
padding: 0 8px;
|
||||
box-sizing: border-box;
|
||||
justify-content: space-between;
|
||||
user-select: none;
|
||||
border-right: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
|
||||
border-bottom: 1px solid ${(props) => props.theme.table?.border || props.theme.borderColor};
|
||||
background-color: ${(props) => props.theme.tableHeader} !important;
|
||||
border-bottom: 1px solid ${(props) => props.theme.table?.border || props.theme.borderColor};
|
||||
border-right: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
|
||||
box-sizing: border-box;
|
||||
color: ${(props) => props.theme.palette.text.primary};
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
font-size: 0.76rem;
|
||||
font-weight: 700;
|
||||
justify-content: space-between;
|
||||
letter-spacing: 0;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
padding: 0 0.45rem 0 0.65rem;
|
||||
position: relative;
|
||||
transition: background-color 120ms ease, color 120ms ease;
|
||||
user-select: none;
|
||||
&:first-child-of-type {
|
||||
border-left: 0;
|
||||
}
|
||||
&:hover {
|
||||
background-color: ${(props) => props.theme.table?.headerHover || props.theme.tableHover};
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.MythicResizableGrid-headerCellNoSort {
|
||||
cursor: default;
|
||||
}
|
||||
.MythicResizableGrid-headerContent {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: 0.4rem;
|
||||
justify-content: space-between;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.MythicResizableGrid-headerCell .MythicResizableGrid-cellInner {
|
||||
font-size: 0.76rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.MythicResizableGrid-headerLabel {
|
||||
color: ${(props) => props.theme.palette.text.primary};
|
||||
display: block;
|
||||
letter-spacing: 0;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.MythicResizableGrid-headerActions {
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
gap: 0.25rem;
|
||||
min-width: 0;
|
||||
}
|
||||
.MythicResizableGrid-headerIndicator {
|
||||
align-items: center;
|
||||
background-color: ${(props) => props.theme.palette.background.paper};
|
||||
border: 1px solid ${(props) => alpha(props.theme.palette.text.primary, props.theme.palette.mode === "dark" ? 0.28 : 0.22)};
|
||||
border-radius: ${(props) => props.theme.shape.borderRadius}px;
|
||||
box-shadow: ${(props) => props.theme.palette.mode === "dark" ? "0 0 0 1px rgba(255,255,255,0.04)" : "0 1px 2px rgba(15, 23, 42, 0.08)"};
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
font-size: 0.95rem;
|
||||
height: 18px;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
width: 18px;
|
||||
}
|
||||
.MythicResizableGrid-headerFilterIcon {
|
||||
background-color: ${(props) => alpha(props.theme.palette.mode === "dark" ? props.theme.palette.info.light : props.theme.palette.info.dark || props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.24 : 0.14)};
|
||||
border-color: ${(props) => alpha(props.theme.palette.mode === "dark" ? props.theme.palette.info.light : props.theme.palette.info.dark || props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.72 : 0.5)};
|
||||
color: ${(props) => props.theme.palette.mode === "dark" ? props.theme.palette.info.light : props.theme.palette.info.dark || props.theme.palette.info.main};
|
||||
}
|
||||
.MythicResizableGrid-headerSortIcon {
|
||||
background-color: ${(props) => alpha(props.theme.palette.mode === "dark" ? props.theme.palette.primary.light : props.theme.palette.primary.dark || props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.24 : 0.14)};
|
||||
border-color: ${(props) => alpha(props.theme.palette.mode === "dark" ? props.theme.palette.primary.light : props.theme.palette.primary.dark || props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.72 : 0.5)};
|
||||
color: ${(props) => props.theme.palette.mode === "dark" ? props.theme.palette.primary.light : props.theme.palette.primary.dark || props.theme.palette.primary.main};
|
||||
}
|
||||
.MythicResizableGrid-headerResizeHandle {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 5px;
|
||||
height: 100%;
|
||||
bottom: 0;
|
||||
cursor: col-resize;
|
||||
user-select: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
touch-action: none;
|
||||
user-select: none;
|
||||
width: 9px;
|
||||
z-index: 2;
|
||||
}
|
||||
.MythicResizableGrid-headerResizeHandle::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
width: 0px;
|
||||
height: 100%;
|
||||
border-left: 1px solid ${(props) => props.theme.palette.text.secondary};
|
||||
border-right: 1px solid ${(props) => props.theme.palette.text.secondary};
|
||||
opacity: 0.55;
|
||||
bottom: 18%;
|
||||
right: 4px;
|
||||
top: 18%;
|
||||
width: 2px;
|
||||
border-radius: 999px;
|
||||
background-color: ${(props) => alpha(props.theme.palette.text.primary, props.theme.palette.mode === "dark" ? 0.5 : 0.38)};
|
||||
opacity: 0.72;
|
||||
pointer-events: none;
|
||||
transition: opacity 120ms ease, background-color 120ms ease;
|
||||
}
|
||||
.MythicResizableGrid-headerResizeHandle::after {
|
||||
content: "";
|
||||
inset: 0 -2px;
|
||||
position: absolute;
|
||||
}
|
||||
.MythicResizableGrid-headerCell:hover .MythicResizableGrid-headerResizeHandle::before {
|
||||
opacity: 1;
|
||||
}
|
||||
.MythicResizableGrid-headerResizeHandle:hover,
|
||||
.MythicResizableGrid-headerResizeHandleActive {
|
||||
background-color: ${(props) => props.theme.palette.info.main + "22"};
|
||||
}
|
||||
.MythicResizableGrid-headerResizeHandle:hover::after,
|
||||
.MythicResizableGrid-headerResizeHandleActive::after {
|
||||
background-color: ${(props) => props.theme.palette.info.main};
|
||||
opacity: 1;
|
||||
background-color: ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.24 : 0.14)};
|
||||
}
|
||||
.MythicResizableGrid-headerResizeHandle:hover::before,
|
||||
.MythicResizableGrid-headerResizeHandleActive::before {
|
||||
border-color: ${(props) => props.theme.palette.info.main};
|
||||
background-color: ${(props) => props.theme.palette.info.main};
|
||||
opacity: 1;
|
||||
}
|
||||
.MythicResizableGrid-hoveredRow {
|
||||
background-color: ${(props) => props.theme.table?.rowHover || props.theme.tableHover + "CC"};
|
||||
.MythicResizableGrid-resizeGuide {
|
||||
background-color: ${(props) => props.theme.palette.info.main};
|
||||
box-shadow: 0 0 0 1px ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.32 : 0.22)},
|
||||
0 0 10px ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.45 : 0.28)};
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
transform: translateX(-1px);
|
||||
width: 2px;
|
||||
z-index: 12;
|
||||
}
|
||||
.MythicResizableGrid-cell {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
background-image: none;
|
||||
background-color: ${(props) => props.theme.palette.background.paper};
|
||||
border-bottom: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
|
||||
border-right: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
|
||||
box-sizing: border-box;
|
||||
color: ${(props) => props.theme.palette.text.primary};
|
||||
cursor: default !important;
|
||||
display: flex;
|
||||
font-family: ${(props) => props.theme.typography.fontFamily};
|
||||
font-size: 0.86rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
border-right: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
|
||||
border-bottom: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
|
||||
cursor: default !important;
|
||||
min-width: 0;
|
||||
padding: 0 0.65rem;
|
||||
position: relative;
|
||||
transition: background-color 120ms ease, background-image 120ms ease, border-color 120ms ease, box-shadow 120ms ease, color 120ms ease;
|
||||
}
|
||||
.MythicResizableGrid-cell.MythicResizableGridRowHighlight {
|
||||
background-color: ${(props) => props.theme.table?.rowStripe || props.theme.tableHover + "66"};
|
||||
}
|
||||
.MythicResizableGrid-cell.MythicResizableGrid-hoveredRow {
|
||||
background-image: linear-gradient(0deg,
|
||||
${(props) => alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.11 : 0.07)},
|
||||
${(props) => alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.11 : 0.07)}) !important;
|
||||
border-bottom-color: ${(props) => alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.3 : 0.2)};
|
||||
color: ${(props) => props.theme.palette.text.primary} !important;
|
||||
}
|
||||
.MythicResizableGrid-cell.MythicResizableGrid-contextRow {
|
||||
background-image: linear-gradient(0deg,
|
||||
${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.18 : 0.1)},
|
||||
${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.18 : 0.1)}) !important;
|
||||
border-bottom-color: ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.52 : 0.34)};
|
||||
box-shadow: inset 0 1px 0 ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.52 : 0.34)};
|
||||
color: ${(props) => props.theme.palette.text.primary} !important;
|
||||
}
|
||||
.MythicResizableGrid-cell.selectedCallback {
|
||||
background-color: ${(props) => props.theme.table?.selected || props.theme.selectedCallbackColor + "CC"} !important;
|
||||
}
|
||||
.MythicResizableGrid-cell.selectedCallbackHierarchy {
|
||||
background-color: ${(props) => props.theme.table?.selectedHierarchy || props.theme.selectedCallbackHierarchyColor + "CC"} !important;
|
||||
}
|
||||
.MythicResizableGrid-cell.MythicResizableGrid-rowInteractive {
|
||||
cursor: pointer !important;
|
||||
}
|
||||
.MythicResizableGrid-cell.MythicResizableGrid-rowFirstCell::before {
|
||||
background-color: ${(props) => alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.72 : 0.58)};
|
||||
border-radius: 999px;
|
||||
bottom: 5px;
|
||||
content: "";
|
||||
left: 3px;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
transition: background-color 120ms ease, opacity 120ms ease, width 120ms ease;
|
||||
width: 3px;
|
||||
}
|
||||
.MythicResizableGrid-cell.MythicResizableGrid-rowFirstCell.MythicResizableGrid-hoveredRow::before {
|
||||
opacity: 0.72;
|
||||
}
|
||||
.MythicResizableGrid-cell.MythicResizableGrid-rowFirstCell.MythicResizableGrid-contextRow::before {
|
||||
background-color: ${(props) => props.theme.palette.info.main};
|
||||
opacity: 1;
|
||||
width: 4px;
|
||||
}
|
||||
.MythicResizableGrid-cell.MythicResizableGrid-rowLastCell {
|
||||
border-right-color: ${(props) => props.theme.table?.border || props.theme.borderColor};
|
||||
}
|
||||
.MythicResizableGrid-cellInner {
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
width: 100%;
|
||||
}
|
||||
.mythic-callback-interactCell {
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
gap: 0.18rem;
|
||||
height: 100%;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
.mythic-callback-iconButton.MuiIconButton-root {
|
||||
border: 1px solid transparent;
|
||||
border-radius: ${(props) => props.theme.shape.borderRadius}px;
|
||||
color: ${(props) => props.theme.palette.text.secondary};
|
||||
flex: 0 0 auto;
|
||||
height: 22px;
|
||||
padding: 0;
|
||||
transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease;
|
||||
width: 22px;
|
||||
}
|
||||
.mythic-callback-iconButton.MuiIconButton-root:hover {
|
||||
background-color: ${(props) => alpha(props.theme.palette.primary.main, props.theme.palette.mode === "dark" ? 0.16 : 0.09)};
|
||||
color: ${(props) => props.theme.palette.primary.main};
|
||||
}
|
||||
.mythic-callback-interactButtonHighIntegrity.MuiIconButton-root {
|
||||
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-callback-interactButtonHighIntegrity.MuiIconButton-root:hover,
|
||||
.mythic-callback-menuButtonHighIntegrity.MuiIconButton-root:hover {
|
||||
background-color: ${(props) => alpha(props.theme.palette.error.main, props.theme.palette.mode === "dark" ? 0.24 : 0.14)};
|
||||
color: ${(props) => props.theme.palette.error.main};
|
||||
}
|
||||
.mythic-callback-menuButton.MuiIconButton-root {
|
||||
margin-left: -0.12rem;
|
||||
}
|
||||
.mythic-callback-menuButtonHighIntegrity.MuiIconButton-root {
|
||||
color: ${(props) => props.theme.palette.error.main};
|
||||
}
|
||||
.mythic-callback-displayId {
|
||||
color: ${(props) => props.theme.palette.text.primary};
|
||||
flex: 0 0 auto;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
max-width: 3.8rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.mythic-callback-lockBadge {
|
||||
align-items: center;
|
||||
background-color: ${(props) => alpha(props.theme.palette.warning.main, props.theme.palette.mode === "dark" ? 0.18 : 0.12)};
|
||||
border: 1px solid ${(props) => alpha(props.theme.palette.warning.main, props.theme.palette.mode === "dark" ? 0.46 : 0.34)};
|
||||
border-radius: ${(props) => props.theme.shape.borderRadius}px;
|
||||
color: ${(props) => props.theme.palette.warning.main};
|
||||
display: inline-flex;
|
||||
flex: 0 1 auto;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 700;
|
||||
gap: 0.12rem;
|
||||
height: 18px;
|
||||
line-height: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
padding: 0 0.28rem;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.mythic-callback-lockBadge span {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.mythic-callback-statusIcon {
|
||||
flex: 0 0 auto;
|
||||
font-size: 0.9rem;
|
||||
margin-left: 0.08rem;
|
||||
}
|
||||
.mythic-callback-statusIconSuccess {
|
||||
color: ${(props) => props.theme.palette.success.main};
|
||||
}
|
||||
.MythicInteractiveTerminal {
|
||||
background-color: ${(props) => props.theme.outputBackgroundColor};
|
||||
@@ -589,6 +922,212 @@ tspan {
|
||||
min-height: 30px;
|
||||
text-transform: none;
|
||||
}
|
||||
.mythic-ui-settings-title-row {
|
||||
align-items: flex-start;
|
||||
}
|
||||
.mythic-ui-settings-title-copy {
|
||||
display: flex;
|
||||
flex: 1 1 22rem;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
min-width: 0;
|
||||
}
|
||||
.mythic-ui-settings-title {
|
||||
color: ${(props) => props.theme.palette.text.primary};
|
||||
font-size: 1.05rem;
|
||||
font-weight: 750;
|
||||
line-height: 1.25;
|
||||
}
|
||||
.mythic-ui-settings-subtitle {
|
||||
color: ${(props) => props.theme.palette.text.secondary};
|
||||
line-height: 1.35;
|
||||
}
|
||||
.mythic-ui-settings-title-actions {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex: 0 1 auto;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.4rem;
|
||||
justify-content: flex-end;
|
||||
min-width: 0;
|
||||
}
|
||||
.mythic-ui-settings-title-button.MuiButton-root {
|
||||
min-height: 31px;
|
||||
min-width: auto;
|
||||
padding: 0.35rem 0.65rem;
|
||||
}
|
||||
.mythic-ui-settings-title-button-info.MuiButton-root:hover {
|
||||
background-color: ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.18 : 0.1)} !important;
|
||||
border-color: ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.58 : 0.42)} !important;
|
||||
color: ${(props) => props.theme.palette.info.main} !important;
|
||||
}
|
||||
.mythic-ui-settings-title-button-success.MuiButton-root:hover {
|
||||
background-color: ${(props) => alpha(props.theme.palette.success.main, props.theme.palette.mode === "dark" ? 0.18 : 0.1)} !important;
|
||||
border-color: ${(props) => alpha(props.theme.palette.success.main, props.theme.palette.mode === "dark" ? 0.58 : 0.42)} !important;
|
||||
color: ${(props) => props.theme.palette.success.main} !important;
|
||||
}
|
||||
.mythic-tasking-visibility-panel {
|
||||
background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.035)" : "rgba(15,23,42,0.025)"};
|
||||
border: 1px solid ${(props) => props.theme.table?.borderSoft || props.theme.borderColor};
|
||||
border-radius: ${(props) => props.theme.shape.borderRadius}px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
padding: 0.8rem;
|
||||
}
|
||||
.mythic-tasking-visibility-summary-panel {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(12rem, 0.85fr) minmax(12rem, 1.15fr);
|
||||
}
|
||||
.mythic-tasking-visibility-header {
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.65rem;
|
||||
justify-content: space-between;
|
||||
min-width: 0;
|
||||
}
|
||||
.mythic-tasking-visibility-title {
|
||||
color: ${(props) => props.theme.palette.text.primary};
|
||||
font-size: 0.92rem;
|
||||
font-weight: 800;
|
||||
line-height: 1.25;
|
||||
}
|
||||
.mythic-tasking-visibility-description {
|
||||
color: ${(props) => props.theme.palette.text.secondary};
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.35;
|
||||
}
|
||||
.mythic-tasking-visibility-count {
|
||||
background-color: ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.16 : 0.1)};
|
||||
border: 1px solid ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.34 : 0.24)};
|
||||
border-radius: ${(props) => props.theme.shape.borderRadius}px;
|
||||
color: ${(props) => props.theme.palette.info.main};
|
||||
font-size: 0.72rem;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
padding: 0.35rem 0.5rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mythic-tasking-visibility-summary-actions {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.45rem;
|
||||
justify-content: flex-end;
|
||||
min-width: 0;
|
||||
}
|
||||
.mythic-tasking-visibility-manage-button.MuiButton-root {
|
||||
min-height: 30px;
|
||||
padding: 0.3rem 0.65rem;
|
||||
}
|
||||
.mythic-tasking-visibility-chip-row {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.35rem;
|
||||
grid-column: 1 / -1;
|
||||
min-width: 0;
|
||||
}
|
||||
.mythic-tasking-visibility-chip {
|
||||
align-items: center;
|
||||
background-color: ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.14 : 0.08)};
|
||||
border: 1px solid ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.38 : 0.24)};
|
||||
border-radius: ${(props) => props.theme.shape.borderRadius}px;
|
||||
color: ${(props) => props.theme.palette.text.primary};
|
||||
display: inline-flex;
|
||||
font-size: 0.76rem;
|
||||
font-weight: 750;
|
||||
gap: 0.35rem;
|
||||
line-height: 1.2;
|
||||
min-height: 28px;
|
||||
min-width: 0;
|
||||
padding: 0.22rem 0.52rem 0.22rem 0.3rem;
|
||||
}
|
||||
.mythic-tasking-visibility-chip-index {
|
||||
align-items: center;
|
||||
background-color: ${(props) => props.theme.palette.info.main};
|
||||
border-radius: ${(props) => Math.max(4, props.theme.shape.borderRadius - 1)}px;
|
||||
color: ${(props) => props.theme.palette.info.contrastText};
|
||||
display: inline-flex;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 850;
|
||||
height: 19px;
|
||||
justify-content: center;
|
||||
min-width: 19px;
|
||||
padding: 0 0.24rem;
|
||||
}
|
||||
.mythic-tasking-visibility-empty {
|
||||
color: ${(props) => props.theme.palette.text.secondary};
|
||||
font-size: 0.78rem;
|
||||
font-style: italic;
|
||||
}
|
||||
.mythic-tasking-visibility-grid {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr));
|
||||
}
|
||||
.mythic-tasking-visibility-option.MuiButton-root {
|
||||
align-items: flex-start;
|
||||
background-color: ${(props) => 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: none;
|
||||
color: ${(props) => props.theme.palette.text.secondary};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
justify-content: flex-start;
|
||||
min-height: 4.2rem;
|
||||
padding: 0.65rem 0.75rem;
|
||||
text-align: left;
|
||||
text-transform: none;
|
||||
}
|
||||
.mythic-tasking-visibility-option.MuiButton-root:hover {
|
||||
background-color: ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.13 : 0.07)};
|
||||
border-color: ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.46 : 0.28)};
|
||||
}
|
||||
.mythic-tasking-visibility-option.MuiButton-root.selected {
|
||||
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.62 : 0.42)};
|
||||
color: ${(props) => props.theme.palette.text.primary};
|
||||
box-shadow: inset 3px 0 0 ${(props) => props.theme.palette.info.main};
|
||||
}
|
||||
.mythic-tasking-visibility-option-title {
|
||||
color: inherit;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 800;
|
||||
line-height: 1.2;
|
||||
}
|
||||
.mythic-tasking-visibility-option-description {
|
||||
color: ${(props) => props.theme.palette.text.secondary};
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
}
|
||||
.mythic-tasking-metadata-row .mythic-reorder-row-main {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: 0.12rem;
|
||||
}
|
||||
.mythic-reorder-row-description {
|
||||
color: ${(props) => props.theme.palette.text.secondary};
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@media (max-width: 760px) {
|
||||
.mythic-tasking-visibility-summary-panel {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.mythic-tasking-visibility-summary-actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
.mythic-dialog-title-select {
|
||||
background-color: ${(props) => props.theme.palette.mode === "dark" ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.035)"};
|
||||
border-radius: ${(props) => props.theme.shape.borderRadius}px;
|
||||
@@ -1018,6 +1557,65 @@ tspan {
|
||||
border-color: ${(props) => props.theme.table?.borderSoft || props.theme.borderColor} !important;
|
||||
color: ${(props) => props.theme.palette.text.disabled} !important;
|
||||
}
|
||||
.mythic-raw-select-dialog-content.MuiDialogContent-root {
|
||||
background-color: ${(props) => props.theme.palette.background.paper};
|
||||
padding: 0;
|
||||
}
|
||||
.mythic-raw-select-list {
|
||||
gap: 0.45rem;
|
||||
max-height: min(55vh, 28rem);
|
||||
overflow-y: auto;
|
||||
padding: 0.6rem;
|
||||
}
|
||||
.mythic-raw-select-row {
|
||||
align-items: center;
|
||||
background-color: ${(props) => props.theme.surfaces?.muted || alpha(props.theme.palette.text.primary, props.theme.palette.mode === "dark" ? 0.045 : 0.035)};
|
||||
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.primary};
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
justify-content: space-between;
|
||||
min-height: 42px;
|
||||
min-width: 0;
|
||||
padding: 0.45rem 0.55rem 0.45rem 0.75rem;
|
||||
transition: background-color 120ms ease, border-color 120ms ease, box-shadow 120ms ease;
|
||||
width: 100%;
|
||||
}
|
||||
.mythic-raw-select-row:hover {
|
||||
background-color: ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.13 : 0.075)};
|
||||
border-color: ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.44 : 0.32)};
|
||||
}
|
||||
.mythic-raw-select-row:focus-visible {
|
||||
border-color: ${(props) => props.theme.palette.info.main};
|
||||
box-shadow: 0 0 0 2px ${(props) => alpha(props.theme.palette.info.main, props.theme.palette.mode === "dark" ? 0.28 : 0.18)};
|
||||
outline: none;
|
||||
}
|
||||
.mythic-raw-select-value.MuiTypography-root {
|
||||
color: ${(props) => props.theme.palette.text.primary};
|
||||
font-size: 0.86rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.mythic-raw-select-action.MuiButton-root {
|
||||
flex: 0 0 auto;
|
||||
min-height: 28px;
|
||||
min-width: 5rem;
|
||||
padding: 0.25rem 0.65rem;
|
||||
}
|
||||
.mythic-raw-select-empty {
|
||||
align-items: center;
|
||||
color: ${(props) => props.theme.palette.text.secondary};
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
min-height: 8rem;
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
.mythic-dialog-grid {
|
||||
align-items: start;
|
||||
display: grid;
|
||||
@@ -1516,9 +2114,15 @@ tspan {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.55rem;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
overflow-x: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
.mythic-browser-script-response > * {
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
.mythic-browser-script-response-expanded {
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
@@ -1645,6 +2249,20 @@ tspan {
|
||||
padding: 0.5rem;
|
||||
width: 100%;
|
||||
}
|
||||
.mythic-response-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
.mythic-response-table-grid {
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
.mythic-browser-scripts-table {
|
||||
max-width: 100%;
|
||||
min-width: 60rem;
|
||||
|
||||
Reference in New Issue
Block a user