diff --git a/lib/static/modules/action-names.ts b/lib/static/modules/action-names.ts index 335bb524c..5d26e8284 100644 --- a/lib/static/modules/action-names.ts +++ b/lib/static/modules/action-names.ts @@ -69,5 +69,7 @@ export default { UPDATE_LOADING_PROGRESS: 'UPDATE_LOADING_PROGRESS', UPDATE_LOADING_VISIBILITY: 'UPDATE_LOADING_VISIBILITY', UPDATE_LOADING_TITLE: 'UPDATE_LOADING_TITLE', - UPDATE_LOADING_IS_IN_PROGRESS: 'UPDATE_LOADING_IS_IN_PROGRESS' + UPDATE_LOADING_IS_IN_PROGRESS: 'UPDATE_LOADING_IS_IN_PROGRESS', + SELECT_ALL: 'SELECT_ALL', + DESELECT_ALL: 'DESELECT_ALL' } as const; diff --git a/lib/static/modules/actions/index.js b/lib/static/modules/actions/index.js index d25cc7491..c663a3896 100644 --- a/lib/static/modules/actions/index.js +++ b/lib/static/modules/actions/index.js @@ -246,6 +246,8 @@ export const toggleLoading = (payload) => ({type: actionNames.TOGGLE_LOADING, pa export const closeSections = (payload) => ({type: actionNames.CLOSE_SECTIONS, payload}); export const runFailed = () => ({type: actionNames.RUN_FAILED_TESTS}); export const expandAll = () => ({type: actionNames.VIEW_EXPAND_ALL}); +export const selectAll = () => ({type: actionNames.SELECT_ALL}); +export const deselectAll = () => ({type: actionNames.DESELECT_ALL}); export const expandErrors = () => ({type: actionNames.VIEW_EXPAND_ERRORS}); export const expandRetries = () => ({type: actionNames.VIEW_EXPAND_RETRIES}); export const collapseAll = () => ({type: actionNames.VIEW_COLLAPSE_ALL}); diff --git a/lib/static/modules/reducers/tree/index.js b/lib/static/modules/reducers/tree/index.js index c8954e86a..93359f7e1 100644 --- a/lib/static/modules/reducers/tree/index.js +++ b/lib/static/modules/reducers/tree/index.js @@ -20,6 +20,7 @@ import {isCommitedStatus, isStagedStatus, isSuccessStatus} from '../../../../com import {applyStateUpdate, ensureDiffProperty, getUpdatedProperty} from '../../utils/state'; import {changeNodeState, getStaticAccepterStateNameImages, resolveUpdatedStatuses, updateImagesStatus} from './helpers'; import * as staticImageAccepter from '../../static-image-accepter'; +import {CHECKED, UNCHECKED} from '@/constants/checked-statuses'; export default ((state, action) => { const diff = {tree: {}}; @@ -147,6 +148,18 @@ export default ((state, action) => { return applyStateUpdate(state, diff); } + case actionNames.SELECT_ALL: { + changeAllNodesState(state.tree, {checkStatus: CHECKED}, diff.tree); + + return applyStateUpdate(state, diff); + } + + case actionNames.DESELECT_ALL: { + changeAllNodesState(state.tree, {checkStatus: UNCHECKED}, diff.tree); + + return applyStateUpdate(state, diff); + } + case actionNames.CLOSE_SECTIONS: { const closeImageIds = action.payload; diff --git a/lib/static/modules/selectors/tree.js b/lib/static/modules/selectors/tree.js index 5200124e2..9c06732fd 100644 --- a/lib/static/modules/selectors/tree.js +++ b/lib/static/modules/selectors/tree.js @@ -3,8 +3,16 @@ import {createSelector} from 'reselect'; 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'; +import {isNodeFailed, isNodeSuccessful, isAcceptable, iterateSuites, isScreenRevertable} from '../utils'; +import { + getAllRootSuiteIds, + getBrowsers, + getImages, getIsGui, + getIsStaticImageAccepterEnabled, + getResults, + getSuites +} from '@/static/new-ui/store/selectors'; +import {CHECKED} from '@/constants/checked-statuses'; const getSuitesStates = (state) => state.tree.suites.stateById; const getBrowserIds = (state) => state.tree.browsers.allIds; @@ -173,6 +181,43 @@ export const getFailedOpenedImageIds = createSelector( (activeImages) => activeImages.filter(isNodeFailed).map((image) => image.id) ); +export const getVisibleBrowserIds = createSelector( + getBrowserIds, getBrowsersStates, + (browserIds, browsersStates) => browserIds.filter((browserId) => browsersStates[browserId].shouldBeShown) +); + +export const getSelectedBrowserIds = createSelector( + getBrowserIds, getBrowsers, getBrowsersStates, + (browserIds, browsers, browsersStates) => { + return browserIds.filter((browserId) => browsersStates[browserId].checkStatus === CHECKED); + } +); + +const getAcceptableOrRevertableImages = (browserIds, browsersById, resultsById, imagesById, isStaticImageAccepterEnabled, gui) => { + const visibleResultIds = browserIds.map(browserId => last(browsersById[browserId].resultIds)); + + return visibleResultIds.flatMap(resultId => { + return resultsById[resultId].imageIds + .map(imageId => imagesById[imageId]) + .filter(image => isAcceptable(image) || isScreenRevertable({ + image, + isLastResult: true, + isStaticImageAccepterEnabled, + gui + })); + }); +}; + +export const getVisibleImages = createSelector( + getVisibleBrowserIds, getBrowsers, getResults, getImages, getIsStaticImageAccepterEnabled, getIsGui, + getAcceptableOrRevertableImages +); + +export const getSelectedImages = createSelector( + getSelectedBrowserIds, getBrowsers, getResults, getImages, getIsStaticImageAccepterEnabled, getIsGui, + getAcceptableOrRevertableImages +); + export const getVisibleRootSuiteIds = createSelector( getRootSuiteIds, getSuitesStates, (rootSuiteIds, suitesStates) => rootSuiteIds.filter((suiteId) => suitesStates[suiteId].shouldBeShown) diff --git a/lib/static/new-ui.css b/lib/static/new-ui.css index 7547b37c2..0fdde8e3a 100644 --- a/lib/static/new-ui.css +++ b/lib/static/new-ui.css @@ -72,3 +72,8 @@ body { /* Sets spinner color */ --g-color-line-brand: var(--g-color-text-hint); } + +.text-hint { + color: var(--g-color-private-black-400); + font-weight: 500; +} diff --git a/lib/static/new-ui/components/AssertViewStatus/index.tsx b/lib/static/new-ui/components/AssertViewStatus/index.tsx index 93411773e..34bde6f17 100644 --- a/lib/static/new-ui/components/AssertViewStatus/index.tsx +++ b/lib/static/new-ui/components/AssertViewStatus/index.tsx @@ -1,45 +1,12 @@ import React, {ReactNode} from 'react'; -import {ImageEntity, ImageEntityError} from '@/static/new-ui/types/store'; -import {TestStatus} from '@/constants'; -import {Icon} from '@gravity-ui/uikit'; -import { - ArrowRightArrowLeft, - CircleCheck, - FileCheck, - FileExclamation, - FileLetterX, - FilePlus, - SquareExclamation, - SquareXmark, - FileArrowUp -} from '@gravity-ui/icons'; -import {isInvalidRefImageError, isNoRefImageError} from '@/common-utils'; +import {ImageEntity} from '@/static/new-ui/types/store'; import styles from './index.module.css'; +import {getAssertViewStatusIcon, getAssertViewStatusMessage} from '@/static/new-ui/utils/assert-view-status'; interface AssertViewStatusProps { image: ImageEntity | null; } export function AssertViewStatus({image}: AssertViewStatusProps): ReactNode { - let status = <>Failed to compare; - - if (image === null) { - status = <>Image is absent; - } else if (image.status === TestStatus.SUCCESS) { - status = <>Images match; - } else if (image.status === TestStatus.STAGED) { - status = <>Image is staged; - } else if (image.status === TestStatus.COMMITED) { - status = <>Image was committed; - } else if (isNoRefImageError((image as ImageEntityError).error)) { - status = <>Reference not found; - } else if (isInvalidRefImageError((image as ImageEntityError).error)) { - status = <>Reference is broken; - } else if (image.status === TestStatus.FAIL) { - status = <>Difference detected; - } else if (image.status === TestStatus.UPDATED) { - status = <>Reference updated; - } - - return
{status}
; + return
{getAssertViewStatusIcon(image)}{getAssertViewStatusMessage(image)}
; } diff --git a/lib/static/new-ui/components/Card/TextHintCard.module.css b/lib/static/new-ui/components/Card/TextHintCard.module.css index e3cfdfc2f..6bba2718d 100644 --- a/lib/static/new-ui/components/Card/TextHintCard.module.css +++ b/lib/static/new-ui/components/Card/TextHintCard.module.css @@ -3,9 +3,5 @@ display: flex; align-items: center; justify-content: center; -} - -.hint { - color: var(--g-color-private-black-400); - font-weight: 500; + composes: text-hint from global; } diff --git a/lib/static/new-ui/components/Card/TextHintCard.tsx b/lib/static/new-ui/components/Card/TextHintCard.tsx index 80b67167f..6047b6bdb 100644 --- a/lib/static/new-ui/components/Card/TextHintCard.tsx +++ b/lib/static/new-ui/components/Card/TextHintCard.tsx @@ -5,11 +5,9 @@ import {Card, CardProps} from '.'; import styles from './TextHintCard.module.css'; interface TextHintCardProps extends CardProps { - hint: string; + children: ReactNode; } export function TextHintCard(props: TextHintCardProps): ReactNode { - return - {props.hint} - ; + return {props.children}; } diff --git a/lib/static/new-ui/components/IconButton/index.tsx b/lib/static/new-ui/components/IconButton/index.tsx new file mode 100644 index 000000000..723bc9574 --- /dev/null +++ b/lib/static/new-ui/components/IconButton/index.tsx @@ -0,0 +1,16 @@ +import {Button, ButtonView, Tooltip} from '@gravity-ui/uikit'; +import React, {ReactNode} from 'react'; + +interface IconButtonProps { + icon: ReactNode; + tooltip: string; + onClick?: () => void; + view?: ButtonView; + disabled?: boolean; +} + +export function IconButton(props: IconButtonProps): ReactNode { + return + + ; +} diff --git a/lib/static/new-ui/components/ImageWithMagnifier/index.tsx b/lib/static/new-ui/components/ImageWithMagnifier/index.tsx index 155cee4cd..c8fece914 100644 --- a/lib/static/new-ui/components/ImageWithMagnifier/index.tsx +++ b/lib/static/new-ui/components/ImageWithMagnifier/index.tsx @@ -1,13 +1,13 @@ -import classnames from 'classnames'; import React, {ReactNode, useEffect, useRef, useState} from 'react'; import {createPortal} from 'react-dom'; import styles from './index.module.css'; +import {Screenshot} from '@/static/new-ui/components/Screenshot'; +import {ImageFile} from '@/types'; const DEFAULT_ZOOM_LEVEL = 3; interface ImageWithMagnifierProps { - src: string; - alt: string; + image: ImageFile; className?: string; style?: React.CSSProperties; magnifierHeight?: number; @@ -18,9 +18,7 @@ interface ImageWithMagnifierProps { } export function ImageWithMagnifier({ - src, - alt, - className = '', + image, style, magnifierHeight = 150, magnifierWidth = 150, @@ -94,7 +92,7 @@ export function ImageWithMagnifier({ display: showMagnifier ? '' : 'none', height: `${magnifierHeight}px`, width: `${magnifierWidth}px`, - backgroundImage: `url('${src}')`, + backgroundImage: `url('${image.path}')`, top: `${mouseY - magnifierHeight / 2}px`, left: `${mouseX - magnifierWidth / 2}px`, backgroundSize: `${imgWidth * zoomLevel}px ${imgHeight * zoomLevel}px`, @@ -104,16 +102,13 @@ export function ImageWithMagnifier({ }, [showMagnifier, imgWidth, imgHeight, x, y]); return
- {alt} mouseEnter(e)} onMouseLeave={(e): void => mouseLeave(e)} - onMouseMove={(e): void => mouseMove(e)} - ref={imgRef} - /> + onMouseMove={(e): void => mouseMove(e)}/> {createPortal(
state.tree.browsers.byId); + const isReportEmpty = isInitialized && Object.keys(browsersById).length === 0; + const [visiblePanel, setVisiblePanel] = useState(null); const onFooterItemClick = (item: GravityMenuItem): void => { visiblePanel ? setVisiblePanel(null) : setVisiblePanel(item.id as PanelId); @@ -52,7 +60,25 @@ export function MainLayout(props: MainLayoutProps): JSX.Element { menuItems={gravityMenuItems} customBackground={
} customBackgroundClassName={styles.asideHeaderBgWrapper} - renderContent={(): React.ReactNode => props.children} + renderContent={(): React.ReactNode => { + if (isReportEmpty) { + return
+ + icon + This report is empty +
+ {[ + 'Check if your project contains any tests', + 'Check if the tool you are using is configured correctly and is able to find your tests', + 'Check logs to see if some critical error has occurred and prevented report from collecting any results' + ].map((hintText, index) =>
{hintText}
)} +
+
+
; + } + + return props.children; + }} hideCollapseButton={true} renderFooter={(): ReactNode =>