Skip to content
Snippets Groups Projects
Commit 75ab0b2a authored by Václav Bartoš's avatar Václav Bartoš
Browse files

TheHiveButton: controller: form state is now kept when modal is closed

Lifted the React state up to the button controller, so the state is not lost when modal is closed.
parent 01c24cd1
No related branches found
No related tags found
No related merge requests found
......@@ -26,6 +26,11 @@ import {
makeId,
} from '@elastic/eui';
// TODO:
// - Explicit reset button
// - Reset when data (query) changes
// - Hide modal when clicked outside
// ********** React components **********
......@@ -63,12 +68,50 @@ class NewCaseButton extends Component {
constructor(props) {
super(props);
const n_obs = props.observables.length;
// initial state of form fields - used to reset form
this.initial_case_state = {
// Case parameters
title: "",
description: "\n\n--\nCreated from Kibana",
severity: "2", // medium
tlp: "2", // amber
tags: [], // TODO (not implemented yet)
// An array of values for each editable part of observables, prefilled with default values
obsDescrs: new Array(n_obs).fill(""),
obsTLPs: new Array(n_obs).fill(2), // TODO (not implemented yet)
obsIOCs: new Array(n_obs).fill(false),
obsTags: new Array(n_obs).fill([]), // TODO (not implemented yet)
obsSelected: new Array(), // list of indices of selected observables
}
// The complete state is here, so it's kept even when modal is closed
this.state = {
isModalVisible: false,
};
...this.initial_case_state, // TODO isn't deep copy needed here (and in reset_form below)?
}
// Each handler function in a class (method) must be "binded" this way
this.closeModal = this.closeModal.bind(this);
this.showModal = this.showModal.bind(this);
this.onTitleChange = this.onTitleChange.bind(this);
this.onSeverityChange = this.onSeverityChange.bind(this);
this.onTLPChange = this.onTLPChange.bind(this);
this.onDescriptionChange = this.onDescriptionChange.bind(this);
this.onObsDescrChange = this.onObsDescrChange.bind(this);
this.onObsTLPChange = this.onObsTLPChange.bind(this);
this.onObsIOCChange = this.onObsIOCChange.bind(this);
this.onObsTagsChange = this.onObsTagsChange.bind(this);
this.onObsSelectionChange = this.onObsSelectionChange.bind(this);
this.submitCase = this.submitCase.bind(this);
}
resetForm() {
this.setState(this.initial_case_state);
}
closeModal() {
......@@ -79,10 +122,199 @@ class NewCaseButton extends Component {
this.setState({ isModalVisible: true });
}
// Event handlers for change of case parameter
onTitleChange(evt) {
this.setState({title: evt.target.value});
}
onSeverityChange(value) {
this.setState({severity: value});
}
onTLPChange(value) {
this.setState({tlp: value});
}
onDescriptionChange(evt) {
this.setState({description: evt.target.value});
}
// Event handlers for change of observable parameters
onObsDescrChange(evt, rowindex) {
const val = evt.target.value;
this.setState((state, props) => {
// Copy old obsDescrs, update the corresponding item and pass as new state
let descrs = [...state.obsDescrs];
descrs[rowindex] = val;
return {obsDescrs: descrs};
});
}
onObsTLPChange(evt, rowindex) { // TODO
const val = evt.target.value;
this.setState((state, props) => {
// Copy old obsTLPs, update the corresponding item and pass as new state
let tlps = [...state.obsTLPs];
tlps[rowindex] = val;
return {obsTLPs: tlps};
});
}
onObsIOCChange(evt, rowindex) {
const val = evt.target.checked;
this.setState((state, props) => {
// Copy old obsIOCs, update the corresponding item and pass as new state
let iocs = [...state.obsIOCs];
iocs[rowindex] = val;
return {obsIOCs: iocs};
});
}
onObsTagsChange(evt, rowindex) { // TODO
const val = evt.target.value;
this.setState((state, props) => {
// Copy old obsTags, update the corresponding item and pass as new state
let tags = [...state.obsTags];
tags[rowindex] = val;
return {obsTags: tags};
});
}
// Event handler for row (de)selection
onObsSelectionChange(selectedItems) {
// Extract indices from the items and store them into state
const selectedIndices = selectedItems.map(item => item.i);
this.setState({obsSelected: selectedIndices});
}
// Submit case button handler
submitCase(evt) {
const params = this.props.params;
// Get case parameters
const title = this.state.title;
const descr = this.state.description;
const severity = parseInt(this.state.severity);
const start_date = null;
const owner = params.owner;
const flag = false;
const tlp = parseInt(this.state.tlp);
const tags = this.state.tags;
if (!title) {
toastNotifications.addDanger("Title can't be empty");
return;
}
// Get list of selected observables and their params
let observables = [];
let selectionIndices = [...this.state.obsSelected]; // make a copy
selectionIndices.sort();
for (let i = 0; i < selectionIndices.length; i++) {
const j = selectionIndices[i]; // index of a selected obs. in the list of all observables
// fill in observable definition according to model at
// https://github.com/TheHive-Project/TheHiveDocs/blob/master/api/artifact.md
const obs = {
dataType: 'ip',
data: this.props.observables[j],
message: this.state.obsDescrs[j],
tlp: this.state.obsTLPs[j],
ioc: this.state.obsIOCs[j],
tags: this.state.obsTags[j],
};
observables.push(obs);
}
//console.log("Selected observables:", observables);
// All data cached in local variables - reset the form fields
this.resetForm();
// Submit request to create the case, handle response
// TODO rewite to await
createHiveCase(params.url, params.apikey, title, descr, severity, start_date, owner, flag, tlp, tags)
.then((resp) => {
if ('error' in resp) {
// Error contacting The Hive
console.error("TheHiveButton: ERROR when trying to create new case:", resp.error);
toastNotifications.addDanger("ERROR: " + resp.error);
return;
}
console.log("TheHiveButton: Case created:", resp);
const case_id = resp.id;
const case_url = params.url + "index.html#/case/" + case_id + "/details";
// Show notification
let obs_text;
if (observables.length > 0) {
obs_text = "Adding " + observables.length + " observables in background ...";
}
else {
obs_text = "(no observables added)";
}
toastNotifications.add({
title: "Case created",
color: "success",
iconType: "checkInCircleFilled",
text: (
<div>
<p><b><a href={case_url} target="_blank">Edit the new Case</a></b></p>
<p>{obs_text}</p>
</div>
),
});
// Open a new window with the case in The Hive
// (adding observables may take some time, so the case is opened first;
// The Hive web is dynamic so the observables appear as they are added)
window.open(case_url, '_blank');
if (observables.length == 0)
return;
// Submit request to add observables
console.log("TheHiveButton: adding " + observables.length + " observables ...");
addCaseObservables(params.url, params.apikey, case_id, observables)
.then((resp) => {
if ('error' in resp) {
console.error("TheHiveButton: ERROR when trying to add observables: " + resp.error);
toastNotifications.addDanger("ERROR when trying to add observables: " + resp.error);
}
else {
console.log("TheHiveButton: Done, observables added.");
toastNotifications.add("Done, observables added.");
}
}); // addObservables().then() func
}); // createHiveCase.then() func
// Close the popup window
this.closeModal();
}
// Render function
render() {
let modal;
if (this.state.isModalVisible) {
modal = <ModalContent close={this.closeModal} params={this.props.params} observables={this.props.observables} />;
modal = <ModalContent
close={this.closeModal}
observables={this.props.observables}
// form state
title={this.state.title}
description={this.state.description}
severity={this.state.severity}
tlp={this.state.tlp}
tags={this.state.tags}
obsDescrs={this.state.obsDescrs}
obsTLPs={this.state.obsTLPs}
obsIOCs={this.state.obsIOCs}
obsTags={this.state.obsTags}
obsSelected={this.state.obsSelected}
// event handlers
onTitleChange={this.onTitleChange}
onSeverityChange={this.onSeverityChange}
onTLPChange={this.onTLPChange}
onDescriptionChange={this.onDescriptionChange}
onObsDescrChange={this.onObsDescrChange}
onObsTLPChange={this.onObsTLPChange}
onObsIOCChange={this.onObsIOCChange}
onObsTagsChange={this.onObsTagsChange}
onObsSelectionChange={this.onObsSelectionChange}
submitCase={this.submitCase}
/>;
}
return (
<div>
......@@ -93,32 +325,12 @@ class NewCaseButton extends Component {
}
}
// The popup window with a form
// Props:
// .close - function to close the Modal
// .params - visualization parameters (from vis.params)
// .observables - list of IP addrsses or other observables to add to the Case
class ModalContent extends Component {
constructor(props) {
super(props);
const observables = props.observables;
const n_obs = observables.length;
// Initialize state (all changeable case parameters)
// TODO: move state up to the button, so it persists when modal is closed (and add explicit reset button + reset on submit)
this.state = {
title: "",
description: "\n\n--\nCreated from Kibana",
severity: "2", // medium
tlp: "2", // amber
tags: [],
// An array of values for each editable part of observables, prefilled with default values
obsDescrs: new Array(n_obs).fill(""),
obsTLPs: new Array(n_obs).fill(2),
obsIOCs: new Array(n_obs).fill(false),
obsTags: new Array(n_obs).fill([]),
obsSelected: new Array(), // list of indices of selected observables
};
// No state here, everything is in the parent class (NewCaseButton)
// "Select" options
this.severityOptions = [
......@@ -133,7 +345,7 @@ class ModalContent extends Component {
{value: "3", inputDisplay: "red"},
];
// Table column definition
// Table columns definition
this.columns = [
{
field: "id",
......@@ -145,7 +357,7 @@ class ModalContent extends Component {
description: "Description of the observable in the context of the case",
render: (value, item) => (<EuiFieldText
value={item.descr}
onChange={(e) => this.onChangeDescr(e, item.i)}
onChange={(e) => this.props.onObsDescrChange(e, item.i)}
disabled={!item.selected}
/>)
},
......@@ -163,7 +375,7 @@ class ModalContent extends Component {
render: (value, item) => (<EuiCheckbox
id={"ioc-checkbox-"+item.id}
checked={item.ioc}
onChange={(e) => this.onChangeIOC(e, item.i)}
onChange={(e) => this.props.onObsIOCChange(e, item.i)}
disabled={!item.selected}
/>)
},
......@@ -174,63 +386,11 @@ class ModalContent extends Component {
},*/
]
// Each handler function in a class (method) must be "bind" this way
this.submitCase = this.submitCase.bind(this);
this.onChangeDescr = this.onChangeDescr.bind(this);
this.onChangeTLP = this.onChangeTLP.bind(this);
this.onChangeIOC = this.onChangeIOC.bind(this);
this.onChangeTags = this.onChangeTags.bind(this);
this.onSelectionChange = this.onSelectionChange.bind(this);
}
// Event handlers - change observable parameters
onChangeDescr(evt, rowindex) {
const val = evt.target.value;
this.setState((state, props) => {
// Copy old obsDescrs, update the corresponding item and pass as new state
let descrs = [...state.obsDescrs];
descrs[rowindex] = val;
return {obsDescrs: descrs};
});
}
onChangeTLP(evt, rowindex) { // TODO
const val = evt.target.value;
this.setState((state, props) => {
// Copy old obsTLPs, update the corresponding item and pass as new state
let tlps = [...state.obsTLPs];
tlps[rowindex] = val;
return {obsTLPs: tlps};
});
}
onChangeIOC(evt, rowindex) {
const val = evt.target.checked;
this.setState((state, props) => {
// Copy old obsIOCs, update the corresponding item and pass as new state
let iocs = [...state.obsIOCs];
iocs[rowindex] = val;
return {obsIOCs: iocs};
});
}
onChangeTags(evt, rowindex) { // TODO
const val = evt.target.value;
this.setState((state, props) => {
// Copy old obsTags, update the corresponding item and pass as new state
let tags = [...state.obsTags];
tags[rowindex] = val;
return {obsTags: tags};
});
}
// Event handler - a row is (de)selected
onSelectionChange(selectedItems) {
// Extract indices from the items and store them into state
const selectedIndices = selectedItems.map(item => item.i);
this.setState({obsSelected: selectedIndices});
// Create a reference to observables table, so it's node can be accessed in componentDidMount
this.obsTableRef = React.createRef();
}
// Main render function
// TODO should't it be easier to simply store whole table_data in State?
// everything has to be re-rendered on any state change anyway
render() {
const obs = this.props.observables;
// Create data for the observables table
......@@ -238,16 +398,16 @@ class ModalContent extends Component {
for (let i = 0; i < obs.length; i++) {
table_data[i] = {
id: obs[i],
descr: this.state.obsDescrs[i],
tlp: this.state.obsTLPs[i],
ioc: this.state.obsIOCs[i],
tags: this.state.obsTags[i],
descr: this.props.obsDescrs[i],
tlp: this.props.obsTLPs[i],
ioc: this.props.obsIOCs[i],
tags: this.props.obsTags[i],
// auxiliary fields, not shown in table:
i: i, // row index
selected: this.state.obsSelected.includes(i),
selected: this.props.obsSelected.includes(i),
};
}
//console.log("render(): Table data:", table_data);
this.table_data = table_data; // needed in componentDidMount
return (
<EuiOverlayMask>
......@@ -261,15 +421,15 @@ class ModalContent extends Component {
<EuiFlexGroup>
<EuiFlexItem grow={1}>
<EuiFormRow label="Title" fullWidth>
<EuiFieldText name="title" value={this.state.title} onChange={(e) => this.setState({title: e.target.value})} required={true} fullWidth />
<EuiFieldText name="title" value={this.props.title} onChange={this.props.onTitleChange} required={true} fullWidth />
</EuiFormRow>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiFormRow label="Severity">
<EuiSuperSelect
options={this.severityOptions}
valueOfSelected={this.state.severity}
onChange={(val) => this.setState({severity: val})}
valueOfSelected={this.props.severity}
onChange={this.props.onSeverityChange}
/>
</EuiFormRow>
</EuiFlexItem>
......@@ -278,16 +438,16 @@ class ModalContent extends Component {
<EuiSuperSelect
prepend="TLP"
options={this.tlpOptions}
valueOfSelected={this.state.tlp}
onChange={(val) => this.setState({tlp: val})}
valueOfSelected={this.props.tlp}
onChange={this.props.onTLPChange}
/>
</EuiFormRow>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFormRow label="Description" fullWidth>
<EuiTextArea
defaultValue={this.state.description}
onChange={(e) => this.setState({description: e.target.value})}
defaultValue={this.props.description}
onChange={this.props.onDescriptionChange}
rows={4}
fullWidth
/>
......@@ -295,10 +455,11 @@ class ModalContent extends Component {
<EuiTitle size="s"><h3>Add observables from current query ...</h3></EuiTitle>
<EuiBasicTable
ref={this.obsTableRef}
columns={this.columns}
items={table_data}
itemId={(item) => item.id}
selection={ {onSelectionChange: this.onSelectionChange} }
selection={ {onSelectionChange: this.props.onObsSelectionChange} }
noItemsMessage="No observables found"
rowProps={{
// Hack to allow selection by clicking anywhere in the table row
......@@ -318,110 +479,29 @@ class ModalContent extends Component {
<EuiModalFooter>
<EuiButtonEmpty onClick={this.props.close}>Cancel</EuiButtonEmpty>
<EuiButton onClick={this.submitCase} fill>Create Case</EuiButton>
<EuiButton onClick={this.props.submitCase} fill>Create Case</EuiButton>
</EuiModalFooter>
</EuiModal>
</EuiOverlayMask>
);
}
submitCase(evt) {
const params = this.props.params;
// Get case parameters
const title = this.state.title;
const descr = this.state.description;
const severity = parseInt(this.state.severity);
const start_date = null;
const owner = params.owner;
const flag = false;
const tlp = parseInt(this.state.tlp);
const tags = this.state.tags;
if (!title) {
toastNotifications.addDanger("Title can't be empty");
return;
}
componentDidMount() {
// There's no way to specify initially selected items in EuiBasicTable by
// props, but we may need to select some (in case a user selects some obs.,
// closes the modal and opens it again).
// However, the selection is stored as a 'selection' field of table's state,
// so here we directly edit the state just after the table crated.
// Get list of selected observables and their params
let observables = [];
let selectionIndices = [...this.state.obsSelected]; // make a copy
selectionIndices.sort();
for (let i = 0; i < selectionIndices.length; i++) {
const j = selectionIndices[i]; // index of a selected obs. in the list of all observables
// fill in observable definition according to model at
// https://github.com/TheHive-Project/TheHiveDocs/blob/master/api/artifact.md
const obs = {
dataType: 'ip',
data: this.props.observables[j],
message: this.state.obsDescrs[j],
tlp: this.state.obsTLPs[j],
ioc: this.state.obsIOCs[j],
tags: this.state.obsTags[j],
};
observables.push(obs);
// Prepare the 'selection' array - it should contain a list of selected row specifications
let selection = [];
for (let i = 0; i < this.props.obsSelected.length; i++) {
selection.push(this.table_data[this.props.obsSelected[i]]);
}
console.log("Selected observables:", observables);
// Submit request to create the case, handle response
createHiveCase(params.url, params.apikey, title, descr, severity, start_date, owner, flag, tlp, tags)
.then((resp) => {
if ('error' in resp) {
// Error contacting The Hive
console.error("TheHiveButton: ERROR when trying to create new case:", resp.error);
toastNotifications.addDanger("ERROR: " + resp.error);
return;
}
console.log("TheHiveButton: Case created:", resp);
const case_id = resp.id;
const case_url = params.url + "index.html#/case/" + case_id + "/details";
// Show notification
let obs_text;
if (observables.length > 0) {
obs_text = "Adding " + observables.length + " observables in background ...";
}
else {
obs_text = "(no observables added)";
}
toastNotifications.add({
title: "Case created",
color: "success",
iconType: "checkInCircleFilled",
text: (
<div>
<p><b><a href={case_url} target="_blank">Edit the new Case</a></b></p>
<p>{obs_text}</p>
</div>
),
});
// Open a new window with the case in The Hive
// (adding observables may take some time, so the case is opened first;
// The Hive web is dynamic so the observables appear as they are added)
window.open(case_url, '_blank');
if (observables.length == 0)
return;
// Submit request to add observables
console.log("TheHiveButton: adding " + observables.length + " observables ...");
addCaseObservables(params.url, params.apikey, case_id, observables)
.then((resp) => {
if ('error' in resp) {
console.error("TheHiveButton: ERROR when trying to add observables: " + resp.error);
toastNotifications.addDanger("ERROR when trying to add observables: " + resp.error);
}
else {
console.log("TheHiveButton: Done, observables added.");
toastNotifications.add("Done, observables added.");
}
}); // addObservables().then() func
}); // createHiveCase.then() func
// Close the popup window
this.props.close();
// Get ref to EuiBasicTable element and update its state
const table_node = this.obsTableRef.current;
table_node.setState({selection: selection});
}
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment