From 92c06cf442cf193aa90842f020f389005e64637d Mon Sep 17 00:00:00 2001 From: shadowusr Date: Fri, 16 Aug 2024 16:20:36 +0300 Subject: [PATCH] refactor: split suites page into components --- lib/static/gui.jsx | 196 +----------------- lib/static/new-ui.css | 15 -- lib/static/new-ui/app/App.tsx | 4 +- lib/static/new-ui/app/gui.tsx | 4 +- lib/static/new-ui/app/report.tsx | 4 +- .../components/ImageWithMagnifier.tsx | 1 - .../suites/components/StatusFilter.tsx | 42 ---- .../components/SuitesPage/index.module.css | 4 - .../suites/components/SuitesPage/index.tsx | 177 +--------------- .../components/SuitesTreeView/index.tsx | 155 ++++++++++++++ .../selectors.ts | 0 .../components/TestNameFilter/index.tsx | 34 +++ .../components/TreeViewItemSubtitle/index.tsx | 2 +- 13 files changed, 207 insertions(+), 431 deletions(-) rename lib/static/new-ui/{features/suites => }/components/ImageWithMagnifier.tsx (98%) delete mode 100644 lib/static/new-ui/features/suites/components/StatusFilter.tsx create mode 100644 lib/static/new-ui/features/suites/components/SuitesTreeView/index.tsx rename lib/static/new-ui/features/suites/components/{SuitesPage => SuitesTreeView}/selectors.ts (100%) create mode 100644 lib/static/new-ui/features/suites/components/TestNameFilter/index.tsx diff --git a/lib/static/gui.jsx b/lib/static/gui.jsx index 64c3a0ffb..2637afb6b 100644 --- a/lib/static/gui.jsx +++ b/lib/static/gui.jsx @@ -3,210 +3,18 @@ import {createRoot} from 'react-dom/client'; import {Provider} from 'react-redux'; import store from './modules/store'; import Gui from './components/gui'; -import {Button, Icon, RadioButton, Select, TextInput, ThemeProvider, Popover} from '@gravity-ui/uikit'; -import {AsideHeader} from '@gravity-ui/navigation'; -import TestplaneIcon from './icons/testplane.svg'; - -import Split from 'react-split'; +import {ThemeProvider} from '@gravity-ui/uikit'; import '@gravity-ui/uikit/styles/fonts.css'; import '@gravity-ui/uikit/styles/styles.css'; -import {CircleDashed, SquareCheck, ListCheck, Eye, CircleInfo, Sliders, Check, Xmark, ArrowRotateLeft, BarsDescendingAlignLeftArrowDown, Layers3Diagonal, ChevronsExpandVertical, Globe, Ban, ArrowsRotateLeft, CloudCheck} from '@gravity-ui/icons'; -import {unstable_ListContainerView as ListContainerView, unstable_ListItemView as ListItemView} from '@gravity-ui/uikit/unstable'; -import {AutoSizer} from 'react-virtualized'; -import {VariableSizeList} from 'react-window'; - const rootEl = document.getElementById('app'); const root = createRoot(rootEl); root.render( - { - console.log(args); - const treeItems = [ - {id: 'id-1', title: 'hey'}, - {id: 'id-2', title: 'hey2'} - ]; - - return -
-
-
-

Suites

-
- -

Options

- -
- } openOnHover={false}> - - - - -
-
-
- - -
-
- 20,256
}, - {value: 'passed', content:
10,123
}, - {value: 'failed', content:
10,453
}, - {value: 'retried', content:
792
}, - {value: 'skipped', content:
132
}, - {value: 'updated', content:
256
}, - {value: 'commited', content:
10
} - ]}> - -
- - - {({width, height}) => ( - 30} - > - {({index, style, data}) => ( -
- -
- )} -
- )} -
-
- - -
- -
-
; - } - } hideCollapseButton={true} /> +
); diff --git a/lib/static/new-ui.css b/lib/static/new-ui.css index 91e213369..d0063f5c7 100644 --- a/lib/static/new-ui.css +++ b/lib/static/new-ui.css @@ -1,20 +1,5 @@ .g-root { --g-font-family-sans: 'Jost', sans-serif; - - /*--g-text-header-font-weight: 600;*/ - /*--g-text-subheader-font-weight: 600;*/ - /*--g-text-display-font-weight: 600;*/ - /*--g-text-accent-font-weight: 600;*/ - - /*--g-color-base-brand: rgb(117, 155, 255);*/ - /*--g-color-base-brand-hover: rgb(99, 143, 255);*/ - /*--g-color-base-selection: rgba(82, 130, 255, 0.05);*/ - /*--g-color-base-selection-hover: rgba(82, 130, 255, 0.1);*/ - /*--g-color-line-brand: rgb(117, 155, 255);*/ - /*--g-color-text-brand: rgb(117, 155, 255);*/ - /*--g-color-text-brand-contrast: rgb(255, 255 ,255);*/ - /*--g-color-text-link: rgb(117, 155, 255);*/ - /*--g-color-text-link-hover: rgb(82, 130, 255);*/ } .report { diff --git a/lib/static/new-ui/app/App.tsx b/lib/static/new-ui/app/App.tsx index ddb30f268..e5db30ba8 100644 --- a/lib/static/new-ui/app/App.tsx +++ b/lib/static/new-ui/app/App.tsx @@ -1,5 +1,5 @@ import {ThemeProvider} from '@gravity-ui/uikit'; -import React, {StrictMode} from 'react'; +import React, {ReactNode, StrictMode} from 'react'; import {MainLayout} from '../components/MainLayout'; import {HashRouter, Navigate, Route, Routes} from 'react-router-dom'; import {CircleInfo, Eye, ListCheck} from '@gravity-ui/icons'; @@ -13,7 +13,7 @@ import '../../new-ui.css'; import {Provider} from 'react-redux'; import store from '../../modules/store'; -export function App(): JSX.Element { +export function App(): ReactNode { const pages = [ { title: 'Suites', diff --git a/lib/static/new-ui/app/gui.tsx b/lib/static/new-ui/app/gui.tsx index 5d02acfb3..5c9f3682f 100644 --- a/lib/static/new-ui/app/gui.tsx +++ b/lib/static/new-ui/app/gui.tsx @@ -1,4 +1,4 @@ -import React, {useEffect} from 'react'; +import React, {ReactNode, useEffect} from 'react'; import {createRoot} from 'react-dom/client'; import {App} from './App'; import store from '../../modules/store'; @@ -7,7 +7,7 @@ import {finGuiReport, initGuiReport} from '../../modules/actions'; const rootEl = document.getElementById('app') as HTMLDivElement; const root = createRoot(rootEl); -function Gui(): React.JSX.Element { +function Gui(): ReactNode { useEffect(() => { store.dispatch(initGuiReport()); diff --git a/lib/static/new-ui/app/report.tsx b/lib/static/new-ui/app/report.tsx index ee7a0163c..2323e0294 100644 --- a/lib/static/new-ui/app/report.tsx +++ b/lib/static/new-ui/app/report.tsx @@ -1,4 +1,4 @@ -import React, {useEffect} from 'react'; +import React, {ReactNode, useEffect} from 'react'; import {createRoot} from 'react-dom/client'; import {App} from './App'; import store from '../../modules/store'; @@ -7,7 +7,7 @@ import {initStaticReport, finStaticReport} from '../../modules/actions'; const rootEl = document.getElementById('app') as HTMLDivElement; const root = createRoot(rootEl); -function Gui(): React.JSX.Element { +function Gui(): ReactNode { useEffect(() => { store.dispatch(initStaticReport()); diff --git a/lib/static/new-ui/features/suites/components/ImageWithMagnifier.tsx b/lib/static/new-ui/components/ImageWithMagnifier.tsx similarity index 98% rename from lib/static/new-ui/features/suites/components/ImageWithMagnifier.tsx rename to lib/static/new-ui/components/ImageWithMagnifier.tsx index 583b21e2a..4ff41fc4c 100644 --- a/lib/static/new-ui/features/suites/components/ImageWithMagnifier.tsx +++ b/lib/static/new-ui/components/ImageWithMagnifier.tsx @@ -55,7 +55,6 @@ export function ImageWithMagnifier({ useEffect(() => { setMagnifierStyle({ display: showMagnifier ? '' : 'none', - // position: 'absolute', position: 'fixed', pointerEvents: 'none', height: `${magnifierHeight}px`, diff --git a/lib/static/new-ui/features/suites/components/StatusFilter.tsx b/lib/static/new-ui/features/suites/components/StatusFilter.tsx deleted file mode 100644 index 597a41eec..000000000 --- a/lib/static/new-ui/features/suites/components/StatusFilter.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import {ArrowRotateLeft, ArrowsRotateLeft, Ban, Check, CircleDashed, CloudCheck, Xmark} from '@gravity-ui/icons'; -import {ControlGroupOption, RadioButton} from '@gravity-ui/uikit'; -import React from 'react'; -import {connect} from 'react-redux'; -import {getStatsFilteredByBrowsers} from '../../../../modules/selectors/stats'; - -interface StatusFilterInternalProps { - stats: { - total: number; - passed: number; - failed: number; - skipped: number; - retries: number; - }, -} - -function StatusFilterInternal(props: StatusFilterInternalProps): JSX.Element { - const statusToIcon = { - total: , - passed: , - failed: , - retried: , - skipped: , - updated: , - commited: - } as const; - - const options: ControlGroupOption[] = Object.entries(props.stats) - .filter(([status, count]) => count > 0 || status === 'total') - .map(([status, count]) => ({ - value: status, - content:
{statusToIcon[status as keyof typeof statusToIcon]}{count}
- })); - - return ; -} - -export const StatusFilter = connect( - (state) => ({ - stats: getStatsFilteredByBrowsers(state) - }) -)(StatusFilterInternal); diff --git a/lib/static/new-ui/features/suites/components/SuitesPage/index.module.css b/lib/static/new-ui/features/suites/components/SuitesPage/index.module.css index 0ff23045b..db28706b6 100644 --- a/lib/static/new-ui/features/suites/components/SuitesPage/index.module.css +++ b/lib/static/new-ui/features/suites/components/SuitesPage/index.module.css @@ -6,10 +6,6 @@ } .tree-view { - /*--g-text-subheader-1-font-size: 50px;*/ - /*--g-text-subheader-1-line-height: 50px;*/ - /*font-size: var(--g-text-body-3-font-size);*/ - /*user-select: none;*/ margin-top: var(--g-spacing-2); } .tree-view__container { 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 8db29d105..9349935d0 100644 --- a/lib/static/new-ui/features/suites/components/SuitesPage/index.tsx +++ b/lib/static/new-ui/features/suites/components/SuitesPage/index.tsx @@ -1,184 +1,25 @@ -import React, {ChangeEvent, useCallback, useEffect, useState} from 'react'; -import {Box, Flex, TextInput} from '@gravity-ui/uikit'; -import {debounce} from 'lodash'; -import classNames from 'classnames'; +import {Flex} from '@gravity-ui/uikit'; +import React, {ReactNode} from 'react'; import {connect} from 'react-redux'; -import { - unstable_useList as useList, - unstable_ListContainerView as ListContainerView, - unstable_ListItemView as ListItemView, - unstable_getItemRenderState as getItemRenderState -} from '@gravity-ui/uikit/unstable'; -import styles from './index.module.css'; -import {useVirtualizer} from '@tanstack/react-virtual'; -import {bindActionCreators} from 'redux'; -import {State} from '@/static/new-ui/types/store'; -import { - getTreeViewExpandedById, - getTreeViewItems -} from '@/static/new-ui/features/suites/components/SuitesPage/selectors'; -import { - TreeViewBrowserData, - TreeViewItem, TreeViewItemType, - TreeViewSuiteData -} from '@/static/new-ui/features/suites/components/SuitesPage/types'; import {SplitViewLayout} from '@/static/new-ui/components/SplitViewLayout'; -import {getIconByStatus} from '@/static/new-ui/utils'; -import {TreeViewItemTitle} from '@/static/new-ui/features/suites/components/TreeViewItemTitle'; -import {TreeViewItemSubtitle} from '@/static/new-ui/features/suites/components/TreeViewItemSubtitle'; -import * as actions from '@/static/modules/actions'; -import {useNavigate, useParams} from 'react-router-dom'; - -interface SuitesPageInternalProps { - treeViewItems: TreeViewItem[]; - treeViewExpandedById: Record; - actions: typeof actions; - isInitialized: boolean; - currentSuiteId: string | null; - testNameFilter: string; -} - -function SuitesPageInternal(props: SuitesPageInternalProps): JSX.Element { - const navigate = useNavigate(); - const {suiteId} = useParams(); - const itemsOriginal = props.treeViewItems; - - const list = useList({ - items: itemsOriginal, - withExpandedState: true, - getItemId: item => { - return item.fullTitle; - }, - controlledState: { - expandedById: props.treeViewExpandedById - } - }); - - const onItemClick = useCallback(({id}: {id: string}): void => { - const item = list.structure.itemsById[id]; - - if (item.type === TreeViewItemType.Suite && list.state.expandedById && id in list.state.expandedById && list.state.setExpanded) { - props.actions.toggleSuiteSection({suiteId: item.fullTitle, shouldBeOpened: !props.treeViewExpandedById[item.fullTitle]}); - } else if (item.type === TreeViewItemType.Browser) { - props.actions.suitesPageSetCurrentSuite(id); - - navigate(encodeURIComponent(item.fullTitle)); - } - }, [list, props.actions, props.treeViewExpandedById]); - - const updateTestNameFilter = useCallback(debounce( - (testName) => props.actions.updateTestNameFilter(testName), - 500, - {maxWait: 3000} - ), []); - - const parentRef = React.useRef(null); - - const virtualizer = useVirtualizer({ - count: list.structure.visibleFlattenIds.length, - getScrollElement: () => parentRef.current, - estimateSize: () => 28, - getItemKey: useCallback((index: number) => list.structure.visibleFlattenIds[index], [list]), - overscan: 200 - }); - - useEffect(() => { - if (!suiteId && props.currentSuiteId && suiteId !== props.currentSuiteId) { - navigate(encodeURIComponent(props.currentSuiteId)); - } - }, []); - - useEffect(() => { - if (!props.isInitialized) { - return; - } - - props.actions.setStrictMatchFilter(false); - - if (suiteId) { - props.actions.suitesPageSetCurrentSuite(suiteId); - virtualizer.scrollToIndex(list.structure.visibleFlattenIds.indexOf(suiteId), {align: 'start'}); - } - }, [props.isInitialized]); - - const [testNameFilter, setTestNameFilter] = useState(props.testNameFilter); - const onChange = useCallback((event: ChangeEvent): void => { - setTestNameFilter(event.target.value); - updateTestNameFilter(event.target.value); - }, [setTestNameFilter, updateTestNameFilter]); - - const items = virtualizer.getVirtualItems(); +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'; +function SuitesPageInternal(): ReactNode { return

Suites

- +
- -
-
-
- {items.map((virtualRow) => { - const item = list.structure.itemsById[virtualRow.key]; - const classes = [styles['tree-view__item']]; - if (item.fullTitle === props.currentSuiteId) { - classes.push(styles['tree-item--current']); - } else if ((item.status === 'fail' || item.status === 'error') && item.type === TreeViewItemType.Browser) { - classes.push(styles['tree-item--error']); - } - if (item.type === TreeViewItemType.Browser) { - classes.push(styles['tree-item--browser']); - } - - return - { - return { - startSlot: getIconByStatus(x.status), - title: , - subtitle: - }; - } - }).props}/> - ; - })} -
-
-
-
+
; } -export const SuitesPage = connect( - (state: State) => ({ - isInitialized: state.app.isInitialized, - currentSuiteId: state.app.currentSuiteId, - treeViewItems: getTreeViewItems(state), - treeViewExpandedById: getTreeViewExpandedById(state), - testNameFilter: state.view.testNameFilter - }), - (dispatch) => ({actions: bindActionCreators(actions, dispatch)}) -)(SuitesPageInternal); +export const SuitesPage = connect()(SuitesPageInternal); diff --git a/lib/static/new-ui/features/suites/components/SuitesTreeView/index.tsx b/lib/static/new-ui/features/suites/components/SuitesTreeView/index.tsx new file mode 100644 index 000000000..f133d2ebd --- /dev/null +++ b/lib/static/new-ui/features/suites/components/SuitesTreeView/index.tsx @@ -0,0 +1,155 @@ +import {Box} from '@gravity-ui/uikit'; +import { + unstable_getItemRenderState as getItemRenderState, unstable_ListContainerView as ListContainerView, + unstable_ListItemView as ListItemView, unstable_useList as useList +} from '@gravity-ui/uikit/unstable'; +import {useVirtualizer} from '@tanstack/react-virtual'; +import classNames from 'classnames'; +import React, {ReactNode, useCallback, useEffect} from 'react'; +import {connect} from 'react-redux'; +import {useNavigate, useParams} from 'react-router-dom'; +import {bindActionCreators} from 'redux'; + +import * as actions from '@/static/modules/actions'; +import styles from '@/static/new-ui/features/suites/components/SuitesPage/index.module.css'; +import { + TreeViewBrowserData, + TreeViewItem, + TreeViewItemType, + TreeViewSuiteData +} from '@/static/new-ui/features/suites/components/SuitesPage/types'; +import {getIconByStatus} from '@/static/new-ui/utils'; +import {TreeViewItemTitle} from '@/static/new-ui/features/suites/components/TreeViewItemTitle'; +import {TreeViewItemSubtitle} from '@/static/new-ui/features/suites/components/TreeViewItemSubtitle'; +import {State} from '@/static/new-ui/types/store'; +import { + getTreeViewExpandedById, + getTreeViewItems +} from '@/static/new-ui/features/suites/components/SuitesTreeView/selectors'; + +interface SuitesTreeViewProps { + treeViewItems: TreeViewItem[]; + treeViewExpandedById: Record; + actions: typeof actions; + isInitialized: boolean; + currentSuiteId: string | null; +} + +function SuitesTreeViewInternal(props: SuitesTreeViewProps): ReactNode { + const navigate = useNavigate(); + const {suiteId} = useParams(); + + const list = useList({ + items: props.treeViewItems, + withExpandedState: true, + getItemId: item => { + return item.fullTitle; + }, + controlledState: { + expandedById: props.treeViewExpandedById + } + }); + + const parentRef = React.useRef(null); + + const virtualizer = useVirtualizer({ + count: list.structure.visibleFlattenIds.length, + getScrollElement: () => parentRef.current, + estimateSize: () => 28, + getItemKey: useCallback((index: number) => list.structure.visibleFlattenIds[index], [list]), + overscan: 200 + }); + + const virtualizedItems = virtualizer.getVirtualItems(); + + // Effects + useEffect(() => { + if (!suiteId && props.currentSuiteId && suiteId !== props.currentSuiteId) { + navigate(encodeURIComponent(props.currentSuiteId)); + } + }, []); + + useEffect(() => { + if (!props.isInitialized) { + return; + } + + props.actions.setStrictMatchFilter(false); + + if (suiteId) { + props.actions.suitesPageSetCurrentSuite(suiteId); + virtualizer.scrollToIndex(list.structure.visibleFlattenIds.indexOf(suiteId), {align: 'start'}); + } + }, [props.isInitialized]); + + // Event handlers + const onItemClick = useCallback(({id}: {id: string}): void => { + const item = list.structure.itemsById[id]; + + if (item.type === TreeViewItemType.Suite && list.state.expandedById && id in list.state.expandedById && list.state.setExpanded) { + props.actions.toggleSuiteSection({suiteId: item.fullTitle, shouldBeOpened: !props.treeViewExpandedById[item.fullTitle]}); + } else if (item.type === TreeViewItemType.Browser) { + props.actions.suitesPageSetCurrentSuite(id); + + navigate(encodeURIComponent(item.fullTitle)); + } + }, [list, props.actions, props.treeViewExpandedById]); + + return +
+
+
+ {virtualizedItems.map((virtualRow) => { + const item = list.structure.itemsById[virtualRow.key]; + const classes = [styles['tree-view__item']]; + if (item.fullTitle === props.currentSuiteId) { + classes.push(styles['tree-item--current']); + } else if ((item.status === 'fail' || item.status === 'error') && item.type === TreeViewItemType.Browser) { + classes.push(styles['tree-item--error']); + } + if (item.type === TreeViewItemType.Browser) { + classes.push(styles['tree-item--browser']); + } + + return + { + return { + startSlot: getIconByStatus(x.status), + title: , + subtitle: + }; + } + }).props}/> + ; + })} +
+
+
+
; +} + +export const SuitesTreeView = connect((state: State) => ({ + isInitialized: state.app.isInitialized, + currentSuiteId: state.app.currentSuiteId, + treeViewItems: getTreeViewItems(state), + treeViewExpandedById: getTreeViewExpandedById(state) +}), +(dispatch) => ({actions: bindActionCreators(actions, dispatch)}))(SuitesTreeViewInternal); diff --git a/lib/static/new-ui/features/suites/components/SuitesPage/selectors.ts b/lib/static/new-ui/features/suites/components/SuitesTreeView/selectors.ts similarity index 100% rename from lib/static/new-ui/features/suites/components/SuitesPage/selectors.ts rename to lib/static/new-ui/features/suites/components/SuitesTreeView/selectors.ts diff --git a/lib/static/new-ui/features/suites/components/TestNameFilter/index.tsx b/lib/static/new-ui/features/suites/components/TestNameFilter/index.tsx new file mode 100644 index 000000000..bc5679d71 --- /dev/null +++ b/lib/static/new-ui/features/suites/components/TestNameFilter/index.tsx @@ -0,0 +1,34 @@ +import {TextInput} from '@gravity-ui/uikit'; +import React, {ChangeEvent, ReactNode, useCallback, useState} from 'react'; +import {debounce} from 'lodash'; +import {connect} from 'react-redux'; +import {bindActionCreators} from 'redux'; +import * as actions from '@/static/modules/actions'; +import {State} from '@/static/new-ui/types/store'; + +interface TestNameFilterProps { + testNameFilter: string; + actions: typeof actions; +} + +function TestNameFilterInternal(props: TestNameFilterProps): ReactNode { + const [testNameFilter, setTestNameFilter] = useState(props.testNameFilter); + + const updateTestNameFilter = useCallback(debounce( + (testName) => props.actions.updateTestNameFilter(testName), + 500, + {maxWait: 3000} + ), []); + + const onChange = useCallback((event: ChangeEvent): void => { + setTestNameFilter(event.target.value); + updateTestNameFilter(event.target.value); + }, [setTestNameFilter, updateTestNameFilter]); + + return ; +} + +export const TestNameFilter = connect( + (state: State) => ({testNameFilter: state.view.testNameFilter}), + (dispatch) => ({actions: bindActionCreators(actions, dispatch)}) +)(TestNameFilterInternal); diff --git a/lib/static/new-ui/features/suites/components/TreeViewItemSubtitle/index.tsx b/lib/static/new-ui/features/suites/components/TreeViewItemSubtitle/index.tsx index a4bcf62a6..f9af37f59 100644 --- a/lib/static/new-ui/features/suites/components/TreeViewItemSubtitle/index.tsx +++ b/lib/static/new-ui/features/suites/components/TreeViewItemSubtitle/index.tsx @@ -1,7 +1,7 @@ import React, {ReactNode} from 'react'; import {TreeViewData, TreeViewItemType} from '@/static/new-ui/features/suites/components/SuitesPage/types'; import styles from './index.module.css'; -import {ImageWithMagnifier} from '@/static/new-ui/features/suites/components/ImageWithMagnifier'; +import {ImageWithMagnifier} from '@/static/new-ui/components/ImageWithMagnifier'; export function TreeViewItemSubtitle(props: {item: TreeViewData}): ReactNode { if (props.item.type === TreeViewItemType.Browser && props.item.diffImg) {