Skip to content

Commit

Permalink
Add test history tab to Result page
Browse files Browse the repository at this point in the history
  • Loading branch information
john-dupuy committed Jan 12, 2022
1 parent b3f7fe9 commit f8ed19e
Show file tree
Hide file tree
Showing 3 changed files with 307 additions and 4 deletions.
25 changes: 22 additions & 3 deletions frontend/src/components/result.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
Tabs,
Tab
} from '@patternfly/react-core';
import { FileAltIcon, FileImageIcon, InfoCircleIcon, CodeIcon } from '@patternfly/react-icons';
import { FileAltIcon, FileImageIcon, InfoCircleIcon, CodeIcon, SearchIcon } from '@patternfly/react-icons';
import { Link } from 'react-router-dom';
import Linkify from 'react-linkify';
import ReactJson from 'react-json-view';
Expand All @@ -29,6 +29,7 @@ import { linkifyDecorator } from './decorators'
import { Settings } from '../settings';
import { getIconForResult, round } from '../utilities';
import { TabTitle } from './tabs';
import { TestHistoryTable } from './test-history';

const MockTest = {
id: null,
Expand Down Expand Up @@ -60,6 +61,7 @@ export class ResultView extends React.Component {
resultId: PropTypes.string,
hideSummary: PropTypes.bool,
hideTestObject: PropTypes.bool,
hideTestHistory: PropTypes.bool,
history: PropTypes.object,
location: PropTypes.object
}
Expand All @@ -71,7 +73,8 @@ export class ResultView extends React.Component {
id: this.props.resultId || null,
artifacts: [],
activeTab: this.getTabIndex(this.getDefaultTab()),
artifactTabs: []
artifactTabs: [],
testHistoryTable: null,
};
if (this.props.history) {
// Watch the history to update tabs
Expand Down Expand Up @@ -103,6 +106,12 @@ export class ResultView extends React.Component {
}
}

updateTab(tabIndex) {
if (tabIndex === 'test-history') {
this.getTestHistoryTable();
}
}

onTabSelect = (event, tabIndex) => {
if (this.props.history) {
const loc = this.props.history.location;
Expand All @@ -113,8 +122,13 @@ export class ResultView extends React.Component {
});
}
this.setState({activeTab: tabIndex});
this.updateTab(tabIndex);
};

getTestHistoryTable = () => {
this.setState({testHistoryTable: <TestHistoryTable testResult={this.state.testResult}/>});
}

getTestResult(resultId) {
HttpClient.get([Settings.serverUrl, 'result', resultId])
.then(response => HttpClient.handleResponse(response))
Expand Down Expand Up @@ -200,7 +214,7 @@ export class ResultView extends React.Component {
}

render() {
let { testResult, artifactTabs, activeTab } = this.state;
let { testResult, artifactTabs, activeTab, testHistoryTable } = this.state;
if (activeTab === null) {
activeTab = this.getDefaultTab();
}
Expand Down Expand Up @@ -487,6 +501,11 @@ export class ResultView extends React.Component {
</Tab>
}
{artifactTabs}
{!this.props.hideTestHistory &&
<Tab eventKey="test-history" title={<TabTitle icon={SearchIcon} text="Test History"/>} style={{backgroundColor: "white"}}>
{testHistoryTable}
</Tab>
}
{!this.props.hideTestObject &&
<Tab eventKey="test-object" title={<TabTitle icon={CodeIcon} text="Test Object" />} style={{backgroundColor: "white"}}>
<Card>
Expand Down
245 changes: 245 additions & 0 deletions frontend/src/components/test-history.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
import React from 'react';
import PropTypes from 'prop-types';

import {
Button,
Card,
CardHeader,
CardBody,
Checkbox,
Dropdown,
DropdownItem,
DropdownToggle,
Flex,
FlexItem,
TextContent,
Text,
Tooltip
} from '@patternfly/react-core';
import {
TableVariant,
expandable
} from '@patternfly/react-table';

import { HttpClient } from '../services/http';
import { Settings } from '../settings';
import {
buildParams,
toAPIFilter,
getSpinnerRow,
resultToTestHistoryRow,
} from '../utilities';
import {
FilterTable
} from './index';


export class TestHistoryTable extends React.Component {
static propTypes = {
filters: PropTypes.object,
testResult: PropTypes.object,
}

constructor(props) {
super(props);
this.state = {
columns: [{title: 'Result', cellFormatters: [expandable]}, 'Source', 'Exception Name', 'Duration', 'Start Time'],
rows: [getSpinnerRow(5)],
results: [],
cursor: null,
pageSize: 10,
page: 1,
totalItems: 0,
totalPages: 0,
isEmpty: false,
isError: false,
isFieldOpen: false,
isOperationOpen: false,
isDropdownOpen: false,
onlyFailures: false,
dropdownSelection: '2 Weeks',
filters: Object.assign({
'result': {op: 'in', val: "passed;skipped;failed;error;xpassed;xfailed"},
'test_id': {op: 'eq', val: props.testResult.test_id},
'env': {op: 'eq', val: props.testResult.env},
// default to filter only from 2 weeks ago to the most test's start_time.
'start_time': {op: 'gt', val: new Date(new Date(props.testResult.start_time).getTime() - (0.5 * 30 * 86400 * 1000)).toISOString()}
}, props.filters),
};
this.refreshResults = this.refreshResults.bind(this);
this.onCollapse = this.onCollapse.bind(this);
}

refreshResults = () => {
this.getResultsForTable();
}

onCollapse(event, rowIndex, isOpen) {
const { rows } = this.state;
rows[rowIndex].isOpen = isOpen;
this.setState({
rows
});
}

setPage = (_event, pageNumber) => {
this.setState({page: pageNumber}, () => {
this.getResultsForTable();
});
}

pageSizeSelect = (_event, perPage) => {
this.setState({pageSize: perPage}, () => {
this.getResultsForTable();
});
}

updateFilters(name, operator, value, callback) {
let filters = this.state.filters;
if ((value === null) || (value.length === 0)) {
delete filters[name];
}
else {
filters[name] = {'op': operator, 'val': value};
}
this.setState({filters: filters, page: 1}, callback);
}

setFilter = (field, value) => {
// 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 !== "result") && (id !== "test_id")) { // Don't allow removal of error/failure filter
this.updateFilters(id, null, null, this.refreshResults)
}
}

onFailuresCheck = (checked) => {
let { filters } = this.state;
filters["result"]["val"] = ("failed;error") + ((checked) ? ";skipped;xfailed" : "skipped;xfailed;xpassed;passed")
this.setState(
{onlyFailures: checked, filters},
this.refreshResults
);
}

onDropdownToggle = isOpen => {
this.setState({isDropdownOpen: isOpen});
}

onDropdownSelect = event => {
let { filters } = this.state;
let { testResult } = this.props;
let startTime = new Date(testResult.start_time);
let months = event.target.getAttribute('value');
let selection = event.target.text
// here a month is considered to be 30 days, and there are 86400*1000 ms in a day
let timeRange = new Date(startTime.getTime() - (months * 30 * 86400 * 1000));
// set the filters
filters["start_time"] = {op: "gt", val: timeRange.toISOString()}
this.setState({filters, isDropdownOpen: false, dropdownSelection: selection}, this.refreshResults);
}

getResultsForTable() {
const filters = this.state.filters;
this.setState({rows: [getSpinnerRow(4)], 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) => resultToTestHistoryRow(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});
});
}

componentDidMount() {
this.getResultsForTable();
}

render() {
const {
columns,
rows,
onlyFailures
} = this.state;
const pagination = {
pageSize: this.state.pageSize,
page: this.state.page,
totalItems: this.state.totalItems
}

return (
<Card className="pf-u-mt-lg">
<CardHeader>
<Flex style={{ width: '100%' }}>
<FlexItem grow={{ default: 'grow' }}>
<TextContent>
<Text component="h2" className="pf-c-title pf-m-xl">Test History</Text>
</TextContent>
</FlexItem>
<FlexItem>
<TextContent>
<Checkbox id="only-failures" label="Only show failures/errors" isChecked={onlyFailures} aria-label="only-failures-checkbox" onChange={this.onFailuresCheck}/>
</TextContent>
</FlexItem>
<FlexItem>
<Tooltip content={'Amount of time to gather results'}>
<Dropdown
toggle={<DropdownToggle isDisabled={false} onToggle={this.onDropdownToggle}>{this.state.dropdownSelection}</DropdownToggle>}
onSelect={this.onDropdownSelect}
isOpen={this.state.isDropdownOpen}
dropdownItems={[
<DropdownItem key='2 Weeks' value={0.5}>{'2 Weeks'}</DropdownItem>,
<DropdownItem key='1 Month' value={1.0}>{'1 Month'}</DropdownItem>,
<DropdownItem key='2 Months' value={2.0}>{'2 Months'}</DropdownItem>,
<DropdownItem key='3 Months' value={3.0}>{'3 Months'}</DropdownItem>,
<DropdownItem key='5 Months' value={5.0}>{'5 Months'}</DropdownItem>,
]}
/>
</Tooltip>
</FlexItem>
<FlexItem>
<Button variant="secondary" onClick={this.refreshResults}>Refresh results</Button>
</FlexItem>
</Flex>
</CardHeader>
<CardBody>
<FilterTable
columns={columns}
rows={rows}
pagination={pagination}
isEmpty={this.state.isEmpty}
isError={this.state.isError}
onCollapse={this.onCollapse}
onSetPage={this.setPage}
onSetPageSize={this.pageSizeSelect}
canSelectAll={false}
variant={TableVariant.compact}
activeFilters={this.state.filters}
onRemoveFilter={this.removeFilter}
hideFilters={["project_id", "result", "test_id"]}
/>
</CardBody>
</Card>
);
}

}
41 changes: 40 additions & 1 deletion frontend/src/utilities.js
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,46 @@ export function resultToClassificationRow(result, index, filterFunc) {
// child row
{
"parent": 2*index,
"cells": [{title: <ResultView hideSummary={hideSummary} hideTestObject={hideTestObject} testResult={result}/>}]
"cells": [{title: <ResultView hideTestHistory={false} hideSummary={hideSummary} hideTestObject={hideTestObject} testResult={result}/>}]
}
];
}

export function resultToTestHistoryRow(result, index, filterFunc) {
let resultIcon = getIconForResult(result.result);
let hideSummary = true;
let hideTestObject = true;
let exceptionBadge;

if (filterFunc) {
exceptionBadge = buildBadge('exception_name', result.metadata.exception_name, false,
() => filterFunc('metadata.exception_name', result.metadata.exception_name));
}
else {
exceptionBadge = buildBadge('exception_name', result.metadata.exception_name, false);
}

if (["passed", "skipped"].includes(result.result)) {
hideSummary=false;
hideTestObject=false;
}

return [
// parent row
{
"isOpen": false,
"cells": [
{title: <span className={result.result}>{resultIcon} {toTitleCase(result.result)}</span>},
{title: <span className={result.source}>{result.source}</span>},
{title: <React.Fragment>{exceptionBadge}</React.Fragment>},
{title: round(result.duration) + 's'},
{title: (new Date(result.start_time).toLocaleString())},
],
},
// child row
{
"parent": 2*index,
"cells": [{title: <ResultView hideTestHistory={true} hideSummary={hideSummary} hideTestObject={hideTestObject} testResult={result}/>}]
}
];
}
Expand Down

0 comments on commit f8ed19e

Please sign in to comment.