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äľ",