Skip to content

Commit

Permalink
feat: implement filtering by status in new ui
Browse files Browse the repository at this point in the history
  • Loading branch information
shadowusr committed Aug 18, 2024
1 parent 77156c9 commit 8990f67
Show file tree
Hide file tree
Showing 12 changed files with 164 additions and 12 deletions.
4 changes: 4 additions & 0 deletions lib/constants/test-statuses.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ export enum TestStatus {
* @note used by staticImageAccepter only
*/
COMMITED = 'commited',
/**
* @note used in new UI only for rendering icons
*/
RETRY = 'retry'
}

export const IDLE = TestStatus.IDLE;
Expand Down
4 changes: 4 additions & 0 deletions lib/static/new-ui.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
--g-font-family-sans: 'Jost', sans-serif;
}

body {
margin: 0;
}

.report {
font-family: var(--g-font-family-sans), sans-serif !important;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,19 @@ import {connect} from 'react-redux';
import {SplitViewLayout} from '@/static/new-ui/components/SplitViewLayout';
import {TestNameFilter} from '@/static/new-ui/features/suites/components/TestNameFilter';
import {SuitesTreeView} from '@/static/new-ui/features/suites/components/SuitesTreeView';
import styles from './index.module.css';
import {TestStatusFilter} from '@/static/new-ui/features/suites/components/TestStatusFilter';

function SuitesPageInternal(): ReactNode {
return <SplitViewLayout>
<div>
<Flex direction={'column'} spacing={{p: '2'}} style={{height: '100vh'}}>
<h2 className="text-display-1">Suites</h2>
<div className={styles.controlsRow}>
<Flex>
<TestNameFilter/>
</div>
</Flex>
<Flex spacing={{mt: 2}}>
<TestStatusFilter/>
</Flex>
<SuitesTreeView/>
</Flex>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
--g-text-body-1-line-height: 22px;
--g-text-body-font-weight: normal;

line-height: 20px;

padding: 4px 0;
font-size: 18px;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
import {TestStatus} from '@/constants';
import {
getAllRootSuiteIds,
getBrowsers,
getBrowsers, getBrowsersState,
getImages,
getResults,
getSuites,
Expand All @@ -27,16 +27,16 @@ import {ImageFile} from '@/types';

// Converts the existing store structure to the one that can be consumed by GravityUI
export const getTreeViewItems = createSelector(
[getSuites, getSuitesState, getAllRootSuiteIds, getBrowsers, getResults, getImages],
(suites, suitesState, rootSuiteIds, browsers, results, images): TreeViewItem<TreeViewSuiteData | TreeViewBrowserData>[] => {
[getSuites, getSuitesState, getAllRootSuiteIds, getBrowsers, getBrowsersState, getResults, getImages],
(suites, suitesState, rootSuiteIds, browsers, browsersState, results, images): TreeViewItem<TreeViewSuiteData | TreeViewBrowserData>[] => {
const EMPTY_SUITE: TreeViewSuiteData = {
type: TreeViewItemType.Suite,
title: '',
fullTitle: '',
status: TestStatus.IDLE
};

const formatBrowser = (browserData: BrowserEntity, parentSuite: TreeViewSuiteData): TreeViewItem<TreeViewBrowserData> => {
const formatBrowser = (browserData: BrowserEntity, parentSuite: TreeViewSuiteData): TreeViewItem<TreeViewBrowserData> | null => {
// Assuming test in concrete browser always has at least one result, even never launched (idle result)
const lastResult = results[last(browserData.resultIds) as string];

Expand All @@ -61,6 +61,10 @@ export const getTreeViewItems = createSelector(
diffImg
};

if (!browsersState[data.fullTitle].shouldBeShown) {
return null;
}

return {data};
};

Expand All @@ -79,7 +83,9 @@ export const getTreeViewItems = createSelector(
if (isSuiteEntityLeaf(suiteData)) {
return {
data,
children: suiteData.browserIds.map((browserId) => formatBrowser(browsers[browserId], data))
children: suiteData.browserIds
.map((browserId) => formatBrowser(browsers[browserId], data))
.filter(Boolean) as TreeViewItem<TreeViewBrowserData>[]
};
} else {
return {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.test-status-filter-option {
display: flex;
align-items: center;
justify-content: center;
}

.test-status-filter-option__count {
margin-left: var(--g-spacing-1);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {RadioButton} from '@gravity-ui/uikit';
import React, {ReactNode} from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';

import {TestStatus, ViewMode} from '@/constants';
import * as actions from '@/static/modules/actions';
import {getStatusCounts, StatusCounts} from '@/static/new-ui/features/suites/components/TestStatusFilter/selectors';
import {State} from '@/static/new-ui/types/store';
import {getIconByStatus} from '@/static/new-ui/utils';
import styles from './index.module.css';

interface TestStatusFilterOptionProps {
status: TestStatus | 'total';
count: number;
}

function TestStatusFilterOption(props: TestStatusFilterOptionProps): ReactNode {
return <div className={styles['test-status-filter-option']}>
{props.status === 'total' ? <span>All</span> : getIconByStatus(props.status)}<span className={styles['test-status-filter-option__count']}>{props.count}</span>
</div>;
}

interface TestStatusFilterProps {
statusCounts: StatusCounts;
actions: typeof actions;
viewMode: ViewMode;
}

function TestStatusFilterInternal({statusCounts, actions, viewMode}: TestStatusFilterProps): ReactNode {
return <RadioButton width={'max'} onChange={(e): void => void actions.changeViewMode(e.target.value)} value={viewMode}>
<RadioButton.Option value={ViewMode.ALL} content={<TestStatusFilterOption status={'total'} count={statusCounts.total}/>} />
<RadioButton.Option value={ViewMode.PASSED} content={<TestStatusFilterOption status={TestStatus.SUCCESS} count={statusCounts.success}/>} />
<RadioButton.Option value={ViewMode.FAILED} content={<TestStatusFilterOption status={TestStatus.FAIL} count={statusCounts.fail}/>} />
<RadioButton.Option value={ViewMode.RETRIED} content={<TestStatusFilterOption status={TestStatus.RETRY} count={statusCounts.retried}/>} />
<RadioButton.Option value={ViewMode.SKIPPED} content={<TestStatusFilterOption status={TestStatus.SKIPPED} count={statusCounts.skipped}/>} />
</RadioButton>;
}

export const TestStatusFilter = connect(
(state: State) => ({
statusCounts: getStatusCounts(state),
viewMode: state.view.viewMode
}),
(dispatch) => ({actions: bindActionCreators(actions, dispatch)})
)(TestStatusFilterInternal);
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {createSelector} from 'reselect';
import {getAllBrowserIds, getResults} from '@/static/new-ui/store/selectors';

export interface StatusCounts {
success: number;
fail: number;
skipped: number;
total: number;
retried: number;
retries: number;
idle: number;
}

export const getStatusCounts = createSelector(
[getResults, getAllBrowserIds],
(results, browserIds) => {
const latestAttempts: Record<string, {attempt: number; status: string, timestamp: number}> = {};
const retriedTests = new Set<string>();

let retries = 0;
Object.values(results).forEach(result => {
const {parentId: testId, attempt, status, timestamp} = result;
if (attempt > 0) {
retriedTests.add(testId);
}
if (!latestAttempts[testId] || latestAttempts[testId].timestamp < timestamp) {
retries -= latestAttempts[testId]?.attempt ?? 0;
retries += attempt;

latestAttempts[testId] = {attempt, status, timestamp};
}
});

const counts: StatusCounts = {
success: 0,
fail: 0,
skipped: 0,
total: browserIds.length,
retried: retriedTests.size,
retries,
idle: 0
};

Object.values(latestAttempts).forEach(({status}) => {
if (Object.prototype.hasOwnProperty.call(counts, status)) {
counts[status as keyof StatusCounts]++;
}
});

return counts;
}
);
12 changes: 11 additions & 1 deletion lib/static/new-ui/store/selectors.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import {State, BrowserEntity, ImageEntity, ResultEntity, SuiteEntity, SuiteState} from '@/static/new-ui/types/store';
import {
State,
BrowserEntity,
ImageEntity,
ResultEntity,
SuiteEntity,
SuiteState,
BrowserState
} from '@/static/new-ui/types/store';

export const getAllRootSuiteIds = (state: State): string[] => state.tree.suites.allRootIds;
export const getSuites = (state: State): Record<string, SuiteEntity> => state.tree.suites.byId;
export const getSuitesState = (state: State): Record<string, SuiteState> => state.tree.suites.stateById;
export const getBrowsers = (state: State): Record<string, BrowserEntity> => state.tree.browsers.byId;
export const getBrowsersState = (state: State): Record<string, BrowserState> => state.tree.browsers.stateById;
export const getAllBrowserIds = (state: State): string[] => state.tree.browsers.allIds;
export const getResults = (state: State): Record<string, ResultEntity> => state.tree.results.byId;
export const getImages = (state: State): Record<string, ImageEntity> => state.tree.images.byId;
14 changes: 12 additions & 2 deletions lib/static/new-ui/types/store.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {TestStatus} from '@/constants';
import {TestStatus, ViewMode} from '@/constants';
import {ImageFile} from '@/types';

export interface SuiteEntityNode {
Expand All @@ -23,8 +23,11 @@ export interface BrowserEntity {
}

export interface ResultEntityCommon {
parentId: string;
attempt: number;
imageIds: string[];
status: TestStatus;
timestamp: number;
}

export interface ResultEntityError extends ResultEntityCommon {
Expand Down Expand Up @@ -54,14 +57,20 @@ export interface SuiteState {
shouldBeShown: boolean;
}

export interface BrowserState {
shouldBeShown: boolean;
}

export interface State {
app: {
isInitialized: boolean;
currentSuiteId: string | null;
}
tree: {
browsers: {
byId: Record<string, BrowserEntity>
allIds: string[];
byId: Record<string, BrowserEntity>;
stateById: Record<string, BrowserState>
};
images: {
byId: Record<string, ImageEntity>;
Expand All @@ -77,5 +86,6 @@ export interface State {
}
view: {
testNameFilter: string;
viewMode: ViewMode;
}
}
4 changes: 3 additions & 1 deletion lib/static/new-ui/utils/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {TestStatus} from '@/constants';
import {CircleCheck, CircleDashed, CircleXmark, CircleMinus} from '@gravity-ui/icons';
import {ArrowRotateLeft, CircleCheck, CircleDashed, CircleMinus, CircleXmark} from '@gravity-ui/icons';
import React from 'react';

export const getIconByStatus = (status: TestStatus): React.JSX.Element => {
Expand All @@ -9,6 +9,8 @@ export const getIconByStatus = (status: TestStatus): React.JSX.Element => {
return <CircleCheck className={'icon-success'} />;
} else if (status === TestStatus.SKIPPED) {
return <CircleMinus className={'icon-skip'} />;
} else if (status === TestStatus.RETRY) {
return <ArrowRotateLeft className={'icon-retry'}/>;
}

return <CircleDashed />;
Expand Down
4 changes: 4 additions & 0 deletions lib/static/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1067,3 +1067,7 @@ a:active {
.icon-skip {
color: var(--g-color-private-cool-grey-600-solid);
}

.icon-retry {
color: var(--g-color-private-orange-600-solid);
}

0 comments on commit 8990f67

Please sign in to comment.