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 <josh@trtr.co>
This commit is contained in:
Laura Benz
2024-07-15 13:45:33 +03:00
committed by GitHub
co-authored by joshhunt
parent cd6f018c95
commit 29ac7fd3c4
7 changed files with 147 additions and 76 deletions
@@ -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 (
<Page navId="dashboards/recently-deleted">
<Page.Contents>
<EmptyState
variant="completed"
message={t('recently-deleted.page.empty-state', "You haven't deleted any dashboards recently.")}
/>
</Page.Contents>
</Page>
);
}
return (
<Page navId="dashboards/recently-deleted">
<Page.Contents>
{searchState.result && (
<>
<Stack direction="column">
<FilterInput
placeholder={t('recentlyDeleted.filter.placeholder', 'Search for dashboards')}
value={searchState.query}
escapeRegex={false}
onChange={stateManager.onQueryChange}
<Page.Contents className={styles.pageContents}>
<FilterInput
placeholder={t('recentlyDeleted.filter.placeholder', 'Search for dashboards')}
value={searchState.query}
escapeRegex={false}
onChange={stateManager.onQueryChange}
/>
<ActionRow
state={searchState}
getTagOptions={stateManager.getTagOptions}
getSortOptions={getGrafanaSearcher().getSortOptions}
sortPlaceholder={getGrafanaSearcher().sortPlaceholder}
onLayoutChange={stateManager.onLayoutChange}
onSortChange={stateManager.onSortChange}
onTagFilterChange={stateManager.onTagFilterChange}
onDatasourceChange={stateManager.onDatasourceChange}
onPanelTypeChange={stateManager.onPanelTypeChange}
onSetIncludePanels={stateManager.onSetIncludePanels}
/>
<RecentlyDeletedActions />
<div className={styles.subView}>
<AutoSizer>
{({ width, height }) => (
<SearchView
canSelect={canSelect}
width={width}
height={height}
searchStateManager={stateManager}
searchState={searchState}
emptyState={<RecentlyDeletedEmptyState searchState={searchState} />}
/>
<ActionRow
state={searchState}
getTagOptions={stateManager.getTagOptions}
getSortOptions={getGrafanaSearcher().getSortOptions}
sortPlaceholder={getGrafanaSearcher().sortPlaceholder}
onLayoutChange={stateManager.onLayoutChange}
onSortChange={stateManager.onSortChange}
onTagFilterChange={stateManager.onTagFilterChange}
onDatasourceChange={stateManager.onDatasourceChange}
onPanelTypeChange={stateManager.onPanelTypeChange}
onSetIncludePanels={stateManager.onSetIncludePanels}
/>
</Stack>
<RecentlyDeletedActions />
<AutoSizer>
{({ width, height }) => (
<SearchView
canSelect={canSelect}
width={width}
height={height}
searchStateManager={stateManager}
searchState={searchState}
/>
)}
</AutoSizer>
</>
)}
)}
</AutoSizer>
</div>
</Page.Contents>
</Page>
);
});
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;
@@ -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<SearchState>) {
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;
@@ -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 (
<EmptyState
message={
userIsSearching
? t('recently-deleted.page.no-search-result', 'No results found for your query')
: t('recently-deleted.page.no-deleted-dashboards', "You haven't deleted any dashboards recently.")
}
variant={userIsSearching ? 'not-found' : 'completed'}
role="alert"
/>
);
};
@@ -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 (
<div style={{ width }}>
<EmptyState
button={
<Button variant="secondary" onClick={stateManager.onClearSearchAndFilters}>
<Trans i18nKey="browse-dashboards.no-results.clear">Clear search and filters</Trans>
</Button>
}
message={t('browse-dashboards.no-results.text', 'No results found for your query')}
variant="not-found"
role="alert"
/>
</div>
const emptyState = emptyStateProp ?? (
<EmptyState
button={
<Button variant="secondary" onClick={stateManager.onClearSearchAndFilters}>
<Trans i18nKey="browse-dashboards.no-results.clear">Clear search and filters</Trans>
</Button>
}
message={t('browse-dashboards.no-results.text', 'No results found for your query')}
variant="not-found"
role="alert"
/>
);
return <div style={{ width }}>{emptyState}</div>;
}
const props: SearchResultsProps = {
@@ -197,14 +197,14 @@ export class SearchStateManager extends StateManagerBase<SearchState> {
};
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
);
}
+2 -1
View File
@@ -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",
+2 -1
View File
@@ -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": "Đęľęŧę",