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
This commit is contained in:
Josh Hunt 2024-06-21 11:58:28 +01:00 committed by GitHub
parent 957957e62a
commit 06a1e8e22b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 94 additions and 4 deletions

View File

@ -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 (
<div className={styles.cell} {...p.cellProps}>
{!response.isItemLoaded(p.row.index) ? (
<Skeleton width={200} />
) : isDeleted ? (
<span className={classNames}>{name}</span>
) : (
<a href={p.userProps.href} onClick={p.userProps.onClick} className={classNames} title={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<Date | undefined>,
width: number,
styles: Record<string, string>
): 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 (
<div {...p.cellProps} className={cx(styles.cell, styles.typeCell)}>
<Skeleton width={100} />
</div>
);
}
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 (
<div {...p.cellProps} className={cx(styles.cell, styles.typeCell)}>
<Tooltip content={formatDate(deletedDate, { dateStyle: 'medium', timeStyle: 'short' })}>
<span>{formatted}</span>
</Tooltip>
</div>
);
},
};
}
function makeTypeColumn(
response: QueryResponse,
kindField: Field<string>,
@ -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 };
}

View File

@ -158,6 +158,8 @@ export class SQLSearcher implements GrafanaSearcher {
const tags: string[][] = [];
const location: string[] = [];
const sortBy: number[] = [];
const isDeleted: boolean[] = [];
const permanentlyDeleteDate: Array<Date | undefined> = [];
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: {

View File

@ -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;

View File

@ -30,6 +30,8 @@ export interface DashboardSearchHit extends WithAccessControlMetadata {
url: string;
sortMeta?: number;
sortMetaName?: string;
isDeleted?: boolean;
permanentlyDeleteDate?: string;
}
/**

View File

@ -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",

View File

@ -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": "Ŧäģş",