Skip to content

Commit 11c52dc

Browse files
committed
WIP: ui: Enable mixed-state behavior for master checkbox in AdavncedMultiSelect
The AdvancedMultiSelect should adhere to some set of human interface guidelines. In the absence of a formal, agreed upon set of guidelines for Infection Monkey, this commit uses KDE's guidelines for checkboxes: https://hig.kde.org/components/editing/checkbox.html When child checkboxes are not all checked, the master checkbox displays a mixed-state icon, instead of a checked icon. Clicking the mixed-state icon checks all child checkboxes. Clicking an unchecked master checkbox also enables all child checkboxes. In the past, clicking an unchecked master checkbox checked only the *default* child checkboxes. While this may seem desirable so that unsafe exploits do not accidentally get selected by the user, it will confuse and frustrate users, as master/child checkboxes do not normally function this way. If there is concern that users may unknowingly select unsafe exploits/options, we should pop up a warning to inform the user when the config is saved/submitted. Issue guardicore#891
1 parent 878f959 commit 11c52dc

File tree

1 file changed

+45
-16
lines changed

1 file changed

+45
-16
lines changed

monkey/monkey_island/cc/ui/src/components/ui-components/AdvancedMultiSelect.js

+45-16
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,20 @@ import React from "react";
22
import {Card, Button, Form} from 'react-bootstrap';
33
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
44
import {faCheckSquare} from '@fortawesome/free-solid-svg-icons';
5+
import {faMinusSquare} from '@fortawesome/free-solid-svg-icons';
56
import {faSquare} from '@fortawesome/free-regular-svg-icons';
67
import {cloneDeep} from 'lodash';
78

89
import {getComponentHeight} from './utils/HeightCalculator';
910
import {resolveObjectPath} from './utils/ObjectPathResolver';
1011
import InfoPane from './InfoPane';
1112

13+
const MasterCheckboxState = {
14+
NONE: 0,
15+
MIXED: 1,
16+
ALL: 2
17+
}
18+
1219

1320
// Definitions passed to components only contains value and label,
1421
// custom fields like "info" or "links" must be pulled from registry object using this function
@@ -40,12 +47,19 @@ function MasterCheckbox(props) {
4047
checkboxState
4148
} = props;
4249

50+
var newCheckboxIcon = faCheckSquare;
51+
52+
if (checkboxState == MasterCheckboxState.NONE)
53+
newCheckboxIcon = faSquare;
54+
else if (checkboxState == MasterCheckboxState.MIXED)
55+
newCheckboxIcon = faMinusSquare;
56+
4357
return (
4458
<Card.Header>
4559
<Button key={`${title}-button`} value={value}
4660
variant={'link'} disabled={disabled}
4761
onClick={onClick}>
48-
<FontAwesomeIcon icon={checkboxState ? faCheckSquare : faSquare}/>
62+
<FontAwesomeIcon icon={newCheckboxIcon}/>
4963
</Button>
5064
<span className={'header-title'}>{title}</span>
5165
</Card.Header>
@@ -76,43 +90,59 @@ function ChildCheckbox(props) {
7690

7791
class AdvancedMultiSelect extends React.Component {
7892
constructor(props) {
93+
console.log("CONSTRUCTED");
7994
super(props)
80-
this.state = {masterCheckbox: true, infoPaneParams: getDefaultPaneParams(props.schema.items.$ref, props.registry)};
95+
this.state = {
96+
masterCheckboxState: this.getMasterCheckboxState(props.value),
97+
infoPaneParams: getDefaultPaneParams(props.schema.items.$ref, props.registry)
98+
};
8199
this.onMasterCheckboxClick = this.onMasterCheckboxClick.bind(this);
82100
this.onChildCheckboxClick = this.onChildCheckboxClick.bind(this);
83101
this.setPaneInfo = this.setPaneInfo.bind(this, props.schema.items.$ref, props.registry);
84102
}
85103

86104
onMasterCheckboxClick() {
87-
if (this.state.masterCheckbox) {
88-
this.props.onChange([]);
89-
} else {
90-
this.props.onChange(this.props.schema.default);
105+
var newValues = this.props.options.enumOptions.map(({value}) => value);
106+
107+
if (this.state.masterCheckboxState == MasterCheckboxState.ALL) {
108+
newValues = [];
91109
}
92110

93-
this.toggleMasterCheckbox();
111+
this.props.onChange(newValues);
112+
this.setMasterCheckboxState(newValues);
94113
}
95114

96115
onChildCheckboxClick(value) {
97-
this.props.onChange(this.getSelectValuesAfterClick(value));
116+
var selectValues = this.getSelectValuesAfterClick(value)
117+
this.props.onChange(selectValues);
118+
119+
this.setMasterCheckboxState(selectValues);
98120
}
99121

100122
getSelectValuesAfterClick(clickedValue) {
101123
const valueArray = cloneDeep(this.props.value);
102124

103125
if (valueArray.includes(clickedValue)) {
104-
return valueArray.filter((e) => {
105-
return e !== clickedValue;
106-
});
126+
return valueArray.filter(e => e !== clickedValue);
107127
} else {
108128
valueArray.push(clickedValue);
109129
return valueArray;
110130
}
111131
}
112132

113-
toggleMasterCheckbox() {
114-
this.setState((state) => ({
115-
masterCheckbox: !state.masterCheckbox
133+
getMasterCheckboxState(selectValues) {
134+
if (selectValues.length == 0)
135+
return MasterCheckboxState.NONE;
136+
137+
if (selectValues.length != this.props.options.enumOptions.length)
138+
return MasterCheckboxState.MIXED;
139+
140+
return MasterCheckboxState.ALL;
141+
}
142+
143+
setMasterCheckboxState(selectValues) {
144+
this.setState(() => ({
145+
masterCheckboxState: this.getMasterCheckboxState(selectValues)
116146
}));
117147
}
118148

@@ -132,7 +162,6 @@ class AdvancedMultiSelect extends React.Component {
132162
readonly,
133163
multiple,
134164
autofocus,
135-
onChange,
136165
registry
137166
} = this.props;
138167

@@ -143,7 +172,7 @@ class AdvancedMultiSelect extends React.Component {
143172
<div className={'advanced-multi-select'}>
144173
<MasterCheckbox title={schema.title} value={value}
145174
disabled={disabled} onClick={this.onMasterCheckboxClick}
146-
checkboxState={this.state.masterCheckbox}/>
175+
checkboxState={this.state.masterCheckboxState}/>
147176
<Form.Group
148177
style={{height: `${getComponentHeight(enumOptions.length)}px`}}
149178
id={id}

0 commit comments

Comments
 (0)