From f1e8abf9be70470db00d1a1a862c86f8a6622c91 Mon Sep 17 00:00:00 2001 From: shadowusr Date: Thu, 25 Jul 2024 21:39:21 +0300 Subject: [PATCH 01/11] draft: save working state --- lib/gui/server.ts | 1 + lib/server-utils.ts | 3 +- lib/static/gui.jsx | 196 +++++++++++++++++- lib/static/new-ui/App.tsx | 34 +++ lib/static/new-ui/gui.tsx | 21 ++ lib/static/new-ui/layouts/MainLayout.tsx | 33 +++ .../new-ui/layouts/SplitViewLayout.module.css | 20 ++ lib/static/new-ui/layouts/SplitViewLayout.tsx | 13 ++ lib/static/new-ui/pages/InfoPage/index.tsx | 5 + .../new-ui/pages/SuitesPage/StatusFilter.tsx | 42 ++++ lib/static/new-ui/pages/SuitesPage/index.tsx | 155 ++++++++++++++ .../new-ui/pages/VisualChecksPage/index.tsx | 5 + lib/static/new-ui/report.tsx | 7 + lib/static/new-ui/typings.d.ts | 9 + lib/static/styles.css | 150 ++++++++++++++ lib/static/template.html | 1 + package.json | 8 + webpack.common.js | 28 ++- 18 files changed, 726 insertions(+), 5 deletions(-) create mode 100644 lib/static/new-ui/App.tsx create mode 100644 lib/static/new-ui/gui.tsx create mode 100644 lib/static/new-ui/layouts/MainLayout.tsx create mode 100644 lib/static/new-ui/layouts/SplitViewLayout.module.css create mode 100644 lib/static/new-ui/layouts/SplitViewLayout.tsx create mode 100644 lib/static/new-ui/pages/InfoPage/index.tsx create mode 100644 lib/static/new-ui/pages/SuitesPage/StatusFilter.tsx create mode 100644 lib/static/new-ui/pages/SuitesPage/index.tsx create mode 100644 lib/static/new-ui/pages/VisualChecksPage/index.tsx create mode 100644 lib/static/new-ui/report.tsx create mode 100644 lib/static/new-ui/typings.d.ts diff --git a/lib/gui/server.ts b/lib/gui/server.ts index 231904b20..8cdbb2c9a 100644 --- a/lib/gui/server.ts +++ b/lib/gui/server.ts @@ -36,6 +36,7 @@ export const start = async (args: ServerArgs): Promise => { server.use(express.static(path.join(process.cwd(), reporterConfig.path))); server.get('/', (_req, res) => res.sendFile(path.join(__dirname, '../static', 'gui.html'))); + server.get('/new-ui', (_req, res) => res.sendFile(path.join(__dirname, '../static', 'new-ui-gui.html'))); server.get('/events', (_req, res) => { res.writeHead(OK, {'Content-Type': 'text/event-stream'}); diff --git a/lib/server-utils.ts b/lib/server-utils.ts index 96833b13c..bfce2e393 100644 --- a/lib/server-utils.ts +++ b/lib/server-utils.ts @@ -133,8 +133,9 @@ export async function saveStaticFilesToReportDir(htmlReporter: HtmlReporter, plu prepareCommonJSData(getDataForStaticFile(htmlReporter, pluginConfig)), 'utf8' ), - copyToReportDir(destPath, ['report.min.js', 'report.min.css'], staticFolder), + copyToReportDir(destPath, ['report.min.js', 'report.min.css', 'newReport.min.js', 'newReport.min.css'], staticFolder), fs.copy(path.resolve(staticFolder, 'index.html'), path.resolve(destPath, 'index.html')), + fs.copy(path.resolve(staticFolder, 'new-ui-report.html'), path.resolve(destPath, 'new-ui.html')), fs.copy(path.resolve(staticFolder, 'icons'), path.resolve(destPath, 'icons')), fs.copy(require.resolve('@gemini-testing/sql.js/dist/sql-wasm.js'), path.resolve(destPath, 'sql-wasm.js')), fs.copy(require.resolve('@gemini-testing/sql.js/dist/sql-wasm.wasm'), path.resolve(destPath, 'sql-wasm.wasm')), diff --git a/lib/static/gui.jsx b/lib/static/gui.jsx index 2637afb6b..eff9b389c 100644 --- a/lib/static/gui.jsx +++ b/lib/static/gui.jsx @@ -3,18 +3,210 @@ import {createRoot} from 'react-dom/client'; import {Provider} from 'react-redux'; import store from './modules/store'; import Gui from './components/gui'; -import {ThemeProvider} from '@gravity-ui/uikit'; +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 '@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/App.tsx b/lib/static/new-ui/App.tsx new file mode 100644 index 000000000..b4a8a6a43 --- /dev/null +++ b/lib/static/new-ui/App.tsx @@ -0,0 +1,34 @@ +import {ThemeProvider} from '@gravity-ui/uikit'; +import React from 'react'; +import {MainLayout} from './layouts/MainLayout'; +import {HashRouter, Navigate, Route, Routes} from 'react-router-dom'; +import {CircleInfo, Eye, ListCheck} from '@gravity-ui/icons'; +import {SuitesPage} from './pages/SuitesPage'; +import {VisualChecksPage} from './pages/VisualChecksPage'; +import {InfoPage} from './pages/InfoPage'; + +import '@gravity-ui/uikit/styles/fonts.css'; +import '@gravity-ui/uikit/styles/styles.css'; +import {Provider} from 'react-redux'; +import store from '../modules/store'; + +export function App(): JSX.Element { + const pages = [ + {title: 'Suites', url: '/suites', icon: ListCheck, element: }, + {title: 'Visual Checks', url: '/visual-checks', icon: Eye, element: }, + {title: 'Info', url: '/info', icon: CircleInfo, element: } + ]; + + return + + + + + } path={'/'}/> + {pages.map(page => )} + + + + + ; +} diff --git a/lib/static/new-ui/gui.tsx b/lib/static/new-ui/gui.tsx new file mode 100644 index 000000000..d85c67397 --- /dev/null +++ b/lib/static/new-ui/gui.tsx @@ -0,0 +1,21 @@ +import React, {useEffect} from 'react'; +import ReactDOM from 'react-dom'; +import {App} from './App'; +import store from '../modules/store'; +import {finGuiReport, initGuiReport} from '../modules/actions'; + +const rootEl = document.getElementById('app'); + +function Gui(): JSX.Element { + useEffect(() => { + store.dispatch(initGuiReport()); + + return () => { + store.dispatch(finGuiReport()); + }; + }, []); + + return ; +} + +ReactDOM.render(, rootEl); diff --git a/lib/static/new-ui/layouts/MainLayout.tsx b/lib/static/new-ui/layouts/MainLayout.tsx new file mode 100644 index 000000000..f0cd44f2f --- /dev/null +++ b/lib/static/new-ui/layouts/MainLayout.tsx @@ -0,0 +1,33 @@ +import {AsideHeader, MenuItem as GravityMenuItem} from '@gravity-ui/navigation'; +import React from 'react'; +import {useNavigate, matchPath, useLocation} from 'react-router-dom'; +import TestplaneIcon from '../../icons/testplane.svg'; + +interface MenuItem { + title: string; + url: string; + icon: GravityMenuItem['icon']; +} + +export interface MainLayoutProps { + children: React.ReactNode; + menuItems: MenuItem[]; +} + +export function MainLayout(props: MainLayoutProps): JSX.Element { + const navigate = useNavigate(); + const location = useLocation(); + + const gravityMenuItems: GravityMenuItem[] = props.menuItems.map(item => ({ + id: item.url, + title: item.title, + icon: item.icon, + current: Boolean(matchPath(`${item.url.replace(/\/$/, '')}/*`, location.pathname)), + onItemClick: () => navigate(item.url) + })); + + return navigate('/')}} compact={true} + headerDecoration={true} menuItems={gravityMenuItems} + renderContent={(): React.ReactNode => props.children} hideCollapseButton={true} + />; +} diff --git a/lib/static/new-ui/layouts/SplitViewLayout.module.css b/lib/static/new-ui/layouts/SplitViewLayout.module.css new file mode 100644 index 000000000..01e7b0a9c --- /dev/null +++ b/lib/static/new-ui/layouts/SplitViewLayout.module.css @@ -0,0 +1,20 @@ +.split { + display: flex; + flex-direction: row; + height: 100vh; +} + + .split > div { + overflow-y: scroll; + } + + .split :global(.gutter) { + background-color: #eee; + background-repeat: no-repeat; + background-position: 50%; + } + + .split :global(.gutter.gutter-horizontal) { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg=='); + cursor: col-resize; + } diff --git a/lib/static/new-ui/layouts/SplitViewLayout.tsx b/lib/static/new-ui/layouts/SplitViewLayout.tsx new file mode 100644 index 000000000..24da06db4 --- /dev/null +++ b/lib/static/new-ui/layouts/SplitViewLayout.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import Split from 'react-split'; +import styles from './SplitViewLayout.module.css'; + +interface SplitViewLayoutProps { + children?: React.ReactNode; +} + +export function SplitViewLayout(props: SplitViewLayoutProps): JSX.Element { + return + {props.children} + ; +} diff --git a/lib/static/new-ui/pages/InfoPage/index.tsx b/lib/static/new-ui/pages/InfoPage/index.tsx new file mode 100644 index 000000000..a9c015798 --- /dev/null +++ b/lib/static/new-ui/pages/InfoPage/index.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export function InfoPage(): JSX.Element { + return
hello from info page!
; +} diff --git a/lib/static/new-ui/pages/SuitesPage/StatusFilter.tsx b/lib/static/new-ui/pages/SuitesPage/StatusFilter.tsx new file mode 100644 index 000000000..aca4c58cb --- /dev/null +++ b/lib/static/new-ui/pages/SuitesPage/StatusFilter.tsx @@ -0,0 +1,42 @@ +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/pages/SuitesPage/index.tsx b/lib/static/new-ui/pages/SuitesPage/index.tsx new file mode 100644 index 000000000..3b0f0f2ed --- /dev/null +++ b/lib/static/new-ui/pages/SuitesPage/index.tsx @@ -0,0 +1,155 @@ +import React, {useMemo} from 'react'; +import {SplitViewLayout} from '../../layouts/SplitViewLayout'; +import {Flex} from '@gravity-ui/uikit'; +import {StatusFilter} from './StatusFilter'; + +import {connect} from 'react-redux'; +import {AutoSizer, CellMeasurer, CellMeasurerCache, List} from 'react-virtualized'; +// import {VariableSizeList} from 'react-window'; +import { + unstable_useList as useList, + unstable_ListContainerView as ListContainerView, + unstable_ListItemView as ListItemView, + unstable_getItemRenderState as getItemRenderState, + unstable_getListItemClickHandler as getListItemClickHandler, +} from '@gravity-ui/uikit/unstable'; +import ResizeObserver from 'rc-resize-observer'; + +interface SuitesPageInternalProps { + suites: any; + browsers: any; +} + +function SuitesPageInternal(props: SuitesPageInternalProps): JSX.Element { + console.log(props.suites); + + const formatBrowser = (browserData: any) => { + if (browserData.parentId === 'describe (hey) some-it') { + return { + data: {title: browserData.name}, + children: [{ + data: {title: 'pic', subtitle: wow
omg
just to be sure!
}, + children: [] + }] + }; + } else { + return {data: {title: browserData.name}, children: []}; + } + }; + + const formatSuite = (suiteData: any) => { + if (suiteData.browserIds) { + return { + data: {title: suiteData.name}, + children: suiteData.browserIds.map((browserId: any) => formatBrowser(props.browsers.byId[browserId])), + }; + } else { + return { + data: {title: suiteData.name}, + children: suiteData.suiteIds.map((suiteId: any) => formatSuite(props.suites.byId[suiteId])) + }; + } + }; + + const items = useMemo(() => props.suites.allRootIds.map((rootId: any) => { + return { + data: { title: rootId }, + children: props.suites.byId[rootId].suiteIds.map((suiteId: any) => formatSuite(props.suites.byId[suiteId])) + }; + }), [props.suites]); + + const list = useList({items}); + console.log(items); + console.log(list); + + const _suitesMeasurementCache = new CellMeasurerCache({ + fixedWidth: true, + defaultHeight: 30 + }); + + const onItemClick = getListItemClickHandler({list}); + + return +
+ +

Suites

+ + + + {({width, height}) => ( + 30} + rowRenderer={({index, key, style, parent}) => { + // console.log(getItemRenderState({ + // qa: '', + // list: list, + // onItemClick: () => {}, + // mapItemDataToProps: (x) => x as any, + // size: 'm', + // // multiple: , + // id: data[index], + // })); + + return ( + + {({measure}) => ( +
+ + x as any, + size: 'm', + // multiple: , + id: list.structure.visibleFlattenIds[index] + }).props as any} /> + +
+ )} +
+ //
+ // + // + //
+ ); + }} + rowHeight={_suitesMeasurementCache.rowHeight} + /> + )} +
+
+
+
+
+
; +} + +// export default connect( +// (state) => { +// return { +// processing: state.processing, +// acceptableOpenedImageIds: getAcceptableOpenedImageIds(state), +// isStaticImageAccepterEnabled: state.staticImageAccepter.enabled, +// }; +// }, +// (dispatch) => ({actions: bindActionCreators(actions, dispatch)}) +// )(AcceptOpenedButton); + +export const SuitesPage = connect( + (state: any) => ({ + suites: state.tree.suites, + browsers: state.tree.browsers, + }) +)(SuitesPageInternal); diff --git a/lib/static/new-ui/pages/VisualChecksPage/index.tsx b/lib/static/new-ui/pages/VisualChecksPage/index.tsx new file mode 100644 index 000000000..d364acd04 --- /dev/null +++ b/lib/static/new-ui/pages/VisualChecksPage/index.tsx @@ -0,0 +1,5 @@ +import React from 'react'; + +export function VisualChecksPage(): JSX.Element { + return
hello from visual checks page!
; +} diff --git a/lib/static/new-ui/report.tsx b/lib/static/new-ui/report.tsx new file mode 100644 index 000000000..8a3c120ef --- /dev/null +++ b/lib/static/new-ui/report.tsx @@ -0,0 +1,7 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import {App} from './App'; + +const rootEl = document.getElementById('app'); + +ReactDOM.render(, rootEl); diff --git a/lib/static/new-ui/typings.d.ts b/lib/static/new-ui/typings.d.ts new file mode 100644 index 000000000..1d8d50c6c --- /dev/null +++ b/lib/static/new-ui/typings.d.ts @@ -0,0 +1,9 @@ +declare module '*.svg' { + const value: string; + export = value; +} + +declare module '*.module.css' { + const classes: { [key: string]: string }; + export default classes; +} diff --git a/lib/static/styles.css b/lib/static/styles.css index 22ba9d433..60c41c0df 100644 --- a/lib/static/styles.css +++ b/lib/static/styles.css @@ -946,3 +946,153 @@ a:active { flex-grow: 1; height: 25px; } + +.g-root_theme_light { + --g-color-base-brand: var(--g-color-private-color-400); + --g-color-base-brand-hover: var(--g-color-private-color-150); + --g-color-base-selection: var(--g-color-private-color-100); + --g-color-base-selection-hover: var(--g-color-private-color-150); + --g-color-line-brand: var(--g-color-private-color-200); + --g-color-text-brand: var(--g-color-private-color-300); + --g-color-text-link: var(--g-color-private-color-300); + --g-color-text-link-hover: var(--g-color-private-color-500); + --gn-aside-header-decoration-collapsed-background-color: var(--g-color-private-color-150); + + --g-color-private-color-50: rgba(108,71,255,0.1); + --g-color-private-color-100: rgba(108,71,255,0.15); + --g-color-private-color-150: rgba(108,71,255,0.2); + --g-color-private-color-200: rgba(108,71,255,0.3); + --g-color-private-color-250: rgba(108,71,255,0.4); + --g-color-private-color-300: rgba(108,71,255,0.5); + --g-color-private-color-350: rgba(108,71,255,0.6); + --g-color-private-color-400: rgba(108,71,255,0.7); + --g-color-private-color-450: rgba(108,71,255,0.8); + --g-color-private-color-500: rgba(108,71,255,0.9); + + --g-color-private-color-50-solid: rgb(240,237,255); + --g-color-private-color-100-solid: rgb(233,227,255); + --g-color-private-color-150-solid: rgb(226,218,255); + --g-color-private-color-200-solid: rgb(211,200,255); + --g-color-private-color-250-solid: rgb(196,181,255); + --g-color-private-color-300-solid: rgb(182,163,255); + --g-color-private-color-350-solid: rgb(167,145,255); + --g-color-private-color-400-solid: rgb(152,126,255); + --g-color-private-color-450-solid: rgb(137,108,255); + --g-color-private-color-500-solid: rgb(123,89,255); + --g-color-private-color-550-solid: rgb(108,71,255); + --g-color-private-color-600-solid: rgb(102,68,235); + --g-color-private-color-650-solid: rgb(95,66,214); + --g-color-private-color-700-solid: rgb(89,63,194); + --g-color-private-color-750-solid: rgb(83,60,173); + --g-color-private-color-800-solid: rgb(77,58,153); + --g-color-private-color-850-solid: rgb(70,55,133); + --g-color-private-color-900-solid: rgb(64,52,112); + --g-color-private-color-950-solid: rgb(58,49,92); + --g-color-private-color-1000-solid: rgb(54,48,82); +} + +.split { + display: flex; + flex-direction: row; + height: 100vh; +} + +.split > div { + overflow: scroll; +} + +.gutter { + background-color: #eee; + background-repeat: no-repeat; + background-position: 50%; +} + +.gutter.gutter-horizontal { + background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg=='); + cursor: col-resize; +} + +/* Gravity UI Font Styles */ +.text-display-1 { + font-family: Jost, sans-serif; + font-weight: 600; + font-size: var(--g-text-display-1-font-size); + line-height: var(--g-text-display-1-line-height); + margin: 0; +} + +.text-display-2 { + font-family: Jost, sans-serif; + font-weight: 600; + font-size: 32px; + line-height: 40px; +} + +.text-display-3 { + font-family: Jost, sans-serif; + font-weight: 600; + font-size: var(--g-text-display-3-font-size); + line-height: var(--g-text-display-3-line-height); +} + +.text-header-1 { + font-family: Jost, sans-serif; + font-weight: 600; + font-size: var(--g-text-header-1-font-size); + line-height: var(--g-text-header-1-line-height); +} + +.text-subheader-1 { + font-family: Jost, sans-serif; + font-weight: 600; + font-size: var(--g-text-subheader-1-font-size); + line-height: var(--g-text-subheader-1-line-height); +} + +.text-subheader-3 { + font-family: Jost, sans-serif; + font-weight: 600; + font-size: var(--g-text-subheader-3-font-size); + line-height: var(--g-text-subheader-3-line-height); +} + +.controls { + display: flex; + flex-direction: column; + margin: var(--g-spacing-2); +} + .controls-row { + align-items: center; + display: flex; + height: 28px; + margin-top: var(--g-spacing-2); + } + .controls-row:first-child { + margin-top: 0; + } + + .controls-row__heading { + margin-right: auto; + } + + .controls-row > * { + margin-left: var(--g-spacing-2); + } + + .controls-row > *:first-child { + margin-left: 0; + } + +.status-switcher__option { + align-items: center; + display: flex; + justify-content: center; +} + + .status-switcher__option > *:first-child { + margin-right: 2px; + } + +.g-radio-button__option-text { + margin: 0 4px !important; +} diff --git a/lib/static/template.html b/lib/static/template.html index 03fc91e82..bfa502904 100644 --- a/lib/static/template.html +++ b/lib/static/template.html @@ -8,6 +8,7 @@ +
diff --git a/package.json b/package.json index 6b3fa89e8..3f6951af2 100644 --- a/package.json +++ b/package.json @@ -85,6 +85,7 @@ "@babel/runtime": "^7.22.5", "@gemini-testing/commander": "^2.15.3", "@gemini-testing/sql.js": "^2.0.0", + "@gravity-ui/navigation": "^2.16.0", "ansi-html-community": "^0.0.8", "axios": "1.6.3", "better-sqlite3": "^10.0.0", @@ -111,6 +112,9 @@ "ora": "^5.4.1", "p-queue": "^5.0.0", "qs": "^6.9.1", + "react-split": "^2.0.14", + "react-virtualized-auto-sizer": "^1.0.24", + "react-window": "^1.8.10", "signal-exit": "^4.1.0", "strip-ansi": "^6.0.1", "tmp": "^0.1.0", @@ -146,6 +150,9 @@ "@types/npm-which": "^3.0.3", "@types/opener": "^1.4.0", "@types/proxyquire": "^1.3.28", + "@types/react-dom": "^16.9.8", + "@types/react-virtualized": "^9.21.30", + "@types/react-window": "^1.8.8", "@types/sinon": "^4.3.3", "@types/tmp": "^0.1.0", "@types/urijs": "^1.19.19", @@ -209,6 +216,7 @@ "react-redux": "^7.2.1", "react-virtualized": "^9.22.5", "reapop": "^4.2.2", + "react-router-dom": "^6.25.1", "reduce-reducers": "^1.0.4", "redux": "^4.0.5", "redux-logger": "^3.0.6", diff --git a/webpack.common.js b/webpack.common.js index 925f12041..84c5018ba 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -15,7 +15,9 @@ const staticPath = path.resolve(__dirname, 'build', 'lib', 'static'); module.exports = { entry: { report: ['./index.jsx', './variables.css', './styles.css'], - gui: ['./gui.jsx', './variables.css', './styles.css', './gui.css'] + gui: ['./gui.jsx', './variables.css', './styles.css', './gui.css'], + newReport: ['./new-ui/report.tsx', './variables.css', './styles.css'], + newGui: ['./new-ui/gui.tsx', './variables.css', './styles.css', './gui.css'] }, resolve: { extensions: ['.js', '.jsx', '.ts', '.tsx'], @@ -62,7 +64,7 @@ module.exports = { } }, generator: { - filename: path.resolve(__dirname, 'lib', 'static', 'assets', '[contenthash][ext][query]') + filename: path.join('assets', '[contenthash][ext][query]') } } @@ -107,6 +109,28 @@ module.exports = { files: ['gui.html'], scripts: ['sql-wasm.js'], append: false + }), + new HtmlWebpackPlugin({ + title: 'HTML report (New UI)', + filename: 'new-ui-report.html', + template: 'template.html', + chunks: ['newReport'] + }), + new HtmlWebpackTagsPlugin({ + files: ['new-ui-report.html'], + scripts: ['data.js', 'sql-wasm.js'], + append: false + }), + new HtmlWebpackPlugin({ + title: 'GUI report (New UI)', + filename: 'new-ui-gui.html', + template: 'template.html', + chunks: ['newGui'] + }), + new HtmlWebpackTagsPlugin({ + files: ['new-ui-gui.html'], + scripts: ['sql-wasm.js'], + append: false }) ], optimization: { From be6a7c5800df16e51be3be265e34d7bbb6cd1373 Mon Sep 17 00:00:00 2001 From: shadowusr Date: Tue, 30 Jul 2024 00:50:39 +0300 Subject: [PATCH 02/11] feat: incorporated react-virtual --- lib/static/new-ui.css | 22 ++ lib/static/new-ui/App.tsx | 1 + .../new-ui/pages/SuitesPage/index.module.css | 16 + lib/static/new-ui/pages/SuitesPage/index.tsx | 329 ++++++++++++++---- package.json | 1 + webpack.common.js | 10 +- 6 files changed, 302 insertions(+), 77 deletions(-) create mode 100644 lib/static/new-ui.css create mode 100644 lib/static/new-ui/pages/SuitesPage/index.module.css diff --git a/lib/static/new-ui.css b/lib/static/new-ui.css new file mode 100644 index 000000000..91e213369 --- /dev/null +++ b/lib/static/new-ui.css @@ -0,0 +1,22 @@ +.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 { + font-family: var(--g-font-family-sans), sans-serif !important; +} diff --git a/lib/static/new-ui/App.tsx b/lib/static/new-ui/App.tsx index b4a8a6a43..a4e63f2dc 100644 --- a/lib/static/new-ui/App.tsx +++ b/lib/static/new-ui/App.tsx @@ -9,6 +9,7 @@ import {InfoPage} from './pages/InfoPage'; import '@gravity-ui/uikit/styles/fonts.css'; import '@gravity-ui/uikit/styles/styles.css'; +import '../new-ui.css'; import {Provider} from 'react-redux'; import store from '../modules/store'; diff --git a/lib/static/new-ui/pages/SuitesPage/index.module.css b/lib/static/new-ui/pages/SuitesPage/index.module.css new file mode 100644 index 000000000..1ed2e8bd7 --- /dev/null +++ b/lib/static/new-ui/pages/SuitesPage/index.module.css @@ -0,0 +1,16 @@ +.controls-row { + align-items: center; + display: flex; + height: 28px; + margin-top: var(--g-spacing-2); +} + +.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);*/ +} + +.tree-view :global(.g-text_ellipsis) { + /*white-space: normal;*/ +} diff --git a/lib/static/new-ui/pages/SuitesPage/index.tsx b/lib/static/new-ui/pages/SuitesPage/index.tsx index 3b0f0f2ed..784d1d204 100644 --- a/lib/static/new-ui/pages/SuitesPage/index.tsx +++ b/lib/static/new-ui/pages/SuitesPage/index.tsx @@ -1,66 +1,134 @@ -import React, {useMemo} from 'react'; +import React, {ChangeEvent, useCallback, useEffect, useMemo, memo} from 'react'; import {SplitViewLayout} from '../../layouts/SplitViewLayout'; -import {Flex} from '@gravity-ui/uikit'; +import {Flex, TextInput} from '@gravity-ui/uikit'; import {StatusFilter} from './StatusFilter'; import {connect} from 'react-redux'; -import {AutoSizer, CellMeasurer, CellMeasurerCache, List} from 'react-virtualized'; -// import {VariableSizeList} from 'react-window'; +import {AutoSizer, CellMeasurer, CellMeasurerCache} from 'react-virtualized'; +import {VariableSizeList as List} from 'react-window'; import { unstable_useList as useList, unstable_ListContainerView as ListContainerView, unstable_ListItemView as ListItemView, unstable_getItemRenderState as getItemRenderState, unstable_getListItemClickHandler as getListItemClickHandler, + // unstable_useListFilter as useListFilter, } from '@gravity-ui/uikit/unstable'; import ResizeObserver from 'rc-resize-observer'; +import styles from './index.module.css'; +import {useVirtualizer} from '@tanstack/react-virtual'; + interface SuitesPageInternalProps { suites: any; browsers: any; } -function SuitesPageInternal(props: SuitesPageInternalProps): JSX.Element { - console.log(props.suites); - - const formatBrowser = (browserData: any) => { - if (browserData.parentId === 'describe (hey) some-it') { - return { - data: {title: browserData.name}, - children: [{ - data: {title: 'pic', subtitle: wow
omg
just to be sure!
}, - children: [] - }] - }; - } else { - return {data: {title: browserData.name}, children: []}; - } - }; +function ListItem({key, measure, style, props,}: any) { + return
+ + + +
; +} + +function ListViewProxy(props) { + console.log('re-render!'); + console.log(props); - const formatSuite = (suiteData: any) => { - if (suiteData.browserIds) { - return { - data: {title: suiteData.name}, - children: suiteData.browserIds.map((browserId: any) => formatBrowser(props.browsers.byId[browserId])), - }; - } else { - return { - data: {title: suiteData.name}, - children: suiteData.suiteIds.map((suiteId: any) => formatSuite(props.suites.byId[suiteId])) - }; + useEffect(() => { + return () => { + console.log('list item unmounts...'); } - }; + }, []); - const items = useMemo(() => props.suites.allRootIds.map((rootId: any) => { - return { - data: { title: rootId }, - children: props.suites.byId[rootId].suiteIds.map((suiteId: any) => formatSuite(props.suites.byId[suiteId])) - }; - }), [props.suites]); + return ; +} + +const ListItemViewMemo = memo(ListViewProxy, (...args) => { + // console.log('memo:'); + // console.log(args); + return true; +}); + +function SuitesPageInternal(props: SuitesPageInternalProps): JSX.Element { + console.log(props); + // + // const formatBrowser = (browserData: any, parentSuite: any) => { + // const data = {title: browserData.name, fullTitle: (parentSuite.fullTitle + ' ' + browserData.name).trim()}; + // if (browserData.parentId === 'describe (hey) some-it') { + // return { + // data, + // children: [{ + // data: {title: 'pic', + // subtitle: wow
omg
just to be sure!
, + // fullTitle: (parentSuite.fullTitle + ' ' + browserData.name).trim() + // } + // }] + // }; + // } else { + // return {data}; + // } + // }; + // + // const formatSuite = (suiteData: any, parentSuite: any) => { + // const data = {title: suiteData.name, fullTitle: (parentSuite.fullTitle + ' ' + suiteData.name).trim()}; + // if (suiteData.browserIds) { + // return { + // data, + // children: suiteData.browserIds.map((browserId: any) => formatBrowser(props.browsers.byId[browserId], data)) + // }; + // } else { + // return { + // data, + // children: suiteData.suiteIds.map((suiteId: any) => formatSuite(props.suites.byId[suiteId], data)) + // }; + // } + // }; + // + // const items = useMemo(() => props.suites.allRootIds.map((rootId: any) => { + // return { + // data: { title: rootId, fullTitle: rootId }, + // children: props.suites.byId[rootId].suiteIds.map((suiteId: any) => formatSuite(props.suites.byId[suiteId], {fullTitle: rootId})) + // }; + // }), [props.suites]); + // + // const {onFilterUpdate, items: filteredItems} = useListFilter({ + // items, + // filterItem(value: string, item: any): boolean { + // console.log('filtering! by ' + value); + // console.log(item); + // + // return item.fullTitle.includes(value); + // // return true; + // }, + // }) + + const itemsOriginal: any[] = useMemo(() => { + const result = [] as any[]; + for (let i = 0; i < 200; i++) { + const item = {data: {title: 'Some Title ' + i, fullTitle: 'Some-full-title ' + i}, + children: [ + {data: {title: 'Child #' + i, fullTitle: 'full-child-title' + i}} + ]}; + + if (i === 50) { + (item.children[0].data as any).subtitle =
Hello!
+ } - const list = useList({items}); - console.log(items); + result.push(item); + } + return result; + }, []); + + const list = useList({items: itemsOriginal}); + // console.log(items); console.log(list); + console.log('length:' + list.structure.items.length); const _suitesMeasurementCache = new CellMeasurerCache({ fixedWidth: true, @@ -69,55 +137,165 @@ function SuitesPageInternal(props: SuitesPageInternalProps): JSX.Element { const onItemClick = getListItemClickHandler({list}); + const onChangeFilter = (value: ChangeEvent) => { + console.log((value.target as any).value); + // onFilterUpdate((value.target as any).value); + }; + + useEffect(() => { + return () => { + console.log('component unmounting!'); + }; + }, []); + + console.log('re-rendering the whole thing!!'); + const parentRef = React.useRef(null) + + const virtualizer = useVirtualizer({ + count: list.structure.visibleFlattenIds.length, + getScrollElement: () => parentRef.current, + estimateSize: () => 45, + getItemKey: useCallback((index) => list.structure.visibleFlattenIds[index], [list]), + enabled: true, + overscan: 25, + }); + + useEffect(() => { + setTimeout(() => { + console.log('scrolling tooooo !!'); + virtualizer.scrollToIndex(100, {align: 'start'}); + }, 2000); + }, []); + + const items = virtualizer.getVirtualItems() + return

Suites

- - - +
+ +
+
+ +
+ +
+
+
+ {items.map((virtualRow) => ( +
+ ({ + title: x.title, + subtitle: x.subtitle + }), + size: 'm', + // multiple: , + id: list.structure.visibleFlattenIds[virtualRow.index] + }).props}/> + {/*
+
Row {virtualRow.index}
+
{list.structure.items[virtualRow.index].data.title}
+
*/} +
+ ))} +
+
+
+ {/* {({width, height}) => ( 30} - rowRenderer={({index, key, style, parent}) => { - // console.log(getItemRenderState({ - // qa: '', - // list: list, - // onItemClick: () => {}, - // mapItemDataToProps: (x) => x as any, - // size: 'm', - // // multiple: , - // id: data[index], - // })); - + itemCount={list.structure.visibleFlattenIds.length} + itemData={{data: list, newId: list.structure.visibleFlattenIds}} + itemSize={(_index) => 30} + // rowHeight={_suitesMeasurementCache.rowHeight} + > + {({index, style, ...other}) => { + console.log('other'); + console.log(index); + console.log(style); + console.log(parent); + console.log(other); return ( - {({measure}) => ( -
- - x as any, - size: 'm', - // multiple: , - id: list.structure.visibleFlattenIds[index] - }).props as any} /> - -
- )} + {({measure}) => { + console.log('hey! debugging key!'); + console.log((list.structure.visibleFlattenIds[index] as any)) + + return ( + ({ + title: x.title, + subtitle: x.subtitle + }), + size: 'm', + // multiple: , + id: list.structure.visibleFlattenIds[index] + }).props}/> + //
+ // + // ({ + // title: x.title, + // subtitle: x.subtitle + // }), + // size: 'm', + // // multiple: , + // id: list.structure.visibleFlattenIds[index] + // }).props as any} /> + // + //
+ ); + }}
//
// @@ -125,10 +303,9 @@ function SuitesPageInternal(props: SuitesPageInternalProps): JSX.Element { //
); }} - rowHeight={_suitesMeasurementCache.rowHeight} - /> +
)} -
+
*/}
diff --git a/package.json b/package.json index 3f6951af2..373aa6886 100644 --- a/package.json +++ b/package.json @@ -132,6 +132,7 @@ "@playwright/test": "^1.44.1", "@react-hook/resize-observer": "^2.0.1", "@swc/core": "^1.3.64", + "@tanstack/react-virtual": "^3.8.3", "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.5.2", "@types/babel__core": "^7.20.5", diff --git a/webpack.common.js b/webpack.common.js index 84c5018ba..969fecf1a 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -40,7 +40,15 @@ module.exports = { rules: [ { test: /\.css$/, - use: [MiniCssExtractPlugin.loader, 'css-loader'] + use: [MiniCssExtractPlugin.loader, { + loader: 'css-loader', + options: { + modules: { + auto: true, + exportLocalsConvention: 'camelCase' + } + } + }] }, { test: /\.less$/, From 4a9609ed17f44fe8f3cf35fc646612550f0ffd96 Mon Sep 17 00:00:00 2001 From: shadowusr Date: Wed, 31 Jul 2024 15:14:41 +0300 Subject: [PATCH 03/11] feat: implement zoom feature and previews in tree --- lib/static/new-ui/gui.tsx | 9 +- .../new-ui/pages/SuitesPage/index.module.css | 49 ++- lib/static/new-ui/pages/SuitesPage/index.tsx | 306 +++++++++---- package-lock.json | 416 ++++++++++++------ package.json | 4 +- 5 files changed, 551 insertions(+), 233 deletions(-) diff --git a/lib/static/new-ui/gui.tsx b/lib/static/new-ui/gui.tsx index d85c67397..1564aa0eb 100644 --- a/lib/static/new-ui/gui.tsx +++ b/lib/static/new-ui/gui.tsx @@ -1,12 +1,13 @@ import React, {useEffect} from 'react'; -import ReactDOM from 'react-dom'; +import {createRoot} from 'react-dom/client'; import {App} from './App'; import store from '../modules/store'; import {finGuiReport, initGuiReport} from '../modules/actions'; -const rootEl = document.getElementById('app'); +const rootEl = document.getElementById('app') as HTMLDivElement; +const root = createRoot(rootEl); -function Gui(): JSX.Element { +function Gui(): React.JSX.Element { useEffect(() => { store.dispatch(initGuiReport()); @@ -18,4 +19,4 @@ function Gui(): JSX.Element { return ; } -ReactDOM.render(, rootEl); +root.render(); diff --git a/lib/static/new-ui/pages/SuitesPage/index.module.css b/lib/static/new-ui/pages/SuitesPage/index.module.css index 1ed2e8bd7..c77b247d6 100644 --- a/lib/static/new-ui/pages/SuitesPage/index.module.css +++ b/lib/static/new-ui/pages/SuitesPage/index.module.css @@ -6,11 +6,52 @@ } .tree-view { - --g-text-subheader-1-font-size: 50px; - --g-text-subheader-1-line-height: 50px; + /*--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;*/ } + .tree-view__item > div { + align-items: start !important; + } -.tree-view :global(.g-text_ellipsis) { - /*white-space: normal;*/ + .tree-view__item { + --g-text-subheader-1-font-size: 18px; + --g-text-subheader-1-line-height: 22px; + --g-text-subheader-font-weight: normal; + + --g-text-body-1-font-size: 18px; + --g-text-body-1-line-height: 22px; + --g-text-body-font-weight: normal; + + padding: 4px 0; + font-size: 18px; + } + + .tree-item__error-title { + color: var(--g-color-private-red-600-solid); + margin-left: var(--g-spacing-1); + } + + .tree-item__error-stack { + white-space: pre; + overflow: hidden; + text-overflow: ellipsis; + line-height: 22px; + color: var(--g-color-base-danger-heavy); + margin-top: 8px; + } + + .tree-item--error { + background: var(--g-color-private-red-50); + } + + + .tree-view :global(.g-text_ellipsis) { + white-space: normal; + } + +.fail-color { + color: var(--g-color-private-red-600-solid); } + diff --git a/lib/static/new-ui/pages/SuitesPage/index.tsx b/lib/static/new-ui/pages/SuitesPage/index.tsx index 784d1d204..8fddca24c 100644 --- a/lib/static/new-ui/pages/SuitesPage/index.tsx +++ b/lib/static/new-ui/pages/SuitesPage/index.tsx @@ -1,7 +1,9 @@ -import React, {ChangeEvent, useCallback, useEffect, useMemo, memo} from 'react'; +import React, {ChangeEvent, useCallback, useEffect, useMemo, memo, useState} from 'react'; import {SplitViewLayout} from '../../layouts/SplitViewLayout'; -import {Flex, TextInput} from '@gravity-ui/uikit'; +import {Box, Flex, TextInput} from '@gravity-ui/uikit'; import {StatusFilter} from './StatusFilter'; +import {last} from 'lodash'; +import {CircleXmark, CircleDashed} from '@gravity-ui/icons'; import {connect} from 'react-redux'; import {AutoSizer, CellMeasurer, CellMeasurerCache} from 'react-virtualized'; @@ -14,30 +16,34 @@ import { unstable_getListItemClickHandler as getListItemClickHandler, // unstable_useListFilter as useListFilter, } from '@gravity-ui/uikit/unstable'; -import ResizeObserver from 'rc-resize-observer'; +// import ResizeObserver from 'rc-resize-observer'; import styles from './index.module.css'; import {useVirtualizer} from '@tanstack/react-virtual'; +import {trimArray} from '../../../../common-utils'; interface SuitesPageInternalProps { suites: any; browsers: any; + results: any; + images: any; } -function ListItem({key, measure, style, props,}: any) { - return
- - - -
; -} +// function ListItem({key, measure, style, props,}: any) { +// return
+// +// +// +//
; +// } function ListViewProxy(props) { console.log('re-render!'); console.log(props); + console.log(styles); useEffect(() => { return () => { @@ -48,6 +54,87 @@ function ListViewProxy(props) { return ; } +function ImageWithMagnifier({ + src, + className = '', + style, + onStyleUpdate, + // width, + // height, + // alt, + magnifierHeight = 150, + magnifierWidth = 150, + zoomLevel = 3 +}) { + const [showMagnifier, setShowMagnifier] = useState(false); + const [[imgWidth, imgHeight], setSize] = useState([0, 0]); + const [[x, y], setXY] = useState([0, 0]); + + const mouseEnter = (e) => { + const el = e.currentTarget; + + const { width, height } = el.getBoundingClientRect(); + setSize([width, height]); + setShowMagnifier(true); + } + + const mouseLeave = (e) => { + e.preventDefault(); + setShowMagnifier(false); + } + + const mouseMove = (e) => { + const el = e.currentTarget; + const { top, left } = el.getBoundingClientRect(); + + const x = e.pageX - left - window.scrollX; + const y = e.pageY - top - window.scrollY; + + setXY([x, y]); + }; + + useEffect(() => { + onStyleUpdate({ + display: showMagnifier ? '' : 'none', + // position: 'absolute', + position: 'fixed', + pointerEvents: 'none', + height: `${magnifierHeight}px`, + width: `${magnifierWidth}px`, + opacity: '1', + border: '1px solid lightgrey', + backgroundColor: 'white', + borderRadius: '5px', + backgroundImage: `url('${src}')`, + backgroundRepeat: 'no-repeat', + // top: `${y - magnifierHeight / 2}px`, + // left: `${x - magnifierWidth / 2}px`, + top: '20px', + right: '20px', + backgroundSize: `${imgWidth * zoomLevel}px ${imgHeight * zoomLevel}px`, + backgroundPositionX: `${-x * zoomLevel + magnifierWidth / 2}px`, + backgroundPositionY: `${-y * zoomLevel + magnifierHeight / 2}px`, + }); + }, [showMagnifier, imgWidth, imgHeight, x, y]); + + return
+ {alt} mouseEnter(e)} + onMouseLeave={(e) => mouseLeave(e)} + onMouseMove={(e) => mouseMove(e)} + /> + {/**/} +
+} + const ListItemViewMemo = memo(ListViewProxy, (...args) => { // console.log('memo:'); // console.log(args); @@ -55,47 +142,72 @@ const ListItemViewMemo = memo(ListViewProxy, (...args) => { }); function SuitesPageInternal(props: SuitesPageInternalProps): JSX.Element { + console.log('new version'); console.log(props); // - // const formatBrowser = (browserData: any, parentSuite: any) => { - // const data = {title: browserData.name, fullTitle: (parentSuite.fullTitle + ' ' + browserData.name).trim()}; - // if (browserData.parentId === 'describe (hey) some-it') { - // return { - // data, - // children: [{ - // data: {title: 'pic', - // subtitle: wow
omg
just to be sure!
, - // fullTitle: (parentSuite.fullTitle + ' ' + browserData.name).trim() - // } - // }] - // }; - // } else { - // return {data}; - // } - // }; - // - // const formatSuite = (suiteData: any, parentSuite: any) => { - // const data = {title: suiteData.name, fullTitle: (parentSuite.fullTitle + ' ' + suiteData.name).trim()}; - // if (suiteData.browserIds) { - // return { - // data, - // children: suiteData.browserIds.map((browserId: any) => formatBrowser(props.browsers.byId[browserId], data)) - // }; - // } else { - // return { - // data, - // children: suiteData.suiteIds.map((suiteId: any) => formatSuite(props.suites.byId[suiteId], data)) - // }; - // } - // }; - // - // const items = useMemo(() => props.suites.allRootIds.map((rootId: any) => { - // return { - // data: { title: rootId, fullTitle: rootId }, - // children: props.suites.byId[rootId].suiteIds.map((suiteId: any) => formatSuite(props.suites.byId[suiteId], {fullTitle: rootId})) - // }; - // }), [props.suites]); + const formatBrowser = (browserData: any, parentSuite: any) => { + const lastResult = props.results.byId[last(browserData.resultIds)]; + let children: any[] | undefined = undefined; + + // if (browserData.parentId === 'describe (hey) some-it') { + // children = [{ + // data: { + // title: 'pic', + // subtitle: wow
omg
just to be sure!
, + // fullTitle: (parentSuite.fullTitle + ' ' + browserData.name).trim() + // } + // }]; + // } + const diffImgId = lastResult.imageIds.find(imageId => props.images.byId[imageId].stateName); + const diffImg = props.images.byId[diffImgId]?.diffImg; + + let errorStack; + if (lastResult.status === 'error' && lastResult.error?.stack) { + const stackLines = trimArray(lastResult.error.stack.split('\n')); + errorStack = stackLines.slice(0, 3).join('\n'); + } + + const data = { + title: browserData.name, + fullTitle: (parentSuite.fullTitle + ' ' + browserData.name).trim(), + status: lastResult.status, + errorTitle: lastResult.error?.name, + errorStack, + hasChildren: Boolean(children?.length), + diffImg + }; + + return {data, children}; + }; + + const formatSuite = (suiteData: any, parentSuite: any) => { + const data = { + title: suiteData.name, + fullTitle: (parentSuite.fullTitle + ' ' + suiteData.name).trim(), + status: suiteData.status, + hasChildren: true, + }; + if (suiteData.browserIds) { + return { + data, + children: suiteData.browserIds.map((browserId: any) => formatBrowser(props.browsers.byId[browserId], data)) + }; + } else { + return { + data, + children: suiteData.suiteIds.map((suiteId: any) => formatSuite(props.suites.byId[suiteId], data)) + }; + } + }; + + const itemsOriginal = useMemo(() => props.suites.allRootIds.map((rootId: any) => { + return formatSuite(props.suites.byId[rootId], {fullTitle: ''}) + // return { + // data: { title: rootId, fullTitle: rootId }, + // children: props.suites.byId[rootId].suiteIds.map((suiteId: any) => formatSuite(props.suites.byId[suiteId], {fullTitle: rootId})) + // }; + }), [props.suites]); // // const {onFilterUpdate, items: filteredItems} = useListFilter({ // items, @@ -108,32 +220,32 @@ function SuitesPageInternal(props: SuitesPageInternalProps): JSX.Element { // }, // }) - const itemsOriginal: any[] = useMemo(() => { - const result = [] as any[]; - for (let i = 0; i < 200; i++) { - const item = {data: {title: 'Some Title ' + i, fullTitle: 'Some-full-title ' + i}, - children: [ - {data: {title: 'Child #' + i, fullTitle: 'full-child-title' + i}} - ]}; - - if (i === 50) { - (item.children[0].data as any).subtitle =
Hello!
- } - - result.push(item); - } - return result; - }, []); + // const itemsOriginal: any[] = useMemo(() => { + // const result = [] as any[]; + // for (let i = 0; i < 200; i++) { + // const item = {data: {title: 'Some Title ' + i, fullTitle: 'Some-full-title ' + i}, + // children: [ + // {data: {title: 'Child #' + i, fullTitle: 'full-child-title' + i}} + // ]}; + // + // if (i === 50) { + // (item.children[0].data as any).subtitle =
Hello!
+ // } + // + // result.push(item); + // } + // return result; + // }, []); const list = useList({items: itemsOriginal}); // console.log(items); console.log(list); console.log('length:' + list.structure.items.length); - const _suitesMeasurementCache = new CellMeasurerCache({ - fixedWidth: true, - defaultHeight: 30 - }); + // const _suitesMeasurementCache = new CellMeasurerCache({ + // fixedWidth: true, + // defaultHeight: 30 + // }); const onItemClick = getListItemClickHandler({list}); @@ -167,6 +279,8 @@ function SuitesPageInternal(props: SuitesPageInternalProps): JSX.Element { }, 2000); }, []); + const [magnifierStyle, setMagnifierStyle] = useState({display: 'none'}); + const items = virtualizer.getVirtualItems() return @@ -179,7 +293,7 @@ function SuitesPageInternal(props: SuitesPageInternalProps): JSX.Element {
- +
({ - title: x.title, - subtitle: x.subtitle - }), + mapItemDataToProps: (x) => { + let statusIcon; + if (x.status === 'fail' || x.status === 'error') { + statusIcon = ; + } else if (x.status === 'idle') { + statusIcon = ; + } + + const title =
+ {x.title} + {x.errorTitle && {x.errorTitle}} +
; + + let subtitle; + if (x.diffImg) { + // subtitle = + subtitle = + } else if (x.errorStack) { + subtitle =
+ {x.errorStack} +
; + } + + const classNames = [styles['tree-view__item']]; + if ((x.status === 'fail' || x.status === 'error') && !x.hasChildren) { + classNames.push(styles['tree-item--error']); + } + + return { + startSlot: {statusIcon}, + title, + subtitle, + className: classNames.join(' ') + // status: x.status + }; + }, size: 'm', // multiple: , id: list.structure.visibleFlattenIds[virtualRow.index] @@ -309,7 +455,9 @@ function SuitesPageInternal(props: SuitesPageInternalProps): JSX.Element {
-
+
+
+
; } @@ -328,5 +476,7 @@ export const SuitesPage = connect( (state: any) => ({ suites: state.tree.suites, browsers: state.tree.browsers, + results: state.tree.results, + images: state.tree.images, }) )(SuitesPageInternal); diff --git a/package-lock.json b/package-lock.json index b6bcc6b56..5d5e6619c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@babel/runtime": "^7.22.5", "@gemini-testing/commander": "^2.15.3", "@gemini-testing/sql.js": "^2.0.0", + "@gravity-ui/navigation": "^2.16.0", "ansi-html-community": "^0.0.8", "axios": "1.6.3", "better-sqlite3": "^10.0.0", @@ -43,6 +44,9 @@ "ora": "^5.4.1", "p-queue": "^5.0.0", "qs": "^6.9.1", + "react-split": "^2.0.14", + "react-virtualized-auto-sizer": "^1.0.24", + "react-window": "^1.8.10", "signal-exit": "^4.1.0", "strip-ansi": "^6.0.1", "tmp": "^0.1.0", @@ -63,6 +67,7 @@ "@playwright/test": "^1.44.1", "@react-hook/resize-observer": "^2.0.1", "@swc/core": "^1.3.64", + "@tanstack/react-virtual": "^3.8.3", "@testing-library/react": "^16.0.0", "@testing-library/user-event": "^14.5.2", "@types/babel__core": "^7.20.5", @@ -81,6 +86,9 @@ "@types/npm-which": "^3.0.3", "@types/opener": "^1.4.0", "@types/proxyquire": "^1.3.28", + "@types/react-dom": "^18.3.0", + "@types/react-virtualized": "^9.21.30", + "@types/react-window": "^1.8.8", "@types/sinon": "^4.3.3", "@types/tmp": "^0.1.0", "@types/urijs": "^1.19.19", @@ -142,6 +150,7 @@ "react-hotkeys": "^2.0.0", "react-markdown": "^6.0.3", "react-redux": "^7.2.1", + "react-router-dom": "^6.25.1", "react-virtualized": "^9.22.5", "reapop": "^4.2.2", "reduce-reducers": "^1.0.4", @@ -2178,8 +2187,7 @@ "node_modules/@bem-react/classname": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@bem-react/classname/-/classname-1.6.0.tgz", - "integrity": "sha512-SFBwUHMcb7TFFK5ld88+JhecoEun3/kHZ6KvLDjj3w5hv/tfRV8mtGHA8N42uMctXLF4bPEcr96xwXXcRFuweg==", - "dev": true + "integrity": "sha512-SFBwUHMcb7TFFK5ld88+JhecoEun3/kHZ6KvLDjj3w5hv/tfRV8mtGHA8N42uMctXLF4bPEcr96xwXXcRFuweg==" }, "node_modules/@bundled-es-modules/cookie": { "version": "2.0.0", @@ -2828,7 +2836,6 @@ "version": "3.7.0", "resolved": "https://registry.npmjs.org/@gravity-ui/components/-/components-3.7.0.tgz", "integrity": "sha512-cWmVOa6rErZY+F3Ws9zD6vvKlU2nn4eUFzS17R0TlVa4uaIcT2WsPGwAracby/o3Ys1gpSK71AkpW/c8lgcvog==", - "dev": true, "dependencies": { "@bem-react/classname": "^1.6.0", "@gravity-ui/date-utils": "^2.1.0", @@ -2848,7 +2855,6 @@ "version": "2.5.3", "resolved": "https://registry.npmjs.org/@gravity-ui/date-utils/-/date-utils-2.5.3.tgz", "integrity": "sha512-WetzttqlW454yGsh/LBo0AxuqIFT0erENCMYAfemhu6qLiuqy35NdDGP6VA4iDkWiSPFuQ1sRoVU8tC+OwiPEg==", - "dev": true, "dependencies": { "dayjs": "1.11.10", "lodash": "^4.17.0" @@ -2857,14 +2863,12 @@ "node_modules/@gravity-ui/i18n": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@gravity-ui/i18n/-/i18n-1.5.1.tgz", - "integrity": "sha512-ZvaQtRUf4Yl9zi0+SMzjlDeHp9+p5IXkNu2k6RtW04c+RYKA1jX+umeKNwzft4iR3+KxDlpLX2trTFEW6W7HKQ==", - "dev": true + "integrity": "sha512-ZvaQtRUf4Yl9zi0+SMzjlDeHp9+p5IXkNu2k6RtW04c+RYKA1jX+umeKNwzft4iR3+KxDlpLX2trTFEW6W7HKQ==" }, "node_modules/@gravity-ui/icons": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/@gravity-ui/icons/-/icons-2.10.0.tgz", "integrity": "sha512-xS0G4+TM7cD2cCKS4wVc01c4lLe/OreKjm4sHwrOtJWH4EawaRbpkuwtgUDcUvY2EryIcI6lgV+8o714m6lcyQ==", - "dev": true, "peerDependencies": { "react": "*" }, @@ -2874,11 +2878,27 @@ } } }, + "node_modules/@gravity-ui/navigation": { + "version": "2.17.2", + "resolved": "https://registry.npmjs.org/@gravity-ui/navigation/-/navigation-2.17.2.tgz", + "integrity": "sha512-cuYSAJYHgj2RCqwxyoq9nWeHXbHX/uXF3zNuowsUrs4P+gVp2oWP2F5fmbwX89Uafg8WMQ+MrIAAmEGSnz/b9A==", + "dependencies": { + "react-transition-group": "^4.4.1", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@bem-react/classname": "^1.6.0", + "@gravity-ui/components": "^3.0.0", + "@gravity-ui/icons": "^2.2.0", + "@gravity-ui/uikit": "^6.15.0", + "react": "^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@gravity-ui/uikit": { "version": "6.20.0", "resolved": "https://registry.npmjs.org/@gravity-ui/uikit/-/uikit-6.20.0.tgz", "integrity": "sha512-ngeFZH0CgF+6cNwWTrtwecYkb1Rb7TWFg3eUg87TGLvXiEemepJuwLvM4t9WX3yZFzFCzIoHML2h/SM6VXhjyQ==", - "dev": true, "dependencies": { "@bem-react/classname": "^1.6.0", "@gravity-ui/i18n": "^1.3.0", @@ -3385,7 +3405,6 @@ "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", - "dev": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" @@ -3550,6 +3569,15 @@ "react": ">=18" } }, + "node_modules/@remix-run/router": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.18.0.tgz", + "integrity": "sha512-L3jkqmqoSVBVKHfpGZmLrex0lxR5SucGA0sUfFzGctehw+S/ggL9L/0NnC5mw6P8HUWpFZ3nQw3cRApjjWx9Sw==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@semantic-ui-react/event-stack": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/@semantic-ui-react/event-stack/-/event-stack-3.1.3.tgz", @@ -3823,6 +3851,33 @@ "node": ">=14.16" } }, + "node_modules/@tanstack/react-virtual": { + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.8.4.tgz", + "integrity": "sha512-Dq0VQr3QlTS2qL35g360QaJWBt7tCn/0xw4uZ0dHXPLO1Ak4Z4nVX4vuj1Npg1b/jqNMDToRtR5OIxM2NXRBWg==", + "dev": true, + "dependencies": { + "@tanstack/virtual-core": "3.8.4" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@tanstack/virtual-core": { + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.8.4.tgz", + "integrity": "sha512-iO5Ujgw3O1yIxWDe9FgUPNkGjyT657b1WNX52u+Wv1DyBFEpdCdGkuVaky0M3hHFqNWjAmHWTn4wgj9rTr7ZQg==", + "dev": true, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@testing-library/dom": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", @@ -4088,8 +4143,7 @@ "node_modules/@types/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", - "dev": true + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==" }, "node_modules/@types/debug": { "version": "4.1.8", @@ -4187,7 +4241,6 @@ "version": "3.3.5", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", - "dev": true, "dependencies": { "@types/react": "*", "hoist-non-react-statics": "^3.3.0" @@ -4358,8 +4411,7 @@ "node_modules/@types/prop-types": { "version": "15.7.4", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz", - "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==", - "dev": true + "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==" }, "node_modules/@types/proxyquire": { "version": "1.3.28", @@ -4383,17 +4435,24 @@ "version": "18.3.3", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", - "dev": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, + "node_modules/@types/react-dom": { + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-redux": { "version": "7.1.33", "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.33.tgz", "integrity": "sha512-NF8m5AjWCkert+fosDsN3hAlHzpjSiXlVy9EgQEmLoBhaNXbmyeGs/aj5dQzKuF+/q+S7JQagorGDW8pJ28Hmg==", - "dev": true, "dependencies": { "@types/hoist-non-react-statics": "^3.3.0", "@types/react": "*", @@ -4401,6 +4460,25 @@ "redux": "^4.0.0" } }, + "node_modules/@types/react-virtualized": { + "version": "9.21.30", + "resolved": "https://registry.npmjs.org/@types/react-virtualized/-/react-virtualized-9.21.30.tgz", + "integrity": "sha512-4l2TFLQ8BCjNDQlvH85tU6gctuZoEdgYzENQyZHpgTHU7hoLzYgPSOALMAeA58LOWua8AzC6wBivPj1lfl6JgQ==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "@types/react": "*" + } + }, + "node_modules/@types/react-window": { + "version": "1.8.8", + "resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.8.tgz", + "integrity": "sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/responselike": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", @@ -8681,8 +8759,7 @@ "node_modules/blueimp-md5": { "version": "2.19.0", "resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz", - "integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==", - "dev": true + "integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==" }, "node_modules/bn.js": { "version": "5.2.0", @@ -9730,8 +9807,7 @@ "node_modules/classnames": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", - "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==", - "dev": true + "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" }, "node_modules/clean-css": { "version": "5.3.2", @@ -11497,7 +11573,6 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", - "dev": true, "dependencies": { "toggle-selection": "^1.0.6" } @@ -11951,7 +12026,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", - "dev": true, "dependencies": { "tiny-invariant": "^1.0.6" } @@ -12271,8 +12345,7 @@ "node_modules/csstype": { "version": "3.0.10", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.10.tgz", - "integrity": "sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA==", - "dev": true + "integrity": "sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA==" }, "node_modules/currently-unhandled": { "version": "0.4.1", @@ -12405,8 +12478,7 @@ "node_modules/dayjs": { "version": "1.11.10", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", - "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==", - "dev": true + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" }, "node_modules/debug": { "version": "4.3.4", @@ -13356,7 +13428,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", - "dev": true, "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" @@ -15441,7 +15512,6 @@ "version": "7.5.4", "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.5.4.tgz", "integrity": "sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==", - "dev": true, "dependencies": { "tabbable": "^6.2.0" } @@ -17075,7 +17145,6 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "dev": true, "dependencies": { "react-is": "^16.7.0" } @@ -19020,8 +19089,7 @@ "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "node_modules/js-yaml": { "version": "3.14.1", @@ -19781,7 +19849,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -20021,8 +20088,7 @@ "node_modules/memoize-one": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", - "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", - "dev": true + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" }, "node_modules/memory-fs": { "version": "0.4.1", @@ -21594,7 +21660,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -23414,7 +23479,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -23720,8 +23784,7 @@ "node_modules/raf-schd": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", - "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==", - "dev": true + "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==" }, "node_modules/randombytes": { "version": "2.1.0", @@ -23783,7 +23846,6 @@ "version": "10.6.2", "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-10.6.2.tgz", "integrity": "sha512-FjkoFjyvUQWcBo1F3RgSglky3ar0+qHLM41PlFVYB4Bj3RD8E/Mv7kqMouLFBU+3aFglMzzctAIWRwajEuueSw==", - "dev": true, "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.5", @@ -23801,7 +23863,6 @@ "version": "5.43.0", "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.43.0.tgz", "integrity": "sha512-AzC7KKOXFqAdIBqdGWepL9Xn7cm3vnAmjlHqUnoQaTMZYhM4VlXGLkkHHxj/BZ7Td0+SOPKB4RGPboBVKT9htw==", - "dev": true, "dependencies": { "@babel/runtime": "^7.18.3", "react-is": "^18.2.0" @@ -23814,14 +23875,12 @@ "node_modules/rc-util/node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "dev": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -23833,7 +23892,6 @@ "version": "13.1.1", "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz", "integrity": "sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==", - "dev": true, "dependencies": { "@babel/runtime": "^7.9.2", "css-box-model": "^1.2.0", @@ -23880,7 +23938,6 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz", "integrity": "sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A==", - "dev": true, "dependencies": { "copy-to-clipboard": "^3.3.1", "prop-types": "^15.8.1" @@ -23893,7 +23950,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "dev": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -23905,8 +23961,7 @@ "node_modules/react-fast-compare": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", - "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", - "dev": true + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==" }, "node_modules/react-hotkeys": { "version": "2.0.0", @@ -23923,8 +23978,7 @@ "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "node_modules/react-lifecycles-compat": { "version": "3.0.4", @@ -23980,7 +24034,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz", "integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==", - "dev": true, "dependencies": { "react-fast-compare": "^3.0.1", "warning": "^4.0.2" @@ -24001,7 +24054,6 @@ "version": "7.2.9", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==", - "dev": true, "dependencies": { "@babel/runtime": "^7.15.4", "@types/react-redux": "^7.1.20", @@ -24025,14 +24077,56 @@ "node_modules/react-redux/node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" + }, + "node_modules/react-router": { + "version": "6.25.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.25.1.tgz", + "integrity": "sha512-u8ELFr5Z6g02nUtpPAggP73Jigj1mRePSwhS/2nkTrlPU5yEkH1vYzWNyvSnSzeeE2DNqWdH+P8OhIh9wuXhTw==", + "dev": true, + "dependencies": { + "@remix-run/router": "1.18.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.25.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.25.1.tgz", + "integrity": "sha512-0tUDpbFvk35iv+N89dWNrJp+afLgd+y4VtorJZuOCXK0kkCWjEvb3vTJM++SYvMEpbVwXKf3FjeVveVEb6JpDQ==", + "dev": true, + "dependencies": { + "@remix-run/router": "1.18.0", + "react-router": "6.25.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/react-split": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/react-split/-/react-split-2.0.14.tgz", + "integrity": "sha512-bKWydgMgaKTg/2JGQnaJPg51T6dmumTWZppFgEbbY0Fbme0F5TuatAScCLaqommbGQQf/ZT1zaejuPDriscISA==", + "dependencies": { + "prop-types": "^15.5.7", + "split.js": "^1.6.0" + }, + "peerDependencies": { + "react": "*" + } }, "node_modules/react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", - "dev": true, "dependencies": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", @@ -24066,7 +24160,6 @@ "version": "1.0.24", "resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.24.tgz", "integrity": "sha512-3kCn7N9NEb3FlvJrSHWGQ4iVl+ydQObq2fHMn12i5wbtm74zHOPhz/i64OL3c1S1vi9i2GXtZqNqUJTQ+BnNfg==", - "dev": true, "peerDependencies": { "react": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0", "react-dom": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0" @@ -24076,7 +24169,6 @@ "version": "1.8.10", "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.10.tgz", "integrity": "sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg==", - "dev": true, "dependencies": { "@babel/runtime": "^7.0.0", "memoize-one": ">=3.1.1 <6" @@ -24266,7 +24358,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.2.tgz", "integrity": "sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw==", - "dev": true, "dependencies": { "@babel/runtime": "^7.9.2" } @@ -24488,8 +24579,7 @@ "node_modules/resize-observer-polyfill": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", - "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", - "dev": true + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" }, "node_modules/resolve": { "version": "1.20.0", @@ -24799,7 +24889,6 @@ "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "dev": true, "dependencies": { "loose-envify": "^1.1.0" } @@ -25885,6 +25974,11 @@ "node": ">=0.10.0" } }, + "node_modules/split.js": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/split.js/-/split.js-1.6.5.tgz", + "integrity": "sha512-mPTnGCiS/RiuTNsVhCm9De9cCAUsrNFFviRbADdKiiV+Kk8HKp/0fWu7Kr8pi3/yBmsqLFHuXGT9UUZ+CNLwFw==" + }, "node_modules/split2": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-2.2.0.tgz", @@ -27026,8 +27120,7 @@ "node_modules/tabbable": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", - "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", - "dev": true + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" }, "node_modules/tapable": { "version": "2.2.1", @@ -28160,8 +28253,7 @@ "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", - "dev": true + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" }, "node_modules/tmp": { "version": "0.1.0", @@ -28248,8 +28340,7 @@ "node_modules/toggle-selection": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", - "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", - "dev": true + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" }, "node_modules/toidentifier": { "version": "1.0.0", @@ -28962,7 +29053,6 @@ "version": "6.1.3", "resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-6.1.3.tgz", "integrity": "sha512-AETYRrhpRgl9T1YtnODmQE32G81U3A+f3HO3ZeK7efbXqe3x+RNOW4RTpV0iff7zJWhGYJA6EI0Mm+w50aFTAw==", - "dev": true, "dependencies": { "@types/cookie": "^0.6.0", "cookie": "^0.6.0" @@ -28972,7 +29062,6 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -29190,7 +29279,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz", "integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==", - "dev": true, "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0" } @@ -29353,7 +29441,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", - "dev": true, "dependencies": { "loose-envify": "^1.0.0" } @@ -33248,8 +33335,7 @@ "@bem-react/classname": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@bem-react/classname/-/classname-1.6.0.tgz", - "integrity": "sha512-SFBwUHMcb7TFFK5ld88+JhecoEun3/kHZ6KvLDjj3w5hv/tfRV8mtGHA8N42uMctXLF4bPEcr96xwXXcRFuweg==", - "dev": true + "integrity": "sha512-SFBwUHMcb7TFFK5ld88+JhecoEun3/kHZ6KvLDjj3w5hv/tfRV8mtGHA8N42uMctXLF4bPEcr96xwXXcRFuweg==" }, "@bundled-es-modules/cookie": { "version": "2.0.0", @@ -33624,7 +33710,6 @@ "version": "3.7.0", "resolved": "https://registry.npmjs.org/@gravity-ui/components/-/components-3.7.0.tgz", "integrity": "sha512-cWmVOa6rErZY+F3Ws9zD6vvKlU2nn4eUFzS17R0TlVa4uaIcT2WsPGwAracby/o3Ys1gpSK71AkpW/c8lgcvog==", - "dev": true, "requires": { "@bem-react/classname": "^1.6.0", "@gravity-ui/date-utils": "^2.1.0", @@ -33639,7 +33724,6 @@ "version": "2.5.3", "resolved": "https://registry.npmjs.org/@gravity-ui/date-utils/-/date-utils-2.5.3.tgz", "integrity": "sha512-WetzttqlW454yGsh/LBo0AxuqIFT0erENCMYAfemhu6qLiuqy35NdDGP6VA4iDkWiSPFuQ1sRoVU8tC+OwiPEg==", - "dev": true, "requires": { "dayjs": "1.11.10", "lodash": "^4.17.0" @@ -33648,21 +33732,27 @@ "@gravity-ui/i18n": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@gravity-ui/i18n/-/i18n-1.5.1.tgz", - "integrity": "sha512-ZvaQtRUf4Yl9zi0+SMzjlDeHp9+p5IXkNu2k6RtW04c+RYKA1jX+umeKNwzft4iR3+KxDlpLX2trTFEW6W7HKQ==", - "dev": true + "integrity": "sha512-ZvaQtRUf4Yl9zi0+SMzjlDeHp9+p5IXkNu2k6RtW04c+RYKA1jX+umeKNwzft4iR3+KxDlpLX2trTFEW6W7HKQ==" }, "@gravity-ui/icons": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/@gravity-ui/icons/-/icons-2.10.0.tgz", "integrity": "sha512-xS0G4+TM7cD2cCKS4wVc01c4lLe/OreKjm4sHwrOtJWH4EawaRbpkuwtgUDcUvY2EryIcI6lgV+8o714m6lcyQ==", - "dev": true, "requires": {} }, + "@gravity-ui/navigation": { + "version": "2.17.2", + "resolved": "https://registry.npmjs.org/@gravity-ui/navigation/-/navigation-2.17.2.tgz", + "integrity": "sha512-cuYSAJYHgj2RCqwxyoq9nWeHXbHX/uXF3zNuowsUrs4P+gVp2oWP2F5fmbwX89Uafg8WMQ+MrIAAmEGSnz/b9A==", + "requires": { + "react-transition-group": "^4.4.1", + "tslib": "^2.4.0" + } + }, "@gravity-ui/uikit": { "version": "6.20.0", "resolved": "https://registry.npmjs.org/@gravity-ui/uikit/-/uikit-6.20.0.tgz", "integrity": "sha512-ngeFZH0CgF+6cNwWTrtwecYkb1Rb7TWFg3eUg87TGLvXiEemepJuwLvM4t9WX3yZFzFCzIoHML2h/SM6VXhjyQ==", - "dev": true, "requires": { "@bem-react/classname": "^1.6.0", "@gravity-ui/i18n": "^1.3.0", @@ -34054,8 +34144,7 @@ "@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", - "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", - "dev": true + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==" }, "@puppeteer/browsers": { "version": "1.3.0", @@ -34173,6 +34262,12 @@ "@react-hook/passive-layout-effect": "^1.2.0" } }, + "@remix-run/router": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.18.0.tgz", + "integrity": "sha512-L3jkqmqoSVBVKHfpGZmLrex0lxR5SucGA0sUfFzGctehw+S/ggL9L/0NnC5mw6P8HUWpFZ3nQw3cRApjjWx9Sw==", + "dev": true + }, "@semantic-ui-react/event-stack": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/@semantic-ui-react/event-stack/-/event-stack-3.1.3.tgz", @@ -34327,6 +34422,21 @@ "defer-to-connect": "^2.0.1" } }, + "@tanstack/react-virtual": { + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.8.4.tgz", + "integrity": "sha512-Dq0VQr3QlTS2qL35g360QaJWBt7tCn/0xw4uZ0dHXPLO1Ak4Z4nVX4vuj1Npg1b/jqNMDToRtR5OIxM2NXRBWg==", + "dev": true, + "requires": { + "@tanstack/virtual-core": "3.8.4" + } + }, + "@tanstack/virtual-core": { + "version": "3.8.4", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.8.4.tgz", + "integrity": "sha512-iO5Ujgw3O1yIxWDe9FgUPNkGjyT657b1WNX52u+Wv1DyBFEpdCdGkuVaky0M3hHFqNWjAmHWTn4wgj9rTr7ZQg==", + "dev": true + }, "@testing-library/dom": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", @@ -34549,8 +34659,7 @@ "@types/cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", - "dev": true + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==" }, "@types/debug": { "version": "4.1.8", @@ -34648,7 +34757,6 @@ "version": "3.3.5", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", "integrity": "sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg==", - "dev": true, "requires": { "@types/react": "*", "hoist-non-react-statics": "^3.3.0" @@ -34819,8 +34927,7 @@ "@types/prop-types": { "version": "15.7.4", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz", - "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==", - "dev": true + "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==" }, "@types/proxyquire": { "version": "1.3.28", @@ -34844,17 +34951,24 @@ "version": "18.3.3", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", - "dev": true, "requires": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, + "@types/react-dom": { + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/react-redux": { "version": "7.1.33", "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.33.tgz", "integrity": "sha512-NF8m5AjWCkert+fosDsN3hAlHzpjSiXlVy9EgQEmLoBhaNXbmyeGs/aj5dQzKuF+/q+S7JQagorGDW8pJ28Hmg==", - "dev": true, "requires": { "@types/hoist-non-react-statics": "^3.3.0", "@types/react": "*", @@ -34862,6 +34976,25 @@ "redux": "^4.0.0" } }, + "@types/react-virtualized": { + "version": "9.21.30", + "resolved": "https://registry.npmjs.org/@types/react-virtualized/-/react-virtualized-9.21.30.tgz", + "integrity": "sha512-4l2TFLQ8BCjNDQlvH85tU6gctuZoEdgYzENQyZHpgTHU7hoLzYgPSOALMAeA58LOWua8AzC6wBivPj1lfl6JgQ==", + "dev": true, + "requires": { + "@types/prop-types": "*", + "@types/react": "*" + } + }, + "@types/react-window": { + "version": "1.8.8", + "resolved": "https://registry.npmjs.org/@types/react-window/-/react-window-1.8.8.tgz", + "integrity": "sha512-8Ls660bHR1AUA2kuRvVG9D/4XpRC6wjAaPT9dil7Ckc76eP9TKWZwwmgfq8Q1LANX3QNDnoU4Zp48A3w+zK69Q==", + "dev": true, + "requires": { + "@types/react": "*" + } + }, "@types/responselike": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", @@ -38069,8 +38202,7 @@ "blueimp-md5": { "version": "2.19.0", "resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz", - "integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==", - "dev": true + "integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==" }, "bn.js": { "version": "5.2.0", @@ -38911,8 +39043,7 @@ "classnames": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", - "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==", - "dev": true + "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" }, "clean-css": { "version": "5.3.2", @@ -40341,7 +40472,6 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==", - "dev": true, "requires": { "toggle-selection": "^1.0.6" } @@ -40702,7 +40832,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz", "integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==", - "dev": true, "requires": { "tiny-invariant": "^1.0.6" } @@ -40924,8 +41053,7 @@ "csstype": { "version": "3.0.10", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.10.tgz", - "integrity": "sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA==", - "dev": true + "integrity": "sha512-2u44ZG2OcNUO9HDp/Jl8C07x6pU/eTR3ncV91SiK3dhG9TWvRVsCoJw14Ckx5DgWkzGA3waZWO3d7pgqpUI/XA==" }, "currently-unhandled": { "version": "0.4.1", @@ -41018,8 +41146,7 @@ "dayjs": { "version": "1.11.10", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", - "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==", - "dev": true + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==" }, "debug": { "version": "4.3.4", @@ -41740,7 +41867,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", - "dev": true, "requires": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" @@ -43355,7 +43481,6 @@ "version": "7.5.4", "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.5.4.tgz", "integrity": "sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==", - "dev": true, "requires": { "tabbable": "^6.2.0" } @@ -44590,7 +44715,6 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "dev": true, "requires": { "react-is": "^16.7.0" } @@ -46033,8 +46157,7 @@ "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "js-yaml": { "version": "3.14.1", @@ -46637,7 +46760,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, "requires": { "js-tokens": "^3.0.0 || ^4.0.0" } @@ -46827,8 +46949,7 @@ "memoize-one": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", - "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==", - "dev": true + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" }, "memory-fs": { "version": "0.4.1", @@ -48039,8 +48160,7 @@ "object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, "object-copy": { "version": "0.1.0", @@ -49418,7 +49538,6 @@ "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "dev": true, "requires": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", @@ -49663,8 +49782,7 @@ "raf-schd": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz", - "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==", - "dev": true + "integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==" }, "randombytes": { "version": "2.1.0", @@ -49717,7 +49835,6 @@ "version": "10.6.2", "resolved": "https://registry.npmjs.org/rc-slider/-/rc-slider-10.6.2.tgz", "integrity": "sha512-FjkoFjyvUQWcBo1F3RgSglky3ar0+qHLM41PlFVYB4Bj3RD8E/Mv7kqMouLFBU+3aFglMzzctAIWRwajEuueSw==", - "dev": true, "requires": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.5", @@ -49728,7 +49845,6 @@ "version": "5.43.0", "resolved": "https://registry.npmjs.org/rc-util/-/rc-util-5.43.0.tgz", "integrity": "sha512-AzC7KKOXFqAdIBqdGWepL9Xn7cm3vnAmjlHqUnoQaTMZYhM4VlXGLkkHHxj/BZ7Td0+SOPKB4RGPboBVKT9htw==", - "dev": true, "requires": { "@babel/runtime": "^7.18.3", "react-is": "^18.2.0" @@ -49737,8 +49853,7 @@ "react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" } } }, @@ -49746,7 +49861,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "dev": true, "requires": { "loose-envify": "^1.1.0" } @@ -49755,7 +49869,6 @@ "version": "13.1.1", "resolved": "https://registry.npmjs.org/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz", "integrity": "sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ==", - "dev": true, "requires": { "@babel/runtime": "^7.9.2", "css-box-model": "^1.2.0", @@ -49792,7 +49905,6 @@ "version": "5.1.0", "resolved": "https://registry.npmjs.org/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz", "integrity": "sha512-k61RsNgAayIJNoy9yDsYzDe/yAZAzEbEgcz3DZMhF686LEyukcE1hzurxe85JandPUG+yTfGVFzuEw3xt8WP/A==", - "dev": true, "requires": { "copy-to-clipboard": "^3.3.1", "prop-types": "^15.8.1" @@ -49802,7 +49914,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", - "dev": true, "requires": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -49811,8 +49922,7 @@ "react-fast-compare": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", - "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", - "dev": true + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==" }, "react-hotkeys": { "version": "2.0.0", @@ -49826,8 +49936,7 @@ "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "dev": true + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" }, "react-lifecycles-compat": { "version": "3.0.4", @@ -49877,7 +49986,6 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz", "integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==", - "dev": true, "requires": { "react-fast-compare": "^3.0.1", "warning": "^4.0.2" @@ -49893,7 +50001,6 @@ "version": "7.2.9", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz", "integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==", - "dev": true, "requires": { "@babel/runtime": "^7.15.4", "@types/react-redux": "^7.1.20", @@ -49906,16 +50013,42 @@ "react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" } } }, + "react-router": { + "version": "6.25.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.25.1.tgz", + "integrity": "sha512-u8ELFr5Z6g02nUtpPAggP73Jigj1mRePSwhS/2nkTrlPU5yEkH1vYzWNyvSnSzeeE2DNqWdH+P8OhIh9wuXhTw==", + "dev": true, + "requires": { + "@remix-run/router": "1.18.0" + } + }, + "react-router-dom": { + "version": "6.25.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.25.1.tgz", + "integrity": "sha512-0tUDpbFvk35iv+N89dWNrJp+afLgd+y4VtorJZuOCXK0kkCWjEvb3vTJM++SYvMEpbVwXKf3FjeVveVEb6JpDQ==", + "dev": true, + "requires": { + "@remix-run/router": "1.18.0", + "react-router": "6.25.1" + } + }, + "react-split": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/react-split/-/react-split-2.0.14.tgz", + "integrity": "sha512-bKWydgMgaKTg/2JGQnaJPg51T6dmumTWZppFgEbbY0Fbme0F5TuatAScCLaqommbGQQf/ZT1zaejuPDriscISA==", + "requires": { + "prop-types": "^15.5.7", + "split.js": "^1.6.0" + } + }, "react-transition-group": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", - "dev": true, "requires": { "@babel/runtime": "^7.5.5", "dom-helpers": "^5.0.1", @@ -49941,14 +50074,12 @@ "version": "1.0.24", "resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.24.tgz", "integrity": "sha512-3kCn7N9NEb3FlvJrSHWGQ4iVl+ydQObq2fHMn12i5wbtm74zHOPhz/i64OL3c1S1vi9i2GXtZqNqUJTQ+BnNfg==", - "dev": true, "requires": {} }, "react-window": { "version": "1.8.10", "resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.10.tgz", "integrity": "sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg==", - "dev": true, "requires": { "@babel/runtime": "^7.0.0", "memoize-one": ">=3.1.1 <6" @@ -50107,7 +50238,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.2.tgz", "integrity": "sha512-SH8PglcebESbd/shgf6mii6EIoRM0zrQyjcuQ+ojmfxjTtE0z9Y8pa62iA/OJ58qjP6j27uyW4kUF4jl/jd6sw==", - "dev": true, "requires": { "@babel/runtime": "^7.9.2" } @@ -50286,8 +50416,7 @@ "resize-observer-polyfill": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", - "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==", - "dev": true + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" }, "resolve": { "version": "1.20.0", @@ -50534,7 +50663,6 @@ "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "dev": true, "requires": { "loose-envify": "^1.1.0" } @@ -51426,6 +51554,11 @@ "extend-shallow": "^3.0.0" } }, + "split.js": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/split.js/-/split.js-1.6.5.tgz", + "integrity": "sha512-mPTnGCiS/RiuTNsVhCm9De9cCAUsrNFFviRbADdKiiV+Kk8HKp/0fWu7Kr8pi3/yBmsqLFHuXGT9UUZ+CNLwFw==" + }, "split2": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/split2/-/split2-2.2.0.tgz", @@ -52319,8 +52452,7 @@ "tabbable": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", - "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", - "dev": true + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==" }, "tapable": { "version": "2.2.1", @@ -53161,8 +53293,7 @@ "tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", - "dev": true + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" }, "tmp": { "version": "0.1.0", @@ -53233,8 +53364,7 @@ "toggle-selection": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/toggle-selection/-/toggle-selection-1.0.6.tgz", - "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==", - "dev": true + "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==" }, "toidentifier": { "version": "1.0.0", @@ -53753,7 +53883,6 @@ "version": "6.1.3", "resolved": "https://registry.npmjs.org/universal-cookie/-/universal-cookie-6.1.3.tgz", "integrity": "sha512-AETYRrhpRgl9T1YtnODmQE32G81U3A+f3HO3ZeK7efbXqe3x+RNOW4RTpV0iff7zJWhGYJA6EI0Mm+w50aFTAw==", - "dev": true, "requires": { "@types/cookie": "^0.6.0", "cookie": "^0.6.0" @@ -53762,8 +53891,7 @@ "cookie": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==", - "dev": true + "integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==" } } }, @@ -53938,7 +54066,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.3.tgz", "integrity": "sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==", - "dev": true, "requires": {} }, "userhome": { @@ -54070,7 +54197,6 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", - "dev": true, "requires": { "loose-envify": "^1.0.0" } diff --git a/package.json b/package.json index 373aa6886..312cb87ee 100644 --- a/package.json +++ b/package.json @@ -151,7 +151,7 @@ "@types/npm-which": "^3.0.3", "@types/opener": "^1.4.0", "@types/proxyquire": "^1.3.28", - "@types/react-dom": "^16.9.8", + "@types/react-dom": "^18.3.0", "@types/react-virtualized": "^9.21.30", "@types/react-window": "^1.8.8", "@types/sinon": "^4.3.3", @@ -215,9 +215,9 @@ "react-hotkeys": "^2.0.0", "react-markdown": "^6.0.3", "react-redux": "^7.2.1", + "react-router-dom": "^6.25.1", "react-virtualized": "^9.22.5", "reapop": "^4.2.2", - "react-router-dom": "^6.25.1", "reduce-reducers": "^1.0.4", "redux": "^4.0.5", "redux-logger": "^3.0.6", From 3454a56e1d31361a96cb96fb8d0e3ff6a0e8d5ac Mon Sep 17 00:00:00 2001 From: shadowusr Date: Thu, 1 Aug 2024 13:53:52 +0300 Subject: [PATCH 04/11] chore: working on new ui redux store --- lib/static/modules/action-names.ts | 4 ++- lib/static/modules/actions/index.js | 1 + lib/static/modules/actions/suites-page.ts | 11 ++++++++ lib/static/modules/reducers/index.js | 4 ++- lib/static/modules/reducers/suites-page.ts | 10 +++++++ lib/static/new-ui/pages/SuitesPage/index.tsx | 29 +++++++++++++++----- 6 files changed, 50 insertions(+), 9 deletions(-) create mode 100644 lib/static/modules/actions/suites-page.ts create mode 100644 lib/static/modules/reducers/suites-page.ts diff --git a/lib/static/modules/action-names.ts b/lib/static/modules/action-names.ts index 34a0b334c..eed0cc1e5 100644 --- a/lib/static/modules/action-names.ts +++ b/lib/static/modules/action-names.ts @@ -59,5 +59,7 @@ export default { TOGGLE_GROUP_CHECKBOX: 'TOGGLE_GROUP_CHECKBOX', UPDATE_BOTTOM_PROGRESS_BAR: 'UPDATE_BOTTOM_PROGRESS_BAR', GROUP_TESTS_BY_KEY: 'GROUP_TESTS_BY_KEY', - TOGGLE_BROWSER_CHECKBOX: 'TOGGLE_BROWSER_CHECKBOX' + TOGGLE_BROWSER_CHECKBOX: 'TOGGLE_BROWSER_CHECKBOX', + SUITES_PAGE_UPDATE_LIST: 'SUITES_PAGE_SELECT_TEST', + SUITES_PAGE_SELECT_TEST: 'SUITES_PAGE_SELECT_TEST', } as const; diff --git a/lib/static/modules/actions/index.js b/lib/static/modules/actions/index.js index 17cd6953c..f1048d19a 100644 --- a/lib/static/modules/actions/index.js +++ b/lib/static/modules/actions/index.js @@ -13,6 +13,7 @@ import * as plugins from '../plugins'; import performanceMarks from '../../../constants/performance-marks'; export * from './static-accepter'; +export * from './suites-page'; export const createNotification = (id, status, message, props = {}) => { const notificationProps = { diff --git a/lib/static/modules/actions/suites-page.ts b/lib/static/modules/actions/suites-page.ts new file mode 100644 index 000000000..e5f0c6a8e --- /dev/null +++ b/lib/static/modules/actions/suites-page.ts @@ -0,0 +1,11 @@ +import actionNames from '../action-names'; +import {Action} from './types'; + +type SuitesPageUpdateListPayload = {list: any}; +export const suitesPageUpdateList = (payload: SuitesPageUpdateListPayload): Action => { + return {type: actionNames.SUITES_PAGE_UPDATE_LIST, payload} as const; +}; + +export const suitesPageSelectTest = (id: string): Action => { + return {type: actionNames.SUITES_PAGE_SELECT_TEST, payload: id} as const; +}; diff --git a/lib/static/modules/reducers/index.js b/lib/static/modules/reducers/index.js index 1945fb461..4cea53479 100644 --- a/lib/static/modules/reducers/index.js +++ b/lib/static/modules/reducers/index.js @@ -26,6 +26,7 @@ import groupedTests from './grouped-tests'; import stopping from './stopping'; import progressBar from './bottom-progress-bar'; import staticImageAccepter from './static-image-accepter'; +import suitesPage from './suites-page'; // The order of specifying reducers is important. // At the top specify reducers that does not depend on other state fields. @@ -56,5 +57,6 @@ export default reduceReducers( tree, groupedTests, plugins, - progressBar + progressBar, + suitesPage ); diff --git a/lib/static/modules/reducers/suites-page.ts b/lib/static/modules/reducers/suites-page.ts new file mode 100644 index 000000000..7610e7040 --- /dev/null +++ b/lib/static/modules/reducers/suites-page.ts @@ -0,0 +1,10 @@ +import actionNames from '../action-names'; + +export default (state, action) => { + switch (action.type) { + case actionNames.SUITES_PAGE_UPDATE_LIST: + return {...state, suitesPage: {...state.suitesPage, list: action.payload.list}}; + default: + return state; + } +}; diff --git a/lib/static/new-ui/pages/SuitesPage/index.tsx b/lib/static/new-ui/pages/SuitesPage/index.tsx index 8fddca24c..8267e7042 100644 --- a/lib/static/new-ui/pages/SuitesPage/index.tsx +++ b/lib/static/new-ui/pages/SuitesPage/index.tsx @@ -21,6 +21,9 @@ import { import styles from './index.module.css'; import {useVirtualizer} from '@tanstack/react-virtual'; import {trimArray} from '../../../../common-utils'; +import {bindActionCreators} from 'redux'; +import * as actions from '../../../modules/actions'; +import {suitesPageUpdateList} from '../../../modules/actions/suites-page'; interface SuitesPageInternalProps { suites: any; @@ -107,10 +110,12 @@ function ImageWithMagnifier({ borderRadius: '5px', backgroundImage: `url('${src}')`, backgroundRepeat: 'no-repeat', + top: `${y + magnifierHeight / 2}px`, + left: `${x + magnifierWidth / 2}px`, // top: `${y - magnifierHeight / 2}px`, // left: `${x - magnifierWidth / 2}px`, - top: '20px', - right: '20px', + // top: '20px', + // right: '20px', backgroundSize: `${imgWidth * zoomLevel}px ${imgHeight * zoomLevel}px`, backgroundPositionX: `${-x * zoomLevel + magnifierWidth / 2}px`, backgroundPositionY: `${-y * zoomLevel + magnifierHeight / 2}px`, @@ -237,7 +242,12 @@ function SuitesPageInternal(props: SuitesPageInternalProps): JSX.Element { // return result; // }, []); - const list = useList({items: itemsOriginal}); + const list = useList({items: itemsOriginal, withExpandedState: true}); + // console.log(props.actions); + + useEffect(() => { + props.actions.suitesPageUpdateList({list}); + }, [list]); // console.log(items); console.log(list); console.log('length:' + list.structure.items.length); @@ -281,7 +291,7 @@ function SuitesPageInternal(props: SuitesPageInternalProps): JSX.Element { const [magnifierStyle, setMagnifierStyle] = useState({display: 'none'}); - const items = virtualizer.getVirtualItems() + const items = virtualizer.getVirtualItems(); return
@@ -329,7 +339,11 @@ function SuitesPageInternal(props: SuitesPageInternalProps): JSX.Element { { + console.log('item clicked!'); + list.state.setExpanded(args[0].id, !list.state.expandedById[args[0].id]); + console.log(args); + }, mapItemDataToProps: (x) => { let statusIcon; if (x.status === 'fail' || x.status === 'error') { @@ -346,7 +360,7 @@ function SuitesPageInternal(props: SuitesPageInternalProps): JSX.Element { let subtitle; if (x.diffImg) { // subtitle = - subtitle = + subtitle = } else if (x.errorStack) { subtitle =
{x.errorStack} @@ -478,5 +492,6 @@ export const SuitesPage = connect( browsers: state.tree.browsers, results: state.tree.results, images: state.tree.images, - }) + }), + (dispatch) => ({actions: bindActionCreators(actions, dispatch)}) )(SuitesPageInternal); From c0a7208c797037598c569fb95e7f9e66c819223d Mon Sep 17 00:00:00 2001 From: shadowusr Date: Fri, 16 Aug 2024 10:27:54 +0300 Subject: [PATCH 05/11] feat: finish suites page implementation --- lib/static/gui.jsx | 2 +- lib/static/modules/action-names.ts | 3 +- lib/static/modules/actions/suites-page.ts | 11 +- lib/static/modules/default-state.ts | 10 +- lib/static/modules/reducers/index.js | 4 +- lib/static/modules/reducers/is-initialized.js | 12 + lib/static/modules/reducers/suites-page.ts | 12 +- lib/static/modules/selectors/tree.js | 6 +- lib/static/new-ui/App.tsx | 35 -- lib/static/new-ui/app/App.tsx | 43 ++ lib/static/new-ui/{ => app}/gui.tsx | 4 +- lib/static/new-ui/app/report.tsx | 22 + .../{layouts => components}/MainLayout.tsx | 0 .../SplitViewLayout.module.css | 0 .../SplitViewLayout.tsx | 0 .../features/info/components/InfoPage.tsx | 8 + .../suites/components/ImageWithMagnifier.tsx | 93 ++++ .../suites/components}/StatusFilter.tsx | 6 +- .../components/SuitesPage/index.module.css | 91 ++++ .../suites/components/SuitesPage/index.tsx | 180 +++++++ .../suites/components/SuitesPage/selectors.ts | 112 ++++ .../suites/components/SuitesPage/types.ts | 31 ++ .../TreeViewItemSubtitle/index.module.css | 7 + .../components/TreeViewItemSubtitle/index.tsx | 16 + .../TreeViewItemTitle/index.module.css | 4 + .../components/TreeViewItemTitle/index.tsx | 14 + .../components/VisualChecksPage.tsx | 8 + lib/static/new-ui/pages/InfoPage/index.tsx | 5 - .../new-ui/pages/SuitesPage/index.module.css | 57 -- lib/static/new-ui/pages/SuitesPage/index.tsx | 497 ------------------ .../new-ui/pages/VisualChecksPage/index.tsx | 5 - lib/static/new-ui/report.tsx | 7 - lib/static/new-ui/store/selectors.ts | 8 + lib/static/new-ui/types/store.ts | 81 +++ lib/static/new-ui/{ => types}/typings.d.ts | 2 +- lib/static/new-ui/utils/index.tsx | 15 + lib/static/styles.css | 12 + lib/static/tsconfig.json | 5 +- package-lock.json | 16 +- package.json | 2 +- webpack.common.js | 9 +- 41 files changed, 809 insertions(+), 646 deletions(-) create mode 100644 lib/static/modules/reducers/is-initialized.js delete mode 100644 lib/static/new-ui/App.tsx create mode 100644 lib/static/new-ui/app/App.tsx rename lib/static/new-ui/{ => app}/gui.tsx (80%) create mode 100644 lib/static/new-ui/app/report.tsx rename lib/static/new-ui/{layouts => components}/MainLayout.tsx (100%) rename lib/static/new-ui/{layouts => components}/SplitViewLayout.module.css (100%) rename lib/static/new-ui/{layouts => components}/SplitViewLayout.tsx (100%) create mode 100644 lib/static/new-ui/features/info/components/InfoPage.tsx create mode 100644 lib/static/new-ui/features/suites/components/ImageWithMagnifier.tsx rename lib/static/new-ui/{pages/SuitesPage => features/suites/components}/StatusFilter.tsx (91%) create mode 100644 lib/static/new-ui/features/suites/components/SuitesPage/index.module.css create mode 100644 lib/static/new-ui/features/suites/components/SuitesPage/index.tsx create mode 100644 lib/static/new-ui/features/suites/components/SuitesPage/selectors.ts create mode 100644 lib/static/new-ui/features/suites/components/SuitesPage/types.ts create mode 100644 lib/static/new-ui/features/suites/components/TreeViewItemSubtitle/index.module.css create mode 100644 lib/static/new-ui/features/suites/components/TreeViewItemSubtitle/index.tsx create mode 100644 lib/static/new-ui/features/suites/components/TreeViewItemTitle/index.module.css create mode 100644 lib/static/new-ui/features/suites/components/TreeViewItemTitle/index.tsx create mode 100644 lib/static/new-ui/features/visual-checks/components/VisualChecksPage.tsx delete mode 100644 lib/static/new-ui/pages/InfoPage/index.tsx delete mode 100644 lib/static/new-ui/pages/SuitesPage/index.module.css delete mode 100644 lib/static/new-ui/pages/SuitesPage/index.tsx delete mode 100644 lib/static/new-ui/pages/VisualChecksPage/index.tsx delete mode 100644 lib/static/new-ui/report.tsx create mode 100644 lib/static/new-ui/store/selectors.ts create mode 100644 lib/static/new-ui/types/store.ts rename lib/static/new-ui/{ => types}/typings.d.ts (74%) create mode 100644 lib/static/new-ui/utils/index.tsx diff --git a/lib/static/gui.jsx b/lib/static/gui.jsx index eff9b389c..64c3a0ffb 100644 --- a/lib/static/gui.jsx +++ b/lib/static/gui.jsx @@ -188,7 +188,7 @@ root.render( height={height} itemCount={treeItems.length} itemData={treeItems} - itemSize={(index) => 30} + itemSize={() => 30} > {({index, style, data}) => (
diff --git a/lib/static/modules/action-names.ts b/lib/static/modules/action-names.ts index eed0cc1e5..47bbc83ee 100644 --- a/lib/static/modules/action-names.ts +++ b/lib/static/modules/action-names.ts @@ -60,6 +60,5 @@ export default { UPDATE_BOTTOM_PROGRESS_BAR: 'UPDATE_BOTTOM_PROGRESS_BAR', GROUP_TESTS_BY_KEY: 'GROUP_TESTS_BY_KEY', TOGGLE_BROWSER_CHECKBOX: 'TOGGLE_BROWSER_CHECKBOX', - SUITES_PAGE_UPDATE_LIST: 'SUITES_PAGE_SELECT_TEST', - SUITES_PAGE_SELECT_TEST: 'SUITES_PAGE_SELECT_TEST', + SUITES_PAGE_SET_CURRENT_SUITE: 'SUITES_PAGE_SET_CURRENT_SUITE' } as const; diff --git a/lib/static/modules/actions/suites-page.ts b/lib/static/modules/actions/suites-page.ts index e5f0c6a8e..38bc211b0 100644 --- a/lib/static/modules/actions/suites-page.ts +++ b/lib/static/modules/actions/suites-page.ts @@ -1,11 +1,6 @@ import actionNames from '../action-names'; -import {Action} from './types'; +import {SuitesPageSetCurrentSuiteAction} from '@/static/modules/reducers/suites-page'; -type SuitesPageUpdateListPayload = {list: any}; -export const suitesPageUpdateList = (payload: SuitesPageUpdateListPayload): Action => { - return {type: actionNames.SUITES_PAGE_UPDATE_LIST, payload} as const; -}; - -export const suitesPageSelectTest = (id: string): Action => { - return {type: actionNames.SUITES_PAGE_SELECT_TEST, payload: id} as const; +export const suitesPageSetCurrentSuite = (suiteId: string): SuitesPageSetCurrentSuiteAction => { + return {type: actionNames.SUITES_PAGE_SET_CURRENT_SUITE, payload: {suiteId}}; }; diff --git a/lib/static/modules/default-state.ts b/lib/static/modules/default-state.ts index e8172817a..e0600715c 100644 --- a/lib/static/modules/default-state.ts +++ b/lib/static/modules/default-state.ts @@ -4,6 +4,7 @@ import {DiffModes} from '../../constants/diff-modes'; import {EXPAND_ERRORS} from '../../constants/expand-modes'; import {RESULT_KEYS} from '../../constants/group-tests'; import {ToolName} from '../../constants'; +import {State} from '@/static/new-ui/types/store'; export default Object.assign({config: configDefaults}, { gui: true, @@ -28,7 +29,8 @@ export default Object.assign({config: configDefaults}, { byId: {}, allIds: [], allRootIds: [], - failedRootIds: [] + failedRootIds: [], + stateById: {} }, browsers: { byId: {}, @@ -87,5 +89,9 @@ export default Object.assign({config: configDefaults}, { acceptableImages: {}, accepterDelayedImages: [] as {imageId: string; stateName: string; stateNameImageId: string}[], imagesToCommitCount: 0 + }, + app: { + isInitialized: false, + currentSuiteId: null } -}); +}) satisfies State; diff --git a/lib/static/modules/reducers/index.js b/lib/static/modules/reducers/index.js index 4cea53479..6867e4cbf 100644 --- a/lib/static/modules/reducers/index.js +++ b/lib/static/modules/reducers/index.js @@ -27,6 +27,7 @@ import stopping from './stopping'; import progressBar from './bottom-progress-bar'; import staticImageAccepter from './static-image-accepter'; import suitesPage from './suites-page'; +import isInitialized from './is-initialized'; // The order of specifying reducers is important. // At the top specify reducers that does not depend on other state fields. @@ -58,5 +59,6 @@ export default reduceReducers( groupedTests, plugins, progressBar, - suitesPage + suitesPage, + isInitialized ); diff --git a/lib/static/modules/reducers/is-initialized.js b/lib/static/modules/reducers/is-initialized.js new file mode 100644 index 000000000..00a9bb564 --- /dev/null +++ b/lib/static/modules/reducers/is-initialized.js @@ -0,0 +1,12 @@ +import actionNames from '../action-names'; + +export default (state, action) => { + switch (action.type) { + case actionNames.INIT_GUI_REPORT: + case actionNames.INIT_STATIC_REPORT: + return {...state, app: {...state.app, isInitialized: true}}; + + default: + return state; + } +}; diff --git a/lib/static/modules/reducers/suites-page.ts b/lib/static/modules/reducers/suites-page.ts index 7610e7040..426350894 100644 --- a/lib/static/modules/reducers/suites-page.ts +++ b/lib/static/modules/reducers/suites-page.ts @@ -1,9 +1,15 @@ import actionNames from '../action-names'; +import {State} from '@/static/new-ui/types/store'; +import {Action} from '@/static/modules/actions/types'; -export default (state, action) => { +export type SuitesPageSetCurrentSuiteAction = Action; + +export default (state: State, action: SuitesPageSetCurrentSuiteAction): State => { switch (action.type) { - case actionNames.SUITES_PAGE_UPDATE_LIST: - return {...state, suitesPage: {...state.suitesPage, list: action.payload.list}}; + case actionNames.SUITES_PAGE_SET_CURRENT_SUITE: + return {...state, app: {...state.app, currentSuiteId: action.payload.suiteId}}; default: return state; } diff --git a/lib/static/modules/selectors/tree.js b/lib/static/modules/selectors/tree.js index 950ba5776..5200124e2 100644 --- a/lib/static/modules/selectors/tree.js +++ b/lib/static/modules/selectors/tree.js @@ -4,16 +4,12 @@ import {getViewMode} from './view'; import {ViewMode} from '../../../constants/view-modes'; import {isIdleStatus} from '../../../common-utils'; import {isNodeFailed, isNodeSuccessful, isAcceptable, iterateSuites} from '../utils'; +import {getAllRootSuiteIds, getBrowsers, getImages, getResults, getSuites} from '@/static/new-ui/store/selectors'; -const getSuites = (state) => state.tree.suites.byId; const getSuitesStates = (state) => state.tree.suites.stateById; -const getBrowsers = (state) => state.tree.browsers.byId; const getBrowserIds = (state) => state.tree.browsers.allIds; const getBrowsersStates = (state) => state.tree.browsers.stateById; -const getResults = (state) => state.tree.results.byId; -const getImages = (state) => state.tree.images.byId; const getImagesStates = (state) => state.tree.images.stateById; -const getAllRootSuiteIds = (state) => state.tree.suites.allRootIds; const getFailedRootSuiteIds = (state) => state.tree.suites.failedRootIds; const getRootSuiteIds = (state) => { const viewMode = getViewMode(state); diff --git a/lib/static/new-ui/App.tsx b/lib/static/new-ui/App.tsx deleted file mode 100644 index a4e63f2dc..000000000 --- a/lib/static/new-ui/App.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import {ThemeProvider} from '@gravity-ui/uikit'; -import React from 'react'; -import {MainLayout} from './layouts/MainLayout'; -import {HashRouter, Navigate, Route, Routes} from 'react-router-dom'; -import {CircleInfo, Eye, ListCheck} from '@gravity-ui/icons'; -import {SuitesPage} from './pages/SuitesPage'; -import {VisualChecksPage} from './pages/VisualChecksPage'; -import {InfoPage} from './pages/InfoPage'; - -import '@gravity-ui/uikit/styles/fonts.css'; -import '@gravity-ui/uikit/styles/styles.css'; -import '../new-ui.css'; -import {Provider} from 'react-redux'; -import store from '../modules/store'; - -export function App(): JSX.Element { - const pages = [ - {title: 'Suites', url: '/suites', icon: ListCheck, element: }, - {title: 'Visual Checks', url: '/visual-checks', icon: Eye, element: }, - {title: 'Info', url: '/info', icon: CircleInfo, element: } - ]; - - return - - - - - } path={'/'}/> - {pages.map(page => )} - - - - - ; -} diff --git a/lib/static/new-ui/app/App.tsx b/lib/static/new-ui/app/App.tsx new file mode 100644 index 000000000..ddb30f268 --- /dev/null +++ b/lib/static/new-ui/app/App.tsx @@ -0,0 +1,43 @@ +import {ThemeProvider} from '@gravity-ui/uikit'; +import React, {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'; +import {SuitesPage} from '../features/suites/components/SuitesPage'; +import {VisualChecksPage} from '../features/visual-checks/components/VisualChecksPage'; +import {InfoPage} from '../features/info/components/InfoPage'; + +import '@gravity-ui/uikit/styles/fonts.css'; +import '@gravity-ui/uikit/styles/styles.css'; +import '../../new-ui.css'; +import {Provider} from 'react-redux'; +import store from '../../modules/store'; + +export function App(): JSX.Element { + const pages = [ + { + title: 'Suites', + url: '/suites', + icon: ListCheck, + element: , + children: [} />] + }, + {title: 'Visual Checks', url: '/visual-checks', icon: Eye, element: }, + {title: 'Info', url: '/info', icon: CircleInfo, element: } + ]; + + return + + + + + + } path={'/'}/> + {pages.map(page => {page.children})} + + + + + + ; +} diff --git a/lib/static/new-ui/gui.tsx b/lib/static/new-ui/app/gui.tsx similarity index 80% rename from lib/static/new-ui/gui.tsx rename to lib/static/new-ui/app/gui.tsx index 1564aa0eb..5d02acfb3 100644 --- a/lib/static/new-ui/gui.tsx +++ b/lib/static/new-ui/app/gui.tsx @@ -1,8 +1,8 @@ import React, {useEffect} from 'react'; import {createRoot} from 'react-dom/client'; import {App} from './App'; -import store from '../modules/store'; -import {finGuiReport, initGuiReport} from '../modules/actions'; +import store from '../../modules/store'; +import {finGuiReport, initGuiReport} from '../../modules/actions'; const rootEl = document.getElementById('app') as HTMLDivElement; const root = createRoot(rootEl); diff --git a/lib/static/new-ui/app/report.tsx b/lib/static/new-ui/app/report.tsx new file mode 100644 index 000000000..ee7a0163c --- /dev/null +++ b/lib/static/new-ui/app/report.tsx @@ -0,0 +1,22 @@ +import React, {useEffect} from 'react'; +import {createRoot} from 'react-dom/client'; +import {App} from './App'; +import store from '../../modules/store'; +import {initStaticReport, finStaticReport} from '../../modules/actions'; + +const rootEl = document.getElementById('app') as HTMLDivElement; +const root = createRoot(rootEl); + +function Gui(): React.JSX.Element { + useEffect(() => { + store.dispatch(initStaticReport()); + + return () => { + store.dispatch(finStaticReport()); + }; + }, []); + + return ; +} + +root.render(); diff --git a/lib/static/new-ui/layouts/MainLayout.tsx b/lib/static/new-ui/components/MainLayout.tsx similarity index 100% rename from lib/static/new-ui/layouts/MainLayout.tsx rename to lib/static/new-ui/components/MainLayout.tsx diff --git a/lib/static/new-ui/layouts/SplitViewLayout.module.css b/lib/static/new-ui/components/SplitViewLayout.module.css similarity index 100% rename from lib/static/new-ui/layouts/SplitViewLayout.module.css rename to lib/static/new-ui/components/SplitViewLayout.module.css diff --git a/lib/static/new-ui/layouts/SplitViewLayout.tsx b/lib/static/new-ui/components/SplitViewLayout.tsx similarity index 100% rename from lib/static/new-ui/layouts/SplitViewLayout.tsx rename to lib/static/new-ui/components/SplitViewLayout.tsx diff --git a/lib/static/new-ui/features/info/components/InfoPage.tsx b/lib/static/new-ui/features/info/components/InfoPage.tsx new file mode 100644 index 000000000..7f354a06b --- /dev/null +++ b/lib/static/new-ui/features/info/components/InfoPage.tsx @@ -0,0 +1,8 @@ +import React, {ReactNode} from 'react'; +import {Box} from '@gravity-ui/uikit'; + +export function InfoPage(): ReactNode { + return +

Info

+
; +} diff --git a/lib/static/new-ui/features/suites/components/ImageWithMagnifier.tsx b/lib/static/new-ui/features/suites/components/ImageWithMagnifier.tsx new file mode 100644 index 000000000..583b21e2a --- /dev/null +++ b/lib/static/new-ui/features/suites/components/ImageWithMagnifier.tsx @@ -0,0 +1,93 @@ +import React, {ReactNode, useEffect, useState} from 'react'; +import {createPortal} from 'react-dom'; + +interface ImageWithMagnifierProps { + src: string; + alt: string; + className?: string; + style?: React.CSSProperties; + magnifierHeight?: number; + magnifierWidth?: number; + zoomLevel?: number; +} + +export function ImageWithMagnifier({ + src, + alt, + className = '', + style, + magnifierHeight = 150, + magnifierWidth = 150, + zoomLevel = 3 +}: ImageWithMagnifierProps): ReactNode { + const [showMagnifier, setShowMagnifier] = useState(false); + const [[imgWidth, imgHeight], setSize] = useState([0, 0]); + const [[x, y], setXY] = useState([0, 0]); + const [[mouseX, mouseY], setMouseXY] = useState([0, 0]); + const [magnifierStyle, setMagnifierStyle] = useState({}); + + const mouseEnter = (e: React.MouseEvent): void => { + const el = e.currentTarget; + + const {width, height} = el.getBoundingClientRect(); + setSize([width, height]); + setShowMagnifier(true); + setMouseXY([e.clientX, e.clientY]); + }; + + const mouseLeave = (e: React.MouseEvent): void => { + e.preventDefault(); + setShowMagnifier(false); + setMouseXY([e.clientX, e.clientY]); + }; + + const mouseMove = (e: React.MouseEvent): void => { + const el = e.currentTarget; + const {top, left} = el.getBoundingClientRect(); + + const x = e.pageX - left - window.scrollX; + const y = e.pageY - top - window.scrollY; + + setXY([x, y]); + setMouseXY([e.clientX, e.clientY]); + }; + + useEffect(() => { + setMagnifierStyle({ + display: showMagnifier ? '' : 'none', + // position: 'absolute', + position: 'fixed', + pointerEvents: 'none', + height: `${magnifierHeight}px`, + width: `${magnifierWidth}px`, + opacity: '1', + border: '1px solid lightgrey', + backgroundColor: 'white', + borderRadius: '5px', + backgroundImage: `url('${src}')`, + backgroundRepeat: 'no-repeat', + top: `${mouseY - magnifierHeight / 2}px`, + left: `${mouseX - magnifierWidth / 2}px`, + backgroundSize: `${imgWidth * zoomLevel}px ${imgHeight * zoomLevel}px`, + backgroundPositionX: `${-x * zoomLevel + magnifierWidth / 2}px`, + backgroundPositionY: `${-y * zoomLevel + magnifierHeight / 2}px`, + zIndex: 1000 + }); + }, [showMagnifier, imgWidth, imgHeight, x, y]); + + return
+ {alt} mouseEnter(e)} + onMouseLeave={(e): void => mouseLeave(e)} + onMouseMove={(e): void => mouseMove(e)} + /> +
+ {createPortal(
, document.body)} +
; +} diff --git a/lib/static/new-ui/pages/SuitesPage/StatusFilter.tsx b/lib/static/new-ui/features/suites/components/StatusFilter.tsx similarity index 91% rename from lib/static/new-ui/pages/SuitesPage/StatusFilter.tsx rename to lib/static/new-ui/features/suites/components/StatusFilter.tsx index aca4c58cb..597a41eec 100644 --- a/lib/static/new-ui/pages/SuitesPage/StatusFilter.tsx +++ b/lib/static/new-ui/features/suites/components/StatusFilter.tsx @@ -2,7 +2,7 @@ import {ArrowRotateLeft, ArrowsRotateLeft, Ban, Check, CircleDashed, CloudCheck, import {ControlGroupOption, RadioButton} from '@gravity-ui/uikit'; import React from 'react'; import {connect} from 'react-redux'; -import {getStatsFilteredByBrowsers} from '../../../modules/selectors/stats'; +import {getStatsFilteredByBrowsers} from '../../../../modules/selectors/stats'; interface StatusFilterInternalProps { stats: { @@ -22,14 +22,14 @@ function StatusFilterInternal(props: StatusFilterInternalProps): JSX.Element { retried: , skipped: , updated: , - commited: , + 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}
, + content:
{statusToIcon[status as keyof typeof statusToIcon]}{count}
})); return ; 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 new file mode 100644 index 000000000..75f1c7113 --- /dev/null +++ b/lib/static/new-ui/features/suites/components/SuitesPage/index.module.css @@ -0,0 +1,91 @@ +.controls-row { + align-items: center; + display: flex; + height: 28px; + margin-top: var(--g-spacing-2); +} + +.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 { + height: 100%; + width: 100%; + overflow-y: auto; + contain: strict; + } + + .tree-view__total-size-wrapper { + width: 100%; + position: relative; + } + + .tree-view__visible-window { + position: absolute; + top: 0; + left: 0; + width: 100%; + } + + .tree-view__item { + --g-text-subheader-1-font-size: 18px; + --g-text-subheader-1-line-height: 22px; + --g-text-subheader-font-weight: normal; + + --g-text-body-1-font-size: 18px; + --g-text-body-1-line-height: 22px; + --g-text-body-font-weight: normal; + + margin-top: 4px; + padding: 4px 0; + font-size: 18px; + + user-select: none; + } + + .tree-view__item svg { + flex-shrink: 0; + } + + .tree-view__item > div > div { + align-items: start !important; + } + + .tree-item--current { + background: #a28aff !important; + color: #fff; + } + + .tree-item--current:hover { + background: #af9aff !important; + } + + .tree-item--current svg { + color: #fff; + } + + .tree-item--browser { + padding-left: 8px; + } + + .tree-item--error { + background: var(--g-color-private-red-100); + color: var(--g-color-private-red-600-solid); + } + + .tree-item--error:hover { + background: var(--g-color-private-red-50) !important; + } + + .tree-item--error svg { + color: var(--g-color-private-red-600-solid); + } + + + .tree-view :global(.g-text_ellipsis) { + white-space: normal; + } diff --git a/lib/static/new-ui/features/suites/components/SuitesPage/index.tsx b/lib/static/new-ui/features/suites/components/SuitesPage/index.tsx new file mode 100644 index 000000000..d4c580158 --- /dev/null +++ b/lib/static/new-ui/features/suites/components/SuitesPage/index.tsx @@ -0,0 +1,180 @@ +import React, {ChangeEvent, useCallback, useEffect, useState} from 'react'; +import {Flex, TextInput} from '@gravity-ui/uikit'; +import {debounce} from 'lodash'; +import classNames from 'classnames'; +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 = ({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)); + } + }; + + 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: () => 24, + getItemKey: useCallback((index: number) => list.structure.visibleFlattenIds[index], [list]), + overscan: 100 + }); + + 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 = (event: ChangeEvent): void => { + setTestNameFilter(event.target.value); + updateTestNameFilter(event.target.value); + }; + + const items = virtualizer.getVirtualItems(); + + 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); diff --git a/lib/static/new-ui/features/suites/components/SuitesPage/selectors.ts b/lib/static/new-ui/features/suites/components/SuitesPage/selectors.ts new file mode 100644 index 000000000..c4a49ab20 --- /dev/null +++ b/lib/static/new-ui/features/suites/components/SuitesPage/selectors.ts @@ -0,0 +1,112 @@ +import {createSelector} from 'reselect'; +import {get, last} from 'lodash'; +import { + isImageEntityFail, + isResultEntityError, + isSuiteEntityLeaf, + BrowserEntity, + SuiteEntity +} from '@/static/new-ui/types/store'; +import { + TreeViewBrowserData, + TreeViewItem, + TreeViewItemType, + TreeViewSuiteData +} from '@/static/new-ui/features/suites/components/SuitesPage/types'; +import {TestStatus} from '@/constants'; +import { + getAllRootSuiteIds, + getBrowsers, + getImages, + getResults, + getSuites, + getSuitesState +} from '@/static/new-ui/store/selectors'; +import {trimArray} from '@/common-utils'; +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[] => { + const EMPTY_SUITE: TreeViewSuiteData = { + type: TreeViewItemType.Suite, + title: '', + fullTitle: '', + status: TestStatus.IDLE + }; + + const formatBrowser = (browserData: BrowserEntity, parentSuite: TreeViewSuiteData): TreeViewItem => { + // Assuming test in concrete browser always has at least one result, even never launched (idle result) + const lastResult = results[last(browserData.resultIds) as string]; + + const diffImgId = lastResult.imageIds.find(imageId => isImageEntityFail(images[imageId])); + const diffImg = get(images, [diffImgId as string, 'diffImg']) as ImageFile | undefined; + + let errorTitle, errorStack; + if (isResultEntityError(lastResult) && lastResult.error?.stack) { + errorTitle = lastResult.error?.name; + + const stackLines = trimArray(lastResult.error.stack.split('\n')); + errorStack = stackLines.slice(0, 3).join('\n'); + } + + const data: TreeViewBrowserData = { + type: TreeViewItemType.Browser, + title: browserData.name, + fullTitle: (parentSuite.fullTitle + ' ' + browserData.name).trim(), + status: lastResult.status, + errorTitle, + errorStack, + diffImg + }; + + return {data}; + }; + + const formatSuite = (suiteData: SuiteEntity, parentSuite: TreeViewSuiteData): TreeViewItem | null => { + const data: TreeViewSuiteData = { + type: TreeViewItemType.Suite, + title: suiteData.name, + fullTitle: (parentSuite.fullTitle + ' ' + suiteData.name).trim(), + status: suiteData.status + }; + + if (!suitesState[data.fullTitle].shouldBeShown) { + return null; + } + + if (isSuiteEntityLeaf(suiteData)) { + return { + data, + children: suiteData.browserIds.map((browserId) => formatBrowser(browsers[browserId], data)) + }; + } else { + return { + data, + children: suiteData.suiteIds + .map((suiteId) => formatSuite(suites[suiteId], data)) + .filter(Boolean) as TreeViewItem[] + }; + } + }; + + return rootSuiteIds + .map((rootId) => { + return formatSuite(suites[rootId], EMPTY_SUITE); + }) + .filter(Boolean) as TreeViewItem[]; + }); + +export const getTreeViewExpandedById = createSelector( + [getSuites, getSuitesState], + (suites, suitesState) => { + const result: Record = {}; + + for (const key of Object.keys(suites)) { + result[key] = suitesState[key].shouldBeOpened; + } + + return result; + } +); diff --git a/lib/static/new-ui/features/suites/components/SuitesPage/types.ts b/lib/static/new-ui/features/suites/components/SuitesPage/types.ts new file mode 100644 index 000000000..a7ae0e80b --- /dev/null +++ b/lib/static/new-ui/features/suites/components/SuitesPage/types.ts @@ -0,0 +1,31 @@ +import {TestStatus} from '@/constants'; +import {ImageFile} from '@/types'; + +export interface TreeViewItem { + data: T; + children?: TreeViewItem[]; +} + +export enum TreeViewItemType { + Suite, + Browser, +} + +export interface TreeViewSuiteData { + type: TreeViewItemType.Suite; + title: string; + fullTitle: string; + status: TestStatus; +} + +export interface TreeViewBrowserData { + type: TreeViewItemType.Browser; + title: string; + fullTitle: string; + status: TestStatus; + errorTitle?: string; + errorStack?: string; + diffImg?: ImageFile; +} + +export type TreeViewData = TreeViewSuiteData | TreeViewBrowserData; diff --git a/lib/static/new-ui/features/suites/components/TreeViewItemSubtitle/index.module.css b/lib/static/new-ui/features/suites/components/TreeViewItemSubtitle/index.module.css new file mode 100644 index 000000000..e5e9a3cf6 --- /dev/null +++ b/lib/static/new-ui/features/suites/components/TreeViewItemSubtitle/index.module.css @@ -0,0 +1,7 @@ +.tree-view-item-subtitle__error-stack { + white-space: pre; + overflow: hidden; + text-overflow: ellipsis; + line-height: 22px; + margin-top: 8px; +} diff --git a/lib/static/new-ui/features/suites/components/TreeViewItemSubtitle/index.tsx b/lib/static/new-ui/features/suites/components/TreeViewItemSubtitle/index.tsx new file mode 100644 index 000000000..a4bcf62a6 --- /dev/null +++ b/lib/static/new-ui/features/suites/components/TreeViewItemSubtitle/index.tsx @@ -0,0 +1,16 @@ +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'; + +export function TreeViewItemSubtitle(props: {item: TreeViewData}): ReactNode { + if (props.item.type === TreeViewItemType.Browser && props.item.diffImg) { + return ; + } else if (props.item.type === TreeViewItemType.Browser && props.item.errorStack) { + return
+ {props.item.errorStack} +
; + } + + return null; +} diff --git a/lib/static/new-ui/features/suites/components/TreeViewItemTitle/index.module.css b/lib/static/new-ui/features/suites/components/TreeViewItemTitle/index.module.css new file mode 100644 index 000000000..3c82eeb78 --- /dev/null +++ b/lib/static/new-ui/features/suites/components/TreeViewItemTitle/index.module.css @@ -0,0 +1,4 @@ +.tree-view-item__error-title { + color: var(--g-color-private-red-600-solid); + margin-left: var(--g-spacing-1); +} diff --git a/lib/static/new-ui/features/suites/components/TreeViewItemTitle/index.tsx b/lib/static/new-ui/features/suites/components/TreeViewItemTitle/index.tsx new file mode 100644 index 000000000..3255938ca --- /dev/null +++ b/lib/static/new-ui/features/suites/components/TreeViewItemTitle/index.tsx @@ -0,0 +1,14 @@ +import React from 'react'; +import {TreeViewData, TreeViewItemType} from '@/static/new-ui/features/suites/components/SuitesPage/types'; +import styles from './index.module.css'; + +export function TreeViewItemTitle(props: {item: TreeViewData}): React.JSX.Element { + return
+ {props.item.title} + { + props.item.type === TreeViewItemType.Browser && + props.item.errorTitle && + {props.item.errorTitle} + } +
; +} diff --git a/lib/static/new-ui/features/visual-checks/components/VisualChecksPage.tsx b/lib/static/new-ui/features/visual-checks/components/VisualChecksPage.tsx new file mode 100644 index 000000000..1ff92dc0a --- /dev/null +++ b/lib/static/new-ui/features/visual-checks/components/VisualChecksPage.tsx @@ -0,0 +1,8 @@ +import React, {ReactNode} from 'react'; +import {Box} from '@gravity-ui/uikit'; + +export function VisualChecksPage(): ReactNode { + return +

Visual Checks

+
; +} diff --git a/lib/static/new-ui/pages/InfoPage/index.tsx b/lib/static/new-ui/pages/InfoPage/index.tsx deleted file mode 100644 index a9c015798..000000000 --- a/lib/static/new-ui/pages/InfoPage/index.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from 'react'; - -export function InfoPage(): JSX.Element { - return
hello from info page!
; -} diff --git a/lib/static/new-ui/pages/SuitesPage/index.module.css b/lib/static/new-ui/pages/SuitesPage/index.module.css deleted file mode 100644 index c77b247d6..000000000 --- a/lib/static/new-ui/pages/SuitesPage/index.module.css +++ /dev/null @@ -1,57 +0,0 @@ -.controls-row { - align-items: center; - display: flex; - height: 28px; - margin-top: var(--g-spacing-2); -} - -.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;*/ -} - .tree-view__item > div { - align-items: start !important; - } - - .tree-view__item { - --g-text-subheader-1-font-size: 18px; - --g-text-subheader-1-line-height: 22px; - --g-text-subheader-font-weight: normal; - - --g-text-body-1-font-size: 18px; - --g-text-body-1-line-height: 22px; - --g-text-body-font-weight: normal; - - padding: 4px 0; - font-size: 18px; - } - - .tree-item__error-title { - color: var(--g-color-private-red-600-solid); - margin-left: var(--g-spacing-1); - } - - .tree-item__error-stack { - white-space: pre; - overflow: hidden; - text-overflow: ellipsis; - line-height: 22px; - color: var(--g-color-base-danger-heavy); - margin-top: 8px; - } - - .tree-item--error { - background: var(--g-color-private-red-50); - } - - - .tree-view :global(.g-text_ellipsis) { - white-space: normal; - } - -.fail-color { - color: var(--g-color-private-red-600-solid); -} - diff --git a/lib/static/new-ui/pages/SuitesPage/index.tsx b/lib/static/new-ui/pages/SuitesPage/index.tsx deleted file mode 100644 index 8267e7042..000000000 --- a/lib/static/new-ui/pages/SuitesPage/index.tsx +++ /dev/null @@ -1,497 +0,0 @@ -import React, {ChangeEvent, useCallback, useEffect, useMemo, memo, useState} from 'react'; -import {SplitViewLayout} from '../../layouts/SplitViewLayout'; -import {Box, Flex, TextInput} from '@gravity-ui/uikit'; -import {StatusFilter} from './StatusFilter'; -import {last} from 'lodash'; -import {CircleXmark, CircleDashed} from '@gravity-ui/icons'; - -import {connect} from 'react-redux'; -import {AutoSizer, CellMeasurer, CellMeasurerCache} from 'react-virtualized'; -import {VariableSizeList as List} from 'react-window'; -import { - unstable_useList as useList, - unstable_ListContainerView as ListContainerView, - unstable_ListItemView as ListItemView, - unstable_getItemRenderState as getItemRenderState, - unstable_getListItemClickHandler as getListItemClickHandler, - // unstable_useListFilter as useListFilter, -} from '@gravity-ui/uikit/unstable'; -// import ResizeObserver from 'rc-resize-observer'; - -import styles from './index.module.css'; -import {useVirtualizer} from '@tanstack/react-virtual'; -import {trimArray} from '../../../../common-utils'; -import {bindActionCreators} from 'redux'; -import * as actions from '../../../modules/actions'; -import {suitesPageUpdateList} from '../../../modules/actions/suites-page'; - -interface SuitesPageInternalProps { - suites: any; - browsers: any; - results: any; - images: any; -} - -// function ListItem({key, measure, style, props,}: any) { -// return
-// -// -// -//
; -// } - -function ListViewProxy(props) { - console.log('re-render!'); - console.log(props); - console.log(styles); - - useEffect(() => { - return () => { - console.log('list item unmounts...'); - } - }, []); - - return ; -} - -function ImageWithMagnifier({ - src, - className = '', - style, - onStyleUpdate, - // width, - // height, - // alt, - magnifierHeight = 150, - magnifierWidth = 150, - zoomLevel = 3 -}) { - const [showMagnifier, setShowMagnifier] = useState(false); - const [[imgWidth, imgHeight], setSize] = useState([0, 0]); - const [[x, y], setXY] = useState([0, 0]); - - const mouseEnter = (e) => { - const el = e.currentTarget; - - const { width, height } = el.getBoundingClientRect(); - setSize([width, height]); - setShowMagnifier(true); - } - - const mouseLeave = (e) => { - e.preventDefault(); - setShowMagnifier(false); - } - - const mouseMove = (e) => { - const el = e.currentTarget; - const { top, left } = el.getBoundingClientRect(); - - const x = e.pageX - left - window.scrollX; - const y = e.pageY - top - window.scrollY; - - setXY([x, y]); - }; - - useEffect(() => { - onStyleUpdate({ - display: showMagnifier ? '' : 'none', - // position: 'absolute', - position: 'fixed', - pointerEvents: 'none', - height: `${magnifierHeight}px`, - width: `${magnifierWidth}px`, - opacity: '1', - border: '1px solid lightgrey', - backgroundColor: 'white', - borderRadius: '5px', - backgroundImage: `url('${src}')`, - backgroundRepeat: 'no-repeat', - top: `${y + magnifierHeight / 2}px`, - left: `${x + magnifierWidth / 2}px`, - // top: `${y - magnifierHeight / 2}px`, - // left: `${x - magnifierWidth / 2}px`, - // top: '20px', - // right: '20px', - backgroundSize: `${imgWidth * zoomLevel}px ${imgHeight * zoomLevel}px`, - backgroundPositionX: `${-x * zoomLevel + magnifierWidth / 2}px`, - backgroundPositionY: `${-y * zoomLevel + magnifierHeight / 2}px`, - }); - }, [showMagnifier, imgWidth, imgHeight, x, y]); - - return
- {alt} mouseEnter(e)} - onMouseLeave={(e) => mouseLeave(e)} - onMouseMove={(e) => mouseMove(e)} - /> - {/**/} -
-} - -const ListItemViewMemo = memo(ListViewProxy, (...args) => { - // console.log('memo:'); - // console.log(args); - return true; -}); - -function SuitesPageInternal(props: SuitesPageInternalProps): JSX.Element { - console.log('new version'); - console.log(props); - // - const formatBrowser = (browserData: any, parentSuite: any) => { - const lastResult = props.results.byId[last(browserData.resultIds)]; - let children: any[] | undefined = undefined; - - // if (browserData.parentId === 'describe (hey) some-it') { - // children = [{ - // data: { - // title: 'pic', - // subtitle: wow
omg
just to be sure!
, - // fullTitle: (parentSuite.fullTitle + ' ' + browserData.name).trim() - // } - // }]; - // } - const diffImgId = lastResult.imageIds.find(imageId => props.images.byId[imageId].stateName); - const diffImg = props.images.byId[diffImgId]?.diffImg; - - let errorStack; - if (lastResult.status === 'error' && lastResult.error?.stack) { - const stackLines = trimArray(lastResult.error.stack.split('\n')); - errorStack = stackLines.slice(0, 3).join('\n'); - } - - const data = { - title: browserData.name, - fullTitle: (parentSuite.fullTitle + ' ' + browserData.name).trim(), - status: lastResult.status, - errorTitle: lastResult.error?.name, - errorStack, - hasChildren: Boolean(children?.length), - diffImg - }; - - return {data, children}; - }; - - const formatSuite = (suiteData: any, parentSuite: any) => { - const data = { - title: suiteData.name, - fullTitle: (parentSuite.fullTitle + ' ' + suiteData.name).trim(), - status: suiteData.status, - hasChildren: true, - }; - if (suiteData.browserIds) { - return { - data, - children: suiteData.browserIds.map((browserId: any) => formatBrowser(props.browsers.byId[browserId], data)) - }; - } else { - return { - data, - children: suiteData.suiteIds.map((suiteId: any) => formatSuite(props.suites.byId[suiteId], data)) - }; - } - }; - - const itemsOriginal = useMemo(() => props.suites.allRootIds.map((rootId: any) => { - return formatSuite(props.suites.byId[rootId], {fullTitle: ''}) - // return { - // data: { title: rootId, fullTitle: rootId }, - // children: props.suites.byId[rootId].suiteIds.map((suiteId: any) => formatSuite(props.suites.byId[suiteId], {fullTitle: rootId})) - // }; - }), [props.suites]); - // - // const {onFilterUpdate, items: filteredItems} = useListFilter({ - // items, - // filterItem(value: string, item: any): boolean { - // console.log('filtering! by ' + value); - // console.log(item); - // - // return item.fullTitle.includes(value); - // // return true; - // }, - // }) - - // const itemsOriginal: any[] = useMemo(() => { - // const result = [] as any[]; - // for (let i = 0; i < 200; i++) { - // const item = {data: {title: 'Some Title ' + i, fullTitle: 'Some-full-title ' + i}, - // children: [ - // {data: {title: 'Child #' + i, fullTitle: 'full-child-title' + i}} - // ]}; - // - // if (i === 50) { - // (item.children[0].data as any).subtitle =
Hello!
- // } - // - // result.push(item); - // } - // return result; - // }, []); - - const list = useList({items: itemsOriginal, withExpandedState: true}); - // console.log(props.actions); - - useEffect(() => { - props.actions.suitesPageUpdateList({list}); - }, [list]); - // console.log(items); - console.log(list); - console.log('length:' + list.structure.items.length); - - // const _suitesMeasurementCache = new CellMeasurerCache({ - // fixedWidth: true, - // defaultHeight: 30 - // }); - - const onItemClick = getListItemClickHandler({list}); - - const onChangeFilter = (value: ChangeEvent) => { - console.log((value.target as any).value); - // onFilterUpdate((value.target as any).value); - }; - - useEffect(() => { - return () => { - console.log('component unmounting!'); - }; - }, []); - - console.log('re-rendering the whole thing!!'); - const parentRef = React.useRef(null) - - const virtualizer = useVirtualizer({ - count: list.structure.visibleFlattenIds.length, - getScrollElement: () => parentRef.current, - estimateSize: () => 45, - getItemKey: useCallback((index) => list.structure.visibleFlattenIds[index], [list]), - enabled: true, - overscan: 25, - }); - - useEffect(() => { - setTimeout(() => { - console.log('scrolling tooooo !!'); - virtualizer.scrollToIndex(100, {align: 'start'}); - }, 2000); - }, []); - - const [magnifierStyle, setMagnifierStyle] = useState({display: 'none'}); - - const items = virtualizer.getVirtualItems(); - - return -
- -

Suites

-
- -
-
- -
- -
-
-
- {items.map((virtualRow) => ( -
- { - console.log('item clicked!'); - list.state.setExpanded(args[0].id, !list.state.expandedById[args[0].id]); - console.log(args); - }, - mapItemDataToProps: (x) => { - let statusIcon; - if (x.status === 'fail' || x.status === 'error') { - statusIcon = ; - } else if (x.status === 'idle') { - statusIcon = ; - } - - const title =
- {x.title} - {x.errorTitle && {x.errorTitle}} -
; - - let subtitle; - if (x.diffImg) { - // subtitle = - subtitle = - } else if (x.errorStack) { - subtitle =
- {x.errorStack} -
; - } - - const classNames = [styles['tree-view__item']]; - if ((x.status === 'fail' || x.status === 'error') && !x.hasChildren) { - classNames.push(styles['tree-item--error']); - } - - return { - startSlot: {statusIcon}, - title, - subtitle, - className: classNames.join(' ') - // status: x.status - }; - }, - size: 'm', - // multiple: , - id: list.structure.visibleFlattenIds[virtualRow.index] - }).props}/> - {/*
-
Row {virtualRow.index}
-
{list.structure.items[virtualRow.index].data.title}
-
*/} -
- ))} -
-
-
- {/* - {({width, height}) => ( - 30} - // rowHeight={_suitesMeasurementCache.rowHeight} - > - {({index, style, ...other}) => { - console.log('other'); - console.log(index); - console.log(style); - console.log(parent); - console.log(other); - return ( - - {({measure}) => { - console.log('hey! debugging key!'); - console.log((list.structure.visibleFlattenIds[index] as any)) - - return ( - ({ - title: x.title, - subtitle: x.subtitle - }), - size: 'm', - // multiple: , - id: list.structure.visibleFlattenIds[index] - }).props}/> - //
- // - // ({ - // title: x.title, - // subtitle: x.subtitle - // }), - // size: 'm', - // // multiple: , - // id: list.structure.visibleFlattenIds[index] - // }).props as any} /> - // - //
- ); - }} -
- //
- // - // - //
- ); - }} -
- )} -
*/} -
-
-
-
-
-
-
; -} - -// export default connect( -// (state) => { -// return { -// processing: state.processing, -// acceptableOpenedImageIds: getAcceptableOpenedImageIds(state), -// isStaticImageAccepterEnabled: state.staticImageAccepter.enabled, -// }; -// }, -// (dispatch) => ({actions: bindActionCreators(actions, dispatch)}) -// )(AcceptOpenedButton); - -export const SuitesPage = connect( - (state: any) => ({ - suites: state.tree.suites, - browsers: state.tree.browsers, - results: state.tree.results, - images: state.tree.images, - }), - (dispatch) => ({actions: bindActionCreators(actions, dispatch)}) -)(SuitesPageInternal); diff --git a/lib/static/new-ui/pages/VisualChecksPage/index.tsx b/lib/static/new-ui/pages/VisualChecksPage/index.tsx deleted file mode 100644 index d364acd04..000000000 --- a/lib/static/new-ui/pages/VisualChecksPage/index.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from 'react'; - -export function VisualChecksPage(): JSX.Element { - return
hello from visual checks page!
; -} diff --git a/lib/static/new-ui/report.tsx b/lib/static/new-ui/report.tsx deleted file mode 100644 index 8a3c120ef..000000000 --- a/lib/static/new-ui/report.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from 'react'; -import ReactDOM from 'react-dom'; -import {App} from './App'; - -const rootEl = document.getElementById('app'); - -ReactDOM.render(, rootEl); diff --git a/lib/static/new-ui/store/selectors.ts b/lib/static/new-ui/store/selectors.ts new file mode 100644 index 000000000..b184daf1b --- /dev/null +++ b/lib/static/new-ui/store/selectors.ts @@ -0,0 +1,8 @@ +import {State, BrowserEntity, ImageEntity, ResultEntity, SuiteEntity, SuiteState} 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 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 new file mode 100644 index 000000000..7e2901853 --- /dev/null +++ b/lib/static/new-ui/types/store.ts @@ -0,0 +1,81 @@ +import {TestStatus} from '@/constants'; +import {ImageFile} from '@/types'; + +export interface SuiteEntityNode { + name: string; + status: TestStatus; + suiteIds: string[]; +} + +export interface SuiteEntityLeaf { + name: string; + status: TestStatus; + browserIds: string[]; +} + +export type SuiteEntity = SuiteEntityNode | SuiteEntityLeaf; + +export const isSuiteEntityLeaf = (suite: SuiteEntity): suite is SuiteEntityLeaf => Boolean((suite as SuiteEntityLeaf).browserIds); + +export interface BrowserEntity { + name: string; + resultIds: string[]; +} + +export interface ResultEntityCommon { + imageIds: string[]; + status: TestStatus; +} + +export interface ResultEntityError extends ResultEntityCommon { + error: Error; + status: TestStatus.ERROR; +} + +export type ResultEntity = ResultEntityCommon | ResultEntityError; + +export const isResultEntityError = (result: ResultEntity): result is ResultEntityError => result.status === TestStatus.ERROR; + +export interface ImageEntityError { + status: TestStatus.ERROR; +} + +export interface ImageEntityFail { + stateName: string; + diffImg: ImageFile; +} + +export type ImageEntity = ImageEntityError | ImageEntityFail; + +export const isImageEntityFail = (image: ImageEntity): image is ImageEntityFail => Boolean((image as ImageEntityFail).stateName); + +export interface SuiteState { + shouldBeOpened: boolean; + shouldBeShown: boolean; +} + +export interface State { + app: { + isInitialized: boolean; + currentSuiteId: string | null; + } + tree: { + browsers: { + byId: Record + }; + images: { + byId: Record; + } + results: { + byId: Record; + }; + suites: { + allRootIds: string[]; + byId: Record; + stateById: Record; + }; + } + view: { + testNameFilter: string; + } +} diff --git a/lib/static/new-ui/typings.d.ts b/lib/static/new-ui/types/typings.d.ts similarity index 74% rename from lib/static/new-ui/typings.d.ts rename to lib/static/new-ui/types/typings.d.ts index 1d8d50c6c..f76e77928 100644 --- a/lib/static/new-ui/typings.d.ts +++ b/lib/static/new-ui/types/typings.d.ts @@ -4,6 +4,6 @@ declare module '*.svg' { } declare module '*.module.css' { - const classes: { [key: string]: string }; + const classes: {[key: string]: string}; export default classes; } diff --git a/lib/static/new-ui/utils/index.tsx b/lib/static/new-ui/utils/index.tsx new file mode 100644 index 000000000..0a5086fe6 --- /dev/null +++ b/lib/static/new-ui/utils/index.tsx @@ -0,0 +1,15 @@ +import {TestStatus} from '@/constants'; +import {CircleCheck, CircleDashed, CircleXmark, CircleMinus} from '@gravity-ui/icons'; +import React from 'react'; + +export const getIconByStatus = (status: TestStatus): React.JSX.Element => { + if (status === TestStatus.FAIL || status === TestStatus.ERROR) { + return ; + } else if (status === TestStatus.SUCCESS) { + return ; + } else if (status === TestStatus.SKIPPED) { + return ; + } + + return ; +}; diff --git a/lib/static/styles.css b/lib/static/styles.css index 60c41c0df..d88f9f9f4 100644 --- a/lib/static/styles.css +++ b/lib/static/styles.css @@ -1096,3 +1096,15 @@ a:active { .g-radio-button__option-text { margin: 0 4px !important; } + +.icon-fail { + color: var(--g-color-private-red-600-solid); +} + +.icon-success { + color: var(--g-color-private-green-600-solid); +} + +.icon-skip { + color: var(--g-color-private-cool-grey-600-solid); +} diff --git a/lib/static/tsconfig.json b/lib/static/tsconfig.json index 9daed60a8..a14167cb0 100644 --- a/lib/static/tsconfig.json +++ b/lib/static/tsconfig.json @@ -3,6 +3,9 @@ "include": ["."], "compilerOptions": { "jsx": "react", - "noEmit": true + "noEmit": true, + "paths": { + "@/*": ["../*"] + } } } diff --git a/package-lock.json b/package-lock.json index 5d5e6619c..ca693fe33 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,7 +6,7 @@ "packages": { "": { "name": "html-reporter", - "version": "10.6.0", + "version": "10.6.2", "license": "MIT", "workspaces": [ "test/func/fixtures/*", @@ -63,7 +63,7 @@ "@babel/preset-react": "^7.22.5", "@babel/preset-typescript": "^7.22.5", "@gravity-ui/components": "^3.7.0", - "@gravity-ui/uikit": "^6.20.0", + "@gravity-ui/uikit": "^6.23.0", "@playwright/test": "^1.44.1", "@react-hook/resize-observer": "^2.0.1", "@swc/core": "^1.3.64", @@ -2896,9 +2896,9 @@ } }, "node_modules/@gravity-ui/uikit": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/@gravity-ui/uikit/-/uikit-6.20.0.tgz", - "integrity": "sha512-ngeFZH0CgF+6cNwWTrtwecYkb1Rb7TWFg3eUg87TGLvXiEemepJuwLvM4t9WX3yZFzFCzIoHML2h/SM6VXhjyQ==", + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/@gravity-ui/uikit/-/uikit-6.23.0.tgz", + "integrity": "sha512-v+1QPL+IdaGxv3tuEUHaRSzK5Hp/XNJv3EQxMIMFSV4ojtvio6sQAlzLIYALGN/IULWRwlfF0cqG/TavOo9yog==", "dependencies": { "@bem-react/classname": "^1.6.0", "@gravity-ui/i18n": "^1.3.0", @@ -33750,9 +33750,9 @@ } }, "@gravity-ui/uikit": { - "version": "6.20.0", - "resolved": "https://registry.npmjs.org/@gravity-ui/uikit/-/uikit-6.20.0.tgz", - "integrity": "sha512-ngeFZH0CgF+6cNwWTrtwecYkb1Rb7TWFg3eUg87TGLvXiEemepJuwLvM4t9WX3yZFzFCzIoHML2h/SM6VXhjyQ==", + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/@gravity-ui/uikit/-/uikit-6.23.0.tgz", + "integrity": "sha512-v+1QPL+IdaGxv3tuEUHaRSzK5Hp/XNJv3EQxMIMFSV4ojtvio6sQAlzLIYALGN/IULWRwlfF0cqG/TavOo9yog==", "requires": { "@bem-react/classname": "^1.6.0", "@gravity-ui/i18n": "^1.3.0", diff --git a/package.json b/package.json index 312cb87ee..b0f6db89a 100644 --- a/package.json +++ b/package.json @@ -128,7 +128,7 @@ "@babel/preset-react": "^7.22.5", "@babel/preset-typescript": "^7.22.5", "@gravity-ui/components": "^3.7.0", - "@gravity-ui/uikit": "^6.20.0", + "@gravity-ui/uikit": "^6.23.0", "@playwright/test": "^1.44.1", "@react-hook/resize-observer": "^2.0.1", "@swc/core": "^1.3.64", diff --git a/webpack.common.js b/webpack.common.js index 969fecf1a..347b4f653 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -12,14 +12,19 @@ const {ProgressPlugin} = require('webpack'); const staticPath = path.resolve(__dirname, 'build', 'lib', 'static'); +console.log('here is resolved path; ' + path.resolve(__dirname, 'src')); + module.exports = { entry: { report: ['./index.jsx', './variables.css', './styles.css'], gui: ['./gui.jsx', './variables.css', './styles.css', './gui.css'], - newReport: ['./new-ui/report.tsx', './variables.css', './styles.css'], - newGui: ['./new-ui/gui.tsx', './variables.css', './styles.css', './gui.css'] + newReport: ['./new-ui/app/report.tsx', './variables.css', './styles.css'], + newGui: ['./new-ui/app/gui.tsx', './variables.css', './styles.css', './gui.css'] }, resolve: { + alias: { + '@': path.resolve(__dirname, 'lib') + }, extensions: ['.js', '.jsx', '.ts', '.tsx'], fallback: { buffer: require.resolve('buffer'), From 6ce187fa60468cae0374b16f6c65fa73c91cc11c Mon Sep 17 00:00:00 2001 From: shadowusr Date: Fri, 16 Aug 2024 15:05:14 +0300 Subject: [PATCH 06/11] fix: fix poor scrolling performance --- .../components/SuitesPage/index.module.css | 1 - .../suites/components/SuitesPage/index.tsx | 48 ++++++++++--------- 2 files changed, 26 insertions(+), 23 deletions(-) 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 75f1c7113..0ff23045b 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 @@ -40,7 +40,6 @@ --g-text-body-1-line-height: 22px; --g-text-body-font-weight: normal; - margin-top: 4px; padding: 4px 0; font-size: 18px; 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 d4c580158..8db29d105 100644 --- a/lib/static/new-ui/features/suites/components/SuitesPage/index.tsx +++ b/lib/static/new-ui/features/suites/components/SuitesPage/index.tsx @@ -1,5 +1,5 @@ import React, {ChangeEvent, useCallback, useEffect, useState} from 'react'; -import {Flex, TextInput} from '@gravity-ui/uikit'; +import {Box, Flex, TextInput} from '@gravity-ui/uikit'; import {debounce} from 'lodash'; import classNames from 'classnames'; import {connect} from 'react-redux'; @@ -55,7 +55,7 @@ function SuitesPageInternal(props: SuitesPageInternalProps): JSX.Element { } }); - const onItemClick = ({id}: {id: string}): void => { + 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) { @@ -65,7 +65,7 @@ function SuitesPageInternal(props: SuitesPageInternalProps): JSX.Element { navigate(encodeURIComponent(item.fullTitle)); } - }; + }, [list, props.actions, props.treeViewExpandedById]); const updateTestNameFilter = useCallback(debounce( (testName) => props.actions.updateTestNameFilter(testName), @@ -78,9 +78,9 @@ function SuitesPageInternal(props: SuitesPageInternalProps): JSX.Element { const virtualizer = useVirtualizer({ count: list.structure.visibleFlattenIds.length, getScrollElement: () => parentRef.current, - estimateSize: () => 24, + estimateSize: () => 28, getItemKey: useCallback((index: number) => list.structure.visibleFlattenIds[index], [list]), - overscan: 100 + overscan: 200 }); useEffect(() => { @@ -103,10 +103,10 @@ function SuitesPageInternal(props: SuitesPageInternalProps): JSX.Element { }, [props.isInitialized]); const [testNameFilter, setTestNameFilter] = useState(props.testNameFilter); - const onChange = (event: ChangeEvent): void => { + const onChange = useCallback((event: ChangeEvent): void => { setTestNameFilter(event.target.value); updateTestNameFilter(event.target.value); - }; + }, [setTestNameFilter, updateTestNameFilter]); const items = virtualizer.getVirtualItems(); @@ -139,24 +139,28 @@ function SuitesPageInternal(props: SuitesPageInternalProps): JSX.Element { classes.push(styles['tree-item--browser']); } - return { - return { - startSlot: getIconByStatus(x.status), - title: , - subtitle: - }; - } - }).props}/>; + spacing={{pt: 1}} + > + { + return { + startSlot: getIconByStatus(x.status), + title: , + subtitle: + }; + } + }).props}/> + ; })}
From 0fddcf3e2d1ad9934a54587f3008fa472199482b Mon Sep 17 00:00:00 2001 From: shadowusr Date: Fri, 16 Aug 2024 16:20:36 +0300 Subject: [PATCH 07/11] 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) { From ea44585fe4ff0321efb3e7a2888f1497eb33866d Mon Sep 17 00:00:00 2001 From: shadowusr Date: Fri, 16 Aug 2024 17:06:52 +0300 Subject: [PATCH 08/11] refactor: remove unnecessary styles --- .../components/SuitesPage/index.module.css | 80 ------------------- .../SuitesTreeView/index.module.css | 80 +++++++++++++++++++ .../components/SuitesTreeView/index.tsx | 8 +- lib/static/styles.css | 41 ---------- 4 files changed, 84 insertions(+), 125 deletions(-) create mode 100644 lib/static/new-ui/features/suites/components/SuitesTreeView/index.module.css 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 db28706b6..2d0823863 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 @@ -4,83 +4,3 @@ height: 28px; margin-top: var(--g-spacing-2); } - -.tree-view { - margin-top: var(--g-spacing-2); -} - .tree-view__container { - height: 100%; - width: 100%; - overflow-y: auto; - contain: strict; - } - - .tree-view__total-size-wrapper { - width: 100%; - position: relative; - } - - .tree-view__visible-window { - position: absolute; - top: 0; - left: 0; - width: 100%; - } - - .tree-view__item { - --g-text-subheader-1-font-size: 18px; - --g-text-subheader-1-line-height: 22px; - --g-text-subheader-font-weight: normal; - - --g-text-body-1-font-size: 18px; - --g-text-body-1-line-height: 22px; - --g-text-body-font-weight: normal; - - padding: 4px 0; - font-size: 18px; - - user-select: none; - } - - .tree-view__item svg { - flex-shrink: 0; - } - - .tree-view__item > div > div { - align-items: start !important; - } - - .tree-item--current { - background: #a28aff !important; - color: #fff; - } - - .tree-item--current:hover { - background: #af9aff !important; - } - - .tree-item--current svg { - color: #fff; - } - - .tree-item--browser { - padding-left: 8px; - } - - .tree-item--error { - background: var(--g-color-private-red-100); - color: var(--g-color-private-red-600-solid); - } - - .tree-item--error:hover { - background: var(--g-color-private-red-50) !important; - } - - .tree-item--error svg { - color: var(--g-color-private-red-600-solid); - } - - - .tree-view :global(.g-text_ellipsis) { - white-space: normal; - } 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 new file mode 100644 index 000000000..0c2ed773b --- /dev/null +++ b/lib/static/new-ui/features/suites/components/SuitesTreeView/index.module.css @@ -0,0 +1,80 @@ +.tree-view { + margin-top: var(--g-spacing-2); +} + + .tree-view :global(.g-text_ellipsis) { + white-space: normal; + } + + .tree-view__container { + height: 100%; + width: 100%; + overflow-y: auto; + contain: strict; + } + + .tree-view__total-size-wrapper { + width: 100%; + position: relative; + } + + .tree-view__visible-window { + position: absolute; + top: 0; + left: 0; + width: 100%; + } + + .tree-view__item { + --g-text-subheader-1-font-size: 18px; + --g-text-subheader-1-line-height: 22px; + --g-text-subheader-font-weight: normal; + + --g-text-body-1-font-size: 18px; + --g-text-body-1-line-height: 22px; + --g-text-body-font-weight: normal; + + padding: 4px 0; + font-size: 18px; + + user-select: none; + } + + .tree-view__item svg { + flex-shrink: 0; + } + + .tree-view__item > div > div { + align-items: start !important; + } + + .tree-view__item--current { + background: #a28aff !important; + color: #fff; + } + + .tree-view__item--current:hover { + background: #af9aff !important; + } + + .tree-view__item--current svg { + color: #fff; + } + + .tree-view__item--browser { + padding-left: 8px; + } + + .tree-view__item--error { + background: var(--g-color-private-red-100); + color: var(--g-color-private-red-600-solid); + } + + .tree-view__item--error:hover { + background: var(--g-color-private-red-50) !important; + } + + + .tree-view__item--error svg { + color: var(--g-color-private-red-600-solid); + } diff --git a/lib/static/new-ui/features/suites/components/SuitesTreeView/index.tsx b/lib/static/new-ui/features/suites/components/SuitesTreeView/index.tsx index f133d2ebd..68c3dd789 100644 --- a/lib/static/new-ui/features/suites/components/SuitesTreeView/index.tsx +++ b/lib/static/new-ui/features/suites/components/SuitesTreeView/index.tsx @@ -11,7 +11,6 @@ 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, @@ -26,6 +25,7 @@ import { getTreeViewExpandedById, getTreeViewItems } from '@/static/new-ui/features/suites/components/SuitesTreeView/selectors'; +import styles from './index.module.css'; interface SuitesTreeViewProps { treeViewItems: TreeViewItem[]; @@ -109,12 +109,12 @@ function SuitesTreeViewInternal(props: SuitesTreeViewProps): ReactNode { const item = list.structure.itemsById[virtualRow.key]; const classes = [styles['tree-view__item']]; if (item.fullTitle === props.currentSuiteId) { - classes.push(styles['tree-item--current']); + classes.push(styles['tree-view__item--current']); } else if ((item.status === 'fail' || item.status === 'error') && item.type === TreeViewItemType.Browser) { - classes.push(styles['tree-item--error']); + classes.push(styles['tree-view__item--error']); } if (item.type === TreeViewItemType.Browser) { - classes.push(styles['tree-item--browser']); + classes.push(styles['tree-view__item--browser']); } return * { - margin-left: var(--g-spacing-2); - } - - .controls-row > *:first-child { - margin-left: 0; - } - -.status-switcher__option { - align-items: center; - display: flex; - justify-content: center; -} - - .status-switcher__option > *:first-child { - margin-right: 2px; - } - -.g-radio-button__option-text { - margin: 0 4px !important; -} - .icon-fail { color: var(--g-color-private-red-600-solid); } From a424b5eaba40e5cf1b3ac03aaa586857e796fc6f Mon Sep 17 00:00:00 2001 From: shadowusr Date: Wed, 21 Aug 2024 03:01:08 +0300 Subject: [PATCH 09/11] fix: fix review issues --- .gitignore | 1 + lib/static/new-ui/app/report.tsx | 4 +- .../ImageWithMagnifier/index.module.css | 10 +++ .../index.tsx} | 70 +++++++++++++------ .../SuitesTreeView/index.module.css | 6 +- .../components/SuitesTreeView/index.tsx | 36 +++++----- .../components/SuitesTreeView/selectors.ts | 5 +- .../components/TreeViewItemSubtitle/index.tsx | 10 ++- lib/static/new-ui/utils/index.tsx | 6 ++ test/setup/globals.js | 15 +++- tsconfig.spec.json | 3 +- webpack.common.js | 2 - 12 files changed, 115 insertions(+), 53 deletions(-) create mode 100644 lib/static/new-ui/components/ImageWithMagnifier/index.module.css rename lib/static/new-ui/components/{ImageWithMagnifier.tsx => ImageWithMagnifier/index.tsx} (55%) diff --git a/.gitignore b/.gitignore index 7166e954a..0bcb0a704 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,4 @@ test/func/**/report-backup test/func/**/reports test/func/packages/*/plugin.js test/func/fixtures/playwright/test-results +@ diff --git a/lib/static/new-ui/app/report.tsx b/lib/static/new-ui/app/report.tsx index 2323e0294..6881b9605 100644 --- a/lib/static/new-ui/app/report.tsx +++ b/lib/static/new-ui/app/report.tsx @@ -7,7 +7,7 @@ import {initStaticReport, finStaticReport} from '../../modules/actions'; const rootEl = document.getElementById('app') as HTMLDivElement; const root = createRoot(rootEl); -function Gui(): ReactNode { +function Report(): ReactNode { useEffect(() => { store.dispatch(initStaticReport()); @@ -19,4 +19,4 @@ function Gui(): ReactNode { return ; } -root.render(); +root.render(); diff --git a/lib/static/new-ui/components/ImageWithMagnifier/index.module.css b/lib/static/new-ui/components/ImageWithMagnifier/index.module.css new file mode 100644 index 000000000..fa3275874 --- /dev/null +++ b/lib/static/new-ui/components/ImageWithMagnifier/index.module.css @@ -0,0 +1,10 @@ +.magnifier { + background-color: white; + background-repeat: no-repeat; + border: 1px solid lightgrey; + border-radius: 5px; + opacity: 1; + pointer-events: none; + position: fixed; + z-index: 1000 +} diff --git a/lib/static/new-ui/components/ImageWithMagnifier.tsx b/lib/static/new-ui/components/ImageWithMagnifier/index.tsx similarity index 55% rename from lib/static/new-ui/components/ImageWithMagnifier.tsx rename to lib/static/new-ui/components/ImageWithMagnifier/index.tsx index 4ff41fc4c..155cee4cd 100644 --- a/lib/static/new-ui/components/ImageWithMagnifier.tsx +++ b/lib/static/new-ui/components/ImageWithMagnifier/index.tsx @@ -1,5 +1,9 @@ -import React, {ReactNode, useEffect, useState} from 'react'; +import classnames from 'classnames'; +import React, {ReactNode, useEffect, useRef, useState} from 'react'; import {createPortal} from 'react-dom'; +import styles from './index.module.css'; + +const DEFAULT_ZOOM_LEVEL = 3; interface ImageWithMagnifierProps { src: string; @@ -9,6 +13,8 @@ interface ImageWithMagnifierProps { magnifierHeight?: number; magnifierWidth?: number; zoomLevel?: number; + // Used to detect parent container scrolling and update the magnifier state + scrollContainerRef?: React.RefObject; } export function ImageWithMagnifier({ @@ -18,13 +24,15 @@ export function ImageWithMagnifier({ style, magnifierHeight = 150, magnifierWidth = 150, - zoomLevel = 3 + zoomLevel = DEFAULT_ZOOM_LEVEL, + scrollContainerRef }: ImageWithMagnifierProps): ReactNode { const [showMagnifier, setShowMagnifier] = useState(false); const [[imgWidth, imgHeight], setSize] = useState([0, 0]); const [[x, y], setXY] = useState([0, 0]); - const [[mouseX, mouseY], setMouseXY] = useState([0, 0]); + const mousePositionRef = useRef([0, 0]); const [magnifierStyle, setMagnifierStyle] = useState({}); + const imgRef = useRef(null); const mouseEnter = (e: React.MouseEvent): void => { const el = e.currentTarget; @@ -32,60 +40,82 @@ export function ImageWithMagnifier({ const {width, height} = el.getBoundingClientRect(); setSize([width, height]); setShowMagnifier(true); - setMouseXY([e.clientX, e.clientY]); + mousePositionRef.current = [e.clientX, e.clientY]; }; const mouseLeave = (e: React.MouseEvent): void => { e.preventDefault(); setShowMagnifier(false); - setMouseXY([e.clientX, e.clientY]); + mousePositionRef.current = [e.clientX, e.clientY]; }; const mouseMove = (e: React.MouseEvent): void => { const el = e.currentTarget; const {top, left} = el.getBoundingClientRect(); - const x = e.pageX - left - window.scrollX; - const y = e.pageY - top - window.scrollY; + const x = e.clientX - left - window.scrollX; + const y = e.clientY - top - window.scrollY; setXY([x, y]); - setMouseXY([e.clientX, e.clientY]); + mousePositionRef.current = [e.clientX, e.clientY]; }; useEffect(() => { + if (showMagnifier && scrollContainerRef?.current && imgRef?.current) { + const handleScroll = (): void => { + if (!imgRef.current) { + return; + } + const [mouseX, mouseY] = mousePositionRef.current; + + const el = imgRef.current; + const {top, left} = el.getBoundingClientRect(); + + const x = mouseX - left - window.scrollX; + const y = mouseY - top - window.scrollY; + + setXY([x, y]); + }; + + scrollContainerRef.current.addEventListener('scroll', handleScroll); + + return () => { + scrollContainerRef.current?.removeEventListener('scroll', handleScroll); + }; + } + + return undefined; + }, [showMagnifier, scrollContainerRef]); + + useEffect(() => { + const [mouseX, mouseY] = mousePositionRef.current; + setMagnifierStyle({ display: showMagnifier ? '' : 'none', - position: 'fixed', - pointerEvents: 'none', height: `${magnifierHeight}px`, width: `${magnifierWidth}px`, - opacity: '1', - border: '1px solid lightgrey', - backgroundColor: 'white', - borderRadius: '5px', backgroundImage: `url('${src}')`, - backgroundRepeat: 'no-repeat', top: `${mouseY - magnifierHeight / 2}px`, left: `${mouseX - magnifierWidth / 2}px`, backgroundSize: `${imgWidth * zoomLevel}px ${imgHeight * zoomLevel}px`, backgroundPositionX: `${-x * zoomLevel + magnifierWidth / 2}px`, - backgroundPositionY: `${-y * zoomLevel + magnifierHeight / 2}px`, - zIndex: 1000 + backgroundPositionY: `${-y * zoomLevel + magnifierHeight / 2}px` }); }, [showMagnifier, imgWidth, imgHeight, x, y]); - return
+ return
{alt} mouseEnter(e)} onMouseLeave={(e): void => mouseLeave(e)} onMouseMove={(e): void => mouseMove(e)} + ref={imgRef} /> -
{createPortal(
, document.body)}
; 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 0c2ed773b..84242f152 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 @@ -50,15 +50,15 @@ .tree-view__item--current { background: #a28aff !important; - color: #fff; + color: #fff !important; } - .tree-view__item--current:hover { + .tree-view__item.tree-view__item--current:hover { background: #af9aff !important; } .tree-view__item--current svg { - color: #fff; + color: #fff !important; } .tree-view__item--browser { diff --git a/lib/static/new-ui/features/suites/components/SuitesTreeView/index.tsx b/lib/static/new-ui/features/suites/components/SuitesTreeView/index.tsx index 68c3dd789..f0a175191 100644 --- a/lib/static/new-ui/features/suites/components/SuitesTreeView/index.tsx +++ b/lib/static/new-ui/features/suites/components/SuitesTreeView/index.tsx @@ -1,7 +1,9 @@ import {Box} from '@gravity-ui/uikit'; import { - unstable_getItemRenderState as getItemRenderState, unstable_ListContainerView as ListContainerView, - unstable_ListItemView as ListItemView, unstable_useList as useList + 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'; @@ -26,6 +28,7 @@ import { getTreeViewItems } from '@/static/new-ui/features/suites/components/SuitesTreeView/selectors'; import styles from './index.module.css'; +import {TestStatus} from '@/constants'; interface SuitesTreeViewProps { treeViewItems: TreeViewItem[]; @@ -63,12 +66,6 @@ function SuitesTreeViewInternal(props: SuitesTreeViewProps): ReactNode { const virtualizedItems = virtualizer.getVirtualItems(); // Effects - useEffect(() => { - if (!suiteId && props.currentSuiteId && suiteId !== props.currentSuiteId) { - navigate(encodeURIComponent(props.currentSuiteId)); - } - }, []); - useEffect(() => { if (!props.isInitialized) { return; @@ -86,7 +83,7 @@ function SuitesTreeViewInternal(props: SuitesTreeViewProps): ReactNode { 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) { + if (item.type === TreeViewItemType.Suite) { props.actions.toggleSuiteSection({suiteId: item.fullTitle, shouldBeOpened: !props.treeViewExpandedById[item.fullTitle]}); } else if (item.type === TreeViewItemType.Browser) { props.actions.suitesPageSetCurrentSuite(id); @@ -107,15 +104,14 @@ function SuitesTreeViewInternal(props: SuitesTreeViewProps): ReactNode { > {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-view__item--current']); - } else if ((item.status === 'fail' || item.status === 'error') && item.type === TreeViewItemType.Browser) { - classes.push(styles['tree-view__item--error']); - } - if (item.type === TreeViewItemType.Browser) { - classes.push(styles['tree-view__item--browser']); - } + const classes = [ + styles['tree-view__item'], + { + [styles['tree-view__item--current']]: item.fullTitle === props.currentSuiteId, + [styles['tree-view__item--browser']]: item.type === TreeViewItemType.Browser, + [styles['tree-view__item--error']]: item.type === TreeViewItemType.Browser && (item.status === TestStatus.FAIL || item.status === TestStatus.ERROR) + } + ]; return , - subtitle: + subtitle: }; } }).props}/> 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 c4a49ab20..3f32120cf 100644 --- a/lib/static/new-ui/features/suites/components/SuitesTreeView/selectors.ts +++ b/lib/static/new-ui/features/suites/components/SuitesTreeView/selectors.ts @@ -24,6 +24,7 @@ import { } from '@/static/new-ui/store/selectors'; import {trimArray} from '@/common-utils'; import {ImageFile} from '@/types'; +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( @@ -54,7 +55,7 @@ export const getTreeViewItems = createSelector( const data: TreeViewBrowserData = { type: TreeViewItemType.Browser, title: browserData.name, - fullTitle: (parentSuite.fullTitle + ' ' + browserData.name).trim(), + fullTitle: getFullTitleByTitleParts([parentSuite.fullTitle, browserData.name]), status: lastResult.status, errorTitle, errorStack, @@ -68,7 +69,7 @@ export const getTreeViewItems = createSelector( const data: TreeViewSuiteData = { type: TreeViewItemType.Suite, title: suiteData.name, - fullTitle: (parentSuite.fullTitle + ' ' + suiteData.name).trim(), + fullTitle: getFullTitleByTitleParts([parentSuite.fullTitle, suiteData.name]), status: suiteData.status }; 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 f9af37f59..bc7bb3c9a 100644 --- a/lib/static/new-ui/features/suites/components/TreeViewItemSubtitle/index.tsx +++ b/lib/static/new-ui/features/suites/components/TreeViewItemSubtitle/index.tsx @@ -3,9 +3,15 @@ import {TreeViewData, TreeViewItemType} from '@/static/new-ui/features/suites/co import styles from './index.module.css'; import {ImageWithMagnifier} from '@/static/new-ui/components/ImageWithMagnifier'; -export function TreeViewItemSubtitle(props: {item: TreeViewData}): ReactNode { +interface TreeViewItemSubtitleProps { + item: TreeViewData; + // Passed to image with magnifier to detect parent container scrolling and update magnifier position + scrollContainerRef: React.RefObject; +} + +export function TreeViewItemSubtitle(props: TreeViewItemSubtitleProps): ReactNode { if (props.item.type === TreeViewItemType.Browser && props.item.diffImg) { - return ; + return ; } else if (props.item.type === TreeViewItemType.Browser && props.item.errorStack) { return
{props.item.errorStack} diff --git a/lib/static/new-ui/utils/index.tsx b/lib/static/new-ui/utils/index.tsx index 0a5086fe6..d21f0fc64 100644 --- a/lib/static/new-ui/utils/index.tsx +++ b/lib/static/new-ui/utils/index.tsx @@ -13,3 +13,9 @@ export const getIconByStatus = (status: TestStatus): React.JSX.Element => { return ; }; + +export const getFullTitleByTitleParts = (titleParts: string[]): string => { + const DELIMITER = ' '; + + return titleParts.join(DELIMITER).trim(); +}; diff --git a/test/setup/globals.js b/test/setup/globals.js index 717ac74f9..310b17fbf 100644 --- a/test/setup/globals.js +++ b/test/setup/globals.js @@ -19,4 +19,17 @@ chai.use(require('chai-as-promised')); chai.use(require('chai-dom')); sinon.assert.expose(chai.assert, {prefix: ''}); -require('app-module-path').addPath(path.resolve(__dirname, '..', '..')); +const projectRoot = path.resolve(__dirname, '..', '..'); + +// Resolving imports like lib/.../ +require('app-module-path').addPath(projectRoot); + +// Resolving webpack alias imports like @/.../ +try { + const fs = require('fs'); + fs.symlinkSync(path.join(projectRoot, 'lib'), path.join(projectRoot, '@')); +} catch (e) { + if (e.code !== 'EEXIST') { + throw e; + } +} diff --git a/tsconfig.spec.json b/tsconfig.spec.json index 9cf19863e..8a2555b73 100644 --- a/tsconfig.spec.json +++ b/tsconfig.spec.json @@ -6,7 +6,8 @@ "jsx": "react", "noEmit": true, "paths": { - "lib/*": ["./lib/*"] + "lib/*": ["./lib/*"], + "@/*": ["./lib/*"] } }, } diff --git a/webpack.common.js b/webpack.common.js index 347b4f653..a99d9f08e 100644 --- a/webpack.common.js +++ b/webpack.common.js @@ -12,8 +12,6 @@ const {ProgressPlugin} = require('webpack'); const staticPath = path.resolve(__dirname, 'build', 'lib', 'static'); -console.log('here is resolved path; ' + path.resolve(__dirname, 'src')); - module.exports = { entry: { report: ['./index.jsx', './variables.css', './styles.css'], From 30e2e9be48859ff889d4f0f66527f17b33be3628 Mon Sep 17 00:00:00 2001 From: shadowusr Date: Thu, 22 Aug 2024 02:24:43 +0300 Subject: [PATCH 10/11] fix: add testplane icon --- lib/static/icons/testplane.svg | 1 + 1 file changed, 1 insertion(+) create mode 100644 lib/static/icons/testplane.svg diff --git a/lib/static/icons/testplane.svg b/lib/static/icons/testplane.svg new file mode 100644 index 000000000..c69092c26 --- /dev/null +++ b/lib/static/icons/testplane.svg @@ -0,0 +1 @@ + \ No newline at end of file From 72e2eda3d0764803c7b8486ff8bf9594776b026c Mon Sep 17 00:00:00 2001 From: shadowusr Date: Fri, 23 Aug 2024 00:32:19 +0300 Subject: [PATCH 11/11] ci: try running on larger resource class --- .circleci/config.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 733977edb..8661f26ba 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -4,9 +4,10 @@ jobs: working_directory: ~/html-reporter docker: - image: yinfra/html-reporter-browsers + resource_class: medium+ environment: SERVER_HOST: localhost - + steps: - checkout