diff --git a/.gitignore b/.gitignore
index a32ff609..6f89c4f2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -60,9 +60,12 @@ docs/_build/
# PyBuilder
target/
-#Ipython Notebook
+# Ipython Notebook
.ipynb_checkpoints
+# Editors
+.vscode
+
# NodeJS stuff
node_modules
package-lock.json
@@ -70,6 +73,7 @@ package-lock.json
# Virtual environment
.ibutsu-env
.ibutsu_env
+.ibutsu
# Frontend
frontend/public/version.json
diff --git a/frontend/src/components/classify-failures.js b/frontend/src/components/classify-failures.js
index c4d0bfa1..67355cdc 100644
--- a/frontend/src/components/classify-failures.js
+++ b/frontend/src/components/classify-failures.js
@@ -9,9 +9,6 @@ import {
Checkbox,
Flex,
FlexItem,
- Select,
- SelectOption,
- SelectVariant,
TextContent,
Text,
} from '@patternfly/react-core';
@@ -24,13 +21,15 @@ import { HttpClient } from '../services/http';
import { Settings } from '../settings';
import {
buildParams,
+ toAPIFilter,
getSpinnerRow,
resultToClassificationRow,
} from '../utilities';
-import { OPERATIONS } from '../constants';
+import { FILTERABLE_RESULT_FIELDS } from '../constants';
import {
FilterTable,
MultiClassificationDropdown,
+ MetaFilter,
} from './index';
@@ -38,7 +37,7 @@ export class ClassifyFailuresTable extends React.Component {
static propTypes = {
filters: PropTypes.object,
run_id: PropTypes.string
- };
+ }
constructor(props) {
super(props);
@@ -56,9 +55,6 @@ export class ClassifyFailuresTable extends React.Component {
isError: false,
isFieldOpen: false,
isOperationOpen: false,
- exceptionSelections: [],
- isExceptionOpen: false,
- exceptions: [],
includeSkipped: false,
filters: Object.assign({
'result': {op: 'in', val: 'failed;error'},
@@ -73,39 +69,6 @@ export class ClassifyFailuresTable extends React.Component {
this.getResultsForTable();
}
- onExceptionToggle = isExpanded => {
- this.setState({isExceptionOpen: isExpanded}, this.applyFilter);
- }
-
- applyExceptionFilter = () => {
- let { filters, exceptionSelections } = this.state;
- if (exceptionSelections.length > 0) {
- filters["metadata.exception_name"] = Object.assign({op: 'in', val: exceptionSelections.join(';')});
- this.setState({filters}, this.refreshResults);
- }
- else {
- delete filters["metadata.exception_name"];
- this.setState({filters}, this.refreshResults);
- }
- }
-
- onExceptionSelect = (event, selection) => {
- const exceptionSelections = this.state.exceptionSelections;
- if (exceptionSelections.includes(selection)) {
- this.setState({exceptionSelections: exceptionSelections.filter(item => item !== selection)}, this.applyExceptionFilter);
- }
- else {
- this.setState({exceptionSelections: [...exceptionSelections, selection]}, this.applyExceptionFilter);
- }
- };
-
- onExceptionClear = () => {
- this.setState({
- exceptionSelections: [],
- isExceptionOpen: false
- }, this.applyExceptionFilter);
- };
-
onCollapse(event, rowIndex, isOpen) {
const { rows } = this.state;
rows[rowIndex].isOpen = isOpen;
@@ -146,7 +109,7 @@ export class ClassifyFailuresTable extends React.Component {
updateFilters(name, operator, value, callback) {
let filters = this.state.filters;
- if (!value) {
+ if ((value === null) || (value.length === 0)) {
delete filters[name];
}
else {
@@ -156,27 +119,20 @@ export class ClassifyFailuresTable extends React.Component {
}
setFilter = (field, value) => {
- this.updateFilters(field, 'eq', value, () => {
- this.refreshResults();
- })
- };
+ // maybe process values array to string format here instead of expecting caller to do it?
+ let operator = (value.includes(";")) ? 'in' : 'eq'
+ this.updateFilters(field, operator, value, this.refreshResults)
+ }
removeFilter = id => {
- if (id === "metadata.exception_name") { // only remove exception_name filter
- this.updateFilters(id, null, null, () => {
- this.setState({exceptionSelections: [], page: 1}, this.applyExceptionFilter);
- });
+ if ((id !== "result") && (id !== "run_id")) { // Don't allow removal of error/failure filter
+ this.updateFilters(id, null, null, this.refreshResults)
}
}
onSkipCheck = (checked) => {
let { filters } = this.state;
- if (checked) {
- filters["result"]["val"] += ";skipped;xfailed"
- }
- else {
- filters["result"]["val"] = "failed;error"
- }
+ filters["result"]["val"] = ("failed;error") + ((checked) ? ";skipped;xfailed" : "")
this.setState(
{includeSkipped: checked, filters},
this.refreshResults
@@ -199,17 +155,9 @@ export class ClassifyFailuresTable extends React.Component {
this.setState({rows: [getSpinnerRow(5)], isEmpty: false, isError: false});
// get only failed results
let params = buildParams(filters);
- params['filter'] = [];
+ params['filter'] = toAPIFilter(filters);
params['pageSize'] = this.state.pageSize;
params['page'] = this.state.page;
- // Convert UI filters to API filters
- for (let key in filters) {
- if (Object.prototype.hasOwnProperty.call(filters, key) && !!filters[key]) {
- let val = filters[key]['val'];
- const op = OPERATIONS[filters[key]['op']];
- params.filter.push(key + op + val);
- }
- }
this.setState({rows: [['Loading...', '', '', '', '']]});
HttpClient.get([Settings.serverUrl, 'result'], params)
@@ -229,17 +177,8 @@ export class ClassifyFailuresTable extends React.Component {
});
}
- getExceptions() {
- HttpClient.get([Settings.serverUrl, 'widget', 'result-aggregator'], {group_field: 'metadata.exception_name', run_id: this.props.run_id})
- .then(response => response.json())
- .then(data => {
- this.setState({exceptions: data})
- })
- }
-
componentDidMount() {
this.getResultsForTable();
- this.getExceptions();
}
render() {
@@ -248,35 +187,24 @@ export class ClassifyFailuresTable extends React.Component {
rows,
selectedResults,
includeSkipped,
- isExceptionOpen,
- exceptionSelections,
- exceptions,
+ filters
} = this.state;
+ const { run_id } = this.props
const pagination = {
pageSize: this.state.pageSize,
page: this.state.page,
totalItems: this.state.totalItems
}
- // filters for the exception
- const exceptionFilters = [
-
-
-
+ // filters for the metadata
+ const resultFilters = [
+ ,
]
return (
@@ -314,7 +242,7 @@ export class ClassifyFailuresTable extends React.Component {
onRowSelect={this.onTableRowSelect}
variant={TableVariant.compact}
activeFilters={this.state.filters}
- filters={exceptionFilters}
+ filters={resultFilters}
onRemoveFilter={this.removeFilter}
hideFilters={["run_id", "project_id"]}
/>
diff --git a/frontend/src/components/filtertable.js b/frontend/src/components/filtertable.js
index 9877e020..3157aac6 100644
--- a/frontend/src/components/filtertable.js
+++ b/frontend/src/components/filtertable.js
@@ -9,7 +9,10 @@ import {
Flex,
FlexItem,
Pagination,
- PaginationVariant
+ PaginationVariant,
+ Select,
+ SelectOption,
+ SelectVariant,
} from '@patternfly/react-core';
import {
Table,
@@ -17,6 +20,10 @@ import {
TableHeader
} from '@patternfly/react-table';
+import { Settings } from '../settings';
+import { HttpClient } from '../services/http';
+import { toAPIFilter } from '../utilities';
+
import { TableEmptyState, TableErrorState } from './tablestates';
export class FilterTable extends React.Component {
@@ -160,3 +167,138 @@ export class FilterTable extends React.Component {
);
}
}
+
+
+export class MetaFilter extends React.Component {
+ // TODO Extend this to contain the filter handling functions, and better integrate filter state with FilterTable
+ // https://github.com/ibutsu/ibutsu-server/issues/230
+ static propTypes = {
+ fieldOptions: PropTypes.array, // could reference constants directly
+ runId: PropTypes.string, // make optional?
+ setFilter: PropTypes.func,
+ customFilters: PropTypes.array, // more advanced handling of filter objects? the results-aggregator endpoint takes a string filter
+ };
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ fieldSelection: null,
+ isFieldOpen: false,
+ isValueOpen: false,
+ valueOptions: [],
+ valueSelections: [],
+ };
+ }
+
+ onFieldToggle = isExpanded => {
+ this.setState({isFieldOpen: isExpanded})
+ };
+
+ onValueToggle = isExpanded => {
+ this.setState({isValueOpen: isExpanded})
+ };
+
+ onFieldSelect = (event, selection) => {
+ this.setState(
+ // clear value state too, otherwise the old selection remains selected but is no longer visible
+ {fieldSelection: selection, isFieldOpen: false, valueSelections: [], valueOptions: [], isValueOpen: false},
+ this.updateValueOptions
+ )
+
+ };
+
+ onValueSelect = (event, selection) => {
+ // update state and call setFilter
+ const valueSelections = this.state.valueSelections;
+ let updated_values = (valueSelections.includes(selection))
+ ? valueSelections.filter(item => item !== selection)
+ : [...valueSelections, selection]
+
+ this.setState(
+ {valueSelections: updated_values},
+ () => this.props.setFilter(this.state.fieldSelection, this.state.valueSelections.join(';'))
+ )
+ };
+
+ onFieldClear = () => {
+ this.setState(
+ {fieldSelection: null, valueSelections: [], isFieldOpen: false, isValueOpen: false},
+ )
+ };
+
+ onValueClear = () => {
+ this.setState(
+ {valueSelections: [], isValueOpen: false},
+ () => this.props.setFilter(this.state.fieldSelection, this.state.valueSelections)
+ )
+ }
+
+ updateValueOptions = () => {
+ const {fieldSelection} = this.state
+ const {customFilters} = this.props
+
+ console.log('CUSTOMFILTER: '+customFilters)
+ if (fieldSelection !== null) {
+ let api_filter = toAPIFilter(customFilters).join()
+ console.log('APIFILTER: '+customFilters)
+
+ HttpClient.get(
+ [Settings.serverUrl, 'widget', 'result-aggregator'],
+ {
+ group_field: fieldSelection,
+ run_id: this.props.runId,
+ additional_filters: api_filter,
+ }
+ )
+ .then(response => HttpClient.handleResponse(response))
+ .then(data => {
+ this.setState({valueOptions: data})
+ })
+ }
+ }
+
+ render () {
+ const {isFieldOpen, fieldSelection, isValueOpen, valueOptions, valueSelections} = this.state;
+ let field_selected = this.state.fieldSelection !== null;
+ let values_available = valueOptions.length > 0;
+ let value_placeholder = "Select a field first" ; // default instead of an else block
+ if (field_selected && values_available){ value_placeholder = "Select value(s)";}
+ else if (field_selected && !values_available) { value_placeholder = "No values for selected field";}
+ return (
+
+
+
+
+
+ )
+ }
+}
diff --git a/frontend/src/components/index.js b/frontend/src/components/index.js
index 33eb5223..849535fe 100644
--- a/frontend/src/components/index.js
+++ b/frontend/src/components/index.js
@@ -4,7 +4,7 @@ export { AddTokenModal } from './add-token-modal';
export { DeleteModal } from './delete-modal';
export { EmptyObject } from './empty-object';
export { FileUpload } from './fileupload';
-export { FilterTable } from './filtertable';
+export { FilterTable, MetaFilter } from './filtertable';
export { ParamDropdown } from './widget-components';
export { MultiValueInput } from './multivalueinput';
export { NewDashboardModal } from './new-dashboard-modal';
diff --git a/frontend/src/constants.js b/frontend/src/constants.js
index 5606b8df..204145a9 100644
--- a/frontend/src/constants.js
+++ b/frontend/src/constants.js
@@ -60,6 +60,26 @@ export const STRING_RESULT_FIELDS = [
'start_time', // TODO: handle this with a calendar widget?
'test_id'
];
+export const FILTERABLE_RESULT_FIELDS = [
+ 'env',
+ 'component',
+ 'metadata.assignee',
+ // TODO support array filtering
+ // https://github.com/ibutsu/ibutsu-server/issues/228
+ //'metadata.statuses.call',
+ //'metadata.statuses.setup',
+ //'metadata.statuses.teardown',
+ 'metadata.caseautomation',
+ 'metadata.exception_name',
+ 'metadata.fspath',
+ 'metadata.importance',
+ 'metadata.title',
+ // TODO support object/dict filtering
+ // https://github.com/ibutsu/ibutsu-server/issues/229
+ //'metadata.params',
+ //'metadata.markers',
+
+]
export const RESULT_FIELDS = [...NUMERIC_RESULT_FIELDS, ...STRING_RESULT_FIELDS, ...ARRAY_RESULT_FIELDS];
export const ARRAY_RUN_FIELDS = [
'metadata.tags',
diff --git a/frontend/src/utilities.js b/frontend/src/utilities.js
index 56a3719e..d577535d 100644
--- a/frontend/src/utilities.js
+++ b/frontend/src/utilities.js
@@ -101,6 +101,37 @@ export function buildParams(filters) {
return getParams;
}
+export function buildUrl(url, params) {
+ // shorthand
+ const esc = encodeURIComponent;
+ let query = [];
+ for (const key of Object.keys(params)) {
+ const value = params[key];
+ if (value instanceof Array) {
+ value.forEach(element => {
+ query.push(esc(key) + '=' + esc(element));
+ });
+ }
+ else {
+ query.push(esc(key) + '=' + esc(value));
+ }
+ }
+ return url + '?' + query.join('&');
+}
+
+export function toAPIFilter(filters) {
+ // Take UI style filter object with field/op/val keys and generate an array of filter strings for the API
+ let filter_strings = []
+ for (const key in filters) {
+ if (Object.prototype.hasOwnProperty.call(filters, key) && !!filters[key]) {
+ const val = filters[key]['val'];
+ const op = OPERATIONS[filters[key]['op']];
+ filter_strings.push(key + op + val);
+ }
+ }
+ return filter_strings
+}
+
export function round(number) {
let rounded = Math.round(number * 10);
return rounded / 10;