Add progress bar to top of page for nicer loading indicator styles
This commit is contained in:
parent
708c15eba8
commit
d3a06e1ca8
12 changed files with 133 additions and 34 deletions
|
@ -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
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
|
73
resources/scripts/components/elements/ProgressBar.tsx
Normal file
73
resources/scripts/components/elements/ProgressBar.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
|
@ -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">
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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}/>
|
||||||
:
|
:
|
||||||
|
|
|
@ -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'}>
|
||||||
|
|
8
resources/scripts/easy-peasy.d.ts
vendored
8
resources/scripts/easy-peasy.d.ts
vendored
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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);
|
||||||
|
|
30
resources/scripts/state/progress.ts
Normal file
30
resources/scripts/state/progress.ts
Normal 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;
|
Loading…
Reference in a new issue