diff --git a/lib/db-utils/client.js b/lib/db-utils/client.js index 470580c57..a577bf719 100644 --- a/lib/db-utils/client.js +++ b/lib/db-utils/client.js @@ -8,7 +8,7 @@ import {fetchFile, normalizeUrls} from '../common-utils'; import {DB_SUITES_TABLE_NAME, LOCAL_DATABASE_NAME} from '../constants/database'; -export function fetchDataFromDatabases(dbJsonUrls) { +export function fetchDataFromDatabases(dbJsonUrls, onDownloadProgress) { const loadDbJsonUrl = (dbJsonUrl) => fetchFile(dbJsonUrl); const prepareUrls = (urls, baseUrl) => normalizeUrls(urls, baseUrl); @@ -17,7 +17,15 @@ export function fetchDataFromDatabases(dbJsonUrls) { }; const loadDbUrl = async (dbUrl) => { - const {data, status} = await fetchFile(dbUrl, {responseType: 'arraybuffer'}); + const loadOptions = {responseType: 'arraybuffer'}; + + if (onDownloadProgress) { + loadOptions.onDownloadProgress = onDownloadProgress && ((e) => { + onDownloadProgress(dbUrl, e.loaded / e.total); + }); + } + + const {data, status} = await fetchFile(dbUrl, loadOptions); return {url: dbUrl, status, data}; }; diff --git a/lib/static/modules/action-names.ts b/lib/static/modules/action-names.ts index 5361f5be2..cb00e3f14 100644 --- a/lib/static/modules/action-names.ts +++ b/lib/static/modules/action-names.ts @@ -63,5 +63,6 @@ export default { SUITES_PAGE_SET_CURRENT_SUITE: 'SUITES_PAGE_SET_CURRENT_SUITE', SUITES_PAGE_SET_SECTION_EXPANDED: 'SUITES_PAGE_SET_SECTION_EXPANDED', SUITES_PAGE_SET_STEPS_EXPANDED: 'SUITES_PAGE_SET_STEPS_EXPANDED', - VISUAL_CHECKS_PAGE_SET_CURRENT_NAMED_IMAGE: 'VISUAL_CHECKS_PAGE_SET_CURRENT_NAMED_IMAGE' + VISUAL_CHECKS_PAGE_SET_CURRENT_NAMED_IMAGE: 'VISUAL_CHECKS_PAGE_SET_CURRENT_NAMED_IMAGE', + UPDATE_LOADING_PROGRESS: 'UPDATE_LOADING_PROGRESS' } as const; diff --git a/lib/static/modules/actions/index.js b/lib/static/modules/actions/index.js index 59fd6a466..d25cc7491 100644 --- a/lib/static/modules/actions/index.js +++ b/lib/static/modules/actions/index.js @@ -76,7 +76,12 @@ export const initStaticReport = () => { try { const mainDatabaseUrls = new URL('databaseUrls.json', window.location.href); - const fetchDbResponses = await fetchDataFromDatabases([mainDatabaseUrls.href]); + const fetchDbResponses = await fetchDataFromDatabases([mainDatabaseUrls.href], (dbUrl, progress) => { + dispatch({ + type: actionNames.UPDATE_LOADING_PROGRESS, + payload: {[dbUrl]: progress} + }); + }); performance?.mark?.(performanceMarks.DBS_LOADED); diff --git a/lib/static/modules/default-state.ts b/lib/static/modules/default-state.ts index 861b8b847..86fcd502f 100644 --- a/lib/static/modules/default-state.ts +++ b/lib/static/modules/default-state.ts @@ -100,6 +100,9 @@ export default Object.assign({config: configDefaults}, { }, visualChecksPage: { currentNamedImageId: null + }, + loading: { + progress: {} } }, ui: { diff --git a/lib/static/modules/reducers/loading.js b/lib/static/modules/reducers/loading.js index 5f9143c57..aefc5b5b6 100644 --- a/lib/static/modules/reducers/loading.js +++ b/lib/static/modules/reducers/loading.js @@ -1,4 +1,5 @@ import actionNames from '../action-names'; +import {applyStateUpdate} from '@/static/modules/utils/state'; export default (state, action) => { switch (action.type) { @@ -6,6 +7,14 @@ export default (state, action) => { return {...state, loading: action.payload}; } + case actionNames.UPDATE_LOADING_PROGRESS: { + return applyStateUpdate(state, { + app: { + loading: {progress: action.payload} + } + }); + } + default: return state; } diff --git a/lib/static/new-ui.css b/lib/static/new-ui.css index 343e314b7..4f5b0b1be 100644 --- a/lib/static/new-ui.css +++ b/lib/static/new-ui.css @@ -39,6 +39,30 @@ body { display: none; } +.gn-aside-header__aside { + transform: translateX(-60px); + + animation: aside-header-appear 1s; + animation-delay: 2s; + animation-iteration-count: 1; + animation-fill-mode: forwards; +} + +.aside-header--initialized .gn-aside-header__aside { + transform: none; + animation: none; +} + +@keyframes aside-header-appear { + 0% { + transform: translateX(-60px); + } + + 100% { + transform: translateX(0); + } +} + .action-button { font-size: 15px; font-weight: 450; diff --git a/lib/static/new-ui/app/App.tsx b/lib/static/new-ui/app/App.tsx index e5db30ba8..bd7a6832d 100644 --- a/lib/static/new-ui/app/App.tsx +++ b/lib/static/new-ui/app/App.tsx @@ -12,6 +12,7 @@ import '@gravity-ui/uikit/styles/styles.css'; import '../../new-ui.css'; import {Provider} from 'react-redux'; import store from '../../modules/store'; +import {LoadingBar} from '@/static/new-ui/components/LoadingBar'; export function App(): ReactNode { const pages = [ @@ -31,6 +32,7 @@ export function App(): ReactNode { + } path={'/'}/> {pages.map(page => {page.children})} diff --git a/lib/static/new-ui/app/selectors.ts b/lib/static/new-ui/app/selectors.ts new file mode 100644 index 000000000..e7a5ccbfc --- /dev/null +++ b/lib/static/new-ui/app/selectors.ts @@ -0,0 +1,15 @@ +import {State} from '@/static/new-ui/types/store'; + +export const getTotalLoadingProgress = (state: State): number => { + const progressValues = Object.values(state.app.loading.progress); + + if (progressValues.length === 0) { + return 1; + } + + const totalProgress = progressValues.reduce((acc, currentProgress) => { + return acc + currentProgress; + }, 0); + + return totalProgress / progressValues.length; +}; diff --git a/lib/static/new-ui/components/Card/AnimatedAppearCard.module.css b/lib/static/new-ui/components/Card/AnimatedAppearCard.module.css new file mode 100644 index 000000000..fae2c4f35 --- /dev/null +++ b/lib/static/new-ui/components/Card/AnimatedAppearCard.module.css @@ -0,0 +1,67 @@ +@property --gradient-angle { + syntax: ""; + inherits: false; + initial-value: 0turn; +} + +@property --from-color { + syntax: ""; + inherits: false; + initial-value: #eee; +} + +@property --to-color { + syntax: ""; + inherits: false; + initial-value: #eee; +} + +.animated-appear-card { + background-color: #eee; + position: absolute; + width: 100%; + height: 100%; + z-index: 10; + box-shadow: 0 0 0 2px #eee; + animation: border-pulse 5s ease forwards; + background-image: conic-gradient(from var(--gradient-angle) at -10% 100%, var(--from-color) 0%, var(--to-color) 100%); + padding: 1px; +} + +.background-overlay { + background-color: #eee; + width: 100%; + height: 100%; + border-radius: 10px; +} + +@keyframes border-pulse { + 0% { + --from-color: #eee; + --to-color: #eee; + --gradient-angle: 0turn; + visibility: visible; + } + + 25% { + --from-color: #00FFFF00; + --to-color: #7d7d7d85; + --gradient-angle: 0turn; + } + + 50% { + opacity: 1; + } + + 100% { + --from-color: #00FFFF00; + --to-color: #7d7d7d85; + --gradient-angle: 1turn; + opacity: 0; + visibility: hidden; + } +} + +.hidden { + visibility: hidden !important; +} diff --git a/lib/static/new-ui/components/Card/AnimatedAppearCard.tsx b/lib/static/new-ui/components/Card/AnimatedAppearCard.tsx new file mode 100644 index 000000000..0ce42be6f --- /dev/null +++ b/lib/static/new-ui/components/Card/AnimatedAppearCard.tsx @@ -0,0 +1,15 @@ +import React, {ReactNode} from 'react'; + +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); + + return
+
+
; +} diff --git a/lib/static/new-ui/components/Card/index.module.css b/lib/static/new-ui/components/Card/index.module.css index 29f960a1e..433fd88a5 100644 --- a/lib/static/new-ui/components/Card/index.module.css +++ b/lib/static/new-ui/components/Card/index.module.css @@ -7,4 +7,5 @@ overflow: hidden; box-shadow: rgb(255, 255, 255) 0 0 0 0, rgba(9, 9, 11, 0.05) 0 0 0 1px, rgba(0, 0, 0, 0.05) 0 1px 2px 0; border-radius: 10px; + height: 100%; } diff --git a/lib/static/new-ui/components/LoadingBar/index.module.css b/lib/static/new-ui/components/LoadingBar/index.module.css new file mode 100644 index 000000000..638523752 --- /dev/null +++ b/lib/static/new-ui/components/LoadingBar/index.module.css @@ -0,0 +1,92 @@ +.container { + height: 30px; + width: 100%; + position: absolute; + z-index: 99; + display: flex; + align-items: center; + flex-direction: column; + transition: height .2s ease, opacity .1s ease; + box-shadow: rgba(0, 0, 0, 0.11) 1px 1px 8px 0px; + background-color: var(--g-color-base-brand-hover); +} + +.hidden { + height: 0; +} + +.hidden .message { + opacity: 0; +} + +.container::before { + content: ''; + width: 100%; + height: 100%; + position: absolute; + background-color: var(--g-color-base-brand); + top: 0; + left: -100%; +} + +.message-container { + position: relative; + color: white; + font-weight: 450; + flex-grow: 1; + display: flex; + align-items: center; + z-index: 10; +} + +.message { + display: flex; + align-items: baseline; + gap: 1px; +} + +.loader { + width: 12px; + aspect-ratio: 2; + --_g: no-repeat radial-gradient(circle closest-side,#fff 90%,#fff0); + background: + var(--_g) 0% 50%, + var(--_g) 50% 50%, + var(--_g) 100% 50%; + background-size: calc(100%/3) 50%; + animation: l3 1s infinite linear; +} + +@keyframes l3 { + 20%{background-position:0% 0%, 50% 50%,100% 50%} + 40%{background-position:0% 100%, 50% 0%,100% 50%} + 60%{background-position:0% 50%, 50% 100%,100% 0%} + 80%{background-position:0% 50%, 50% 50%,100% 100%} +} +.progress-container { + height: 100%; + background-color: var(--g-color-base-brand); + position: absolute; + left: 0; + transition: width 0.2s ease; +} + +.progress-pulse { + position: absolute; + right: 0; + height: 100%; + background-color: #5500ff; + animation: progress-pulse 2s ease infinite; +} + +@keyframes progress-pulse { + 0% { + width: 0; + opacity: 1; + } + + 100% { + width: 30px; + opacity: 0; + } +} diff --git a/lib/static/new-ui/components/LoadingBar/index.tsx b/lib/static/new-ui/components/LoadingBar/index.tsx new file mode 100644 index 000000000..fa8faecb8 --- /dev/null +++ b/lib/static/new-ui/components/LoadingBar/index.tsx @@ -0,0 +1,40 @@ +import React, {ReactNode, useEffect, useRef} from 'react'; + +import styles from './index.module.css'; +import {useSelector} from 'react-redux'; +import {getTotalLoadingProgress} from '@/static/new-ui/app/selectors'; +import {State} from '@/static/new-ui/types/store'; +import classNames from 'classnames'; + +export function LoadingBar(): ReactNode { + const isLoaded = useSelector((state: State) => state.app.isInitialized); + const isLoadedRef = useRef(isLoaded); + const progress = useSelector(getTotalLoadingProgress); + + const [hidden, setHidden] = React.useState(true); + + // Delay is needed for smoother experience: when loading is fast, it prevents notification bar from appearing and + // hiding immediately. When loading a lot of data, it helps avoid freezes when everything is loaded. + useEffect(() => { + isLoadedRef.current = isLoaded; + const timeoutId = setTimeout(() => { + if (isLoaded === isLoadedRef.current) { + setHidden(isLoaded); + } + }, 500); + + return () => clearTimeout(timeoutId); + }, [isLoaded]); + + return
+
+
+ Loading Testplane UI +
+
+
+
+
+
+
; +} diff --git a/lib/static/new-ui/components/MainLayout/index.tsx b/lib/static/new-ui/components/MainLayout/index.tsx index 3bbb6a60f..d4e803229 100644 --- a/lib/static/new-ui/components/MainLayout/index.tsx +++ b/lib/static/new-ui/components/MainLayout/index.tsx @@ -1,8 +1,11 @@ import {AsideHeader, MenuItem as GravityMenuItem} from '@gravity-ui/navigation'; +import classNames from 'classnames'; import React from 'react'; +import {useSelector} from 'react-redux'; import {useNavigate, matchPath, useLocation} from 'react-router-dom'; import TestplaneIcon from '../../../icons/testplane-mono.svg'; import styles from './index.module.css'; +import {getIsInitialized} from '@/static/new-ui/store/selectors'; interface MenuItem { title: string; @@ -27,8 +30,17 @@ export function MainLayout(props: MainLayoutProps): JSX.Element { onItemClick: () => navigate(item.url) })); - return navigate('/')}} compact={true} - headerDecoration={false} menuItems={gravityMenuItems} customBackground={
} customBackgroundClassName={styles.asideHeaderBgWrapper} - renderContent={(): React.ReactNode => props.children} hideCollapseButton={true} + const isInitialized = useSelector(getIsInitialized); + + return navigate('/suites')}} + compact={true} + headerDecoration={false} + menuItems={gravityMenuItems} + customBackground={
} + customBackgroundClassName={styles.asideHeaderBgWrapper} + renderContent={(): React.ReactNode => props.children} + hideCollapseButton={true} />; } diff --git a/lib/static/new-ui/components/SplitViewLayout.module.css b/lib/static/new-ui/components/SplitViewLayout.module.css index fb05f806b..9bb37a26a 100644 --- a/lib/static/new-ui/components/SplitViewLayout.module.css +++ b/lib/static/new-ui/components/SplitViewLayout.module.css @@ -1,7 +1,13 @@ +div:has(> .split) { + height: 100vh; + display: flex; + flex-direction: column; +} + .split { + flex-grow: 1; display: flex; flex-direction: row; - height: 100vh; padding: 0 10px; } @@ -17,6 +23,16 @@ display: flex; align-items: center; justify-content: center; + animation: 4s ease forwards gutter-appear; +} + +:global(.is-initialized) .gutter { + animation: none; +} + +@keyframes gutter-appear { + 0% {opacity: 0} + 100% {opacity: 1} } .gutter-handle { diff --git a/lib/static/new-ui/components/SplitViewLayout.tsx b/lib/static/new-ui/components/SplitViewLayout.tsx index 71faf8bb4..e79426580 100644 --- a/lib/static/new-ui/components/SplitViewLayout.tsx +++ b/lib/static/new-ui/components/SplitViewLayout.tsx @@ -1,8 +1,12 @@ import classNames from 'classnames'; import React, {ReactNode} from 'react'; +import {useSelector} from 'react-redux'; import Split from 'react-split'; -import styles from './SplitViewLayout.module.css'; + import {KeepDraggingToHideCard} from '@/static/new-ui/components/Card/KeepDraggingToHideCard'; +import {AnimatedAppearCard} from '@/static/new-ui/components/Card/AnimatedAppearCard'; +import {getIsInitialized} from '@/static/new-ui/store/selectors'; +import styles from './SplitViewLayout.module.css'; interface SplitViewLayoutProps { sections: React.ReactNode[]; @@ -38,9 +42,11 @@ export function SplitViewLayout(props: SplitViewLayoutProps): ReactNode { return gutter; }; + const isInitialized = useSelector(getIsInitialized); + return + {section}
)} ; diff --git a/lib/static/new-ui/features/suites/components/BrowsersSelect/index.tsx b/lib/static/new-ui/features/suites/components/BrowsersSelect/index.tsx index 3598e427d..a2e177d2c 100644 --- a/lib/static/new-ui/features/suites/components/BrowsersSelect/index.tsx +++ b/lib/static/new-ui/features/suites/components/BrowsersSelect/index.tsx @@ -1,11 +1,12 @@ import {PlanetEarth} from '@gravity-ui/icons'; import {Button, Flex, Icon, Select, SelectRenderControlProps, SelectRenderOption} from '@gravity-ui/uikit'; import React, {useState, useEffect, ReactNode} from 'react'; -import {connect} from 'react-redux'; +import {connect, useSelector} from 'react-redux'; import {bindActionCreators} from 'redux'; import * as actions from '@/static/modules/actions'; import {BrowserIcon} from '@/static/new-ui/features/suites/components/BrowsersSelect/BrowserIcon'; +import {getIsInitialized} from '@/static/new-ui/store/selectors'; import {State} from '@/static/new-ui/types/store'; import {BrowserItem} from '@/types'; import styles from './index.module.css'; @@ -108,8 +109,10 @@ function BrowsersSelectInternal({browsers, filteredBrowsers, actions}: BrowsersS } }; + const isInitialized = useSelector(getIsInitialized); + const renderControl = ({onClick, onKeyDown, ref}: SelectRenderControlProps): React.JSX.Element => { - return ; }; diff --git a/lib/static/new-ui/features/suites/components/SuitesPage/TestInfoSkeleton.tsx b/lib/static/new-ui/features/suites/components/SuitesPage/TestInfoSkeleton.tsx new file mode 100644 index 000000000..6f52a63a0 --- /dev/null +++ b/lib/static/new-ui/features/suites/components/SuitesPage/TestInfoSkeleton.tsx @@ -0,0 +1,45 @@ +import {Flex, Skeleton} from '@gravity-ui/uikit'; +import random from 'lodash/random'; +import React, {ReactNode, useEffect, useState} from 'react'; + +export function TestInfoSkeleton(): ReactNode { + const [metaSkeletons, setMetaSkeletons] = useState([]); + const [stepsSkeletons, setStepsSkeletons] = useState([]); + + useEffect(() => { + const newMetaSkeletons = []; + for (let i = 0; i < random(3, 6); i++) { + newMetaSkeletons.push( + + + ); + } + + setMetaSkeletons(newMetaSkeletons); + + const newStepsSkeletons = []; + for (let i = 0; i < random(8, 12); i++) { + newStepsSkeletons.push( + + + ); + } + + setStepsSkeletons(newStepsSkeletons); + }, []); + + return + {/* Breadcrumbs */} + + {/* Title */} + + {/* Attempts */} + + {/* Meta */} + + {metaSkeletons} + {/* Steps */} + + {stepsSkeletons} + ; +} 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 9772c4203..12bc18632 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 @@ -16,7 +16,7 @@ } .card { - height: calc(100vh - 20px); + height: 100%; } .card__title { @@ -42,3 +42,16 @@ padding-bottom: 8px; border-bottom: 1px solid #eee; } + +.hint-container { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +.hint { + color: var(--g-color-private-black-400); + font-weight: 500; +} 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 0d3928d03..1eefca310 100644 --- a/lib/static/new-ui/features/suites/components/SuitesPage/index.tsx +++ b/lib/static/new-ui/features/suites/components/SuitesPage/index.tsx @@ -1,7 +1,8 @@ import {Flex} from '@gravity-ui/uikit'; import classNames from 'classnames'; import React, {ReactNode} from 'react'; -import {connect} from 'react-redux'; +import {connect, useSelector} from 'react-redux'; +import {useParams} from 'react-router-dom'; import {bindActionCreators} from 'redux'; import {TestSteps} from '@/static/new-ui/features/suites/components/TestSteps'; @@ -17,11 +18,13 @@ import {SuiteTitle} from '../../../../components/SuiteTitle'; import * as actions from '@/static/modules/actions'; import {CollapsibleSection} from '@/static/new-ui/features/suites/components/CollapsibleSection'; import {MetaInfo} from '@/static/new-ui/components/MetaInfo'; +import {getIsInitialized} from '@/static/new-ui/store/selectors'; import {ResultEntity, State} from '@/static/new-ui/types/store'; import {AttemptPicker} from '../../../../components/AttemptPicker'; -import {TextHintCard} from '@/static/new-ui/components/Card/TextHintCard'; import styles from './index.module.css'; +import {TestInfoSkeleton} from '@/static/new-ui/features/suites/components/SuitesPage/TestInfoSkeleton'; +import {TreeViewSkeleton} from '@/static/new-ui/features/suites/components/SuitesTreeView/TreeViewSkeleton'; interface SuitesPageProps { actions: typeof actions; @@ -34,6 +37,9 @@ function SuitesPageInternal({currentResult, actions, visibleBrowserIds}: SuitesP const onPreviousSuiteHandler = (): void => void actions.suitesPageSetCurrentSuite(visibleBrowserIds[currentIndex - 1]); const onNextSuiteHandler = (): void => void actions.suitesPageSetCurrentSuite(visibleBrowserIds[currentIndex + 1]); + const {suiteId: suiteIdParam} = useParams(); + const isInitialized = useSelector(getIsInitialized); + return

Suites

@@ -42,10 +48,11 @@ function SuitesPageInternal({currentResult, actions, visibleBrowserIds}: SuitesP - + {isInitialized && } + {!isInitialized && } , - currentResult ? - + + {currentResult && <>
} id={'overview'}/> -
: - + } + {!suiteIdParam && !currentResult &&
Select a test to see details
} + {suiteIdParam && !isInitialized && } +
]} />; } diff --git a/lib/static/new-ui/features/suites/components/SuitesTreeView/TreeViewSkeleton.module.css b/lib/static/new-ui/features/suites/components/SuitesTreeView/TreeViewSkeleton.module.css new file mode 100644 index 000000000..1cb0eb589 --- /dev/null +++ b/lib/static/new-ui/features/suites/components/SuitesTreeView/TreeViewSkeleton.module.css @@ -0,0 +1,11 @@ +.skeleton { + height: 24px; + animation: skeleton-appear 1s ease forwards; + animation-delay: var(--delay); + opacity: 0; +} + +@keyframes skeleton-appear { + from {opacity: 0} + to {opacity: 1} +} diff --git a/lib/static/new-ui/features/suites/components/SuitesTreeView/TreeViewSkeleton.tsx b/lib/static/new-ui/features/suites/components/SuitesTreeView/TreeViewSkeleton.tsx new file mode 100644 index 000000000..f89e9cccc --- /dev/null +++ b/lib/static/new-ui/features/suites/components/SuitesTreeView/TreeViewSkeleton.tsx @@ -0,0 +1,27 @@ +import React, {ReactNode, useEffect, useState} from 'react'; +import random from 'lodash/random'; +import {Flex, Skeleton} from '@gravity-ui/uikit'; +import styles from './TreeViewSkeleton.module.css'; + +export function TreeViewSkeleton(): ReactNode { + const [skeletons, setSkeletons] = useState([]); + useEffect(() => { + let currentLevel = 0; + const skeletons: ReactNode[] = []; + for (let i = 0; i < 24; i++) { + const level = random(0, currentLevel + 1); + currentLevel = level; + + skeletons.push(); + } + + setSkeletons(skeletons); + }, []); + + return + {skeletons} + ; +} 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 ca9f84b1a..0aa06c6cf 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 @@ -53,7 +53,7 @@ } .tree-view__item--current { - background: #a28aff !important; + background: var(--g-color-base-brand) !important; color: #fff !important; /* Sets spinner color */ --g-color-line-brand: #fff; @@ -64,12 +64,12 @@ } .tree-view__item__error--current { - background-color: #8c72ee; + background-color: var(--g-color-private-black-150);; border: 1px solid #00000024; } .tree-view__item.tree-view__item--current:hover { - background: #af9aff !important; + background: var(--g-color-base-brand-hover) !important; } .tree-view__item--current svg { 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 02f5bbda9..c381ce5a7 100644 --- a/lib/static/new-ui/features/suites/components/SuitesTreeView/index.tsx +++ b/lib/static/new-ui/features/suites/components/SuitesTreeView/index.tsx @@ -41,7 +41,7 @@ interface SuitesTreeViewProps { function SuitesTreeViewInternal(props: SuitesTreeViewProps): ReactNode { const navigate = useNavigate(); - const {browserId} = useParams(); + const {suiteId} = useParams(); const list = useList({ items: props.treeViewItems, @@ -74,9 +74,9 @@ function SuitesTreeViewInternal(props: SuitesTreeViewProps): ReactNode { props.actions.setStrictMatchFilter(false); - if (browserId) { - props.actions.suitesPageSetCurrentSuite(browserId); - virtualizer.scrollToIndex(list.structure.visibleFlattenIds.indexOf(browserId), {align: 'start'}); + if (suiteId) { + props.actions.suitesPageSetCurrentSuite(suiteId); + virtualizer.scrollToIndex(list.structure.visibleFlattenIds.indexOf(suiteId), {align: 'start'}); } }, [props.isInitialized]); diff --git a/lib/static/new-ui/features/suites/components/TestNameFilter/index.tsx b/lib/static/new-ui/features/suites/components/TestNameFilter/index.tsx index bc5679d71..1273bc9dd 100644 --- a/lib/static/new-ui/features/suites/components/TestNameFilter/index.tsx +++ b/lib/static/new-ui/features/suites/components/TestNameFilter/index.tsx @@ -1,10 +1,11 @@ import {TextInput} from '@gravity-ui/uikit'; import React, {ChangeEvent, ReactNode, useCallback, useState} from 'react'; import {debounce} from 'lodash'; -import {connect} from 'react-redux'; +import {connect, useSelector} from 'react-redux'; import {bindActionCreators} from 'redux'; import * as actions from '@/static/modules/actions'; import {State} from '@/static/new-ui/types/store'; +import {getIsInitialized} from '@/static/new-ui/store/selectors'; interface TestNameFilterProps { testNameFilter: string; @@ -25,7 +26,9 @@ function TestNameFilterInternal(props: TestNameFilterProps): ReactNode { updateTestNameFilter(event.target.value); }, [setTestNameFilter, updateTestNameFilter]); - return ; + const isInitialized = useSelector(getIsInitialized); + + return ; } export const TestNameFilter = connect( diff --git a/lib/static/new-ui/features/suites/components/TestStatusFilter/index.tsx b/lib/static/new-ui/features/suites/components/TestStatusFilter/index.tsx index 08462bfd7..06ce7157d 100644 --- a/lib/static/new-ui/features/suites/components/TestStatusFilter/index.tsx +++ b/lib/static/new-ui/features/suites/components/TestStatusFilter/index.tsx @@ -1,6 +1,6 @@ import {RadioButton} from '@gravity-ui/uikit'; import React, {ReactNode} from 'react'; -import {connect} from 'react-redux'; +import {connect, useSelector} from 'react-redux'; import {bindActionCreators} from 'redux'; import {TestStatus, ViewMode} from '@/constants'; @@ -8,6 +8,7 @@ import * as actions from '@/static/modules/actions'; import {getStatusCounts, StatusCounts} from '@/static/new-ui/features/suites/components/TestStatusFilter/selectors'; import {State} from '@/static/new-ui/types/store'; import {getIconByStatus} from '@/static/new-ui/utils'; +import {getIsInitialized} from '@/static/new-ui/store/selectors'; import styles from './index.module.css'; interface TestStatusFilterOptionProps { @@ -28,7 +29,9 @@ interface TestStatusFilterProps { } function TestStatusFilterInternal({statusCounts, actions, viewMode}: TestStatusFilterProps): ReactNode { - return void actions.changeViewMode(e.target.value)} value={viewMode}> + const isInitialized = useSelector(getIsInitialized); + + return void actions.changeViewMode(e.target.value)} value={viewMode}> } /> } /> } /> diff --git a/lib/static/new-ui/features/visual-checks/components/VisualChecksPage/AssertViewResultSkeleton.tsx b/lib/static/new-ui/features/visual-checks/components/VisualChecksPage/AssertViewResultSkeleton.tsx new file mode 100644 index 000000000..1dfa6adc8 --- /dev/null +++ b/lib/static/new-ui/features/visual-checks/components/VisualChecksPage/AssertViewResultSkeleton.tsx @@ -0,0 +1,28 @@ +import {Flex, Skeleton} from '@gravity-ui/uikit'; +import React, {ReactNode} from 'react'; + +export function AssertViewResultSkeleton(): ReactNode { + return + {/* Breadcrumbs */} + + + + + {/* Title */} + + {/* Toolbar */} + + + + + + {/* Image title */} + + {/* Images*/} + + + + + + ; +} diff --git a/lib/static/new-ui/features/visual-checks/components/VisualChecksPage/index.module.css b/lib/static/new-ui/features/visual-checks/components/VisualChecksPage/index.module.css index 7ab927ee5..c10dc0f94 100644 --- a/lib/static/new-ui/features/visual-checks/components/VisualChecksPage/index.module.css +++ b/lib/static/new-ui/features/visual-checks/components/VisualChecksPage/index.module.css @@ -1,5 +1,5 @@ .card { - height: calc(100vh - 20px); + height: 100%; } .card__title { diff --git a/lib/static/new-ui/features/visual-checks/components/VisualChecksPage/index.tsx b/lib/static/new-ui/features/visual-checks/components/VisualChecksPage/index.tsx index d0cbd89ba..60ac5ccbe 100644 --- a/lib/static/new-ui/features/visual-checks/components/VisualChecksPage/index.tsx +++ b/lib/static/new-ui/features/visual-checks/components/VisualChecksPage/index.tsx @@ -25,6 +25,9 @@ import { } from '@/static/modules/actions'; import {isAcceptable, isScreenRevertable} from '@/static/modules/utils'; import {AssertViewStatus} from '@/static/new-ui/components/AssertViewStatus'; +import { + AssertViewResultSkeleton +} from '@/static/new-ui/features/visual-checks/components/VisualChecksPage/AssertViewResultSkeleton'; export function VisualChecksPage(): ReactNode { const dispatch = useDispatch(); @@ -65,9 +68,11 @@ export function VisualChecksPage(): ReactNode { const isLastResult = Boolean(currentResultId && currentBrowser && currentResultId === currentBrowser.resultIds[currentBrowser.resultIds.length - 1]); const isUndoAvailable = isScreenRevertable({gui: isGui, image: currentImage ?? {}, isLastResult, isStaticImageAccepterEnabled: false}); + const isInitialized = useSelector((state: State) => state.app.isInitialized); + return -
+ {isInitialized && <>
{currentNamedImage &&
{currentImage && } - {!currentImage &&
This run doesn't have an image with name "{currentNamedImage?.stateName}"
} + {!currentImage &&
This run doesn't have an image with + name "{currentNamedImage?.stateName}"
} + } + {!isInitialized && } ]}/>; } diff --git a/lib/static/new-ui/store/selectors.ts b/lib/static/new-ui/store/selectors.ts index e858daf57..3d7dd17b9 100644 --- a/lib/static/new-ui/store/selectors.ts +++ b/lib/static/new-ui/store/selectors.ts @@ -16,3 +16,5 @@ export const getBrowsersState = (state: State): Record => export const getAllBrowserIds = (state: State): string[] => state.tree.browsers.allIds; export const getResults = (state: State): Record => state.tree.results.byId; export const getImages = (state: State): Record => state.tree.images.byId; + +export const getIsInitialized = (state: State): boolean => state.app.isInitialized; diff --git a/lib/static/new-ui/types/store.ts b/lib/static/new-ui/types/store.ts index 07ae0bc7e..a87a7ece4 100644 --- a/lib/static/new-ui/types/store.ts +++ b/lib/static/new-ui/types/store.ts @@ -138,6 +138,10 @@ export interface State { }; visualChecksPage: { currentNamedImageId: string | null; + }; + loading: { + /** @note Maps ID of a resource to its loading progress. E.g. dbUrl: 88. Progress is measured from 0 to 1. */ + progress: Record; } }; ui: {