Skip to content

Commit

Permalink
feat(new-ui): implement smooth loading experience (#608)
Browse files Browse the repository at this point in the history
* feat(new-ui): implement smooth loading experience
  • Loading branch information
shadowusr authored Oct 11, 2024
1 parent f44740a commit 33ce925
Show file tree
Hide file tree
Showing 31 changed files with 518 additions and 38 deletions.
12 changes: 10 additions & 2 deletions lib/db-utils/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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 (typeof onDownloadProgress === 'function') {
loadOptions.onDownloadProgress = (e) => {
onDownloadProgress(dbUrl, e.loaded / e.total);
};
}

const {data, status} = await fetchFile(dbUrl, loadOptions);

return {url: dbUrl, status, data};
};
Expand Down
3 changes: 2 additions & 1 deletion lib/static/modules/action-names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
7 changes: 6 additions & 1 deletion lib/static/modules/actions/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
3 changes: 3 additions & 0 deletions lib/static/modules/default-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ export default Object.assign({config: configDefaults}, {
},
visualChecksPage: {
currentNamedImageId: null
},
loading: {
progress: {}
}
},
ui: {
Expand Down
9 changes: 9 additions & 0 deletions lib/static/modules/reducers/loading.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
import actionNames from '../action-names';
import {applyStateUpdate} from '@/static/modules/utils/state';

export default (state, action) => {
switch (action.type) {
case actionNames.TOGGLE_LOADING: {
return {...state, loading: action.payload};
}

case actionNames.UPDATE_LOADING_PROGRESS: {
return applyStateUpdate(state, {
app: {
loading: {progress: action.payload}
}
});
}

default:
return state;
}
Expand Down
24 changes: 24 additions & 0 deletions lib/static/new-ui.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions lib/static/new-ui/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand All @@ -31,6 +32,7 @@ export function App(): ReactNode {
<Provider store={store}>
<HashRouter>
<MainLayout menuItems={pages}>
<LoadingBar/>
<Routes>
<Route element={<Navigate to={'/suites'}/>} path={'/'}/>
{pages.map(page => <Route element={page.element} path={page.url} key={page.url}>{page.children}</Route>)}
Expand Down
15 changes: 15 additions & 0 deletions lib/static/new-ui/app/selectors.ts
Original file line number Diff line number Diff line change
@@ -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;
};
67 changes: 67 additions & 0 deletions lib/static/new-ui/components/Card/AnimatedAppearCard.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
@property --gradient-angle {
syntax: "<angle>";
inherits: false;
initial-value: 0turn;
}

@property --from-color {
syntax: "<color>";
inherits: false;
initial-value: #eee;
}

@property --to-color {
syntax: "<color>";
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;
}
15 changes: 15 additions & 0 deletions lib/static/new-ui/components/Card/AnimatedAppearCard.tsx
Original file line number Diff line number Diff line change
@@ -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 <div className={classNames(cardStyles.commonCard, styles.animatedAppearCard, {[styles.hidden]: isInitialized})}>
<div className={styles.backgroundOverlay}></div>
</div>;
}
1 change: 1 addition & 0 deletions lib/static/new-ui/components/Card/index.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -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%;
}
92 changes: 92 additions & 0 deletions lib/static/new-ui/components/LoadingBar/index.module.css
Original file line number Diff line number Diff line change
@@ -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;
--dot: no-repeat radial-gradient(circle closest-side, #fff 90%, #fff0);
background:
var(--dot) 0% 50%,
var(--dot) 50% 50%,
var(--dot) 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;
}
}
40 changes: 40 additions & 0 deletions lib/static/new-ui/components/LoadingBar/index.tsx
Original file line number Diff line number Diff line change
@@ -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 <div className={classNames(styles.container, {[styles.hidden]: hidden})}>
<div className={styles.messageContainer}>
<div className={styles.message}>
<span>Loading Testplane UI</span>
<div className={styles.loader}></div>
</div>
</div>
<div className={styles.progressContainer} style={{width: `${progress * 100}%`}}>
<div className={styles.progressPulse}></div>
</div>
</div>;
}
18 changes: 15 additions & 3 deletions lib/static/new-ui/components/MainLayout/index.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -27,8 +30,17 @@ export function MainLayout(props: MainLayoutProps): JSX.Element {
onItemClick: () => navigate(item.url)
}));

return <AsideHeader logo={{text: 'Testplane UI', iconSrc: TestplaneIcon, iconSize: 32, onClick: () => navigate('/')}} compact={true}
headerDecoration={false} menuItems={gravityMenuItems} customBackground={<div className={styles.asideHeaderBg}/>} customBackgroundClassName={styles.asideHeaderBgWrapper}
renderContent={(): React.ReactNode => props.children} hideCollapseButton={true}
const isInitialized = useSelector(getIsInitialized);

return <AsideHeader
className={classNames({'aside-header--initialized': isInitialized})}
logo={{text: 'Testplane UI', iconSrc: TestplaneIcon, iconSize: 32, onClick: () => navigate('/suites')}}
compact={true}
headerDecoration={false}
menuItems={gravityMenuItems}
customBackground={<div className={styles.asideHeaderBg}/>}
customBackgroundClassName={styles.asideHeaderBgWrapper}
renderContent={(): React.ReactNode => props.children}
hideCollapseButton={true}
/>;
}
Loading

0 comments on commit 33ce925

Please sign in to comment.