Add progress bar to top of page for nicer loading indicator styles

This commit is contained in:
Dane Everitt 2020-04-10 12:41:08 -07:00
parent 708c15eba8
commit d3a06e1ca8
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
12 changed files with 133 additions and 34 deletions

View file

@ -1,6 +1,8 @@
import axios, { AxiosInstance } from 'axios'; import axios, { AxiosInstance } from 'axios';
import { store } from '@/state';
const http: AxiosInstance = axios.create({ const http: AxiosInstance = axios.create({
timeout: 20000,
headers: { headers: {
'X-Requested-With': 'XMLHttpRequest', 'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json', 'Accept': 'application/json',
@ -9,6 +11,18 @@ const http: AxiosInstance = axios.create({
}, },
}); });
http.interceptors.request.use(req => {
store.getActions().progress.startContinuous();
return req;
});
http.interceptors.response.use(resp => {
store.getActions().progress.setComplete();
return resp;
});
// If we have a phpdebugbar instance registered at this point in time go // If we have a phpdebugbar instance registered at this point in time go
// ahead and route the response data through to it so things show up. // ahead and route the response data through to it so things show up.
// @ts-ignore // @ts-ignore

View file

@ -9,6 +9,7 @@ import AuthenticationRouter from '@/routers/AuthenticationRouter';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { SiteSettings } from '@/state/settings'; import { SiteSettings } from '@/state/settings';
import { DefaultTheme, ThemeProvider } from 'styled-components'; import { DefaultTheme, ThemeProvider } from 'styled-components';
import ProgressBar from '@/components/elements/ProgressBar';
interface ExtendedWindow extends Window { interface ExtendedWindow extends Window {
SiteConfiguration?: SiteSettings; SiteConfiguration?: SiteSettings;
@ -57,6 +58,7 @@ const App = () => {
<ThemeProvider theme={theme}> <ThemeProvider theme={theme}>
<StoreProvider store={store}> <StoreProvider store={store}>
<Provider store={store}> <Provider store={store}>
<ProgressBar/>
<div className={'mx-auto w-auto'}> <div className={'mx-auto w-auto'}>
<BrowserRouter basename={'/'} key={'root-router'}> <BrowserRouter basename={'/'} key={'root-router'}>
<Switch> <Switch>

View file

@ -1,19 +0,0 @@
import React from 'react';
import Spinner from '@/components/elements/Spinner';
import { CSSTransition } from 'react-transition-group';
interface Props {
visible: boolean;
children?: React.ReactChild;
}
const ListRefreshIndicator = ({ visible, children }: Props) => (
<CSSTransition timeout={250} in={visible} appear={true} unmountOnExit={true} classNames={'fade'}>
<div className={'flex items-center mb-2'}>
<Spinner size={'tiny'}/>
<p className={'ml-2 text-sm text-neutral-400'}>{children || 'Refreshing listing...'}</p>
</div>
</CSSTransition>
);
export default ListRefreshIndicator;

View file

@ -0,0 +1,73 @@
import React, { useEffect, useRef, useState } from 'react';
import styled from 'styled-components';
import { useStoreActions, useStoreState } from 'easy-peasy';
import { randomInt } from '@/helpers';
import { CSSTransition } from 'react-transition-group';
const BarFill = styled.div`
${tw`h-full bg-cyan-400`};
transition: 250ms ease-in-out;
box-shadow: 0 -2px 10px 2px hsl(178, 78%, 57%);
`;
export default () => {
const interval = useRef<number>(null);
const timeout = useRef<number>(null);
const [ visible, setVisible ] = useState(false);
const progress = useStoreState(state => state.progress.progress);
const continuous = useStoreState(state => state.progress.continuous);
const setProgress = useStoreActions(actions => actions.progress.setProgress);
useEffect(() => {
return () => {
timeout.current && clearTimeout(timeout.current);
interval.current && clearInterval(interval.current);
};
}, []);
useEffect(() => {
setVisible((progress || 0) > 0);
if (progress === 100) {
// @ts-ignore
timeout.current = setTimeout(() => setProgress(undefined), 500);
}
}, [ progress ]);
useEffect(() => {
if (!continuous) {
interval.current && clearInterval(interval.current);
return;
}
if (!progress || progress === 0) {
setProgress(randomInt(20, 30));
}
}, [ continuous ]);
useEffect(() => {
if (continuous) {
interval.current && clearInterval(interval.current);
if ((progress || 0) >= 90) {
setProgress(90);
} else {
// @ts-ignore
interval.current = setTimeout(() => setProgress(progress + randomInt(1, 5)), 500);
}
}
}, [ progress, continuous ]);
return (
<div className={'w-full fixed'} style={{ height: '2px' }}>
<CSSTransition
timeout={250}
appear={true}
in={visible}
unmountOnExit={true}
classNames={'fade'}
>
<BarFill style={{ width: progress === undefined ? '100%' : `${progress}%` }}/>
</CSSTransition>
</div>
);
};

View file

@ -1,6 +1,6 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import Spinner from '@/components/elements/Spinner'; import Spinner from '@/components/elements/Spinner';
import getServerBackups, { ServerBackup } from '@/api/server/backups/getServerBackups'; import getServerBackups from '@/api/server/backups/getServerBackups';
import useServer from '@/plugins/useServer'; import useServer from '@/plugins/useServer';
import useFlash from '@/plugins/useFlash'; import useFlash from '@/plugins/useFlash';
import { httpErrorToHuman } from '@/api/http'; import { httpErrorToHuman } from '@/api/http';
@ -9,7 +9,6 @@ import CreateBackupButton from '@/components/server/backups/CreateBackupButton';
import FlashMessageRender from '@/components/FlashMessageRender'; import FlashMessageRender from '@/components/FlashMessageRender';
import BackupRow from '@/components/server/backups/BackupRow'; import BackupRow from '@/components/server/backups/BackupRow';
import { ServerContext } from '@/state/server'; import { ServerContext } from '@/state/server';
import ListRefreshIndicator from '@/components/elements/ListRefreshIndicator';
export default () => { export default () => {
const { uuid } = useServer(); const { uuid } = useServer();
@ -36,7 +35,6 @@ export default () => {
return ( return (
<div className={'mt-10 mb-6'}> <div className={'mt-10 mb-6'}>
<ListRefreshIndicator visible={loading}/>
<FlashMessageRender byKey={'backups'} className={'mb-4'}/> <FlashMessageRender byKey={'backups'} className={'mb-4'}/>
{!backups.length ? {!backups.length ?
<p className="text-center text-sm text-neutral-400"> <p className="text-center text-sm text-neutral-400">

View file

@ -10,7 +10,6 @@ import CreateDatabaseButton from '@/components/server/databases/CreateDatabaseBu
import Can from '@/components/elements/Can'; import Can from '@/components/elements/Can';
import useFlash from '@/plugins/useFlash'; import useFlash from '@/plugins/useFlash';
import useServer from '@/plugins/useServer'; import useServer from '@/plugins/useServer';
import ListRefreshIndicator from '@/components/elements/ListRefreshIndicator';
export default () => { export default () => {
const { uuid, featureLimits } = useServer(); const { uuid, featureLimits } = useServer();
@ -41,7 +40,6 @@ export default () => {
: :
<CSSTransition classNames={'fade'} timeout={250}> <CSSTransition classNames={'fade'} timeout={250}>
<> <>
<ListRefreshIndicator visible={loading}/>
{databases.length > 0 ? {databases.length > 0 ?
databases.map((database, index) => ( databases.map((database, index) => (
<DatabaseRow <DatabaseRow

View file

@ -1,18 +1,15 @@
import React, { useEffect, useMemo, useState } from 'react'; import React, { useEffect, useState } from 'react';
import getServerSchedules, { Schedule } from '@/api/server/schedules/getServerSchedules'; import getServerSchedules from '@/api/server/schedules/getServerSchedules';
import { ServerContext } from '@/state/server'; import { ServerContext } from '@/state/server';
import Spinner from '@/components/elements/Spinner'; import Spinner from '@/components/elements/Spinner';
import { RouteComponentProps } from 'react-router-dom'; import { RouteComponentProps } from 'react-router-dom';
import FlashMessageRender from '@/components/FlashMessageRender'; import FlashMessageRender from '@/components/FlashMessageRender';
import ScheduleRow from '@/components/server/schedules/ScheduleRow'; import ScheduleRow from '@/components/server/schedules/ScheduleRow';
import { httpErrorToHuman } from '@/api/http'; import { httpErrorToHuman } from '@/api/http';
import { Actions, useStoreActions } from 'easy-peasy';
import { ApplicationStore } from '@/state';
import EditScheduleModal from '@/components/server/schedules/EditScheduleModal'; import EditScheduleModal from '@/components/server/schedules/EditScheduleModal';
import Can from '@/components/elements/Can'; import Can from '@/components/elements/Can';
import useServer from '@/plugins/useServer'; import useServer from '@/plugins/useServer';
import useFlash from '@/plugins/useFlash'; import useFlash from '@/plugins/useFlash';
import ListRefreshIndicator from '@/components/elements/ListRefreshIndicator';
export default ({ match, history }: RouteComponentProps) => { export default ({ match, history }: RouteComponentProps) => {
const { uuid } = useServer(); const { uuid } = useServer();
@ -37,7 +34,6 @@ export default ({ match, history }: RouteComponentProps) => {
return ( return (
<div className={'my-10 mb-6'}> <div className={'my-10 mb-6'}>
<FlashMessageRender byKey={'schedules'} className={'mb-4'}/> <FlashMessageRender byKey={'schedules'} className={'mb-4'}/>
<ListRefreshIndicator visible={loading}/>
{(!schedules.length && loading) ? {(!schedules.length && loading) ?
<Spinner size={'large'} centered={true}/> <Spinner size={'large'} centered={true}/>
: :

View file

@ -9,7 +9,6 @@ import FlashMessageRender from '@/components/FlashMessageRender';
import getServerSubusers from '@/api/server/users/getServerSubusers'; import getServerSubusers from '@/api/server/users/getServerSubusers';
import { httpErrorToHuman } from '@/api/http'; import { httpErrorToHuman } from '@/api/http';
import Can from '@/components/elements/Can'; import Can from '@/components/elements/Can';
import ListRefreshIndicator from '@/components/elements/ListRefreshIndicator';
export default () => { export default () => {
const [ loading, setLoading ] = useState(true); const [ loading, setLoading ] = useState(true);
@ -48,7 +47,6 @@ export default () => {
return ( return (
<div className={'mt-10 mb-6'}> <div className={'mt-10 mb-6'}>
<ListRefreshIndicator visible={loading}/>
<FlashMessageRender byKey={'users'} className={'mb-4'}/> <FlashMessageRender byKey={'users'} className={'mb-4'}/>
{!subusers.length ? {!subusers.length ?
<p className={'text-center text-sm text-neutral-400'}> <p className={'text-center text-sm text-neutral-400'}>

View file

@ -1,9 +1,13 @@
// noinspection ES6UnusedImports // noinspection ES6UnusedImports
import EasyPeasy from 'easy-peasy'; import EasyPeasy, { Actions, State } from 'easy-peasy';
import { ApplicationStore } from '@/state'; import { ApplicationStore } from '@/state';
declare module 'easy-peasy' { declare module 'easy-peasy' {
export function useStoreState<Result>( export function useStoreState<Result>(
mapState: (state: ApplicationStore) => Result, mapState: (state: State<ApplicationStore>) => Result,
): Result;
export function useStoreActions<Result>(
mapActions: (actions: Actions<ApplicationStore>) => Result,
): Result; ): Result;
} }

View file

@ -9,3 +9,5 @@ export function bytesToHuman (bytes: number): string {
} }
export const bytesToMegabytes = (bytes: number) => Math.floor(bytes / 1000 / 1000); export const bytesToMegabytes = (bytes: number) => Math.floor(bytes / 1000 / 1000);
export const randomInt = (low: number, high: number) => Math.floor(Math.random() * (high - low) + low);

View file

@ -3,12 +3,14 @@ import flashes, { FlashStore } from '@/state/flashes';
import user, { UserStore } from '@/state/user'; import user, { UserStore } from '@/state/user';
import permissions, { GloablPermissionsStore } from '@/state/permissions'; import permissions, { GloablPermissionsStore } from '@/state/permissions';
import settings, { SettingsStore } from '@/state/settings'; import settings, { SettingsStore } from '@/state/settings';
import progress, { ProgressStore } from '@/state/progress';
export interface ApplicationStore { export interface ApplicationStore {
permissions: GloablPermissionsStore; permissions: GloablPermissionsStore;
flashes: FlashStore; flashes: FlashStore;
user: UserStore; user: UserStore;
settings: SettingsStore; settings: SettingsStore;
progress: ProgressStore;
} }
const state: ApplicationStore = { const state: ApplicationStore = {
@ -16,6 +18,7 @@ const state: ApplicationStore = {
flashes, flashes,
user, user,
settings, settings,
progress,
}; };
export const store = createStore(state); export const store = createStore(state);

View file

@ -0,0 +1,30 @@
import { action, Action } from 'easy-peasy';
export interface ProgressStore {
continuous: boolean;
progress?: number;
startContinuous: Action<ProgressStore>;
setProgress: Action<ProgressStore, number | undefined>;
setComplete: Action<ProgressStore>;
}
const progress: ProgressStore = {
continuous: false,
progress: undefined,
startContinuous: action(state => {
state.continuous = true;
}),
setProgress: action((state, payload) => {
state.progress = payload;
}),
setComplete: action(state => {
state.progress = 100;
state.continuous = false;
}),
};
export default progress;