diff --git a/public/app/features/search/page/components/columns.tsx b/public/app/features/search/page/components/columns.tsx
index 61d3d0beb8f..22aea99cc16 100644
--- a/public/app/features/search/page/components/columns.tsx
+++ b/public/app/features/search/page/components/columns.tsx
@@ -1,4 +1,5 @@
import { cx } from '@emotion/css';
+import { intervalToDuration } from 'date-fns';
import React from 'react';
import Skeleton from 'react-loading-skeleton';
@@ -11,9 +12,10 @@ import {
getFieldDisplayName,
} from '@grafana/data';
import { config, getDataSourceSrv } from '@grafana/runtime';
-import { Checkbox, Icon, IconName, TagList, Text } from '@grafana/ui';
+import { Checkbox, Icon, IconName, TagList, Text, Tooltip } from '@grafana/ui';
import appEvents from 'app/core/app_events';
import { t } from 'app/core/internationalization';
+import { formatDate, formatDuration } from 'app/core/internationalization/dates';
import { PluginIconName } from 'app/features/plugins/admin/types';
import { ShowModalReactEvent } from 'app/types/events';
@@ -25,6 +27,7 @@ import { ExplainScorePopup } from './ExplainScorePopup';
import { TableColumn } from './SearchResultsTable';
const TYPE_COLUMN_WIDTH = 175;
+const DURATION_COLUMN_WIDTH = 200;
const DATASOURCE_COLUMN_WIDTH = 200;
export const generateColumns = (
@@ -112,15 +115,20 @@ export const generateColumns = (
Cell: (p) => {
let classNames = cx(styles.nameCellStyle);
let name = access.name.values[p.row.index];
+ const isDeleted = access.isDeleted?.values[p.row.index];
+
if (!name?.length) {
const loading = p.row.index >= response.view.dataFrame.length;
name = loading ? 'Loading...' : 'Missing title'; // normal for panels
classNames += ' ' + styles.missingTitleText;
}
+
return (
{!response.isItemLoaded(p.row.index) ? (
+ ) : isDeleted ? (
+
{name}
) : (
{name}
@@ -136,9 +144,18 @@ export const generateColumns = (
});
availableWidth -= width;
- width = TYPE_COLUMN_WIDTH;
- columns.push(makeTypeColumn(response, access.kind, access.panel_type, width, styles));
- availableWidth -= width;
+ const showDeletedRemaining =
+ response.view.fields.permanentlyDeleteDate && hasValue(response.view.fields.permanentlyDeleteDate);
+
+ if (showDeletedRemaining && access.permanentlyDeleteDate) {
+ width = DURATION_COLUMN_WIDTH;
+ columns.push(makeDeletedRemainingColumn(response, access.permanentlyDeleteDate, width, styles));
+ availableWidth -= width;
+ } else {
+ width = TYPE_COLUMN_WIDTH;
+ columns.push(makeTypeColumn(response, access.kind, access.panel_type, width, styles));
+ availableWidth -= width;
+ }
// Show datasources if we have any
if (access.ds_uid && onDatasourceChange) {
@@ -328,6 +345,46 @@ function makeDataSourceColumn(
};
}
+function makeDeletedRemainingColumn(
+ response: QueryResponse,
+ deletedField: Field,
+ width: number,
+ styles: Record
+): TableColumn {
+ return {
+ id: 'column-delete-age',
+ field: deletedField,
+ width,
+ Header: t('search.results-table.deleted-remaining-header', 'Time remaining'),
+ Cell: (p) => {
+ const i = p.row.index;
+ const deletedDate = deletedField.values[i];
+
+ if (!deletedDate || !response.isItemLoaded(p.row.index)) {
+ return (
+
+
+
+ );
+ }
+
+ const duration = calcCoarseDuration(new Date(), deletedDate);
+ const isDeletingSoon = !Object.values(duration).some((v) => v > 0);
+ const formatted = isDeletingSoon
+ ? t('search.results-table.deleted-less-than-1-min', '< 1 min')
+ : formatDuration(duration, { style: 'long' });
+
+ return (
+
+
+ {formatted}
+
+
+ );
+ },
+ };
+}
+
function makeTypeColumn(
response: QueryResponse,
kindField: Field,
@@ -442,3 +499,22 @@ function getDisplayValue({
}
return formattedValueToString(getDisplay(value));
}
+
+/**
+ * Calculates the rough duration between two dates, keeping only the most significant unit
+ */
+function calcCoarseDuration(start: Date, end: Date) {
+ let { years = 0, months = 0, days = 0, hours = 0, minutes = 0 } = intervalToDuration({ start, end });
+
+ if (years > 0) {
+ return { years };
+ } else if (months > 0) {
+ return { months };
+ } else if (days > 0) {
+ return { days };
+ } else if (hours > 0) {
+ return { hours };
+ }
+
+ return { minutes };
+}
diff --git a/public/app/features/search/service/sql.ts b/public/app/features/search/service/sql.ts
index 15f4bff23b8..d3999434813 100644
--- a/public/app/features/search/service/sql.ts
+++ b/public/app/features/search/service/sql.ts
@@ -158,6 +158,8 @@ export class SQLSearcher implements GrafanaSearcher {
const tags: string[][] = [];
const location: string[] = [];
const sortBy: number[] = [];
+ const isDeleted: boolean[] = [];
+ const permanentlyDeleteDate: Array = [];
let sortMetaName: string | undefined;
for (let hit of rsp) {
@@ -168,6 +170,8 @@ export class SQLSearcher implements GrafanaSearcher {
url.push(hit.url);
tags.push(hit.tags);
sortBy.push(hit.sortMeta!);
+ isDeleted.push(hit.isDeleted ?? false);
+ permanentlyDeleteDate.push(hit.permanentlyDeleteDate ? new Date(hit.permanentlyDeleteDate) : undefined);
let v = hit.folderUid;
if (!v && k === 'dashboard') {
@@ -204,6 +208,8 @@ export class SQLSearcher implements GrafanaSearcher {
{ name: 'url', type: FieldType.string, config: {}, values: url },
{ name: 'tags', type: FieldType.other, config: {}, values: tags },
{ name: 'location', type: FieldType.string, config: {}, values: location },
+ { name: 'isDeleted', type: FieldType.boolean, config: {}, values: isDeleted },
+ { name: 'permanentlyDeleteDate', type: FieldType.time, config: {}, values: permanentlyDeleteDate },
],
length: name.length,
meta: {
diff --git a/public/app/features/search/service/types.ts b/public/app/features/search/service/types.ts
index 1dc3748b10d..83ceeb6758a 100644
--- a/public/app/features/search/service/types.ts
+++ b/public/app/features/search/service/types.ts
@@ -39,6 +39,8 @@ export interface DashboardQueryResult {
tags: string[];
location: string; // url that can be split
ds_uid: string[];
+ isDeleted?: boolean;
+ permanentlyDeleteDate?: Date;
// debugging fields
score: number;
diff --git a/public/app/features/search/types.ts b/public/app/features/search/types.ts
index 00f54007d8d..975e6135f12 100644
--- a/public/app/features/search/types.ts
+++ b/public/app/features/search/types.ts
@@ -30,6 +30,8 @@ export interface DashboardSearchHit extends WithAccessControlMetadata {
url: string;
sortMeta?: number;
sortMetaName?: string;
+ isDeleted?: boolean;
+ permanentlyDeleteDate?: string;
}
/**
diff --git a/public/locales/en-US/grafana.json b/public/locales/en-US/grafana.json
index 66cd0278d59..9023c103880 100644
--- a/public/locales/en-US/grafana.json
+++ b/public/locales/en-US/grafana.json
@@ -1673,6 +1673,8 @@
},
"results-table": {
"datasource-header": "Data source",
+ "deleted-less-than-1-min": "< 1 min",
+ "deleted-remaining-header": "Time remaining",
"location-header": "Location",
"name-header": "Name",
"tags-header": "Tags",
diff --git a/public/locales/pseudo-LOCALE/grafana.json b/public/locales/pseudo-LOCALE/grafana.json
index ce0e02527a6..5da2cc41ad0 100644
--- a/public/locales/pseudo-LOCALE/grafana.json
+++ b/public/locales/pseudo-LOCALE/grafana.json
@@ -1673,6 +1673,8 @@
},
"results-table": {
"datasource-header": "Đäŧä şőūřčę",
+ "deleted-less-than-1-min": "< 1 mįʼn",
+ "deleted-remaining-header": "Ŧįmę řęmäįʼnįʼnģ",
"location-header": "Ŀőčäŧįőʼn",
"name-header": "Ńämę",
"tags-header": "Ŧäģş",