Skip to content

Commit

Permalink
[ui-components] Delayed, SpinnerWithText (#20448)
Browse files Browse the repository at this point in the history
## Summary & Motivation

Extract a couple of our common loading-state UI patterns into ui-components.

- `Delayed`, which uses `useDelayedState` to delay showing content. This is useful for delaying when we show a loading state, so that we don't flash it quickly when loading is fairly fast.
- `SpinnerWithText`, to show a body-text spinner with a line of text.

## How I Tested These Changes

Storybook, Jest.
  • Loading branch information
hellendag authored Mar 14, 2024
1 parent d803f30 commit ff8d1bf
Show file tree
Hide file tree
Showing 7 changed files with 110 additions and 3 deletions.
Original file line number Diff line number Diff line change
@@ -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}</> : <span />;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {Box} from './Box';
import {Spinner} from './Spinner';

interface Props {
label: React.ReactNode;
}

export const SpinnerWithText = ({label}: Props) => {
return (
<Box flex={{direction: 'row', alignItems: 'center', gap: 8}}>
<Spinner purpose="body-text" />
<span>{label}</span>
</Box>
);
};
Original file line number Diff line number Diff line change
@@ -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 (
<Box flex={{direction: 'column', gap: 12}}>
<div>Wait 5 seconds for content to appear:</div>
<Delayed delayMsec={5000}>
<Box background={Colors.accentBlue()} padding={20}>
<Heading color={Colors.textDefault()}>Hello world!</Heading>
</Box>
</Delayed>
</Box>
);
};
Original file line number Diff line number Diff line change
@@ -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 <SpinnerWithText label="Hey kid I'm a computer" />;
};
Original file line number Diff line number Diff line change
@@ -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(<Delayed delayMsec={delay}>Hey kid I&apos;m a computer</Delayed>);

// 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();
});
});
Original file line number Diff line number Diff line change
@@ -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;
};
2 changes: 2 additions & 0 deletions js_modules/dagster-ui/packages/ui-components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand Down

1 comment on commit ff8d1bf

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deploy preview for dagit-storybook ready!

✅ Preview
https://dagit-storybook-32e6fbw8g-elementl.vercel.app

Built with commit ff8d1bf.
This pull request is being automatically deployed with vercel-action

Please sign in to comment.