From d3a06e1ca8ee3e0d46babaaf1500fc3a93161de5 Mon Sep 17 00:00:00 2001 From: Dane Everitt Date: Fri, 10 Apr 2020 12:41:08 -0700 Subject: [PATCH] Add progress bar to top of page for nicer loading indicator styles --- resources/scripts/api/http.ts | 14 ++++ resources/scripts/components/App.tsx | 2 + .../elements/ListRefreshIndicator.tsx | 19 ----- .../components/elements/ProgressBar.tsx | 73 +++++++++++++++++++ .../server/backups/BackupContainer.tsx | 4 +- .../server/databases/DatabasesContainer.tsx | 2 - .../server/schedules/ScheduleContainer.tsx | 8 +- .../server/users/UsersContainer.tsx | 2 - resources/scripts/easy-peasy.d.ts | 8 +- resources/scripts/helpers.ts | 2 + resources/scripts/state/index.ts | 3 + resources/scripts/state/progress.ts | 30 ++++++++ 12 files changed, 133 insertions(+), 34 deletions(-) delete mode 100644 resources/scripts/components/elements/ListRefreshIndicator.tsx create mode 100644 resources/scripts/components/elements/ProgressBar.tsx create mode 100644 resources/scripts/state/progress.ts diff --git a/resources/scripts/api/http.ts b/resources/scripts/api/http.ts index 14b27cdbc..d839887b0 100644 --- a/resources/scripts/api/http.ts +++ b/resources/scripts/api/http.ts @@ -1,6 +1,8 @@ import axios, { AxiosInstance } from 'axios'; +import { store } from '@/state'; const http: AxiosInstance = axios.create({ + timeout: 20000, headers: { 'X-Requested-With': 'XMLHttpRequest', '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 // ahead and route the response data through to it so things show up. // @ts-ignore diff --git a/resources/scripts/components/App.tsx b/resources/scripts/components/App.tsx index fb813b361..e8aa002d3 100644 --- a/resources/scripts/components/App.tsx +++ b/resources/scripts/components/App.tsx @@ -9,6 +9,7 @@ import AuthenticationRouter from '@/routers/AuthenticationRouter'; import { Provider } from 'react-redux'; import { SiteSettings } from '@/state/settings'; import { DefaultTheme, ThemeProvider } from 'styled-components'; +import ProgressBar from '@/components/elements/ProgressBar'; interface ExtendedWindow extends Window { SiteConfiguration?: SiteSettings; @@ -57,6 +58,7 @@ const App = () => { +
diff --git a/resources/scripts/components/elements/ListRefreshIndicator.tsx b/resources/scripts/components/elements/ListRefreshIndicator.tsx deleted file mode 100644 index 075f53631..000000000 --- a/resources/scripts/components/elements/ListRefreshIndicator.tsx +++ /dev/null @@ -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) => ( - -
- -

{children || 'Refreshing listing...'}

-
-
-); - -export default ListRefreshIndicator; diff --git a/resources/scripts/components/elements/ProgressBar.tsx b/resources/scripts/components/elements/ProgressBar.tsx new file mode 100644 index 000000000..c4b867928 --- /dev/null +++ b/resources/scripts/components/elements/ProgressBar.tsx @@ -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(null); + const timeout = useRef(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 ( +
+ + + +
+ ); +}; diff --git a/resources/scripts/components/server/backups/BackupContainer.tsx b/resources/scripts/components/server/backups/BackupContainer.tsx index f38410768..e71b3c2f9 100644 --- a/resources/scripts/components/server/backups/BackupContainer.tsx +++ b/resources/scripts/components/server/backups/BackupContainer.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react'; 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 useFlash from '@/plugins/useFlash'; import { httpErrorToHuman } from '@/api/http'; @@ -9,7 +9,6 @@ import CreateBackupButton from '@/components/server/backups/CreateBackupButton'; import FlashMessageRender from '@/components/FlashMessageRender'; import BackupRow from '@/components/server/backups/BackupRow'; import { ServerContext } from '@/state/server'; -import ListRefreshIndicator from '@/components/elements/ListRefreshIndicator'; export default () => { const { uuid } = useServer(); @@ -36,7 +35,6 @@ export default () => { return (
- {!backups.length ?

diff --git a/resources/scripts/components/server/databases/DatabasesContainer.tsx b/resources/scripts/components/server/databases/DatabasesContainer.tsx index 0a90f985a..ce2189aca 100644 --- a/resources/scripts/components/server/databases/DatabasesContainer.tsx +++ b/resources/scripts/components/server/databases/DatabasesContainer.tsx @@ -10,7 +10,6 @@ import CreateDatabaseButton from '@/components/server/databases/CreateDatabaseBu import Can from '@/components/elements/Can'; import useFlash from '@/plugins/useFlash'; import useServer from '@/plugins/useServer'; -import ListRefreshIndicator from '@/components/elements/ListRefreshIndicator'; export default () => { const { uuid, featureLimits } = useServer(); @@ -41,7 +40,6 @@ export default () => { : <> - {databases.length > 0 ? databases.map((database, index) => ( { const { uuid } = useServer(); @@ -37,7 +34,6 @@ export default ({ match, history }: RouteComponentProps) => { return (

- {(!schedules.length && loading) ? : diff --git a/resources/scripts/components/server/users/UsersContainer.tsx b/resources/scripts/components/server/users/UsersContainer.tsx index 699834d6c..8d511a272 100644 --- a/resources/scripts/components/server/users/UsersContainer.tsx +++ b/resources/scripts/components/server/users/UsersContainer.tsx @@ -9,7 +9,6 @@ import FlashMessageRender from '@/components/FlashMessageRender'; import getServerSubusers from '@/api/server/users/getServerSubusers'; import { httpErrorToHuman } from '@/api/http'; import Can from '@/components/elements/Can'; -import ListRefreshIndicator from '@/components/elements/ListRefreshIndicator'; export default () => { const [ loading, setLoading ] = useState(true); @@ -48,7 +47,6 @@ export default () => { return (
- {!subusers.length ?

diff --git a/resources/scripts/easy-peasy.d.ts b/resources/scripts/easy-peasy.d.ts index 939ad54cf..999ea2f56 100644 --- a/resources/scripts/easy-peasy.d.ts +++ b/resources/scripts/easy-peasy.d.ts @@ -1,9 +1,13 @@ // noinspection ES6UnusedImports -import EasyPeasy from 'easy-peasy'; +import EasyPeasy, { Actions, State } from 'easy-peasy'; import { ApplicationStore } from '@/state'; declare module 'easy-peasy' { export function useStoreState( - mapState: (state: ApplicationStore) => Result, + mapState: (state: State) => Result, + ): Result; + + export function useStoreActions( + mapActions: (actions: Actions) => Result, ): Result; } diff --git a/resources/scripts/helpers.ts b/resources/scripts/helpers.ts index 9d531b52c..ad9008d17 100644 --- a/resources/scripts/helpers.ts +++ b/resources/scripts/helpers.ts @@ -9,3 +9,5 @@ export function bytesToHuman (bytes: number): string { } export const bytesToMegabytes = (bytes: number) => Math.floor(bytes / 1000 / 1000); + +export const randomInt = (low: number, high: number) => Math.floor(Math.random() * (high - low) + low); diff --git a/resources/scripts/state/index.ts b/resources/scripts/state/index.ts index ee66d5478..46506eae1 100644 --- a/resources/scripts/state/index.ts +++ b/resources/scripts/state/index.ts @@ -3,12 +3,14 @@ import flashes, { FlashStore } from '@/state/flashes'; import user, { UserStore } from '@/state/user'; import permissions, { GloablPermissionsStore } from '@/state/permissions'; import settings, { SettingsStore } from '@/state/settings'; +import progress, { ProgressStore } from '@/state/progress'; export interface ApplicationStore { permissions: GloablPermissionsStore; flashes: FlashStore; user: UserStore; settings: SettingsStore; + progress: ProgressStore; } const state: ApplicationStore = { @@ -16,6 +18,7 @@ const state: ApplicationStore = { flashes, user, settings, + progress, }; export const store = createStore(state); diff --git a/resources/scripts/state/progress.ts b/resources/scripts/state/progress.ts new file mode 100644 index 000000000..e6d9251f1 --- /dev/null +++ b/resources/scripts/state/progress.ts @@ -0,0 +1,30 @@ +import { action, Action } from 'easy-peasy'; + +export interface ProgressStore { + continuous: boolean; + progress?: number; + + startContinuous: Action; + setProgress: Action; + setComplete: Action; +} + +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;