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:
@@ -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
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "Đęľęŧę",
|
||||
|
||||
Reference in New Issue
Block a user