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';