Skip to content

Commit

Permalink
feat: implement basic suites page in new UI (#590)
Browse files Browse the repository at this point in the history
* feat: incorporated react-virtual

* feat: implement zoom feature and previews in tree

* chore: working on new ui redux store

* feat: finish suites page implementation

* fix: fix poor scrolling performance

* refactor: split suites page into components

* refactor: remove unnecessary styles

* fix: fix review issues

* fix: add testplane icon

* ci: try running on larger resource class
  • Loading branch information
shadowusr authored Aug 22, 2024
1 parent 5a34639 commit fea8cf5
Show file tree
Hide file tree
Showing 47 changed files with 1,441 additions and 171 deletions.
3 changes: 2 additions & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ jobs:
working_directory: ~/html-reporter
docker:
- image: yinfra/html-reporter-browsers
resource_class: medium+
environment:
SERVER_HOST: localhost

steps:
- checkout

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ test/func/**/report-backup
test/func/**/reports
test/func/packages/*/plugin.js
test/func/fixtures/playwright/test-results
@
1 change: 1 addition & 0 deletions lib/gui/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export const start = async (args: ServerArgs): Promise<ServerReadyData> => {
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'});
Expand Down
3 changes: 2 additions & 1 deletion lib/server-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')),
Expand Down
1 change: 1 addition & 0 deletions lib/static/icons/testplane.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion lib/static/modules/action-names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,6 @@ 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_SET_CURRENT_SUITE: 'SUITES_PAGE_SET_CURRENT_SUITE'
} as const;
1 change: 1 addition & 0 deletions lib/static/modules/actions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
6 changes: 6 additions & 0 deletions lib/static/modules/actions/suites-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import actionNames from '../action-names';
import {SuitesPageSetCurrentSuiteAction} from '@/static/modules/reducers/suites-page';

export const suitesPageSetCurrentSuite = (suiteId: string): SuitesPageSetCurrentSuiteAction => {
return {type: actionNames.SUITES_PAGE_SET_CURRENT_SUITE, payload: {suiteId}};
};
10 changes: 8 additions & 2 deletions lib/static/modules/default-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -28,7 +29,8 @@ export default Object.assign({config: configDefaults}, {
byId: {},
allIds: [],
allRootIds: [],
failedRootIds: []
failedRootIds: [],
stateById: {}
},
browsers: {
byId: {},
Expand Down Expand Up @@ -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;
6 changes: 5 additions & 1 deletion lib/static/modules/reducers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ 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';
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.
Expand Down Expand Up @@ -56,5 +58,7 @@ export default reduceReducers(
tree,
groupedTests,
plugins,
progressBar
progressBar,
suitesPage,
isInitialized
);
12 changes: 12 additions & 0 deletions lib/static/modules/reducers/is-initialized.js
Original file line number Diff line number Diff line change
@@ -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;
}
};
16 changes: 16 additions & 0 deletions lib/static/modules/reducers/suites-page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import actionNames from '../action-names';
import {State} from '@/static/new-ui/types/store';
import {Action} from '@/static/modules/actions/types';

export type SuitesPageSetCurrentSuiteAction = Action<typeof actionNames.SUITES_PAGE_SET_CURRENT_SUITE, {
suiteId: string;
}>;

export default (state: State, action: SuitesPageSetCurrentSuiteAction): State => {
switch (action.type) {
case actionNames.SUITES_PAGE_SET_CURRENT_SUITE:
return {...state, app: {...state.app, currentSuiteId: action.payload.suiteId}};
default:
return state;
}
};
6 changes: 1 addition & 5 deletions lib/static/modules/selectors/tree.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
7 changes: 7 additions & 0 deletions lib/static/new-ui.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
.g-root {
--g-font-family-sans: 'Jost', sans-serif;
}

.report {
font-family: var(--g-font-family-sans), sans-serif !important;
}
43 changes: 43 additions & 0 deletions lib/static/new-ui/app/App.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import {ThemeProvider} from '@gravity-ui/uikit';
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';
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(): ReactNode {
const pages = [
{
title: 'Suites',
url: '/suites',
icon: ListCheck,
element: <SuitesPage/>,
children: [<Route key={'suite'} path=':suiteId' element= {<SuitesPage/>} />]
},
{title: 'Visual Checks', url: '/visual-checks', icon: Eye, element: <VisualChecksPage/>},
{title: 'Info', url: '/info', icon: CircleInfo, element: <InfoPage/>}
];

return <StrictMode>
<ThemeProvider theme='light'>
<Provider store={store}>
<HashRouter>
<MainLayout menuItems={pages}>
<Routes>
<Route element={<Navigate to={'/suites'}/>} path={'/'}/>
{pages.map(page => <Route element={page.element} path={page.url} key={page.url}>{page.children}</Route>)}
</Routes>
</MainLayout>
</HashRouter>
</Provider>
</ThemeProvider>
</StrictMode>;
}
22 changes: 22 additions & 0 deletions lib/static/new-ui/app/gui.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React, {ReactNode, useEffect} from 'react';
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') as HTMLDivElement;
const root = createRoot(rootEl);

function Gui(): ReactNode {
useEffect(() => {
store.dispatch(initGuiReport());

return () => {
store.dispatch(finGuiReport());
};
}, []);

return <App/>;
}

root.render(<Gui />);
22 changes: 22 additions & 0 deletions lib/static/new-ui/app/report.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import React, {ReactNode, 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 Report(): ReactNode {
useEffect(() => {
store.dispatch(initStaticReport());

return () => {
store.dispatch(finStaticReport());
};
}, []);

return <App/>;
}

root.render(<Report />);
10 changes: 10 additions & 0 deletions lib/static/new-ui/components/ImageWithMagnifier/index.module.css
Original file line number Diff line number Diff line change
@@ -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
}
122 changes: 122 additions & 0 deletions lib/static/new-ui/components/ImageWithMagnifier/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
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;
alt: string;
className?: string;
style?: React.CSSProperties;
magnifierHeight?: number;
magnifierWidth?: number;
zoomLevel?: number;
// Used to detect parent container scrolling and update the magnifier state
scrollContainerRef?: React.RefObject<HTMLElement>;
}

export function ImageWithMagnifier({
src,
alt,
className = '',
style,
magnifierHeight = 150,
magnifierWidth = 150,
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 mousePositionRef = useRef([0, 0]);
const [magnifierStyle, setMagnifierStyle] = useState({});
const imgRef = useRef<HTMLImageElement>(null);

const mouseEnter = (e: React.MouseEvent<HTMLImageElement>): void => {
const el = e.currentTarget;

const {width, height} = el.getBoundingClientRect();
setSize([width, height]);
setShowMagnifier(true);
mousePositionRef.current = [e.clientX, e.clientY];
};

const mouseLeave = (e: React.MouseEvent<HTMLImageElement>): void => {
e.preventDefault();
setShowMagnifier(false);
mousePositionRef.current = [e.clientX, e.clientY];
};

const mouseMove = (e: React.MouseEvent<HTMLImageElement>): void => {
const el = e.currentTarget;
const {top, left} = el.getBoundingClientRect();

const x = e.clientX - left - window.scrollX;
const y = e.clientY - top - window.scrollY;

setXY([x, y]);
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',
height: `${magnifierHeight}px`,
width: `${magnifierWidth}px`,
backgroundImage: `url('${src}')`,
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`
});
}, [showMagnifier, imgWidth, imgHeight, x, y]);

return <div>
<img
src={src}
className={classnames(className)}
style={style}
alt={alt}
onMouseEnter={(e): void => mouseEnter(e)}
onMouseLeave={(e): void => mouseLeave(e)}
onMouseMove={(e): void => mouseMove(e)}
ref={imgRef}
/>
{createPortal(<div
className={styles.magnifier}
style={magnifierStyle}
/>, document.body)}
</div>;
}
Loading

0 comments on commit fea8cf5

Please sign in to comment.