From adbb005d388e3811772db231dc3f5bfa48ebeef0 Mon Sep 17 00:00:00 2001
From: Laura Benz <48948963+L-M-K-B@users.noreply.github.com>
Date: Tue, 4 Jun 2024 12:08:18 +0200
Subject: [PATCH] RestoreDashboards: Populate page (#88555)
* feat: add search input and filters
* feat: add state management v0
* feat: update search view and add to trash page
* refactor: fix some errors
* feat: page population
* feat: fix error
* Run betterer checks locally
* Run i18n abstraction
* refactor: clean up after merging main
* refactor: clean up after merging main
* Run i18n abstraction
* refactor: remove browse view
---
.betterer.results | 3 -
.../BrowseDashboardsPage.tsx | 8 ++-
.../components/SearchView.tsx | 15 ++--
.../manage-dashboards/RecentlyDeletedPage.tsx | 72 +++++++++++++++++--
.../utils/useRecentlyDeletedStateManager.ts | 17 +++++
public/app/features/search/page/reporting.ts | 2 +
public/app/features/search/service/sql.ts | 2 +
public/app/features/search/service/types.ts | 1 +
.../search/state/SearchStateManager.ts | 9 ++-
public/app/features/search/types.ts | 1 +
public/app/routes/routes.tsx | 2 +-
public/locales/en-US/grafana.json | 5 ++
public/locales/pseudo-LOCALE/grafana.json | 5 ++
13 files changed, 127 insertions(+), 15 deletions(-)
create mode 100644 public/app/features/manage-dashboards/utils/useRecentlyDeletedStateManager.ts
diff --git a/.betterer.results b/.betterer.results
index e86565d5cf4..2ecd80d4909 100644
--- a/.betterer.results
+++ b/.betterer.results
@@ -4785,9 +4785,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Styles should be written using objects.", "2"],
[0, 0, 0, "Styles should be written using objects.", "3"]
],
- "public/app/features/manage-dashboards/RecentlyDeletedPage.tsx:5381": [
- [0, 0, 0, "No untranslated strings. Wrap text with ", "0"]
- ],
"public/app/features/manage-dashboards/components/ImportDashboardForm.tsx:5381": [
[0, 0, 0, "No untranslated strings. Wrap text with ", "0"],
[0, 0, 0, "No untranslated strings. Wrap text with ", "1"],
diff --git a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx
index e52df3958ae..5e5e588a903 100644
--- a/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx
+++ b/public/app/features/browse-dashboards/BrowseDashboardsPage.tsx
@@ -135,7 +135,13 @@ const BrowseDashboardsPage = memo(({ match }: Props) => {
{({ width, height }) =>
isSearching ? (
-
+
) : (
)
diff --git a/public/app/features/browse-dashboards/components/SearchView.tsx b/public/app/features/browse-dashboards/components/SearchView.tsx
index 48493f7e0bc..54253546200 100644
--- a/public/app/features/browse-dashboards/components/SearchView.tsx
+++ b/public/app/features/browse-dashboards/components/SearchView.tsx
@@ -5,8 +5,8 @@ import { Button, EmptyState } from '@grafana/ui';
import { Trans, t } from 'app/core/internationalization';
import { useKeyNavigationListener } from 'app/features/search/hooks/useSearchKeyboardSelection';
import { SearchResultsProps, SearchResultsTable } from 'app/features/search/page/components/SearchResultsTable';
-import { useSearchStateManager } from 'app/features/search/state/SearchStateManager';
-import { DashboardViewItemKind } from 'app/features/search/types';
+import { SearchStateManager } from 'app/features/search/state/SearchStateManager';
+import { DashboardViewItemKind, SearchState } from 'app/features/search/types';
import { useDispatch, useSelector } from 'app/types';
import { setAllSelection, setItemSelectionState, useHasSelection } from '../state';
@@ -15,6 +15,8 @@ interface SearchViewProps {
height: number;
width: number;
canSelect: boolean;
+ searchState: SearchState;
+ searchStateManager: SearchStateManager;
}
const NUM_PLACEHOLDER_ROWS = 50;
@@ -41,13 +43,18 @@ const initialLoadingView = {
totalRows: NUM_PLACEHOLDER_ROWS,
};
-export function SearchView({ width, height, canSelect }: SearchViewProps) {
+export function SearchView({
+ width,
+ height,
+ canSelect,
+ searchState,
+ searchStateManager: stateManager,
+}: SearchViewProps) {
const dispatch = useDispatch();
const selectedItems = useSelector((wholeState) => wholeState.browseDashboards.selectedItems);
const hasSelection = useHasSelection();
const { keyboardEvents } = useKeyNavigationListener();
- const [searchState, stateManager] = useSearchStateManager();
const value = searchState.result ?? initialLoadingView;
diff --git a/public/app/features/manage-dashboards/RecentlyDeletedPage.tsx b/public/app/features/manage-dashboards/RecentlyDeletedPage.tsx
index fdab67a748b..6994dc243ea 100644
--- a/public/app/features/manage-dashboards/RecentlyDeletedPage.tsx
+++ b/public/app/features/manage-dashboards/RecentlyDeletedPage.tsx
@@ -1,15 +1,77 @@
-import React from 'react';
+import React, { memo, useEffect } from 'react';
+import AutoSizer from 'react-virtualized-auto-sizer';
-import { Page } from '../../core/components/Page/Page';
+import { FilterInput } 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';
+import { getGrafanaSearcher } from 'app/features/search/service';
+
+import { useDispatch } from '../../types';
+import { SearchView } from '../browse-dashboards/components/SearchView';
+import { getFolderPermissions } from '../browse-dashboards/permissions';
+import { setAllSelection } from '../browse-dashboards/state';
+
+import { useRecentlyDeletedStateManager } from './utils/useRecentlyDeletedStateManager';
+
+const RecentlyDeletedPage = memo(() => {
+ const dispatch = useDispatch();
+
+ const [searchState, stateManager] = useRecentlyDeletedStateManager();
+
+ const { canEditFolders, canEditDashboards } = getFolderPermissions();
+ const canSelect = canEditFolders || canEditDashboards;
+
+ useEffect(() => {
+ stateManager.initStateFromUrl(undefined);
+
+ // Clear selected state when folderUID changes
+ dispatch(
+ setAllSelection({
+ isSelected: false,
+ folderUID: undefined,
+ })
+ );
+ }, [dispatch, stateManager]);
-const RecentlyDeletedPage = () => {
return (
- page content
+ stateManager.onQueryChange(e)}
+ />
+
+
+ {({ width, height }) => (
+
+ )}
+
);
-};
+});
+RecentlyDeletedPage.displayName = 'RecentlyDeletedPage';
export default RecentlyDeletedPage;
diff --git a/public/app/features/manage-dashboards/utils/useRecentlyDeletedStateManager.ts b/public/app/features/manage-dashboards/utils/useRecentlyDeletedStateManager.ts
new file mode 100644
index 00000000000..21b90ea0deb
--- /dev/null
+++ b/public/app/features/manage-dashboards/utils/useRecentlyDeletedStateManager.ts
@@ -0,0 +1,17 @@
+import { initialState, SearchStateManager } from '../../search/state/SearchStateManager';
+
+let recentlyDeletedStateManager: SearchStateManager;
+function getRecentlyDeletedStateManager() {
+ if (!recentlyDeletedStateManager) {
+ recentlyDeletedStateManager = new SearchStateManager({ ...initialState, includePanels: false, deleted: true });
+ }
+
+ return recentlyDeletedStateManager;
+}
+
+export function useRecentlyDeletedStateManager() {
+ const stateManager = getRecentlyDeletedStateManager();
+ const state = stateManager.useState();
+
+ return [state, stateManager] as const;
+}
diff --git a/public/app/features/search/page/reporting.ts b/public/app/features/search/page/reporting.ts
index 5b89b070c2e..313f6c3ffb5 100644
--- a/public/app/features/search/page/reporting.ts
+++ b/public/app/features/search/page/reporting.ts
@@ -10,6 +10,7 @@ interface QueryProps {
query: string;
tagCount: number;
includePanels?: boolean;
+ deleted: boolean;
}
export const reportDashboardListViewed = (eventTrackingNamespace: EventTrackingNamespace, query: QueryProps) => {
@@ -47,5 +48,6 @@ const getQuerySearchContext = (query: QueryProps) => {
tagCount: query.tagCount ?? 0,
queryLength: query.query?.length ?? 0,
includePanels: query.includePanels ?? false,
+ deleted: query.deleted ?? false,
};
};
diff --git a/public/app/features/search/service/sql.ts b/public/app/features/search/service/sql.ts
index 2481d6c8165..c5d9d7ce49d 100644
--- a/public/app/features/search/service/sql.ts
+++ b/public/app/features/search/service/sql.ts
@@ -23,6 +23,7 @@ interface APIQuery {
sort?: string;
starred?: boolean;
permission?: PermissionLevelString;
+ deleted?: boolean;
}
// Internal object to hold folderId
@@ -89,6 +90,7 @@ export class SQLSearcher implements GrafanaSearcher {
sort: query.sort,
permission: query.permission,
page,
+ deleted: query.deleted,
},
query
);
diff --git a/public/app/features/search/service/types.ts b/public/app/features/search/service/types.ts
index 88a9e232176..1dc3748b10d 100644
--- a/public/app/features/search/service/types.ts
+++ b/public/app/features/search/service/types.ts
@@ -27,6 +27,7 @@ export interface SearchQuery {
from?: number;
starred?: boolean;
permission?: PermissionLevelString;
+ deleted?: boolean;
}
export interface DashboardQueryResult {
diff --git a/public/app/features/search/state/SearchStateManager.ts b/public/app/features/search/state/SearchStateManager.ts
index 6f0478537e9..c85d6c33d41 100644
--- a/public/app/features/search/state/SearchStateManager.ts
+++ b/public/app/features/search/state/SearchStateManager.ts
@@ -25,6 +25,7 @@ export const initialState: SearchState = {
sort: undefined,
prevSort: undefined,
eventTrackingNamespace: 'dashboard_search',
+ deleted: false,
};
export const defaultQueryParams: SearchQueryParams = {
@@ -62,7 +63,7 @@ export class SearchStateManager extends StateManagerBase {
const prevSort = localStorage.getItem(SEARCH_SELECTED_SORT) ?? undefined;
const sort = layout === SearchLayout.List ? stateFromUrl.sort || prevSort : null;
- stateManager.setState({
+ this.setState({
...initialState,
...stateFromUrl,
layout,
@@ -70,6 +71,7 @@ export class SearchStateManager extends StateManagerBase {
prevSort,
folderUid: folderUid,
eventTrackingNamespace: folderUid ? 'manage_dashboards' : 'dashboard_search',
+ deleted: this.state.deleted,
});
if (doInitialSearch && this.hasSearchFilters()) {
@@ -195,6 +197,7 @@ export class SearchStateManager extends StateManagerBase {
this.state.starred ||
this.state.panel_type ||
this.state.sort ||
+ this.state.deleted ||
this.state.layout === SearchLayout.List
);
}
@@ -210,6 +213,7 @@ export class SearchStateManager extends StateManagerBase {
explain: this.state.explain,
withAllowedActions: this.state.explain, // allowedActions are currently not used for anything on the UI and added only in `explain` mode
starred: this.state.starred,
+ deleted: this.state.deleted,
};
// Only dashboards have additional properties
@@ -243,6 +247,7 @@ export class SearchStateManager extends StateManagerBase {
query: this.state.query,
tagCount: this.state.tag?.length,
includePanels: this.state.includePanels,
+ deleted: this.state.deleted,
};
reportSearchQueryInteraction(this.state.eventTrackingNamespace, trackingInfo);
@@ -294,6 +299,7 @@ export class SearchStateManager extends StateManagerBase {
query: this.state.query,
tagCount: this.state.tag?.length,
includePanels: this.state.includePanels,
+ deleted: this.state.deleted,
});
};
@@ -308,6 +314,7 @@ export class SearchStateManager extends StateManagerBase {
query: this.state.query,
tagCount: this.state.tag?.length,
includePanels: this.state.includePanels,
+ deleted: this.state.deleted,
});
};
}
diff --git a/public/app/features/search/types.ts b/public/app/features/search/types.ts
index 46cd8734b4d..00f54007d8d 100644
--- a/public/app/features/search/types.ts
+++ b/public/app/features/search/types.ts
@@ -101,6 +101,7 @@ export interface SearchState {
folderUid?: string;
includePanels?: boolean;
eventTrackingNamespace: EventTrackingNamespace;
+ deleted: boolean;
}
export type OnToggleChecked = (item: DashboardViewItem) => void;
diff --git a/public/app/routes/routes.tsx b/public/app/routes/routes.tsx
index 290affa4bd2..c8556ffc06a 100644
--- a/public/app/routes/routes.tsx
+++ b/public/app/routes/routes.tsx
@@ -439,7 +439,7 @@ export function getAppRoutes(): RouteDescriptor[] {
path: '/dashboard/recentlyDeleted',
roles: () => contextSrv.evaluatePermission([AccessControlAction.DashboardsDelete]),
component: SafeDynamicImport(
- () => import(/* webpackChunkName: "TrashPage" */ '../features/manage-dashboards/RecentlyDeletedPage')
+ () => import(/* webpackChunkName: "RecentlyDeletedPage" */ 'app/features/manage-dashboards/RecentlyDeletedPage')
),
},
{
diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json
index 9c32693051d..cff214e5231 100644
--- a/public/locales/en-US/grafana.json
+++ b/public/locales/en-US/grafana.json
@@ -1478,6 +1478,11 @@
},
"query-editor-not-exported": "Data source plugin does not export any Query Editor component"
},
+ "recentlyDeleted": {
+ "filter": {
+ "placeholder": "Search for dashboards"
+ }
+ },
"refresh-picker": {
"aria-label": {
"choose-interval": "Auto refresh turned off. Choose refresh time interval",
diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json
index d75e1a7b167..8698cebe351 100644
--- a/public/locales/pseudo-LOCALE/grafana.json
+++ b/public/locales/pseudo-LOCALE/grafana.json
@@ -1478,6 +1478,11 @@
},
"query-editor-not-exported": "Đäŧä şőūřčę pľūģįʼn đőęş ʼnőŧ ęχpőřŧ äʼny Qūęřy Ēđįŧőř čőmpőʼnęʼnŧ"
},
+ "recentlyDeleted": {
+ "filter": {
+ "placeholder": "Ŝęäřčĥ ƒőř đäşĥþőäřđş"
+ }
+ },
"refresh-picker": {
"aria-label": {
"choose-interval": "Åūŧő řęƒřęşĥ ŧūřʼnęđ őƒƒ. Cĥőőşę řęƒřęşĥ ŧįmę įʼnŧęřväľ",