From 06a1e8e22b245848ce0f60f1af9b52ae5c058e57 Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Fri, 21 Jun 2024 11:58:28 +0100 Subject: [PATCH] RecentlyDeleted: Update Search table for deleted items (#89311) * RecentlyDeleted: Show hard delete date * format date * don't crash when <60 minutes * tooltip * reimplement course duration * oops fixed missed units * cleanup --- .../search/page/components/columns.tsx | 84 ++++++++++++++++++- public/app/features/search/service/sql.ts | 6 ++ public/app/features/search/service/types.ts | 2 + public/app/features/search/types.ts | 2 + public/locales/en-US/grafana.json | 2 + public/locales/pseudo-LOCALE/grafana.json | 2 + 6 files changed, 94 insertions(+), 4 deletions(-) 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": "Ŧäģş",