diff --git a/frontend/src/components/artifact-tab.js b/frontend/src/components/artifact-tab.js
index 08f6e08b..3c5887ab 100644
--- a/frontend/src/components/artifact-tab.js
+++ b/frontend/src/components/artifact-tab.js
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { Card, CardBody, CardFooter, Text } from '@patternfly/react-core';
import { Editor } from '@monaco-editor/react';
-import { DownloadButton } from './download-button';
+import DownloadButton from './download-button';
import { Settings } from '../settings';
import { HttpClient } from '../services/http';
diff --git a/frontend/src/components/classification-dropdown.js b/frontend/src/components/classification-dropdown.js
index c1b7c138..eee6490d 100644
--- a/frontend/src/components/classification-dropdown.js
+++ b/frontend/src/components/classification-dropdown.js
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import {
@@ -12,125 +12,111 @@ import { HttpClient } from '../services/http';
import { Settings } from '../settings';
import { CLASSIFICATION } from '../constants.js';
+const ClassificationDropdown = (props) => {
+ const [testResult, setTestResult] = useState(props.testResult);
+ const [classificationOpen, setclassificationOpen] = useState(false);
-export class ClassificationDropdown extends React.Component {
- static propTypes = {
- testResult: PropTypes.object,
- };
-
- constructor (props) {
- super(props);
- this.state = {
- testResult: this.props.testResult,
- isClassificationOpen: false
+ const onClassificationSelect = (_event, selection) => {
+ let updatedResult = {
+ ...testResult,
+ 'metadata': {
+ ...testResult.metadata,
+ 'classification': selection
+ }
};
- }
-
- componentDidUpdate (prevProps) {
- if (prevProps !== this.props) {
- this.setState({testResult: this.props.testResult});
- }
- }
-
- onClassificationToggle = () => {
- this.setState({isClassificationOpen: !this.state.isClassificationOpen});
+ setTestResult(updatedResult);
+ setclassificationOpen(!classificationOpen);
+ HttpClient.put([Settings.serverUrl, 'result', testResult['id']], {}, updatedResult)
+ .then(console.log('put classification'))
+ .catch(error => console.error(error));
};
- onClassificationSelect = (_event, selection) => {
- let testResult = this.state.testResult;
- testResult['metadata']['classification'] = selection;
- this.setState({testResult: testResult, isClassificationOpen: !this.state.isClassificationOpen});
- HttpClient.put([Settings.serverUrl, 'result', testResult['id']], {}, testResult);
- };
+ useEffect(()=>{
+ setTestResult(props.testResult);
+ }, [props.testResult]);
- render () {
- const testResult = this.state.testResult;
- return (
- this.setState({isClassificationOpen: false})}
- toggle={toggleRef => (
-
- {CLASSIFICATION[testResult.metadata && testResult.metadata.classification] || '(unset)'}
-
- )}
- >
-
- {Object.keys(CLASSIFICATION).map((key) => (
-
- {CLASSIFICATION[key]}
-
- ))}
-
-
- );
- }
-}
+ return (
+ setclassificationOpen(false)}
+ toggle={toggleRef => (
+ setclassificationOpen(!classificationOpen)}
+ isExpanded={classificationOpen}
+ >
+ {CLASSIFICATION[testResult?.metadata?.classification] || '(unset)'}
+
+ )}
+ >
+
+ {Object.keys(CLASSIFICATION).map((key) => (
+
+ {CLASSIFICATION[key]}
+
+ ))}
+
+
+ );
+};
-export class MultiClassificationDropdown extends React.Component {
- static propTypes = {
- selectedResults: PropTypes.array,
- refreshFunc: PropTypes.func
- };
+ClassificationDropdown.propTypes = {
+ testResult: PropTypes.object,
+};
- constructor (props) {
- super(props);
- this.state = {
- isClassificationOpen: false
- };
- }
- onClassificationToggle = isOpen => {
- this.setState({isClassificationOpen: isOpen});
- };
+const MultiClassificationDropdown = (props) => {
+ // TODO: callback to trigger re-render of the classify failures page
+ const {
+ selectedResults,
+ } = props;
- onClassificationSelect = event => {
- const { selectedResults } = this.props;
- let classification = event.target.getAttribute('value');
+ const [classificationOpen, setclassificationOpen] = useState(false);
+
+ const onClassificationSelect = (_event, selection) => {
if (selectedResults.length === 0) {
- this.setState({isClassificationOpen: !this.state.isClassificationOpen});
+ setclassificationOpen(false);
}
else {
selectedResults.forEach(result => {
- result['metadata']['classification'] = classification;
+ result['metadata']['classification'] = selection;
HttpClient.put([Settings.serverUrl, 'result', result['id']], {}, result)
- .then(this.props.refreshFunc());
+ .catch(error => console.error('Error setting classification: ' + error));
});
- this.setState({isClassificationOpen: !this.state.isClassificationOpen});
+ setclassificationOpen(false);
}
};
+ return (
+ setclassificationOpen(false)}
+ toggle={toggleRef => (
+ setclassificationOpen(!classificationOpen)}
+ isExpanded={classificationOpen}
+ >
+ Classify Selected Failures
+
+ )}
+ >
+
+ {Object.keys(CLASSIFICATION).map((key) => (
+
+ {CLASSIFICATION[key]}
+
+ ))}
+
+
+ );
+};
+
+MultiClassificationDropdown.propTypes = {
+ selectedResults: PropTypes.array,
+};
- render () {
- const { selectedResults } = this.props;
- return (
- this.setState({isClassificationOpen: false})}
- toggle={toggleRef => (
-
- Classify Selected Failures
-
- )}
- >
-
- {Object.keys(CLASSIFICATION).map((key) => (
-
- {CLASSIFICATION[key]}
-
- ))}
-
-
- );
- }
-}
+export {ClassificationDropdown, MultiClassificationDropdown};
diff --git a/frontend/src/components/classify-failures.js b/frontend/src/components/classify-failures.js
index e72b20ae..8a2eaa39 100644
--- a/frontend/src/components/classify-failures.js
+++ b/frontend/src/components/classify-failures.js
@@ -1,8 +1,7 @@
-import React from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import {
- Button,
Card,
CardHeader,
CardBody,
@@ -10,7 +9,8 @@ import {
Flex,
FlexItem,
TextContent,
- Text,
+ Title,
+ Badge,
} from '@patternfly/react-core';
import {
TableVariant,
@@ -20,56 +20,126 @@ import {
import { HttpClient } from '../services/http';
import { Settings } from '../settings';
import {
- buildParams,
toAPIFilter,
getSpinnerRow,
- resultToClassificationRow,
+ getIconForResult,
+ buildBadge,
+ generateId,
+ toTitleCase,
+ round,
} from '../utilities';
-import { MultiClassificationDropdown } from './classification-dropdown';
-import { FilterTable, MetaFilter } from './filtertable';
+import { ClassificationDropdown, MultiClassificationDropdown } from './classification-dropdown';
+import FilterTable from './filtertable';
+import MetaFilter from './metafilter';
import ResultView from './result';
+import { Link } from 'react-router-dom';
+const COLUMNS = [
+ {
+ title: 'Test',
+ cellFormatters: [expandable]
+ },
+ 'Result',
+ 'Exception Name',
+ 'Classification',
+ 'Duration'
+];
-export class ClassifyFailuresTable extends React.Component {
- static propTypes = {
- filters: PropTypes.object,
- run_id: PropTypes.string
- };
+const resultToClassificationRow = (result, index, filterFunc) => {
+ let resultIcon = getIconForResult(result.result);
+ let markers = [];
+ let exceptionBadge;
- constructor (props) {
- super(props);
- this.state = {
- columns: [{title: 'Test', cellFormatters: [expandable]}, 'Result', 'Exception Name', 'Classification', 'Duration'],
- rows: [getSpinnerRow(5)],
- results: [],
- selectedResults: [],
- cursor: null,
- pageSize: 10,
- page: 1,
- totalItems: 0,
- totalPages: 0,
- isEmpty: false,
- isError: false,
- isFieldOpen: false,
- isOperationOpen: false,
- includeSkipped: false,
- filters: Object.assign({
- 'result': {op: 'in', val: 'failed;error'},
- 'run_id': {op: 'eq', val: props.run_id}}, props.filters),
- };
- this.refreshResults = this.refreshResults.bind(this);
- this.onCollapse = this.onCollapse.bind(this);
+ if (filterFunc) {
+ exceptionBadge = buildBadge(`exception_name-${result.id}`, result.metadata.exception_name, false,
+ () => filterFunc('metadata.exception_name', result.metadata.exception_name));
+ }
+ else {
+ exceptionBadge = buildBadge(`exception_name-${result.id}`, result.metadata.exception_name, false);
}
- refreshResults = () => {
- this.setState({selectedResults: []});
- this.getResultsForTable();
- };
+ if (result.metadata && result.metadata.component) {
+ markers.push({result.metadata.component});
+ }
+ if (result.metadata && result.metadata.markers) {
+ for (const marker of result.metadata.markers) {
+ // Don't add duplicate markers
+ if (markers.filter(m => m.key === marker.name).length === 0) {
+ markers.push({marker.name});
+ }
+ }
+ }
- onCollapse (event, rowIndex, isOpen) {
- const { rows } = this.state;
+ return [
+ // parent row
+ {
+ 'isOpen': false,
+ 'result': result,
+ 'cells': [
+ {title: {result.test_id} {markers}},
+ {title: {resultIcon} {toTitleCase(result.result)}},
+ {title: {exceptionBadge}},
+ {title: },
+ {title: round(result.duration) + 's'},
+ ],
+ },
+ // child row (this is set in the onCollapse function for lazy-loading)
+ {
+ 'parent': 2*index,
+ 'cells': [{title:
}]
+ }
+ ];
+};
+
+const ClassifyFailuresTable = (props) => {
+ const {
+ filters,
+ run_id,
+ } = props;
+
+ const [rows, setRows] = useState([getSpinnerRow(5)]);
+ const [filteredResults, setFilteredResults] = useState([]);
+ const [selectedResults, setSelectedResults] = useState([]);
+
+ const [pageSize, setPageSize] = useState(10);
+ const [page, setPage] = useState(1);
+ const [totalItems, setTotalItems] = useState(0);
- // lazy-load the result view so we don't have to make a bunch of artifact requests
+ const [isError, setIsError] = useState(false);
+ const [includeSkipped, setIncludeSkipped] = useState(false);
+ const [appliedFilters, setAppliedFilters] = useState({
+ ...filters,
+ 'result': {op: 'in', val: 'failed;error'},
+ 'run_id': {op: 'eq', val: props.run_id}}
+ );
+
+ // Fetch and set filteredResults on filter and pagination updates
+ useEffect(()=> {
+ setIsError(false);
+
+ // get only filtered results
+ HttpClient.get([Settings.serverUrl, 'result'], {
+ filter: toAPIFilter(appliedFilters),
+ pageSize: pageSize,
+ page: page
+ })
+ .then(response => HttpClient.handleResponse(response))
+ .then(data => {
+ setFilteredResults(data.results);
+ setPage(data.pagination.page);
+ setPageSize(data.pagination.pageSize);
+ setTotalItems(data.pagination.totalItems);
+ })
+ .catch((error) => {
+ console.error('Error fetching result data:', error);
+ setFilteredResults([]);
+ setIsError(true);
+ });
+ }, [page, pageSize, appliedFilters]);
+
+
+ const onCollapse = (_event, rowIndex, isOpen) => {
+ // handle row click opening the child row with ResultView
if (isOpen) {
let result = rows[rowIndex].result;
let hideSummary=true;
@@ -80,184 +150,173 @@ export class ClassifyFailuresTable extends React.Component {
hideTestObject=false;
defaultTab='summary';
}
- rows[rowIndex + 1].cells = [{
- title:
- }];
- }
- rows[rowIndex].isOpen = isOpen;
- this.setState({rows});
- }
- onTableRowSelect = (event, isSelected, rowId) => {
- let rows;
- if (rowId === -1) {
- rows = this.state.rows.map(oneRow => {
- oneRow.selected = isSelected;
- return oneRow;
+ const updatedRows = rows.map((row, index) => {
+ let newRow = {};
+ // set isOpen on the parent row items
+ if (index === rowIndex) {
+ newRow = {...row, isOpen: isOpen};
+ } else if (index === (rowIndex + 1)) {
+ // set the ResultView on the child rows
+ newRow = {
+ ...row,
+ cells: [
+ {title:
+ }
+ ]
+ };
+ } else {
+ newRow = {...row};
+ }
+ return newRow;
+ });
+ setRows(updatedRows);
+ } else {
+ // handle updating isOpen on the clicked row (closing)
+ setRows(prevRows => {
+ const updatedRows = [...prevRows];
+ if (updatedRows[rowIndex]) {
+ updatedRows[rowIndex] = {...updatedRows[rowIndex], isOpen: isOpen};
+ }
+ return updatedRows;
});
}
- else {
- rows = [...this.state.rows];
- rows[rowId].selected = isSelected;
- }
- this.setState({
- rows,
- });
- this.getSelectedResults();
+
};
- setPage = (_event, pageNumber) => {
- this.setState({page: pageNumber}, () => {
- this.getResultsForTable();
- });
+ const onTableRowSelect = (_event, isSelected, rowId) => {
+ // either set every row or single row selected state
+ let mutatedRows = rows.map(
+ (oneRow, index) => {
+ if((index === rowId) || (rowId === -1)) {oneRow.selected = isSelected;}
+ return oneRow;
+ }
+ );
+ setRows(mutatedRows);
+
+ // filter to only pick parent rows
+ let resultsToSelect = mutatedRows.filter(
+ (oneRow) => (oneRow.selected && oneRow.parent === undefined)
+ );
+ // map again to pull the result out of the row
+ resultsToSelect = resultsToSelect.map(
+ (oneRow) => oneRow.result
+ );
+ setSelectedResults(resultsToSelect);
};
- pageSizeSelect = (_event, perPage) => {
- this.setState({pageSize: perPage}, () => {
- this.getResultsForTable();
+ const onSkipCheck = (checked) => {
+ setIncludeSkipped(checked);
+ setAppliedFilters({
+ ...appliedFilters,
+ 'result': {
+ ...appliedFilters.result,
+ 'val': ('failed;error') + ((checked) ? ';skipped;xfailed' : '')
+ }
});
};
- updateFilters (filterId, name, operator, value, callback) {
- let filters = this.state.filters;
+ // METAFILTER FUNCTIONS
+ const updateFilters = useCallback((_filterId, name, operator, value) => {
+ let localFilters = {...appliedFilters};
if ((value === null) || (value.length === 0)) {
- delete filters[name];
+ delete localFilters[name];
}
else {
- filters[name] = {'op': operator, 'val': value};
+ localFilters[name] = {'op': operator, 'val': value};
}
- this.setState({filters: filters, page: 1}, callback);
- }
- setFilter = (filterId, field, value) => {
+ setAppliedFilters(localFilters);
+ setPage(1);
+ }, [appliedFilters]);
+
+ const setFilter = useCallback((filterId, field, value) => {
// maybe process values array to string format here instead of expecting caller to do it?
+ // TODO when value is an object (params field?) .includes blows up
let operator = (value.includes(';')) ? 'in' : 'eq';
- this.updateFilters(filterId, field, operator, value, this.refreshResults);
- };
+ updateFilters(filterId, field, operator, value);
+ }, [updateFilters]);
- removeFilter = (filterId, id) => {
+ const removeFilter = (filterId, id) => {
if ((id !== 'result') && (id !== 'run_id')) { // Don't allow removal of error/failure filter
- this.updateFilters(filterId, id, null, null, this.refreshResults);
+ updateFilters(filterId, id, null, null);
}
};
- onSkipCheck = (checked) => {
- let { filters } = this.state;
- filters['result']['val'] = ('failed;error') + ((checked) ? ';skipped;xfailed' : '');
- this.setState(
- {includeSkipped: checked, filters},
- this.refreshResults
- );
- };
-
- getSelectedResults = () => {
- const { results, rows } = this.state;
- let selectedResults = [];
- for (const [index, row] of rows.entries()) {
- if (row.selected && row.parent === null) { // rows with a parent attr are the child rows
- selectedResults.push(results[index / 2]); // divide by 2 to convert row index to result index
- }
- }
- this.setState({selectedResults});
- };
+ const resultFilters = [
+ ,
+ ];
- getResultsForTable () {
- const filters = this.state.filters;
- this.setState({rows: [getSpinnerRow(5)], isEmpty: false, isError: false});
- // get only failed results
- let params = buildParams(filters);
- params['filter'] = toAPIFilter(filters);
- params['pageSize'] = this.state.pageSize;
- params['page'] = this.state.page;
- this.setState({rows: [['Loading...', '', '', '', '']]});
- HttpClient.get([Settings.serverUrl, 'result'], params)
- .then(response => HttpClient.handleResponse(response))
- .then(data => this.setState({
- results: data.results,
- rows: data.results.map((result, index) => resultToClassificationRow(result, index, this.setFilter)).flat(),
- page: data.pagination.page,
- pageSize: data.pagination.pageSize,
- totalItems: data.pagination.totalItems,
- totalPages: data.pagination.totalPages,
- isEmpty: data.pagination.totalItems === 0,
- }))
- .catch((error) => {
- console.error('Error fetching result data:', error);
- this.setState({rows: [], isEmpty: false, isError: true});
- });
- }
+ useEffect(() => {
+ // set rows when filtered items update
+ setRows(
+ filteredResults.flatMap(
+ (result, index) => resultToClassificationRow(result, index, setFilter)
+ )
+ );
+ }, [filteredResults, setFilter]);
- componentDidMount () {
- this.getResultsForTable();
- }
+ return (
+ // mt-lg == margin top large
+
+
+
+
+
+ Test Failures
+
+
+
+
+ onSkipCheck(checked)}/>
+
+
+
+
+
+
+
+
+ setPage(change)}
+ onSetPageSize={(_event, change) => setPageSize(change)}
+ canSelectAll={true}
+ onRowSelect={onTableRowSelect}
+ variant={TableVariant.compact}
+ filters={resultFilters}
+ />
+
+
+ );
+};
- render () {
- const {
- columns,
- rows,
- selectedResults,
- includeSkipped,
- 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 metadata
- const resultFilters = [
- ,
- ];
- return (
-
-
-
-
-
- Test Failures
-
-
-
-
- this.onSkipCheck(checked)}/>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
- }
+ClassifyFailuresTable.propTypes = {
+ filters: PropTypes.object,
+ run_id: PropTypes.string
+};
-}
+export default ClassifyFailuresTable;
diff --git a/frontend/src/components/download-button.js b/frontend/src/components/download-button.js
index 26e91ddf..54a96365 100644
--- a/frontend/src/components/download-button.js
+++ b/frontend/src/components/download-button.js
@@ -5,8 +5,7 @@ import { Button } from '@patternfly/react-core';
import { HttpClient } from '../services/http';
-
-export function DownloadButton (props) {
+const DownloadButton = (props) => {
let { url, filename, children, ...rest } = props;
if (!filename) {
filename = url.split('/').pop();
@@ -27,10 +26,12 @@ export function DownloadButton (props) {
};
return ;
-}
+};
DownloadButton.propTypes = {
url: PropTypes.string,
filename: PropTypes.string,
children: PropTypes.node
};
+
+export default DownloadButton;
diff --git a/frontend/src/components/filtertable.js b/frontend/src/components/filtertable.js
index 624bbec0..01e76726 100644
--- a/frontend/src/components/filtertable.js
+++ b/frontend/src/components/filtertable.js
@@ -1,4 +1,4 @@
-import React, { useContext, useEffect, useState } from 'react';
+import React from 'react';
import PropTypes from 'prop-types';
import {
@@ -11,23 +11,16 @@ import {
Pagination,
PaginationVariant
} from '@patternfly/react-core';
-import {
- Select,
- SelectOption,
- SelectVariant
-} from '@patternfly/react-core/deprecated';
+
+
import {
Table,
TableBody,
TableHeader
} from '@patternfly/react-table/deprecated';
-import { Settings } from '../settings';
-import { HttpClient } from '../services/http';
-import { toAPIFilter } from '../utilities';
import {TableEmptyState, TableErrorState} from './tablestates';
-import { IbutsuContext } from '../services/context';
const FilterTable = (props) => {
@@ -168,206 +161,4 @@ FilterTable.propTypes = {
variant: PropTypes.node
};
-
-// TODO Extend this to contain the filter handling functions, and better integrate filter state
-// with FilterTable. See https://github.com/ibutsu/ibutsu-server/issues/230
-const MetaFilter = (props) => {
- const {
- setFilter,
- activeFilters,
- hideFilters,
- onRemoveFilter,
- onApplyReport,
- runId,
- id
- } = props;
- const context = useContext(IbutsuContext);
- const {primaryObject} = context;
-
- const [fieldSelection, setFieldSelection] = useState([]);
- const [isFieldOpen, setisFieldOpen] = useState(false);
- const [isValueOpen, setIsValueOpen] = useState(false);
- const [valueOptions, setValueOptions] = useState([]);
- const [valueSelections, setValueSelections] = useState([]);
- const [fieldOptions, setFieldOptions] = useState([]);
-
- const onFieldSelect = (event, selection) => {
- // clear value state too, otherwise the old selection remains selected but is no longer visible
- setFieldSelection(selection);
- setisFieldOpen(false);
- setValueSelections([]);
- setValueOptions([]);
- setIsValueOpen(false);
-
- updateValueOptions();
- };
-
- const onValueSelect = (event, selection) => {
- // update state and call setFilter
- const valueSelections = valueSelections;
- let updatedValues = (valueSelections.includes(selection))
- ? valueSelections.filter(item => item !== selection)
- : [...valueSelections, selection];
-
- setValueSelections(updatedValues);
- setFilter(id, fieldSelection, valueSelections.join(';'));
- };
-
- const onFieldClear = () => {
- setFieldSelection([]);
- setisFieldOpen(false);
- setIsValueOpen(false);
- setValueOptions([]);
- setValueSelections([]);
- };
-
- const onValueClear = () => {
- setValueSelections([]);
- setIsValueOpen(false);
- setFilter(id, fieldSelection, []);
- };
-
- const updateValueOptions = () => {
- const customFilters = activeFilters;
- console.debug('CUSTOMFILTER: ' + customFilters);
-
- if (fieldSelection !== null) {
- let api_filter = toAPIFilter(customFilters).join();
- console.debug('APIFILTER: ' + customFilters);
-
- let projectId = primaryObject ? primaryObject.id : '';
-
- // make runId optional
- let params = {};
- if (runId) {
- params = {
- group_field: fieldSelection,
- run_id: runId,
- additional_filters: api_filter,
- project: projectId
- };
- } else {
- params = {
- days: 30,
- project: projectId,
- group_field: fieldSelection,
- additional_filters: api_filter,
- };
- }
-
- HttpClient.get(
- [Settings.serverUrl, 'widget', 'result-aggregator'],
- params
- )
- .then(response => HttpClient.handleResponse(response))
- .then(data => {
- setValueOptions(data);
- });
- }
- };
-
- useEffect(() => {
- HttpClient.get([Settings.serverUrl, 'project', 'filter-params', primaryObject.id])
- .then(response => HttpClient.handleResponse(response))
- .then(data => {
- setFieldOptions(data);
- });
- }, [primaryObject, setFieldOptions]);
-
- let field_selected = 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 (
-
-
-
-
-
-
-
- {Object.keys(activeFilters).length > 0 &&
-
-
-
- Active filters
-
-
-
- {Object.keys(activeFilters).map(key => (
-
- {!hideFilters.includes(key) &&
-
- {activeFilters[key]['op']}} onClick={() => onRemoveFilter(id, key)}>
- {(typeof activeFilters[key] === 'object') &&
-
- {activeFilters[key]['val']}
-
- }
- {(typeof activeFilters[key] !== 'object') && activeFilters[key]}
-
-
- }
-
- ))}
-
- {onApplyReport &&
-
-
-
-
-
- }
-
- }
-
- );
-};
-
-MetaFilter.propTypes = {
- runId: PropTypes.string,
- setFilter: PropTypes.func,
- customFilters: PropTypes.object, // more advanced handling of filter objects? the results-aggregator endpoint takes a string filter
- onRemoveFilter: PropTypes.func,
- onApplyReport: PropTypes.func,
- hideFilters: PropTypes.array,
- activeFilters: PropTypes.object,
- id: PropTypes.number,
-};
-
-export { FilterTable, MetaFilter };
+export default FilterTable;
diff --git a/frontend/src/components/index.js b/frontend/src/components/index.js
index 55036237..7e7b51a9 100644
--- a/frontend/src/components/index.js
+++ b/frontend/src/components/index.js
@@ -1,4 +1 @@
-export { ClassificationDropdown, MultiClassificationDropdown } from './classification-dropdown';
-export { ClassifyFailuresTable } from './classify-failures';
-export { DownloadButton } from './download-button';
export { IbutsuPage } from './ibutsu-page';
diff --git a/frontend/src/components/metafilter.js b/frontend/src/components/metafilter.js
new file mode 100644
index 00000000..6418dbf0
--- /dev/null
+++ b/frontend/src/components/metafilter.js
@@ -0,0 +1,205 @@
+import React, { useContext, useEffect, useState } from 'react';
+import PropTypes from 'prop-types';
+
+import {
+ Badge,
+ Button,
+ Chip,
+ ChipGroup,
+ Flex,
+ FlexItem,
+} from '@patternfly/react-core';
+import {
+ Select,
+ SelectOption,
+ SelectVariant
+} from '@patternfly/react-core/deprecated';
+
+
+import { Settings } from '../settings';
+import { HttpClient } from '../services/http';
+import { toAPIFilter } from '../utilities';
+
+import { IbutsuContext } from '../services/context';
+
+// TODO Extend this to contain the filter handling functions, and better integrate filter state
+// with FilterTable. See https://github.com/ibutsu/ibutsu-server/issues/230
+const MetaFilter = (props) => {
+ const {
+ setFilter,
+ activeFilters,
+ hideFilters,
+ onRemoveFilter,
+ onApplyReport,
+ runId,
+ id
+ } = props;
+ const context = useContext(IbutsuContext);
+ const {primaryObject} = context;
+
+ const [fieldSelection, setFieldSelection] = useState([]);
+ const [isFieldOpen, setIsFieldOpen] = useState(false);
+ const [isValueOpen, setIsValueOpen] = useState(false);
+
+ const [valueOptions, setValueOptions] = useState([]);
+ const [fieldOptions, setFieldOptions] = useState([]);
+
+ const onFieldSelect = (_event, selection) => {
+ // clear value state too, otherwise the old selection remains selected but is no longer visible
+ setFieldSelection(selection);
+ setIsFieldOpen(false);
+ setValueOptions([]);
+ setIsValueOpen(false);
+ };
+
+ const onValueSelect = (_event, selection) => {
+ // update state and call setFilter
+ setFilter(id, fieldSelection, selection);
+ onFieldClear();
+ };
+
+ const onFieldClear = () => {
+ setFieldSelection([]);
+ setIsFieldOpen(false);
+ setIsValueOpen(false);
+ setValueOptions([]);
+ };
+
+ const onValueClear = () => {
+ setIsValueOpen(false);
+ setFilter(id, fieldSelection, []);
+ };
+
+ useEffect(() => {
+ // fetch the available values for the selected meta field
+ if (fieldSelection.length !== 0) {
+ let api_filter = toAPIFilter(activeFilters).join();
+
+ let projectId = primaryObject ? primaryObject.id : '';
+
+ // make runId optional
+ let params = {
+ group_field: fieldSelection,
+ additional_filters: api_filter,
+ project: projectId
+ };
+ if (runId) {
+ params['run_id'] = runId;
+ } else {
+ params['days'] = 30;
+ }
+
+ HttpClient.get(
+ [Settings.serverUrl, 'widget', 'result-aggregator'],
+ params
+ )
+ .then(response => HttpClient.handleResponse(response))
+ .then(data => {
+ setValueOptions(data);
+ });
+ }
+ }, [fieldSelection, activeFilters, primaryObject, runId]);
+
+ useEffect(() => {
+ // Fetch field options for the project
+ HttpClient.get([Settings.serverUrl, 'project', 'filter-params', primaryObject.id])
+ .then(response => HttpClient.handleResponse(response))
+ .then(data => {
+ setFieldOptions(data);
+ });
+ }, [primaryObject.id]);
+
+ let values_available = valueOptions.length > 0;
+ const valuePlaceholder = !fieldSelection.length ?
+ 'Select a field first' : // default instead of an else block
+ values_available ?
+ 'Select value(s)' :
+ 'No values for selected field';
+
+ return (
+
+
+
+
+
+
+
+ {Object.keys(activeFilters).length > 0 &&
+
+
+
+ Active filters
+
+
+
+ {Object.keys(activeFilters).map(key => (
+
+ {!hideFilters.includes(key) &&
+
+ {activeFilters[key]['op']}} onClick={(id,) => onRemoveFilter(id, key)}>
+ {(typeof activeFilters[key] === 'object') &&
+
+ {activeFilters[key]['val']}
+
+ }
+ {(typeof activeFilters[key] !== 'object') && activeFilters[key]}
+
+
+ }
+
+ ))}
+
+ {onApplyReport &&
+
+
+
+
+
+ }
+
+ }
+
+ );
+};
+
+MetaFilter.propTypes = {
+ runId: PropTypes.string,
+ setFilter: PropTypes.func,
+ onRemoveFilter: PropTypes.func,
+ onApplyReport: PropTypes.func,
+ hideFilters: PropTypes.array,
+ activeFilters: PropTypes.object,
+ id: PropTypes.number,
+};
+
+export default MetaFilter;
diff --git a/frontend/src/components/test-history.js b/frontend/src/components/test-history.js
index 3e75f616..753495a1 100644
--- a/frontend/src/components/test-history.js
+++ b/frontend/src/components/test-history.js
@@ -29,7 +29,8 @@ import {
resultToTestHistoryRow,
} from '../utilities';
-import { FilterTable } from './filtertable';
+import FilterTable from './filtertable';
+
import RunSummary from './runsummary';
import LastPassed from './last-passed';
import ResultView from './result';
diff --git a/frontend/src/pages/admin/project-list.js b/frontend/src/pages/admin/project-list.js
index 12afdad5..732db628 100644
--- a/frontend/src/pages/admin/project-list.js
+++ b/frontend/src/pages/admin/project-list.js
@@ -19,7 +19,7 @@ import { Link } from 'react-router-dom';
import { HttpClient } from '../../services/http';
import { Settings } from '../../settings';
import { getSpinnerRow } from '../../utilities';
-import { FilterTable } from '../../components/filtertable';
+import FilterTable from '../../components/filtertable';
import EmptyObject from '../../components/empty-object';
const COLUMNS = ['Title', 'Name', 'Owner', ''];
diff --git a/frontend/src/pages/admin/user-list.js b/frontend/src/pages/admin/user-list.js
index 5622a903..d7430883 100644
--- a/frontend/src/pages/admin/user-list.js
+++ b/frontend/src/pages/admin/user-list.js
@@ -18,7 +18,7 @@ import { Link } from 'react-router-dom';
import { HttpClient } from '../../services/http';
import { Settings } from '../../settings';
import { getSpinnerRow } from '../../utilities';
-import { FilterTable } from '../../components/filtertable';
+import FilterTable from '../../components/filtertable';
const COLUMNS = ['Display Name', 'Email', 'Projects', 'Status', ''];
diff --git a/frontend/src/pages/profile/tokens.js b/frontend/src/pages/profile/tokens.js
index ae8de9ba..b24f09a4 100644
--- a/frontend/src/pages/profile/tokens.js
+++ b/frontend/src/pages/profile/tokens.js
@@ -18,7 +18,7 @@ import { PlusCircleIcon } from '@patternfly/react-icons';
import { HttpClient } from '../../services/http';
import { Settings } from '../../settings';
import { getSpinnerRow } from '../../utilities';
-import { FilterTable } from '../../components/filtertable';
+import FilterTable from '../../components/filtertable';
import AddTokenModal from '../../components/add-token-modal';
import DeleteModal from '../../components/delete-modal';
import { ALERT_TIMEOUT } from '../../constants';
diff --git a/frontend/src/report-builder.js b/frontend/src/report-builder.js
index ea66d59d..368414a3 100644
--- a/frontend/src/report-builder.js
+++ b/frontend/src/report-builder.js
@@ -31,8 +31,8 @@ import {
parseFilter,
getSpinnerRow,
} from './utilities';
-import { DownloadButton } from './components';
-import {FilterTable} from './components/filtertable';
+import DownloadButton from './components/download-button';
+import FilterTable from './components/filtertable';
import { OPERATIONS } from './constants';
import { IbutsuContext } from './services/context';
import { useLocation } from 'react-router-dom';
diff --git a/frontend/src/result-list.js b/frontend/src/result-list.js
index 17313da1..824669aa 100644
--- a/frontend/src/result-list.js
+++ b/frontend/src/result-list.js
@@ -36,7 +36,7 @@ import {
resultToRow
} from './utilities';
import MultiValueInput from './components/multivalueinput';
-import { FilterTable } from './components/filtertable';
+import FilterTable from './components/filtertable';
import { OPERATIONS, RESULT_FIELDS } from './constants';
import { IbutsuContext } from './services/context';
diff --git a/frontend/src/run-list.js b/frontend/src/run-list.js
index 78b619df..eaed9a21 100644
--- a/frontend/src/run-list.js
+++ b/frontend/src/run-list.js
@@ -36,7 +36,7 @@ import {
round
} from './utilities';
-import { FilterTable } from './components/filtertable';
+import FilterTable from './components/filtertable';
import MultiValueInput from './components/multivalueinput';
import RunSummary from './components/runsummary';
diff --git a/frontend/src/run.js b/frontend/src/run.js
index 393e233f..e523e099 100644
--- a/frontend/src/run.js
+++ b/frontend/src/run.js
@@ -60,14 +60,12 @@ import {
resultToRow,
round
} from './utilities';
-import {
- DownloadButton,
- ClassifyFailuresTable,
-} from './components';
+import DownloadButton from './components/download-button';
import EmptyObject from './components/empty-object';
-import { FilterTable } from './components/filtertable';
+import FilterTable from './components/filtertable';
import ResultView from './components/result';
import TabTitle from './components/tabs';
+import ClassifyFailuresTable from './components/classify-failures';
const MockRun = {
id: null,
diff --git a/frontend/src/utilities.js b/frontend/src/utilities.js
index 84653181..361592aa 100644
--- a/frontend/src/utilities.js
+++ b/frontend/src/utilities.js
@@ -30,7 +30,6 @@ import {
NUMERIC_RUN_FIELDS,
THEME_KEY,
} from './constants';
-import { ClassificationDropdown } from './components';
export function getDateString () {
return String((new Date()).getTime());
@@ -216,52 +215,6 @@ export function resultToRow (result, filterFunc) {
};
}
-export function resultToClassificationRow (result, index, filterFunc) {
- let resultIcon = getIconForResult(result.result);
- let markers = [];
- let exceptionBadge;
-
- if (filterFunc) {
- exceptionBadge = buildBadge(`exception_name-${result.id}`, result.metadata.exception_name, false,
- () => filterFunc('metadata.exception_name', result.metadata.exception_name));
- }
- else {
- exceptionBadge = buildBadge(`exception_name-${result.id}`, result.metadata.exception_name, false);
- }
-
- if (result.metadata && result.metadata.component) {
- markers.push({result.metadata.component});
- }
- if (result.metadata && result.metadata.markers) {
- for (const marker of result.metadata.markers) {
- // Don't add duplicate markers
- if (markers.filter(m => m.key === marker.name).length === 0) {
- markers.push({marker.name});
- }
- }
- }
-
- return [
- // parent row
- {
- 'isOpen': false,
- 'result': result,
- 'cells': [
- {title: {result.test_id} {markers}},
- {title: {resultIcon} {toTitleCase(result.result)}},
- {title: {exceptionBadge}},
- {title: },
- {title: round(result.duration) + 's'},
- ],
- },
- // child row (this is set in the onCollapse function for lazy-loading)
- {
- 'parent': 2*index,
- 'cells': [{title: }]
- }
- ];
-}
-
export function resultToComparisonRow (result, index) {
let resultIcons = [];
let markers = [];
diff --git a/frontend/src/views/accessibilityanalysis.js b/frontend/src/views/accessibilityanalysis.js
index 3c8feb22..80f05cb0 100644
--- a/frontend/src/views/accessibilityanalysis.js
+++ b/frontend/src/views/accessibilityanalysis.js
@@ -36,7 +36,7 @@ import {
getSpinnerRow,
resultToRow,
} from '../utilities';
-import { FilterTable } from '../components/filtertable';
+import FilterTable from '../components/filtertable';
import { IbutsuContext } from '../services/context';
import TabTitle from '../components/tabs';
const MockRun = {
diff --git a/frontend/src/views/accessibilitydashboard.js b/frontend/src/views/accessibilitydashboard.js
index 49d88e63..10916bb0 100644
--- a/frontend/src/views/accessibilitydashboard.js
+++ b/frontend/src/views/accessibilitydashboard.js
@@ -30,7 +30,7 @@ import {
getSpinnerRow,
parseFilter
} from '../utilities';
-import { FilterTable } from '../components/filtertable';
+import FilterTable from '../components/filtertable';
import MultiValueInput from '../components/multivalueinput';
import RunSummary from '../components/runsummary';
import { OPERATIONS, ACCESSIBILITY_FIELDS } from '../constants';
diff --git a/frontend/src/views/compareruns.js b/frontend/src/views/compareruns.js
index 8a8f17b4..1fe9b7b0 100644
--- a/frontend/src/views/compareruns.js
+++ b/frontend/src/views/compareruns.js
@@ -17,7 +17,8 @@ import {
expandable
} from '@patternfly/react-table';
-import { FilterTable, MetaFilter } from '../components/filtertable';
+import FilterTable from '../components/filtertable';
+import MetaFilter from '../components/metafilter';
import { HttpClient } from '../services/http';
import { Settings } from '../settings';
import {
diff --git a/frontend/src/views/jenkinsjob.js b/frontend/src/views/jenkinsjob.js
index 6ac17c08..b7eecd98 100644
--- a/frontend/src/views/jenkinsjob.js
+++ b/frontend/src/views/jenkinsjob.js
@@ -29,7 +29,7 @@ import {
parseFilter
} from '../utilities';
-import { FilterTable } from '../components/filtertable';
+import FilterTable from '../components/filtertable';
import MultiValueInput from '../components/multivalueinput';
import RunSummary from '../components/runsummary';
import { OPERATIONS, JJV_FIELDS } from '../constants';