Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CSV Form widget can now flip between textarea or table #3054

Merged
merged 1 commit into from
Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
CSV Form widget can now flip between textarea or table
While the table view of the CSV artifact parameter type is intutive to
use for adding a few rows manually it is not easy to use when we have
a large collection of CSV items.

This PR adds a button to the widget to allow it to toggle to a simple
textarea. In that case the user can paste a large CSV file into the
GUI.

The GUI will now also validate the CSV warning about errors like
unbalanced columns etc.
  • Loading branch information
scudette committed Oct 26, 2023
commit 133cc92c4efa00cda5de7f559f68c96673a7d060
197 changes: 197 additions & 0 deletions gui/velociraptor/src/components/forms/csv.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import _ from 'lodash';

import PropTypes from 'prop-types';
import { parseCSV, validateCSV, serializeCSV } from '../utils/csv.jsx';
import ButtonGroup from 'react-bootstrap/ButtonGroup';
import Button from 'react-bootstrap/Button';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import Form from 'react-bootstrap/Form';
import cellEditFactory, { Type } from 'react-bootstrap-table2-editor';
import Row from 'react-bootstrap/Row';
import React, { Component } from 'react';
import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
import Tooltip from 'react-bootstrap/Tooltip';
import Col from 'react-bootstrap/Col';
import BootstrapTable from 'react-bootstrap-table-next';
import Alert from 'react-bootstrap/Alert';


const renderToolTip = (props, params) => (
<Tooltip show={params.description} {...props}>
{params.description}
</Tooltip>
);

export default class CSVForm extends Component {
static propTypes = {
param: PropTypes.object,
value: PropTypes.string,
setValue: PropTypes.func.isRequired,
};

state = {
mode: "csv",
error: "",
}

setCSValue = value=>{
// Check if we can parse it properly.
this.setState({error: validateCSV(value)});
this.props.setValue(value);
}

render() {
if (this.state.mode === "csv") {
return this.renderCSVTable();
}
return this.renderTextArea();
}

renderTextArea() {
let param = this.props.param || {};
let name = param.friendly_name || param.name;

return (
<Form.Group as={Row}>
<Form.Label column sm="3">
<OverlayTrigger
delay={{show: 250, hide: 400}}
overlay={(props)=>renderToolTip(props, param)}>
<div>
{name}
</div>
</OverlayTrigger>
</Form.Label>

<Col sm="8">
<Form.Control as="textarea"
placeholder={this.props.param.description}
rows={10}
onChange={(e) => {
this.setCSValue(e.currentTarget.value);
}}
value={this.props.value} />
{ this.state.error ?
<Alert variant="danger">
{this.state.error.code}
</Alert> :
<Button variant="default-outline"
className="full-width"
disabled={this.state.error}
onClick={() => this.setState({mode: "csv"})}
size="sm">
<FontAwesomeIcon icon="pencil-alt"/>
</Button>
}
</Col>
</Form.Group>
);
}

renderCSVTable() {
let param = this.props.param || {};
let name = param.friendly_name || param.name;

let data = parseCSV(this.props.value);
let columns = [{
dataField: "_id",
text: "",
style: {
width: '8%',
},
headerFormatter: (column, colIndex) => {
if (colIndex === 0) {
return <ButtonGroup>
<Button variant="default-outline" size="sm"
onClick={() => {
// Add an extra row at the current row index.
let data = parseCSV(this.props.value);
data.data.splice(0, 0, {});
this.props.setValue(
serializeCSV(data.data,
data.columns));
}}
>
<FontAwesomeIcon icon="plus"/>
</Button>
<Button variant="default-outline" size="sm"
onClick={()=>this.setState({mode: "text"})}
>
<FontAwesomeIcon icon="pencil-alt"/>
</Button>
</ButtonGroup>;
};
return column;
},
formatter: (id, row) => {
return <ButtonGroup>
<Button variant="default-outline" size="sm"
onClick={() => {
// Add an extra row at the current row index.
let data = parseCSV(this.props.value);
data.data.splice(id+1, 0, {});
this.props.setValue(
serializeCSV(data.data,
data.columns));
}}
>
<FontAwesomeIcon icon="plus"/>
</Button>
<Button variant="default-outline" size="sm"
onClick={() => {
// Drop the current row at the current row index.
let data = parseCSV(this.props.value);
data.data.splice(id, 1);
this.props.setValue(
serializeCSV(data.data,
data.columns));
}}
>
<FontAwesomeIcon icon="trash"/>
</Button>
</ButtonGroup>;
},
}];
_.each(data.columns, (name) => {
columns.push({dataField: name,
editor: {
type: Type.TEXTAREA
},
text: name});
});

_.map(data.data, (item, idx) => {item["_id"] = idx;});

return (
<Form.Group as={Row}>
<Form.Label column sm="3">
<OverlayTrigger
delay={{show: 250, hide: 400}}
overlay={(props)=>renderToolTip(props, param)}>
<div>
{name}
</div>
</OverlayTrigger>
</Form.Label>

<Col sm="8">
<BootstrapTable
hover condensed bootstrap4
data={data.data}
keyField="_id"
columns={columns}
cellEdit={ cellEditFactory({
mode: 'click',
afterSaveCell: (oldValue, newValue, row, column) => {
// Update the CSV value.
let new_data = serializeCSV(data.data, data.columns);
this.props.setValue(new_data);
},
blurToSave: true,
}) }
/>
</Col>
</Form.Group>
);
}
}
103 changes: 6 additions & 97 deletions gui/velociraptor/src/components/forms/form.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import OverlayTrigger from 'react-bootstrap/OverlayTrigger';
import ButtonGroup from 'react-bootstrap/ButtonGroup';
import Alert from 'react-bootstrap/Alert';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import CSVForm from './csv.jsx';

import BootstrapTable from 'react-bootstrap-table-next';
import cellEditFactory, { Type } from 'react-bootstrap-table2-editor';
Expand Down Expand Up @@ -245,110 +246,18 @@ export default class VeloForm extends React.Component {
}

render() {
let param = this.props.param;
let param = this.props.param || {};
let name = param.friendly_name || param.name;

switch(param.type) {
case "hidden":
return <></>;

case "csv": {
let data = parseCSV(this.props.value);
let columns = [{
dataField: "_id",
text: "",
style: {
width: '8%',
},
headerFormatter: (column, colIndex) => {
if (colIndex === 0) {
return <ButtonGroup>
<Button variant="default-outline" size="sm"
onClick={() => {
// Add an extra row at the current row index.
let data = parseCSV(this.props.value);
data.data.splice(0, 0, {});
this.props.setValue(
serializeCSV(data.data,
data.columns));
}}
>
<FontAwesomeIcon icon="plus"/>
</Button>
</ButtonGroup>;
};
return column;
},
formatter: (id, row) => {
return <ButtonGroup>
<Button variant="default-outline" size="sm"
onClick={() => {
// Add an extra row at the current row index.
let data = parseCSV(this.props.value);
data.data.splice(id, 0, {});
this.props.setValue(
serializeCSV(data.data,
data.columns));
}}
>
<FontAwesomeIcon icon="plus"/>
</Button>
<Button variant="default-outline" size="sm"
onClick={() => {
// Drop th current row at the current row index.
let data = parseCSV(this.props.value);
data.data.splice(id, 1);
this.props.setValue(
serializeCSV(data.data,
data.columns));
}}
>
<FontAwesomeIcon icon="trash"/>
</Button>
</ButtonGroup>;
},
}];
_.each(data.columns, (name) => {
columns.push({dataField: name,
editor: {
type: Type.TEXTAREA
},
text: name});
});

_.map(data.data, (item, idx) => {item["_id"] = idx;});

return (
<Form.Group as={Row}>
<Form.Label column sm="3">
<OverlayTrigger
delay={{show: 250, hide: 400}}
overlay={(props)=>renderToolTip(props, param)}>
<div>
{name}
</div>
</OverlayTrigger>
</Form.Label>

<Col sm="8">
<BootstrapTable
hover condensed bootstrap4
data={data.data}
keyField="_id"
columns={columns}
cellEdit={ cellEditFactory({
mode: 'click',
afterSaveCell: (oldValue, newValue, row, column) => {
// Update the CSV value.
let new_data = serializeCSV(data.data, data.columns);
this.props.setValue(new_data);
},
blurToSave: true,
}) }
/>
</Col>
</Form.Group>
);
return <CSVForm
param={this.props.param}
value={this.props.value}
setValue={this.props.setValue}/>;
}

case "regex":
Expand Down
4 changes: 4 additions & 0 deletions gui/velociraptor/src/components/forms/forms.css
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,7 @@ input[type="checkbox"][value="Y"] {
border-color: var(--color-btn-default-background-hover);
background-color: var(--accent-color);
}

.full-width {
width: 100%;
}
11 changes: 11 additions & 0 deletions gui/velociraptor/src/components/utils/csv.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@ export function serializeCSV(data, columns) {
}


export function validateCSV(data) {
try {
parse(data, {skip_empty_lines: false});
parse(data, {columns: true, skip_empty_lines: false});
return "";

} catch(e) {
return e;
}
};

export function parseCSV(data) {
try {
let records = parse(data, {skip_empty_lines: false});
Expand Down