diff --git a/lib/static/components/state/index.jsx b/lib/static/components/state/index.jsx index 379f0519b..587c38571 100644 --- a/lib/static/components/state/index.jsx +++ b/lib/static/components/state/index.jsx @@ -80,7 +80,7 @@ class State extends Component { onScreenshotUndo = () => { if (this.props.isStaticImageAccepterEnabled) { - this.props.actions.staticAccepterUnstageScreenshot(this.props.imageId); + this.props.actions.staticAccepterUnstageScreenshot([this.props.imageId]); } else { this.props.actions.undoAcceptImage(this.props.imageId); } diff --git a/lib/static/icons/empty-report.svg b/lib/static/icons/empty-report.svg new file mode 100644 index 000000000..a7ee97e5a --- /dev/null +++ b/lib/static/icons/empty-report.svg @@ -0,0 +1 @@ + diff --git a/lib/static/modules/action-names.ts b/lib/static/modules/action-names.ts index 6b88ae5f2..7cd1e4073 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/actions/static-accepter.ts b/lib/static/modules/actions/static-accepter.ts index 376a69a6d..1580edb71 100644 --- a/lib/static/modules/actions/static-accepter.ts +++ b/lib/static/modules/actions/static-accepter.ts @@ -26,9 +26,9 @@ export const staticAccepterStageScreenshot = (imageIds: string[]): StaticAccepte return {type: actionNames.STATIC_ACCEPTER_STAGE_SCREENSHOT, payload: imageIds}; }; -type StaticAccepterUnstageScreenshotAction = Action; -export const staticAccepterUnstageScreenshot = (imageId: string): StaticAccepterUnstageScreenshotAction => { - return {type: actionNames.STATIC_ACCEPTER_UNSTAGE_SCREENSHOT, payload: {imageId}}; +type StaticAccepterUnstageScreenshotAction = Action; +export const staticAccepterUnstageScreenshot = (imageIds: string[]): StaticAccepterUnstageScreenshotAction => { + return {type: actionNames.STATIC_ACCEPTER_UNSTAGE_SCREENSHOT, payload: imageIds}; }; type StaticAccepterOpenConfirmAction = Action { } case actionNames.STATIC_ACCEPTER_UNSTAGE_SCREENSHOT: { - const {imageId} = action.payload; - const acceptableImagesDiff = {}; const diff = set({}, ['staticImageAccepter', 'acceptableImages'], acceptableImagesDiff); const imagesToCommitCount = get(state, ['staticImageAccepter', 'imagesToCommitCount']); - set(acceptableImagesDiff, [imageId, 'commitStatus'], null); - set(diff, ['staticImageAccepter', 'imagesToCommitCount'], imagesToCommitCount - 1); + set(diff, ['staticImageAccepter', 'imagesToCommitCount'], imagesToCommitCount - action.payload.length); + for (const imageId of action.payload) { + set(acceptableImagesDiff, [imageId, 'commitStatus'], null); + } return applyStateUpdate(state, diff); } diff --git a/lib/static/modules/reducers/tree/index.js b/lib/static/modules/reducers/tree/index.js index c8954e86a..27eb67be4 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; @@ -242,26 +255,37 @@ export default ((state, action) => { case actionNames.STATIC_ACCEPTER_UNSTAGE_SCREENSHOT: { const {tree, view} = state; - const {imageId} = action.payload; - const originalStatus = get(state, ['staticImageAccepter', 'acceptableImages', imageId, 'originalStatus']); + const imageIdsToUnstage = action.payload; + + const failedBrowserIds = []; + const failedSuiteIds = []; + const imageIds = []; - const failedResultId = tree.images.byId[imageId].parentId; - const failedBrowserId = tree.results.byId[failedResultId].parentId; - const failedSuiteId = tree.browsers.byId[failedBrowserId].parentId; + for (const imageId of imageIdsToUnstage) { + const originalStatus = get(state, ['staticImageAccepter', 'acceptableImages', imageId, 'originalStatus']); - ensureDiffProperty(diff, ['tree', 'results', 'byId']); + const failedResultId = tree.images.byId[imageId].parentId; + const failedBrowserId = tree.results.byId[failedResultId].parentId; + const failedSuiteId = tree.browsers.byId[failedBrowserId].parentId; - changeImageStatus(tree, imageId, originalStatus, diff.tree); - changeNodeState(tree.results.byId, failedResultId, {status: FAIL}, diff.tree.results.byId); + ensureDiffProperty(diff, ['tree', 'results', 'byId']); + + changeImageStatus(tree, imageId, originalStatus, diff.tree); + changeNodeState(tree.results.byId, failedResultId, {status: FAIL}, diff.tree.results.byId); + + failedBrowserIds.push(failedBrowserId); + failedSuiteIds.push(failedSuiteId); + imageIds.push(imageId); + } - failSuites(tree, failedSuiteId, diff.tree); + failSuites(tree, failedSuiteIds, diff.tree); - calcSuitesOpenness({tree, expand: view.expand, suiteIds: [failedSuiteId], diff: diff.tree}); - calcBrowsersOpenness({tree, expand: view.expand, browserIds: [failedBrowserId], diff: diff.tree}); - calcImagesOpenness({tree, expand: view.expand, imageIds: [imageId], diff: diff.tree}); + calcSuitesOpenness({tree, expand: view.expand, suiteIds: failedSuiteIds, diff: diff.tree}); + calcBrowsersOpenness({tree, expand: view.expand, browserIds: failedBrowserIds, diff: diff.tree}); + calcImagesOpenness({tree, expand: view.expand, imageIds, diff: diff.tree}); - calcBrowsersShowness({tree, view, browserIds: [failedBrowserId], diff: diff.tree}); - calcSuitesShowness({tree, suiteIds: [failedSuiteId], diff: diff.tree}); + calcBrowsersShowness({tree, view, browserIds: failedBrowserIds, diff: diff.tree}); + calcSuitesShowness({tree, suiteIds: failedSuiteIds, diff: diff.tree}); return applyStateUpdate(state, diff); } 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/AssertViewResult/index.tsx b/lib/static/new-ui/components/AssertViewResult/index.tsx index e015b1dff..b4c90ffdf 100644 --- a/lib/static/new-ui/components/AssertViewResult/index.tsx +++ b/lib/static/new-ui/components/AssertViewResult/index.tsx @@ -1,7 +1,7 @@ import React, {ReactNode} from 'react'; import {connect} from 'react-redux'; -import {ImageEntity, State} from '@/static/new-ui/types/store'; +import {ImageEntity} from '@/static/new-ui/types/store'; import {DiffModeId, TestStatus} from '@/constants'; import {DiffViewer} from '../DiffViewer'; import {Screenshot} from '@/static/new-ui/components/Screenshot'; @@ -43,6 +43,6 @@ function AssertViewResultInternal({result, diffMode, style}: AssertViewResultPro return null; } -export const AssertViewResult = connect((state: State) => ({ +export const AssertViewResult = connect(state => ({ diffMode: state.view.diffMode }))(AssertViewResultInternal); 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/AttemptPicker/index.tsx b/lib/static/new-ui/components/AttemptPicker/index.tsx index 9cbcdd155..51e0cf84b 100644 --- a/lib/static/new-ui/components/AttemptPicker/index.tsx +++ b/lib/static/new-ui/components/AttemptPicker/index.tsx @@ -2,7 +2,6 @@ import React, {ReactNode} from 'react'; import {connect, useDispatch, useSelector} from 'react-redux'; import {ArrowRotateRight} from '@gravity-ui/icons'; -import {State} from '@/static/new-ui/types/store'; import {AttemptPickerItem} from '@/static/new-ui/components/AttemptPickerItem'; import styles from './index.module.css'; import classNames from 'classnames'; @@ -26,9 +25,9 @@ function AttemptPickerInternal(props: AttemptPickerInternalProps): ReactNode { const dispatch = useDispatch(); const currentBrowser = useSelector(getCurrentBrowser); - const isRunTestsAvailable = useSelector((state: State) => state.app.availableFeatures) + const isRunTestsAvailable = useSelector(state => state.app.availableFeatures) .find(feature => feature.name === RunTestsFeature.name); - const isRunning = useSelector((state: State) => state.running); + const isRunning = useSelector(state => state.running); const onAttemptPickHandler = (resultId: string, attemptIndex: number): void => { if (!props.browserId || currentResultId === resultId) { @@ -64,7 +63,7 @@ function AttemptPickerInternal(props: AttemptPickerInternalProps): ReactNode { ; } -export const AttemptPicker = connect((state: State) => { +export const AttemptPicker = connect(state => { let resultIds: string[] = []; let currentResultId = ''; const browserId = state.app.suitesPage.currentBrowserId; diff --git a/lib/static/new-ui/components/Card/AnimatedAppearCard.tsx b/lib/static/new-ui/components/Card/AnimatedAppearCard.tsx index 0ce42be6f..c81b97aab 100644 --- a/lib/static/new-ui/components/Card/AnimatedAppearCard.tsx +++ b/lib/static/new-ui/components/Card/AnimatedAppearCard.tsx @@ -4,10 +4,9 @@ import cardStyles from './index.module.css'; import styles from './AnimatedAppearCard.module.css'; import classNames from 'classnames'; import {useSelector} from 'react-redux'; -import {State} from '@/static/new-ui/types/store'; export function AnimatedAppearCard(): ReactNode { - const isInitialized = useSelector((state: State) => state.app.isInitialized); + const isInitialized = useSelector(state => state.app.isInitialized); return
diff --git a/lib/static/new-ui/components/Card/EmptyReportCard.module.css b/lib/static/new-ui/components/Card/EmptyReportCard.module.css new file mode 100644 index 000000000..c71431920 --- /dev/null +++ b/lib/static/new-ui/components/Card/EmptyReportCard.module.css @@ -0,0 +1,39 @@ +.container { + padding: 10px; + height: 100%; +} + +.hint-card { + height: 100%; + display: flex; + flex-direction: column; +} + +.empty-report-icon { + width: 40px; +} + +.card-title { + color: #000; + margin-top: 16px; +} + +.hints-container { + max-width: 450px; + margin-top: 8px; +} + +.hint { + margin-top: 12px; + display: flex; +} + +.hint-item-icon { + flex-shrink: 0; + margin-right: 8px; + color: var(--g-color-base-brand); +} + +.hint-item-text { + line-height: 1.4; +} diff --git a/lib/static/new-ui/components/Card/EmptyReportCard.tsx b/lib/static/new-ui/components/Card/EmptyReportCard.tsx new file mode 100644 index 000000000..4c038efcf --- /dev/null +++ b/lib/static/new-ui/components/Card/EmptyReportCard.tsx @@ -0,0 +1,27 @@ +import {TextHintCard} from '@/static/new-ui/components/Card/TextHintCard'; +import EmptyReport from '../../../icons/empty-report.svg'; +import classNames from 'classnames'; +import {Icon} from '@gravity-ui/uikit'; +import {Check} from '@gravity-ui/icons'; +import React, {ReactNode} from 'react'; + +import styles from './EmptyReportCard.module.css'; + +export function EmptyReportCard(): ReactNode { + 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}
+
)} +
+
+
; +} 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/CompactAttemptPicker/index.tsx b/lib/static/new-ui/components/CompactAttemptPicker/index.tsx index c1c49cf31..86eb04cba 100644 --- a/lib/static/new-ui/components/CompactAttemptPicker/index.tsx +++ b/lib/static/new-ui/components/CompactAttemptPicker/index.tsx @@ -4,7 +4,6 @@ import React, {ReactNode} from 'react'; import {useDispatch, useSelector} from 'react-redux'; import styles from './index.module.css'; -import {State} from '@/static/new-ui/types/store'; import {getCurrentNamedImage} from '@/static/new-ui/features/visual-checks/selectors'; import {getIconByStatus} from '@/static/new-ui/utils'; import {changeTestRetry} from '@/static/modules/actions'; @@ -13,11 +12,11 @@ export function CompactAttemptPicker(): ReactNode { const dispatch = useDispatch(); const currentImage = useSelector(getCurrentNamedImage); const currentBrowserId = currentImage?.browserId; - const currentBrowser = useSelector((state: State) => currentBrowserId && state.tree.browsers.byId[currentBrowserId]); - const resultsById = useSelector((state: State) => state.tree.results.byId); + const currentBrowser = useSelector(state => currentBrowserId && state.tree.browsers.byId[currentBrowserId]); + const resultsById = useSelector(state => state.tree.results.byId); const totalAttemptsCount = currentBrowser ? currentBrowser.resultIds.length : null; - const currentAttemptIndex = useSelector((state: State) => currentBrowser ? state.tree.browsers.stateById[currentBrowser.id].retryIndex : null); + const currentAttemptIndex = useSelector(state => currentBrowser ? state.tree.browsers.stateById[currentBrowser.id].retryIndex : null); const onUpdate = ([value]: string[]): void => { if (currentBrowserId) { diff --git a/lib/static/new-ui/components/GuiniToolbarOverlay/index.tsx b/lib/static/new-ui/components/GuiniToolbarOverlay/index.tsx index 481dc60c1..5f1473329 100644 --- a/lib/static/new-ui/components/GuiniToolbarOverlay/index.tsx +++ b/lib/static/new-ui/components/GuiniToolbarOverlay/index.tsx @@ -6,7 +6,6 @@ import {CloudArrowUpIn, TriangleExclamation} from '@gravity-ui/icons'; import styles from './index.module.css'; import classNames from 'classnames'; import {useDispatch, useSelector} from 'react-redux'; -import {State} from '@/static/new-ui/types/store'; import { CommitResult, staticAccepterCommitScreenshot, @@ -23,22 +22,22 @@ export function GuiniToolbarOverlay(): ReactNode { const dispatch = useDispatch(); const toaster = useToaster(); - const isInProgress = useSelector((state: State) => state.processing); - const allImagesById = useSelector((state: State) => state.tree.images.byId); - const acceptableImages = useSelector((state: State) => state.staticImageAccepter.acceptableImages); - const delayedImages = useSelector((state: State) => state.staticImageAccepter.accepterDelayedImages); + const isInProgress = useSelector(state => state.processing); + const allImagesById = useSelector(state => state.tree.images.byId); + const acceptableImages = useSelector(state => state.staticImageAccepter.acceptableImages); + const delayedImages = useSelector(state => state.staticImageAccepter.accepterDelayedImages); const stagedImages = Object.values(acceptableImages) .filter(image => image.commitStatus === TestStatus.STAGED); - const staticAccepterConfig = useSelector((state: State) => state.config.staticImageAccepter); - const pullRequestUrl = useSelector((state: State) => state.config.staticImageAccepter.pullRequestUrl); - const offset = useSelector((state: State) => state.ui.staticImageAccepterToolbar.offset); + const staticAccepterConfig = useSelector(state => state.config.staticImageAccepter); + const pullRequestUrl = useSelector(state => state.config.staticImageAccepter.pullRequestUrl); + const offset = useSelector(state => state.ui.staticImageAccepterToolbar.offset); const location = useLocation(); const [isVisible, setIsVisible] = useState(null); const [isModalVisible, setIsModalVisible] = useState(false); - const commitMessage = useSelector((state: State) => state.app.staticImageAccepterModal.commitMessage); + const commitMessage = useSelector(state => state.app.staticImageAccepterModal.commitMessage); useEffect(() => { const newIsVisible = stagedImages.length > 0 && @@ -59,9 +58,7 @@ export function GuiniToolbarOverlay(): ReactNode { }; const onCancelClick = (): void => { - for (const image of stagedImages) { - dispatch(staticAccepterUnstageScreenshot(image.id)); - } + dispatch(staticAccepterUnstageScreenshot(stagedImages.map(image => image.id))); }; const onModalCancelClick = (): void => { 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..7063db91f --- /dev/null +++ b/lib/static/new-ui/components/IconButton/index.tsx @@ -0,0 +1,17 @@ +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; + className?: string; +} + +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..25f17df17 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.app.loading.isVisible); - const isInProgress = useSelector((state: State) => state.app.loading.isInProgress); + const isVisible = useSelector(state => state.app.loading.isVisible); + const isInProgress = useSelector(state => state.app.loading.isInProgress); const isVisibleRef = useRef(isVisible); const progress = useSelector(getTotalLoadingProgress); - const taskTitle = useSelector((state: State) => state.app.loading.taskTitle); + const taskTitle = useSelector(state => state.app.loading.taskTitle); const [hidden, setHidden] = React.useState(true); diff --git a/lib/static/new-ui/components/MainLayout/index.tsx b/lib/static/new-ui/components/MainLayout/index.tsx index 3a04404c6..014b7323e 100644 --- a/lib/static/new-ui/components/MainLayout/index.tsx +++ b/lib/static/new-ui/components/MainLayout/index.tsx @@ -9,6 +9,7 @@ import {SettingsPanel} from '@/static/new-ui/components/SettingsPanel'; import TestplaneIcon from '../../../icons/testplane-mono.svg'; import styles from './index.module.css'; import {Footer} from './Footer'; +import {EmptyReportCard} from '@/static/new-ui/components/Card/EmptyReportCard'; export enum PanelId { Settings = 'settings', @@ -25,7 +26,7 @@ export interface MainLayoutProps { menuItems: MenuItem[]; } -export function MainLayout(props: MainLayoutProps): JSX.Element { +export function MainLayout(props: MainLayoutProps): ReactNode { const navigate = useNavigate(); const location = useLocation(); @@ -39,6 +40,9 @@ export function MainLayout(props: MainLayoutProps): JSX.Element { const isInitialized = useSelector(getIsInitialized); + const browsersById = useSelector(state => 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 +56,13 @@ export function MainLayout(props: MainLayoutProps): JSX.Element { menuItems={gravityMenuItems} customBackground={
} customBackgroundClassName={styles.asideHeaderBgWrapper} - renderContent={(): React.ReactNode => props.children} + renderContent={(): React.ReactNode => { + if (isReportEmpty) { + return ; + } + + return props.children; + }} hideCollapseButton={true} renderFooter={(): ReactNode =>