diff --git a/js_modules/dagster-ui/packages/ui-components/src/components/__stories__/useDelayedState.stories.tsx b/js_modules/dagster-ui/packages/ui-components/src/components/__stories__/useDelayedState.stories.tsx new file mode 100644 index 0000000000000..c75623a6a0da3 --- /dev/null +++ b/js_modules/dagster-ui/packages/ui-components/src/components/__stories__/useDelayedState.stories.tsx @@ -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 ( + +
The button will become enabled after five seconds.
+
+ +
+
+ ); +}; 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 new file mode 100644 index 0000000000000..4f1c601e06ce7 --- /dev/null +++ b/js_modules/dagster-ui/packages/ui-components/src/components/useDelayedState.tsx @@ -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; +}; 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 4c209b68e80ca..cca86333fa3a5 100644 --- a/js_modules/dagster-ui/packages/ui-components/src/index.ts +++ b/js_modules/dagster-ui/packages/ui-components/src/index.ts @@ -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. diff --git a/js_modules/dagster-ui/packages/ui-core/src/overview/OverviewJobsRoot.tsx b/js_modules/dagster-ui/packages/ui-core/src/overview/OverviewJobsRoot.tsx index 2bc0aad15f7b4..f77494a269247 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/overview/OverviewJobsRoot.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/overview/OverviewJobsRoot.tsx @@ -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'; @@ -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({ queryKey: 'search', defaults: {search: ''}, @@ -128,6 +129,8 @@ export const OverviewJobsRoot = () => { return ; }; + const showSearchSpinner = (workspaceLoading && !repoCount) || (loading && !data); + return ( { : undefined + } onChange={(e) => setSearchValue(e.target.value)} placeholder="Filter by job name…" style={{width: '340px'}} diff --git a/js_modules/dagster-ui/packages/ui-core/src/overview/OverviewResourcesRoot.tsx b/js_modules/dagster-ui/packages/ui-core/src/overview/OverviewResourcesRoot.tsx index 5954f15f9ad4f..955bf2dbd5544 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/overview/OverviewResourcesRoot.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/overview/OverviewResourcesRoot.tsx @@ -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'; @@ -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({ queryKey: 'search', defaults: {search: ''}, @@ -133,6 +134,8 @@ export const OverviewResourcesRoot = () => { return ; }; + const showSearchSpinner = (workspaceLoading && !repoCount) || (loading && !data); + return ( { + ) : undefined + } onChange={(e) => setSearchValue(e.target.value)} placeholder="Filter by resource name…" style={{width: '340px'}} diff --git a/js_modules/dagster-ui/packages/ui-core/src/overview/OverviewSchedulesRoot.tsx b/js_modules/dagster-ui/packages/ui-core/src/overview/OverviewSchedulesRoot.tsx index 51430a2709ebf..4cc51cabdd068 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/overview/OverviewSchedulesRoot.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/overview/OverviewSchedulesRoot.tsx @@ -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'; @@ -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({ queryKey: 'search', @@ -245,6 +246,8 @@ export const OverviewSchedulesRoot = () => { ); }; + const showSearchSpinner = (workspaceLoading && !repoCount) || (loading && !data); + return ( { + ) : undefined + } onChange={(e) => { setSearchValue(e.target.value); onToggleAll(false); diff --git a/js_modules/dagster-ui/packages/ui-core/src/overview/OverviewSensorsRoot.tsx b/js_modules/dagster-ui/packages/ui-core/src/overview/OverviewSensorsRoot.tsx index dcc5b93cd6914..c813f641a3aa2 100644 --- a/js_modules/dagster-ui/packages/ui-core/src/overview/OverviewSensorsRoot.tsx +++ b/js_modules/dagster-ui/packages/ui-core/src/overview/OverviewSensorsRoot.tsx @@ -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'; @@ -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({ queryKey: 'search', @@ -245,6 +246,8 @@ export const OverviewSensorsRoot = () => { ); }; + const showSearchSpinner = (workspaceLoading && !repoCount) || (loading && !data); + return ( { + ) : undefined + } onChange={(e) => setSearchValue(e.target.value)} placeholder="Filter by sensor name…" style={{width: '340px'}} diff --git a/js_modules/dagster-ui/packages/ui-core/src/ui/SearchInputSpinner.tsx b/js_modules/dagster-ui/packages/ui-core/src/ui/SearchInputSpinner.tsx new file mode 100644 index 0000000000000..699ac9c95abf9 --- /dev/null +++ b/js_modules/dagster-ui/packages/ui-core/src/ui/SearchInputSpinner.tsx @@ -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 ( + + + + + + ); +};