From 29ac7fd3c4abc1d7563d1ffeb00b72909580ac12 Mon Sep 17 00:00:00 2001 From: Laura Benz <48948963+L-M-K-B@users.noreply.github.com> Date: Mon, 15 Jul 2024 12:45:33 +0200 Subject: [PATCH] RestoreDashboards: Improved Empty and Loading states (#89825) * fix: empty state blocking page completely * refactor: replace epmty state implementation * refactor: first step to fix flickering * refactor: fix TS errors and add translations * refactor * tidy up search emptyState default value * other wip * clean up * undo change we made to ssm * remove log * comment * move empty state into seperate file * remove log * undo change: --------- Co-authored-by: joshhunt --- .../browse-dashboards/RecentlyDeletedPage.tsx | 104 +++++++++--------- .../api/useRecentlyDeletedStateManager.ts | 45 +++++++- .../components/RecentlyDeletedEmptyState.tsx | 22 ++++ .../components/SearchView.tsx | 32 +++--- .../search/state/SearchStateManager.ts | 14 +-- public/locales/en-US/grafana.json | 3 +- public/locales/pseudo-LOCALE/grafana.json | 3 +- 7 files changed, 147 insertions(+), 76 deletions(-) create mode 100644 public/app/features/browse-dashboards/components/RecentlyDeletedEmptyState.tsx diff --git a/public/app/features/browse-dashboards/RecentlyDeletedPage.tsx b/public/app/features/browse-dashboards/RecentlyDeletedPage.tsx index fd243854acc..cf0da923000 100644 --- a/public/app/features/browse-dashboards/RecentlyDeletedPage.tsx +++ b/public/app/features/browse-dashboards/RecentlyDeletedPage.tsx @@ -1,7 +1,9 @@ +import { css } from '@emotion/css'; import { memo, useEffect } from 'react'; import AutoSizer from 'react-virtualized-auto-sizer'; -import { FilterInput, EmptyState, Stack } from '@grafana/ui'; +import { GrafanaTheme2 } from '@grafana/data'; +import { FilterInput, useStyles2 } from '@grafana/ui'; import { Page } from 'app/core/components/Page/Page'; import { t } from 'app/core/internationalization'; import { ActionRow } from 'app/features/search/page/components/ActionRow'; @@ -11,12 +13,14 @@ import { useDispatch } from '../../types'; import { useRecentlyDeletedStateManager } from './api/useRecentlyDeletedStateManager'; import { RecentlyDeletedActions } from './components/RecentlyDeletedActions'; +import { RecentlyDeletedEmptyState } from './components/RecentlyDeletedEmptyState'; import { SearchView } from './components/SearchView'; import { getFolderPermissions } from './permissions'; import { setAllSelection } from './state'; const RecentlyDeletedPage = memo(() => { const dispatch = useDispatch(); + const styles = useStyles2(getStyles); const [searchState, stateManager] = useRecentlyDeletedStateManager(); @@ -35,62 +39,62 @@ const RecentlyDeletedPage = memo(() => { ); }, [dispatch, stateManager]); - if (searchState.loading === false && searchState.result?.totalRows === 0) { - return ( - - - - - - ); - } - return ( - - {searchState.result && ( - <> - - + + + + + +
+ + {({ width, height }) => ( + } /> - - - - - {({ width, height }) => ( - - )} - - - )} + )} + +
); }); +const getStyles = (theme: GrafanaTheme2) => ({ + pageContents: css({ + display: 'grid', + gridTemplateRows: 'auto auto auto 1fr', + height: '100%', + rowGap: theme.spacing(1), + }), + + // AutoSizer needs an element to measure the full height available + subView: css({ + height: '100%', + }), +}); + RecentlyDeletedPage.displayName = 'RecentlyDeletedPage'; export default RecentlyDeletedPage; diff --git a/public/app/features/browse-dashboards/api/useRecentlyDeletedStateManager.ts b/public/app/features/browse-dashboards/api/useRecentlyDeletedStateManager.ts index 21b90ea0deb..cd3648d1560 100644 --- a/public/app/features/browse-dashboards/api/useRecentlyDeletedStateManager.ts +++ b/public/app/features/browse-dashboards/api/useRecentlyDeletedStateManager.ts @@ -1,9 +1,50 @@ +import { SEARCH_SELECTED_SORT } from 'app/features/search/constants'; +import { SearchState } from 'app/features/search/types'; + import { initialState, SearchStateManager } from '../../search/state/SearchStateManager'; -let recentlyDeletedStateManager: SearchStateManager; +// Subclass SearchStateMananger to customise the setStateAndDoSearch behaviour. +// We want to clear the search results when the user clears any search input +// to trigger the skeleton state. +export class TrashStateManager extends SearchStateManager { + setStateAndDoSearch(state: Partial) { + const sort = state.sort || this.state.sort || localStorage.getItem(SEARCH_SELECTED_SORT) || undefined; + + const query = state.query ?? this.state.query; + const tags = state.tag ?? this.state.tag; + + // When the user clears the search, and we revert back to list listing all + const clearResults = query.length === 0 && tags.length === 0; + + // Set internal state + this.setState({ + sort, + result: clearResults ? undefined : this.state.result, + ...state, + }); + + // Update url state + this.updateLocation({ + query: this.state.query.length === 0 ? null : this.state.query, + tag: this.state.tag, + datasource: this.state.datasource, + panel_type: this.state.panel_type, + starred: this.state.starred ? this.state.starred : null, + sort: this.state.sort, + }); + + // Prevent searching when user is only clearing the input. + // We don't show these results anyway + if (this.hasSearchFilters()) { + this.doSearchWithDebounce(); + } + } +} + +let recentlyDeletedStateManager: TrashStateManager; function getRecentlyDeletedStateManager() { if (!recentlyDeletedStateManager) { - recentlyDeletedStateManager = new SearchStateManager({ ...initialState, includePanels: false, deleted: true }); + recentlyDeletedStateManager = new TrashStateManager({ ...initialState, includePanels: false, deleted: true }); } return recentlyDeletedStateManager; diff --git a/public/app/features/browse-dashboards/components/RecentlyDeletedEmptyState.tsx b/public/app/features/browse-dashboards/components/RecentlyDeletedEmptyState.tsx new file mode 100644 index 00000000000..8415b7551fa --- /dev/null +++ b/public/app/features/browse-dashboards/components/RecentlyDeletedEmptyState.tsx @@ -0,0 +1,22 @@ +import { EmptyState } from '@grafana/ui'; +import { t } from 'app/core/internationalization'; +import { SearchState } from 'app/features/search/types'; + +interface RecentlyDeletedEmptyStateProps { + searchState: SearchState; +} + +export const RecentlyDeletedEmptyState = ({ searchState }: RecentlyDeletedEmptyStateProps) => { + const userIsSearching = Boolean(searchState.query || searchState.tag.length); + return ( + + ); +}; diff --git a/public/app/features/browse-dashboards/components/SearchView.tsx b/public/app/features/browse-dashboards/components/SearchView.tsx index 2893f0abea6..ce28108c706 100644 --- a/public/app/features/browse-dashboards/components/SearchView.tsx +++ b/public/app/features/browse-dashboards/components/SearchView.tsx @@ -1,4 +1,4 @@ -import { useCallback } from 'react'; +import { ReactNode, useCallback } from 'react'; import { DataFrameView, toDataFrame } from '@grafana/data'; import { Button, EmptyState } from '@grafana/ui'; @@ -17,9 +17,10 @@ interface SearchViewProps { canSelect: boolean; searchState: SearchState; searchStateManager: SearchStateManager; + emptyState?: ReactNode; } -const NUM_PLACEHOLDER_ROWS = 50; +const NUM_PLACEHOLDER_ROWS = 25; const initialLoadingView = { view: new DataFrameView( toDataFrame({ @@ -49,6 +50,7 @@ export function SearchView({ canSelect, searchState, searchStateManager: stateManager, + emptyState: emptyStateProp, }: SearchViewProps) { const dispatch = useDispatch(); const selectedItems = useSelector((wholeState) => wholeState.browseDashboards.selectedItems); @@ -94,20 +96,20 @@ export function SearchView({ ); if (value.totalRows === 0) { - return ( -
- - Clear search and filters - - } - message={t('browse-dashboards.no-results.text', 'No results found for your query')} - variant="not-found" - role="alert" - /> -
+ const emptyState = emptyStateProp ?? ( + + Clear search and filters + + } + message={t('browse-dashboards.no-results.text', 'No results found for your query')} + variant="not-found" + role="alert" + /> ); + + return
{emptyState}
; } const props: SearchResultsProps = { diff --git a/public/app/features/search/state/SearchStateManager.ts b/public/app/features/search/state/SearchStateManager.ts index f07c12e6038..8ee43e438ea 100644 --- a/public/app/features/search/state/SearchStateManager.ts +++ b/public/app/features/search/state/SearchStateManager.ts @@ -197,14 +197,14 @@ export class SearchStateManager extends StateManagerBase { }; hasSearchFilters() { - return ( + return Boolean( this.state.query || - this.state.tag.length || - this.state.starred || - this.state.panel_type || - this.state.sort || - this.state.deleted || - this.state.layout === SearchLayout.List + this.state.tag.length || + this.state.starred || + this.state.panel_type || + this.state.sort || + this.state.deleted || + this.state.layout === SearchLayout.List ); } diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json index 695e13df35b..9885a315581 100644 --- a/public/locales/en-US/grafana.json +++ b/public/locales/en-US/grafana.json @@ -1731,7 +1731,8 @@ "restore": "Restore" }, "page": { - "empty-state": "You haven't deleted any dashboards recently." + "no-deleted-dashboards": "You haven't deleted any dashboards recently.", + "no-search-result": "No results found for your query" }, "permanently-delete-modal": { "confirm-text": "Delete", diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json index 9357e02114c..4b0ac98fbba 100644 --- a/public/locales/pseudo-LOCALE/grafana.json +++ b/public/locales/pseudo-LOCALE/grafana.json @@ -1731,7 +1731,8 @@ "restore": "Ŗęşŧőřę" }, "page": { - "empty-state": "Ÿőū ĥävęʼn'ŧ đęľęŧęđ äʼny đäşĥþőäřđş řęčęʼnŧľy." + "no-deleted-dashboards": "Ÿőū ĥävęʼn'ŧ đęľęŧęđ äʼny đäşĥþőäřđş řęčęʼnŧľy.", + "no-search-result": "Ńő řęşūľŧş ƒőūʼnđ ƒőř yőūř qūęřy" }, "permanently-delete-modal": { "confirm-text": "Đęľęŧę",