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(new-ui): implement smooth loading experience #608

Merged
merged 2 commits into from
Oct 11, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
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 (onDownloadProgress) {
loadOptions.onDownloadProgress = onDownloadProgress && ((e) => {
shadowusr marked this conversation as resolved.
Show resolved Hide resolved
onDownloadProgress(dbUrl, e.loaded / e.total);
shadowusr marked this conversation as resolved.
Show resolved Hide resolved
});
}

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;
shadowusr marked this conversation as resolved.
Show resolved Hide resolved
--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;
--_g: no-repeat radial-gradient(circle closest-side,#fff 90%,#fff0);
shadowusr marked this conversation as resolved.
Show resolved Hide resolved
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;
}
}
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
Loading