Skip to content

Commit

Permalink
[ui] Show a delayed loading spinner on Overview search inputs (#16607)
Browse files Browse the repository at this point in the history
## Summary & Motivation

Show a delayed loading spinner on Overview search inputs to inform the
user that the workspace is still loading. This way, when they attempt a
search that comes up empty, there will be an obvious reason for it: the
workspace isn't ready yet.

<img width="562" alt="Screenshot 2023-09-18 at 4 59 43 PM"
src="https://github.com/dagster-io/dagster/assets/2823852/8aec765c-7ed3-4430-b623-c7a105bd2052">

I added a utility hook to `ui-components` for a delayed state update,
which will allow us to wait briefly before showing the spinner. This
prevents a quick flash of the spinner in cases where the workspace loads
fairly quickly. I'll use the utility in a couple of places in Cloud that
I've done this in recently.

## How I Tested These Changes

View Overview, verify that spinners appear after a brief delay when the
loading state is forced to be true.

Storybook example for the utility hook.
  • Loading branch information
hellendag authored Sep 19, 2023
1 parent 761cb7f commit 04692d1
Show file tree
Hide file tree
Showing 8 changed files with 95 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {Meta} from '@storybook/react';
import * as React from 'react';

import {Box} from '../Box';
import {Button} from '../Button';
import {useDelayedState} from '../useDelayedState';

// eslint-disable-next-line import/no-default-export
export default {
title: 'useDelayedState',
} as Meta;

export const Default = () => {
const notDisabled = useDelayedState(5000);
return (
<Box flex={{direction: 'column', gap: 12}}>
<div>The button will become enabled after five seconds.</div>
<div>
<Button disabled={!notDisabled}>Wait for it</Button>
</div>
</Box>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as React from 'react';

export const useDelayedState = (delayMsec: number) => {
const [value, setValue] = React.useState(false);

React.useEffect(() => {
const timer = setTimeout(() => setValue(true), delayMsec);
return () => clearTimeout(timer);
}, [delayMsec]);

return value;
};
1 change: 1 addition & 0 deletions js_modules/dagster-ui/packages/ui-components/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export * from './components/useSuggestionsForString';
export * from './components/ErrorBoundary';
export * from './components/useViewport';
export * from './components/StyledRawCodeMirror';
export * from './components/useDelayedState';

// Global font styles, exported as styled-component components to render in
// your app tree root. E.g. <GlobalInconsolata />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {isHiddenAssetGroupJob} from '../asset-graph/Utils';
import {useDocumentTitle} from '../hooks/useDocumentTitle';
import {useQueryPersistedState} from '../hooks/useQueryPersistedState';
import {RepoFilterButton} from '../instance/RepoFilterButton';
import {SearchInputSpinner} from '../ui/SearchInputSpinner';
import {WorkspaceContext} from '../workspace/WorkspaceContext';
import {buildRepoAddress} from '../workspace/buildRepoAddress';
import {repoAddressAsHumanString} from '../workspace/repoAddressAsString';
Expand All @@ -32,7 +33,7 @@ export const OverviewJobsRoot = () => {
useTrackPageView();
useDocumentTitle('Overview | Jobs');

const {allRepos, visibleRepos} = React.useContext(WorkspaceContext);
const {allRepos, visibleRepos, loading: workspaceLoading} = React.useContext(WorkspaceContext);
const [searchValue, setSearchValue] = useQueryPersistedState<string>({
queryKey: 'search',
defaults: {search: ''},
Expand Down Expand Up @@ -128,6 +129,8 @@ export const OverviewJobsRoot = () => {
return <OverviewJobsTable repos={filteredBySearch} />;
};

const showSearchSpinner = (workspaceLoading && !repoCount) || (loading && !data);

return (
<Box flex={{direction: 'column'}} style={{height: '100%', overflow: 'hidden'}}>
<PageHeader
Expand All @@ -142,6 +145,9 @@ export const OverviewJobsRoot = () => {
<TextInput
icon="search"
value={searchValue}
rightElement={
showSearchSpinner ? <SearchInputSpinner tooltipContent="Loading jobs…" /> : undefined
}
onChange={(e) => setSearchValue(e.target.value)}
placeholder="Filter by job name…"
style={{width: '340px'}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {useQueryPersistedState} from '../hooks/useQueryPersistedState';
import {RepoFilterButton} from '../instance/RepoFilterButton';
import {RESOURCE_ENTRY_FRAGMENT} from '../resources/WorkspaceResourcesRoot';
import {ResourceEntryFragment} from '../resources/types/WorkspaceResourcesRoot.types';
import {SearchInputSpinner} from '../ui/SearchInputSpinner';
import {WorkspaceContext} from '../workspace/WorkspaceContext';
import {buildRepoAddress} from '../workspace/buildRepoAddress';
import {repoAddressAsHumanString} from '../workspace/repoAddressAsString';
Expand All @@ -36,7 +37,7 @@ export const OverviewResourcesRoot = () => {
useTrackPageView();
useDocumentTitle('Overview | Resources');

const {allRepos, visibleRepos} = React.useContext(WorkspaceContext);
const {allRepos, visibleRepos, loading: workspaceLoading} = React.useContext(WorkspaceContext);
const [searchValue, setSearchValue] = useQueryPersistedState<string>({
queryKey: 'search',
defaults: {search: ''},
Expand Down Expand Up @@ -133,6 +134,8 @@ export const OverviewResourcesRoot = () => {
return <OverviewResourcesTable repos={filteredBySearch} />;
};

const showSearchSpinner = (workspaceLoading && !repoCount) || (loading && !data);

return (
<Box flex={{direction: 'column'}} style={{height: '100%', overflow: 'hidden'}}>
<PageHeader
Expand All @@ -147,6 +150,11 @@ export const OverviewResourcesRoot = () => {
<TextInput
icon="search"
value={searchValue}
rightElement={
showSearchSpinner ? (
<SearchInputSpinner tooltipContent="Loading resources…" />
) : undefined
}
onChange={(e) => setSearchValue(e.target.value)}
placeholder="Filter by resource name…"
style={{width: '340px'}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {CheckAllBox} from '../ui/CheckAllBox';
import {useFilters} from '../ui/Filters';
import {useCodeLocationFilter} from '../ui/Filters/useCodeLocationFilter';
import {useInstigationStatusFilter} from '../ui/Filters/useInstigationStatusFilter';
import {SearchInputSpinner} from '../ui/SearchInputSpinner';
import {WorkspaceContext} from '../workspace/WorkspaceContext';
import {buildRepoAddress} from '../workspace/buildRepoAddress';
import {repoAddressAsHumanString} from '../workspace/repoAddressAsString';
Expand All @@ -55,7 +56,7 @@ export const OverviewSchedulesRoot = () => {
useTrackPageView();
useDocumentTitle('Overview | Schedules');

const {allRepos, visibleRepos} = React.useContext(WorkspaceContext);
const {allRepos, visibleRepos, loading: workspaceLoading} = React.useContext(WorkspaceContext);
const repoCount = allRepos.length;
const [searchValue, setSearchValue] = useQueryPersistedState<string>({
queryKey: 'search',
Expand Down Expand Up @@ -245,6 +246,8 @@ export const OverviewSchedulesRoot = () => {
);
};

const showSearchSpinner = (workspaceLoading && !repoCount) || (loading && !data);

return (
<Box flex={{direction: 'column'}} style={{height: '100%', overflow: 'hidden'}}>
<PageHeader
Expand All @@ -260,6 +263,11 @@ export const OverviewSchedulesRoot = () => {
<TextInput
icon="search"
value={searchValue}
rightElement={
showSearchSpinner ? (
<SearchInputSpinner tooltipContent="Loading schedules…" />
) : undefined
}
onChange={(e) => {
setSearchValue(e.target.value);
onToggleAll(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {CheckAllBox} from '../ui/CheckAllBox';
import {useFilters} from '../ui/Filters';
import {useCodeLocationFilter} from '../ui/Filters/useCodeLocationFilter';
import {useInstigationStatusFilter} from '../ui/Filters/useInstigationStatusFilter';
import {SearchInputSpinner} from '../ui/SearchInputSpinner';
import {WorkspaceContext} from '../workspace/WorkspaceContext';
import {buildRepoAddress} from '../workspace/buildRepoAddress';
import {repoAddressAsHumanString} from '../workspace/repoAddressAsString';
Expand All @@ -55,7 +56,7 @@ export const OverviewSensorsRoot = () => {
useTrackPageView();
useDocumentTitle('Overview | Sensors');

const {allRepos, visibleRepos} = React.useContext(WorkspaceContext);
const {allRepos, visibleRepos, loading: workspaceLoading} = React.useContext(WorkspaceContext);
const repoCount = allRepos.length;
const [searchValue, setSearchValue] = useQueryPersistedState<string>({
queryKey: 'search',
Expand Down Expand Up @@ -245,6 +246,8 @@ export const OverviewSensorsRoot = () => {
);
};

const showSearchSpinner = (workspaceLoading && !repoCount) || (loading && !data);

return (
<Box flex={{direction: 'column'}} style={{height: '100%', overflow: 'hidden'}}>
<PageHeader
Expand All @@ -266,6 +269,11 @@ export const OverviewSensorsRoot = () => {
<TextInput
icon="search"
value={searchValue}
rightElement={
showSearchSpinner ? (
<SearchInputSpinner tooltipContent="Loading sensors…" />
) : undefined
}
onChange={(e) => setSearchValue(e.target.value)}
placeholder="Filter by sensor name…"
style={{width: '340px'}}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import {Box, Spinner, Tooltip, useDelayedState} from '@dagster-io/ui-components';
import * as React from 'react';

interface Props {
tooltipContent: string | React.ReactElement | null;
}

const SPINNER_WAIT_MSEC = 2000;

export const SearchInputSpinner = (props: Props) => {
const {tooltipContent} = props;
const canShowSpinner = useDelayedState(SPINNER_WAIT_MSEC);

if (!canShowSpinner) {
return null;
}

return (
<Box margin={{top: 1}}>
<Tooltip placement="top" canShow={!!tooltipContent} content={tooltipContent || ''}>
<Spinner purpose="body-text" />
</Tooltip>
</Box>
);
};

2 comments on commit 04692d1

@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-pdo7a4cs7-elementl.vercel.app

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

@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-core-storybook ready!

✅ Preview
https://dagit-core-storybook-382sp4lxj-elementl.vercel.app

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

Please sign in to comment.