From 9951cc888cebc5a98906b7faceada516e2b8290d Mon Sep 17 00:00:00 2001 From: shadowusr Date: Sun, 18 Aug 2024 22:54:02 +0300 Subject: [PATCH] feat: implement filtering by status in new ui --- lib/constants/test-statuses.ts | 4 ++ lib/static/new-ui.css | 4 ++ .../suites/components/SuitesPage/index.tsx | 9 ++-- .../SuitesTreeView/index.module.css | 2 + .../components/SuitesTreeView/selectors.ts | 16 ++++-- .../TestStatusFilter/index.module.css | 9 ++++ .../components/TestStatusFilter/index.tsx | 46 ++++++++++++++++ .../components/TestStatusFilter/selectors.ts | 52 +++++++++++++++++++ lib/static/new-ui/store/selectors.ts | 12 ++++- lib/static/new-ui/types/store.ts | 14 ++++- lib/static/new-ui/utils/index.tsx | 4 +- lib/static/styles.css | 4 ++ 12 files changed, 164 insertions(+), 12 deletions(-) create mode 100644 lib/static/new-ui/features/suites/components/TestStatusFilter/index.module.css create mode 100644 lib/static/new-ui/features/suites/components/TestStatusFilter/index.tsx create mode 100644 lib/static/new-ui/features/suites/components/TestStatusFilter/selectors.ts diff --git a/lib/constants/test-statuses.ts b/lib/constants/test-statuses.ts index 3ee8b9bf8..c77538f20 100644 --- a/lib/constants/test-statuses.ts +++ b/lib/constants/test-statuses.ts @@ -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; diff --git a/lib/static/new-ui.css b/lib/static/new-ui.css index d0063f5c7..6de9c78e2 100644 --- a/lib/static/new-ui.css +++ b/lib/static/new-ui.css @@ -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; } diff --git a/lib/static/new-ui/features/suites/components/SuitesPage/index.tsx b/lib/static/new-ui/features/suites/components/SuitesPage/index.tsx index 9349935d0..dd2c77e79 100644 --- a/lib/static/new-ui/features/suites/components/SuitesPage/index.tsx +++ b/lib/static/new-ui/features/suites/components/SuitesPage/index.tsx @@ -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

Suites

-
+ -
+
+ + +
diff --git a/lib/static/new-ui/features/suites/components/SuitesTreeView/index.module.css b/lib/static/new-ui/features/suites/components/SuitesTreeView/index.module.css index 84242f152..b42f5125a 100644 --- a/lib/static/new-ui/features/suites/components/SuitesTreeView/index.module.css +++ b/lib/static/new-ui/features/suites/components/SuitesTreeView/index.module.css @@ -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; diff --git a/lib/static/new-ui/features/suites/components/SuitesTreeView/selectors.ts b/lib/static/new-ui/features/suites/components/SuitesTreeView/selectors.ts index 3f32120cf..6634952e0 100644 --- a/lib/static/new-ui/features/suites/components/SuitesTreeView/selectors.ts +++ b/lib/static/new-ui/features/suites/components/SuitesTreeView/selectors.ts @@ -16,7 +16,7 @@ import { import {TestStatus} from '@/constants'; import { getAllRootSuiteIds, - getBrowsers, + getBrowsers, getBrowsersState, getImages, getResults, getSuites, @@ -28,8 +28,8 @@ import {getFullTitleByTitleParts} from '@/static/new-ui/utils'; // 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[] => { + [getSuites, getSuitesState, getAllRootSuiteIds, getBrowsers, getBrowsersState, getResults, getImages], + (suites, suitesState, rootSuiteIds, browsers, browsersState, results, images): TreeViewItem[] => { const EMPTY_SUITE: TreeViewSuiteData = { type: TreeViewItemType.Suite, title: '', @@ -37,7 +37,7 @@ export const getTreeViewItems = createSelector( status: TestStatus.IDLE }; - const formatBrowser = (browserData: BrowserEntity, parentSuite: TreeViewSuiteData): TreeViewItem => { + const formatBrowser = (browserData: BrowserEntity, parentSuite: TreeViewSuiteData): TreeViewItem | 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]; @@ -62,6 +62,10 @@ export const getTreeViewItems = createSelector( diffImg }; + if (!browsersState[data.fullTitle].shouldBeShown) { + return null; + } + return {data}; }; @@ -80,7 +84,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[] }; } else { return { diff --git a/lib/static/new-ui/features/suites/components/TestStatusFilter/index.module.css b/lib/static/new-ui/features/suites/components/TestStatusFilter/index.module.css new file mode 100644 index 000000000..9f4a8925a --- /dev/null +++ b/lib/static/new-ui/features/suites/components/TestStatusFilter/index.module.css @@ -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); + } diff --git a/lib/static/new-ui/features/suites/components/TestStatusFilter/index.tsx b/lib/static/new-ui/features/suites/components/TestStatusFilter/index.tsx new file mode 100644 index 000000000..ee4cdcf0c --- /dev/null +++ b/lib/static/new-ui/features/suites/components/TestStatusFilter/index.tsx @@ -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
+ {props.status === 'total' ? All : getIconByStatus(props.status)}{props.count} +
; +} + +interface TestStatusFilterProps { + statusCounts: StatusCounts; + actions: typeof actions; + viewMode: ViewMode; +} + +function TestStatusFilterInternal({statusCounts, actions, viewMode}: TestStatusFilterProps): ReactNode { + return void actions.changeViewMode(e.target.value)} value={viewMode}> + } /> + } /> + } /> + } /> + } /> + ; +} + +export const TestStatusFilter = connect( + (state: State) => ({ + statusCounts: getStatusCounts(state), + viewMode: state.view.viewMode + }), + (dispatch) => ({actions: bindActionCreators(actions, dispatch)}) +)(TestStatusFilterInternal); diff --git a/lib/static/new-ui/features/suites/components/TestStatusFilter/selectors.ts b/lib/static/new-ui/features/suites/components/TestStatusFilter/selectors.ts new file mode 100644 index 000000000..a8983ad50 --- /dev/null +++ b/lib/static/new-ui/features/suites/components/TestStatusFilter/selectors.ts @@ -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 = {}; + const retriedTests = new Set(); + + 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; + } +); diff --git a/lib/static/new-ui/store/selectors.ts b/lib/static/new-ui/store/selectors.ts index b184daf1b..e858daf57 100644 --- a/lib/static/new-ui/store/selectors.ts +++ b/lib/static/new-ui/store/selectors.ts @@ -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 => state.tree.suites.byId; export const getSuitesState = (state: State): Record => state.tree.suites.stateById; export const getBrowsers = (state: State): Record => state.tree.browsers.byId; +export const getBrowsersState = (state: State): Record => state.tree.browsers.stateById; +export const getAllBrowserIds = (state: State): string[] => state.tree.browsers.allIds; export const getResults = (state: State): Record => state.tree.results.byId; export const getImages = (state: State): Record => state.tree.images.byId; diff --git a/lib/static/new-ui/types/store.ts b/lib/static/new-ui/types/store.ts index 7e2901853..b5c57e5e5 100644 --- a/lib/static/new-ui/types/store.ts +++ b/lib/static/new-ui/types/store.ts @@ -1,4 +1,4 @@ -import {TestStatus} from '@/constants'; +import {TestStatus, ViewMode} from '@/constants'; import {ImageFile} from '@/types'; export interface SuiteEntityNode { @@ -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 { @@ -54,6 +57,10 @@ export interface SuiteState { shouldBeShown: boolean; } +export interface BrowserState { + shouldBeShown: boolean; +} + export interface State { app: { isInitialized: boolean; @@ -61,7 +68,9 @@ export interface State { } tree: { browsers: { - byId: Record + allIds: string[]; + byId: Record; + stateById: Record }; images: { byId: Record; @@ -77,5 +86,6 @@ export interface State { } view: { testNameFilter: string; + viewMode: ViewMode; } } diff --git a/lib/static/new-ui/utils/index.tsx b/lib/static/new-ui/utils/index.tsx index d21f0fc64..79871a0b1 100644 --- a/lib/static/new-ui/utils/index.tsx +++ b/lib/static/new-ui/utils/index.tsx @@ -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 => { @@ -9,6 +9,8 @@ export const getIconByStatus = (status: TestStatus): React.JSX.Element => { return ; } else if (status === TestStatus.SKIPPED) { return ; + } else if (status === TestStatus.RETRY) { + return ; } return ; diff --git a/lib/static/styles.css b/lib/static/styles.css index 115df6d69..7789588b9 100644 --- a/lib/static/styles.css +++ b/lib/static/styles.css @@ -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); +}