mirror of
https://github.com/grafana/grafana.git
synced 2024-11-25 02:10:45 -06:00
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:
parent
957957e62a
commit
06a1e8e22b
@ -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 };
|
||||
}
|
||||
|
@ -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: {
|
||||
|
@ -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;
|
||||
|
@ -30,6 +30,8 @@ export interface DashboardSearchHit extends WithAccessControlMetadata {
|
||||
url: string;
|
||||
sortMeta?: number;
|
||||
sortMetaName?: string;
|
||||
isDeleted?: boolean;
|
||||
permanentlyDeleteDate?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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",
|
||||
|
@ -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": "Ŧäģş",
|
||||
|
Loading…
Reference in New Issue
Block a user