diff --git a/js_modules/dagster-ui/packages/ui-components/src/components/Delayed.tsx b/js_modules/dagster-ui/packages/ui-components/src/components/Delayed.tsx new file mode 100644 index 0000000000000..209f16fb7f68b --- /dev/null +++ b/js_modules/dagster-ui/packages/ui-components/src/components/Delayed.tsx @@ -0,0 +1,17 @@ +import {useDelayedState} from './useDelayedState'; + +interface Props { + delayMsec?: number; + children: React.ReactNode; +} + +const DEFAULT_DELAY = 1000; + +/** + * While waiting for a delay to complete, show an empty span. This is useful for + * delayed loading states, e.g. to avoid flashing a spinner during a fast loading period. + */ +export const Delayed = ({delayMsec = DEFAULT_DELAY, children}: Props) => { + const ready = useDelayedState(delayMsec); + return ready ? <>{children} : ; +}; diff --git a/js_modules/dagster-ui/packages/ui-components/src/components/SpinnerWithText.tsx b/js_modules/dagster-ui/packages/ui-components/src/components/SpinnerWithText.tsx new file mode 100644 index 0000000000000..7fe62f11bb8e3 --- /dev/null +++ b/js_modules/dagster-ui/packages/ui-components/src/components/SpinnerWithText.tsx @@ -0,0 +1,15 @@ +import {Box} from './Box'; +import {Spinner} from './Spinner'; + +interface Props { + label: React.ReactNode; +} + +export const SpinnerWithText = ({label}: Props) => { + return ( + + + {label} + + ); +}; diff --git a/js_modules/dagster-ui/packages/ui-components/src/components/__stories__/Delayed.stories.tsx b/js_modules/dagster-ui/packages/ui-components/src/components/__stories__/Delayed.stories.tsx new file mode 100644 index 0000000000000..39dfe34cf7025 --- /dev/null +++ b/js_modules/dagster-ui/packages/ui-components/src/components/__stories__/Delayed.stories.tsx @@ -0,0 +1,25 @@ +import {Meta} from '@storybook/react'; + +import {Box} from '../Box'; +import {Colors} from '../Color'; +import {Delayed} from '../Delayed'; +import {Heading} from '../Text'; + +// eslint-disable-next-line import/no-default-export +export default { + title: 'Delayed', + component: Delayed, +} as Meta; + +export const Default = () => { + return ( + +
Wait 5 seconds for content to appear:
+ + + Hello world! + + +
+ ); +}; diff --git a/js_modules/dagster-ui/packages/ui-components/src/components/__stories__/SpinnerWithText.stories.tsx b/js_modules/dagster-ui/packages/ui-components/src/components/__stories__/SpinnerWithText.stories.tsx new file mode 100644 index 0000000000000..712e058d6adbc --- /dev/null +++ b/js_modules/dagster-ui/packages/ui-components/src/components/__stories__/SpinnerWithText.stories.tsx @@ -0,0 +1,13 @@ +import {Meta} from '@storybook/react'; + +import {SpinnerWithText} from '../SpinnerWithText'; + +// eslint-disable-next-line import/no-default-export +export default { + title: 'SpinnerWithText', + component: SpinnerWithText, +} as Meta; + +export const Default = () => { + return ; +}; diff --git a/js_modules/dagster-ui/packages/ui-components/src/components/__tests__/Delayed.test.tsx b/js_modules/dagster-ui/packages/ui-components/src/components/__tests__/Delayed.test.tsx new file mode 100644 index 0000000000000..566bb04e4c5e9 --- /dev/null +++ b/js_modules/dagster-ui/packages/ui-components/src/components/__tests__/Delayed.test.tsx @@ -0,0 +1,35 @@ +import {act, render, screen} from '@testing-library/react'; + +import {Delayed} from '../Delayed'; + +describe('Delayed', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + it('delays rendering of loading state', async () => { + const delay = 5000; + const checkpoint = 2000; + + render(Hey kid I'm a computer); + + // Initially empty + expect(screen.queryByText(/hey kid/i)).toBeNull(); + + // Run to checkpoint + act(() => jest.advanceTimersByTime(checkpoint)); + + // Still empty + expect(screen.queryByText(/hey kid/i)).toBeNull(); + + // Run to completion + act(() => jest.advanceTimersByTime(delay - checkpoint)); + + // Delay complete, loading text is now visible + expect(await screen.findByText(/hey kid/i)).toBeVisible(); + }); +}); diff --git a/js_modules/dagster-ui/packages/ui-components/src/components/useDelayedState.tsx b/js_modules/dagster-ui/packages/ui-components/src/components/useDelayedState.tsx index 89f2b9538ddc0..7cf9939ca48f1 100644 --- a/js_modules/dagster-ui/packages/ui-components/src/components/useDelayedState.tsx +++ b/js_modules/dagster-ui/packages/ui-components/src/components/useDelayedState.tsx @@ -1,12 +1,12 @@ import {useEffect, useState} from 'react'; export const useDelayedState = (delayMsec: number) => { - const [value, setValue] = useState(false); + const [ready, setReady] = useState(false); useEffect(() => { - const timer = setTimeout(() => setValue(true), delayMsec); + const timer = setTimeout(() => setReady(true), delayMsec); return () => clearTimeout(timer); }, [delayMsec]); - return value; + return ready; }; diff --git a/js_modules/dagster-ui/packages/ui-components/src/index.ts b/js_modules/dagster-ui/packages/ui-components/src/index.ts index 8da80f433a69d..bedef133abff2 100644 --- a/js_modules/dagster-ui/packages/ui-components/src/index.ts +++ b/js_modules/dagster-ui/packages/ui-components/src/index.ts @@ -14,6 +14,7 @@ export * from './components/Countdown'; export * from './components/CursorControls'; export * from './components/CustomTooltipProvider'; export * from './components/Icon'; +export * from './components/Delayed'; export * from './components/Dialog'; export * from './components/Group'; export * from './components/MainContent'; @@ -31,6 +32,7 @@ export * from './components/RefreshableCountdown'; export * from './components/Select'; export * from './components/Slider'; export * from './components/Spinner'; +export * from './components/SpinnerWithText'; export * from './components/SplitPanelContainer'; export * from './components/StyledButton'; export * from './components/SubwayDot';