Restore dashboards: Use k8s API (#105823)
* Add dashboardRestore toggle * Restore the toggle on FE * Add navtree item * Add restore dashboard call * Fix lint * Add transformer * Rename feature toggle * Use the renamed toggle * update API url * List deleted dashboards from sql searcher * Upd imports * Remove permanently delete action * Fix type assertions * Add restore endpoints to the api * Fix unified api * Use db api in search * Fix resource version * Add tests * Cleanup * Extract getting db * Add listDeletedDashboards * Add listDeletedDashboards * Enable restoring dashboards * Restore to a folder * Extract logic * Remove type assertion * betterer * Cleanup * i18n
This commit is contained in:
@@ -2788,13 +2788,6 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"]
|
||||
],
|
||||
"public/app/features/search/service/unified.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"],
|
||||
[0, 0, 0, "Do not use any type assertions.", "1"]
|
||||
],
|
||||
"public/app/features/search/service/utils.ts:5381": [
|
||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||
],
|
||||
"public/app/features/search/state/SearchStateManager.ts:5381": [
|
||||
[0, 0, 0, "Direct usage of localStorage is not allowed. import store from @grafana/data instead", "0"],
|
||||
[0, 0, 0, "Direct usage of localStorage is not allowed. import store from @grafana/data instead", "1"],
|
||||
|
||||
@@ -9,6 +9,7 @@ import { folderAPIv1beta1 as folderAPI } from 'app/api/clients/folder/v1beta1';
|
||||
import { createBaseQuery, handleRequestError } from 'app/api/createBaseQuery';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { contextSrv } from 'app/core/core';
|
||||
import { Resource, ResourceList } from 'app/features/apiserver/types';
|
||||
import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api';
|
||||
import { isDashboardV2Resource, isV1DashboardCommand, isV2DashboardCommand } from 'app/features/dashboard/api/utils';
|
||||
import { SaveDashboardCommand } from 'app/features/dashboard/components/SaveDashboard/types';
|
||||
@@ -53,12 +54,7 @@ interface ImportOptions {
|
||||
}
|
||||
|
||||
interface RestoreDashboardArgs {
|
||||
dashboardUID: string;
|
||||
targetFolderUID: string;
|
||||
}
|
||||
|
||||
interface HardDeleteDashboardArgs {
|
||||
dashboardUID: string;
|
||||
dashboard: Resource<Dashboard | DashboardV2Spec>;
|
||||
}
|
||||
|
||||
export interface ListFolderQueryArgs {
|
||||
@@ -419,42 +415,39 @@ export const browseDashboardsAPI = createApi({
|
||||
},
|
||||
}),
|
||||
|
||||
// restore a dashboard that got soft deleted
|
||||
restoreDashboard: builder.mutation<void, RestoreDashboardArgs>({
|
||||
query: ({ dashboardUID, targetFolderUID }) => ({
|
||||
url: `/dashboards/uid/${dashboardUID}/trash`,
|
||||
body: {
|
||||
folderUid: targetFolderUID,
|
||||
},
|
||||
method: 'PATCH',
|
||||
}),
|
||||
// RTK wrapper for the dashboard API
|
||||
listDeletedDashboards: builder.query<ResourceList<Dashboard | DashboardV2Spec>, void>({
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const api = getDashboardAPI();
|
||||
const response = await api.listDeletedDashboards({});
|
||||
|
||||
return { data: response };
|
||||
} catch (error) {
|
||||
return handleRequestError(error);
|
||||
}
|
||||
},
|
||||
}),
|
||||
|
||||
// permanently delete a dashboard. used in PermanentlyDeleteModal.
|
||||
hardDeleteDashboard: builder.mutation<void, HardDeleteDashboardArgs>({
|
||||
queryFn: async ({ dashboardUID }, _api, _extraOptions, baseQuery) => {
|
||||
const response = await baseQuery({
|
||||
url: `/dashboards/uid/${dashboardUID}/trash`,
|
||||
method: 'DELETE',
|
||||
showSuccessAlert: false,
|
||||
});
|
||||
// restore a dashboard that got deleted
|
||||
restoreDashboard: builder.mutation<void, RestoreDashboardArgs>({
|
||||
queryFn: async ({ dashboard }) => {
|
||||
try {
|
||||
const api = getDashboardAPI();
|
||||
const response = await api.restoreDashboard(dashboard);
|
||||
const name = response.spec.title;
|
||||
|
||||
// @ts-expect-error
|
||||
const name = response?.data?.title;
|
||||
if (name) {
|
||||
appEvents.publish({
|
||||
type: AppEvents.alertSuccess.name,
|
||||
payload: [t('browse-dashboards.restore.success', 'Dashboard {{name}} restored', { name })],
|
||||
});
|
||||
}
|
||||
|
||||
if (name) {
|
||||
appEvents.publish({
|
||||
type: AppEvents.alertSuccess.name,
|
||||
payload: [t('browse-dashboards.hard-delete.success', 'Dashboard {{name}} deleted', { name })],
|
||||
});
|
||||
return { data: undefined };
|
||||
} catch (error) {
|
||||
return handleRequestError(error);
|
||||
}
|
||||
|
||||
return { data: undefined };
|
||||
},
|
||||
onQueryStarted: ({ dashboardUID }, { queryFulfilled, dispatch }) => {
|
||||
queryFulfilled.then(() => {
|
||||
dispatch(refreshParents([dashboardUID]));
|
||||
});
|
||||
},
|
||||
}),
|
||||
}),
|
||||
@@ -473,5 +466,5 @@ export const {
|
||||
useSaveDashboardMutation,
|
||||
useSaveFolderMutation,
|
||||
useRestoreDashboardMutation,
|
||||
useHardDeleteDashboardMutation,
|
||||
useListDeletedDashboardsQuery,
|
||||
} = browseDashboardsAPI;
|
||||
|
||||
@@ -3,25 +3,24 @@ import { useMemo } from 'react';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { Button, Stack } from '@grafana/ui';
|
||||
import appEvents from 'app/core/app_events';
|
||||
import { AnnoKeyFolder } from 'app/features/apiserver/types';
|
||||
import { GENERAL_FOLDER_UID } from 'app/features/search/constants';
|
||||
import { useDispatch } from 'app/types';
|
||||
import { ShowModalReactEvent } from 'app/types/events';
|
||||
|
||||
import appEvents from '../../../core/app_events';
|
||||
import { useDispatch } from '../../../types';
|
||||
import { ShowModalReactEvent } from '../../../types/events';
|
||||
import { useHardDeleteDashboardMutation, useRestoreDashboardMutation } from '../api/browseDashboardsAPI';
|
||||
import { useListDeletedDashboardsQuery, useRestoreDashboardMutation } from '../api/browseDashboardsAPI';
|
||||
import { useRecentlyDeletedStateManager } from '../api/useRecentlyDeletedStateManager';
|
||||
import { clearFolders, setAllSelection, useActionSelectionState } from '../state';
|
||||
|
||||
import { PermanentlyDeleteModal } from './PermanentlyDeleteModal';
|
||||
import { RestoreModal } from './RestoreModal';
|
||||
|
||||
export function RecentlyDeletedActions() {
|
||||
const dispatch = useDispatch();
|
||||
const selectedItemsState = useActionSelectionState();
|
||||
const [searchState, stateManager] = useRecentlyDeletedStateManager();
|
||||
|
||||
const deletedDashboards = useListDeletedDashboardsQuery();
|
||||
const [restoreDashboard, { isLoading: isRestoreLoading }] = useRestoreDashboardMutation();
|
||||
const [deleteDashboard, { isLoading: isDeleteLoading }] = useHardDeleteDashboardMutation();
|
||||
|
||||
const selectedDashboards = useMemo(() => {
|
||||
return Object.entries(selectedItemsState.dashboard)
|
||||
@@ -34,7 +33,7 @@ export function RecentlyDeletedActions() {
|
||||
for (const selectedDashboard of selectedDashboards) {
|
||||
const index = searchState.result.view.fields.uid.values.findIndex((e) => e === selectedDashboard);
|
||||
|
||||
// SQLSearcher changes the location from empty string to 'general' for items with no parent
|
||||
// SQLSearcher changes the location from empty string to 'general' for items with no parent,
|
||||
// but the restore API doesn't work with 'general' folder UID, so we need to convert it back
|
||||
// to an empty string
|
||||
const location = searchState.result.view.fields.location.values[index];
|
||||
@@ -56,7 +55,18 @@ export function RecentlyDeletedActions() {
|
||||
}
|
||||
|
||||
const promises = selectedDashboards.map((uid) => {
|
||||
return restoreDashboard({ dashboardUID: uid, targetFolderUID: restoreTarget });
|
||||
const dashboard = deletedDashboards.data?.items.find((d) => d.metadata.name === uid);
|
||||
if (!dashboard) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
// Clone the dashboard to be able to edit the immutable data from the store
|
||||
const copy = structuredClone(dashboard);
|
||||
copy.metadata = {
|
||||
...copy.metadata,
|
||||
annotations: { ...copy.metadata?.annotations, [AnnoKeyFolder]: restoreTarget },
|
||||
};
|
||||
|
||||
return restoreDashboard({ dashboard: copy });
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
@@ -78,13 +88,6 @@ export function RecentlyDeletedActions() {
|
||||
onActionComplete();
|
||||
};
|
||||
|
||||
const onDelete = async () => {
|
||||
const promises = selectedDashboards.map((uid) => deleteDashboard({ dashboardUID: uid }));
|
||||
|
||||
await Promise.all(promises);
|
||||
onActionComplete();
|
||||
};
|
||||
|
||||
const showRestoreModal = () => {
|
||||
reportInteraction('grafana_restore_clicked', {
|
||||
item_counts: {
|
||||
@@ -104,32 +107,11 @@ export function RecentlyDeletedActions() {
|
||||
);
|
||||
};
|
||||
|
||||
const showDeleteModal = () => {
|
||||
reportInteraction('grafana_delete_permanently_clicked', {
|
||||
item_counts: {
|
||||
dashboard: selectedDashboards.length,
|
||||
},
|
||||
});
|
||||
appEvents.publish(
|
||||
new ShowModalReactEvent({
|
||||
component: PermanentlyDeleteModal,
|
||||
props: {
|
||||
selectedDashboards,
|
||||
onConfirm: onDelete,
|
||||
isLoading: isDeleteLoading,
|
||||
},
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack gap={1}>
|
||||
<Button onClick={showRestoreModal} variant="secondary">
|
||||
<Trans i18nKey="recently-deleted.buttons.restore">Restore</Trans>
|
||||
</Button>
|
||||
<Button onClick={showDeleteModal} variant="destructive">
|
||||
<Trans i18nKey="recently-deleted.buttons.delete">Permanently delete</Trans>
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,13 +2,15 @@ import { DataFrame, DataFrameView, FieldType, getDisplayProcessor, SelectableVal
|
||||
import { config } from '@grafana/runtime';
|
||||
import { TermCount } from 'app/core/components/TagFilter/TagFilter';
|
||||
import { backendSrv } from 'app/core/services/backend_srv';
|
||||
import { PermissionLevelString } from 'app/types';
|
||||
import { isResourceList } from 'app/features/apiserver/guards';
|
||||
import { getDashboardAPI } from 'app/features/dashboard/api/dashboard_api';
|
||||
import { DashboardDataDTO, PermissionLevelString } from 'app/types';
|
||||
|
||||
import { DEFAULT_MAX_VALUES, GENERAL_FOLDER_UID, TYPE_KIND_MAP } from '../constants';
|
||||
import { DashboardSearchHit, DashboardSearchItemType } from '../types';
|
||||
|
||||
import { DashboardQueryResult, GrafanaSearcher, LocationInfo, QueryResponse, SearchQuery, SortOptions } from './types';
|
||||
import { replaceCurrentFolderQuery } from './utils';
|
||||
import { replaceCurrentFolderQuery, resourceToSearchResult, searchHitsToDashboardSearchHits } from './utils';
|
||||
|
||||
interface APIQuery {
|
||||
query?: string;
|
||||
@@ -116,22 +118,6 @@ export class SQLSearcher implements GrafanaSearcher {
|
||||
|
||||
// returns the appropriate sorting options
|
||||
async getSortOptions(): Promise<SelectableValue[]> {
|
||||
// {
|
||||
// "sortOptions": [
|
||||
// {
|
||||
// "description": "Sort results in an alphabetically ascending order",
|
||||
// "displayName": "Alphabetically (A–Z)",
|
||||
// "meta": "",
|
||||
// "name": "alpha-asc"
|
||||
// },
|
||||
// {
|
||||
// "description": "Sort results in an alphabetically descending order",
|
||||
// "displayName": "Alphabetically (Z–A)",
|
||||
// "meta": "",
|
||||
// "name": "alpha-desc"
|
||||
// }
|
||||
// ]
|
||||
// }
|
||||
const opts = await backendSrv.get<SortOptions>('/api/search/sorting');
|
||||
return opts.sortOptions.map((v) => ({
|
||||
value: v.name,
|
||||
@@ -146,7 +132,22 @@ export class SQLSearcher implements GrafanaSearcher {
|
||||
}
|
||||
|
||||
async doAPIQuery(query: APIQuery): Promise<QueryResponse> {
|
||||
const rsp = await backendSrv.get<DashboardSearchHit[]>('/api/search', query);
|
||||
let rsp: DashboardSearchHit[];
|
||||
|
||||
if (query.deleted) {
|
||||
// Deleted dashboards are fetched from a k8s API
|
||||
const api = getDashboardAPI();
|
||||
const deletedResponse = await api.listDeletedDashboards({});
|
||||
|
||||
if (isResourceList<DashboardDataDTO>(deletedResponse)) {
|
||||
const searchHits = resourceToSearchResult(deletedResponse);
|
||||
rsp = searchHitsToDashboardSearchHits(searchHits);
|
||||
} else {
|
||||
rsp = [];
|
||||
}
|
||||
} else {
|
||||
rsp = await backendSrv.get<DashboardSearchHit[]>('/api/search', query);
|
||||
}
|
||||
|
||||
// Field values (columnar)
|
||||
const kind: string[] = [];
|
||||
|
||||
@@ -3,10 +3,11 @@ import { isEmpty } from 'lodash';
|
||||
import { DataFrame, DataFrameView, getDisplayProcessor, SelectableValue, toDataFrame } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { config, getBackendSrv } from '@grafana/runtime';
|
||||
import { getAPIBaseURL } from 'app/api/utils';
|
||||
import { TermCount } from 'app/core/components/TagFilter/TagFilter';
|
||||
import kbn from 'app/core/utils/kbn';
|
||||
|
||||
import { getAPINamespace } from '../../../api/utils';
|
||||
import { isResourceList } from 'app/features/apiserver/guards';
|
||||
import { DashboardDataDTO } from 'app/types';
|
||||
|
||||
import {
|
||||
DashboardQueryResult,
|
||||
@@ -16,13 +17,14 @@ import {
|
||||
SearchQuery,
|
||||
SearchResultMeta,
|
||||
} from './types';
|
||||
import { replaceCurrentFolderQuery } from './utils';
|
||||
import { replaceCurrentFolderQuery, resourceToSearchResult } from './utils';
|
||||
|
||||
// The backend returns an empty frame with a special name to indicate that the indexing engine is being rebuilt,
|
||||
// and that it can not serve any search requests. We are temporarily using the old SQL Search API as a fallback when that happens.
|
||||
const loadingFrameName = 'Loading';
|
||||
|
||||
const searchURI = `apis/dashboard.grafana.app/v0alpha1/namespaces/${getAPINamespace()}/search`;
|
||||
const baseURL = getAPIBaseURL('dashboard.grafana.app', 'v0alpha1');
|
||||
const searchURI = `${baseURL}/search`;
|
||||
|
||||
export type SearchHit = {
|
||||
resource: string; // dashboards | folders
|
||||
@@ -120,9 +122,20 @@ export class UnifiedSearcher implements GrafanaSearcher {
|
||||
return this.fallbackSearcher.search(query);
|
||||
}
|
||||
|
||||
const meta = first.meta?.custom || ({} as SearchResultMeta);
|
||||
const customMeta = first.meta?.custom;
|
||||
const meta: SearchResultMeta = {
|
||||
count: customMeta?.count ?? first.length,
|
||||
max_score: customMeta?.max_score ?? 1,
|
||||
locationInfo: customMeta?.locationInfo ?? {},
|
||||
sortBy: customMeta?.sortBy,
|
||||
};
|
||||
meta.locationInfo = await this.locationInfo;
|
||||
|
||||
// Update the DataFrame meta to point to the typed meta object
|
||||
if (first.meta) {
|
||||
first.meta.custom = meta;
|
||||
}
|
||||
|
||||
// Set the field name to a better display name
|
||||
if (meta.sortBy?.length) {
|
||||
const field = first.fields.find((f) => f.name === meta.sortBy);
|
||||
@@ -165,10 +178,12 @@ export class UnifiedSearcher implements GrafanaSearcher {
|
||||
view.dataFrame.length = length;
|
||||
|
||||
// Add all the location lookup info
|
||||
const submeta = frame.meta?.custom as SearchResultMeta;
|
||||
const submeta = frame.meta?.custom;
|
||||
if (submeta?.locationInfo && meta) {
|
||||
for (const [key, value] of Object.entries(submeta.locationInfo)) {
|
||||
meta.locationInfo[key] = value;
|
||||
// Merge locationInfo from submeta into meta
|
||||
const subLocationInfo = submeta.locationInfo;
|
||||
if (subLocationInfo && typeof subLocationInfo === 'object') {
|
||||
Object.assign(meta.locationInfo, subLocationInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -194,11 +209,16 @@ export class UnifiedSearcher implements GrafanaSearcher {
|
||||
|
||||
async fetchResponse(uri: string) {
|
||||
const rsp = await getBackendSrv().get<SearchAPIResponse>(uri);
|
||||
if (isResourceList<DashboardDataDTO>(rsp)) {
|
||||
const hits = resourceToSearchResult(rsp);
|
||||
const totalHits = rsp.items.length;
|
||||
return { ...rsp, hits, totalHits };
|
||||
}
|
||||
const isFolderCacheStale = await this.isFolderCacheStale(rsp.hits);
|
||||
if (!isFolderCacheStale) {
|
||||
return rsp;
|
||||
}
|
||||
// sync the location info ( folders )
|
||||
// sync the location info (folders)
|
||||
this.locationInfo = loadLocationInfo();
|
||||
// recheck for missing folders
|
||||
const hasMissing = await this.isFolderCacheStale(rsp.hits);
|
||||
@@ -212,13 +232,14 @@ export class UnifiedSearcher implements GrafanaSearcher {
|
||||
return { ...hit, location: 'general', folder: 'general' };
|
||||
}
|
||||
|
||||
// this means user has permission to see this dashboard, but not the folder contents
|
||||
// this means a user has permission to see this dashboard, but not the folder contents
|
||||
if (locationInfo[hit.folder] === undefined) {
|
||||
return { ...hit, location: 'sharedwithme', folder: 'sharedwithme' };
|
||||
}
|
||||
|
||||
return hit;
|
||||
});
|
||||
|
||||
const totalHits = rsp.totalHits - (rsp.hits.length - hits.length);
|
||||
return { ...rsp, hits, totalHits };
|
||||
}
|
||||
@@ -266,6 +287,10 @@ export class UnifiedSearcher implements GrafanaSearcher {
|
||||
// legacy support for filtering by dashboard uid
|
||||
uri += '&' + query.uid.map((name) => `name=${encodeURIComponent(name)}`).join('&');
|
||||
}
|
||||
|
||||
if (query.deleted) {
|
||||
uri = `${getAPIBaseURL('dashboard.grafana.app', 'v1beta1')}/dashboards/?labelSelector=grafana.app/get-trash=true`;
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,12 @@ import { isSharedWithMe } from 'app/features/browse-dashboards/components/utils'
|
||||
import { DashboardViewItemWithUIItems } from 'app/features/browse-dashboards/types';
|
||||
import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv';
|
||||
|
||||
import { DashboardViewItem, DashboardViewItemKind } from '../types';
|
||||
import { DashboardDataDTO } from '../../../types';
|
||||
import { AnnoKeyFolder, ResourceList } from '../../apiserver/types';
|
||||
import { DashboardSearchHit, DashboardSearchItemType, DashboardViewItem, DashboardViewItemKind } from '../types';
|
||||
|
||||
import { DashboardQueryResult, SearchQuery, SearchResultMeta } from './types';
|
||||
import { SearchHit } from './unified';
|
||||
|
||||
/** prepare the query replacing folder:current */
|
||||
export async function replaceCurrentFolderQuery(query: SearchQuery): Promise<SearchQuery> {
|
||||
@@ -64,25 +67,6 @@ export function getIconForItem(item: DashboardViewItemWithUIItems, isOpen?: bool
|
||||
}
|
||||
}
|
||||
|
||||
// export function getIconForItem(itemOrKind: string | DashboardViewItemWithUIItems, isOpen?: boolean): IconName {
|
||||
// const kind = typeof itemOrKind === 'string' ? itemOrKind : itemOrKind.kind;
|
||||
// const item = typeof itemOrKind === 'string' ? undefined : itemOrKind;
|
||||
|
||||
// if (kind === 'dashboard') {
|
||||
// return 'apps';
|
||||
// }
|
||||
|
||||
// if (item && isSharedWithMe(item.uid)) {
|
||||
// return 'users-alt';
|
||||
// }
|
||||
|
||||
// if (kind === 'folder') {
|
||||
// return isOpen ? 'folder-open' : 'folder';
|
||||
// }
|
||||
|
||||
// return 'question-circle';
|
||||
// }
|
||||
|
||||
function parseKindString(kind: string): DashboardViewItemKind {
|
||||
switch (kind) {
|
||||
case 'dashboard':
|
||||
@@ -94,11 +78,16 @@ function parseKindString(kind: string): DashboardViewItemKind {
|
||||
}
|
||||
}
|
||||
|
||||
function isSearchResultMeta(obj: unknown): obj is SearchResultMeta {
|
||||
return obj !== null && typeof obj === 'object' && 'locationInfo' in obj;
|
||||
}
|
||||
|
||||
export function queryResultToViewItem(
|
||||
item: DashboardQueryResult,
|
||||
view?: DataFrameView<DashboardQueryResult>
|
||||
): DashboardViewItem {
|
||||
const meta = view?.dataFrame.meta?.custom as SearchResultMeta | undefined;
|
||||
const customMeta = view?.dataFrame.meta?.custom;
|
||||
const meta: SearchResultMeta | undefined = isSearchResultMeta(customMeta) ? customMeta : undefined;
|
||||
|
||||
const viewItem: DashboardViewItem = {
|
||||
kind: parseKindString(item.kind),
|
||||
@@ -132,3 +121,43 @@ export function queryResultToViewItem(
|
||||
|
||||
return viewItem;
|
||||
}
|
||||
|
||||
export function resourceToSearchResult(resource: ResourceList<DashboardDataDTO>): SearchHit[] {
|
||||
return resource.items.map((item) => {
|
||||
const hit = {
|
||||
resource: 'dashboards',
|
||||
name: item.metadata.name,
|
||||
title: item.spec.title,
|
||||
location: 'general',
|
||||
folder: item?.metadata?.annotations?.[AnnoKeyFolder] ?? 'general',
|
||||
tags: item.spec.tags || [],
|
||||
field: {},
|
||||
url: '',
|
||||
};
|
||||
if (!hit.folder) {
|
||||
return { ...hit, location: 'general', folder: 'general' };
|
||||
}
|
||||
|
||||
return hit;
|
||||
});
|
||||
}
|
||||
|
||||
export function searchHitsToDashboardSearchHits(searchHits: SearchHit[]): DashboardSearchHit[] {
|
||||
return searchHits.map((hit) => {
|
||||
const dashboardHit: DashboardSearchHit = {
|
||||
type: hit.resource === 'folders' ? DashboardSearchItemType.DashFolder : DashboardSearchItemType.DashDB,
|
||||
title: hit.title,
|
||||
uid: hit.name, // k8s name is the uid
|
||||
url: hit.url,
|
||||
tags: hit.tags || [],
|
||||
isDeleted: true, // All results from trash are deleted
|
||||
sortMeta: 0, // Default value for deleted items
|
||||
};
|
||||
|
||||
if (hit.folder && hit.folder !== 'general') {
|
||||
dashboardHit.folderUid = hit.folder;
|
||||
}
|
||||
|
||||
return dashboardHit;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3267,9 +3267,6 @@
|
||||
"search-placeholder": "Search folders",
|
||||
"unknown-error": "Unknown error"
|
||||
},
|
||||
"hard-delete": {
|
||||
"success": "Dashboard {{name}} deleted"
|
||||
},
|
||||
"manage-folder-nav": {
|
||||
"alert-rules": "Alert rules",
|
||||
"dashboards": "Dashboards",
|
||||
@@ -3307,6 +3304,9 @@
|
||||
"clear": "Clear search and filters",
|
||||
"text": "No results found for your query"
|
||||
},
|
||||
"restore": {
|
||||
"success": "Dashboard {{name}} restored"
|
||||
},
|
||||
"text-this-repository-is-read-only": "If you have direct access to the target, copy the JSON and paste it there."
|
||||
},
|
||||
"canvas": {
|
||||
@@ -9750,7 +9750,6 @@
|
||||
},
|
||||
"recently-deleted": {
|
||||
"buttons": {
|
||||
"delete": "Permanently delete",
|
||||
"restore": "Restore"
|
||||
},
|
||||
"page": {
|
||||
|
||||
Reference in New Issue
Block a user