Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement basic suites page in new UI #590

Merged
merged 11 commits into from
Aug 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading