mirror of
https://github.com/grafana/grafana.git
synced 2025-02-16 18:34:52 -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 { cx } from '@emotion/css';
|
||||||
|
import { intervalToDuration } from 'date-fns';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Skeleton from 'react-loading-skeleton';
|
import Skeleton from 'react-loading-skeleton';
|
||||||
|
|
||||||
@ -11,9 +12,10 @@ import {
|
|||||||
getFieldDisplayName,
|
getFieldDisplayName,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { config, getDataSourceSrv } from '@grafana/runtime';
|
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 appEvents from 'app/core/app_events';
|
||||||
import { t } from 'app/core/internationalization';
|
import { t } from 'app/core/internationalization';
|
||||||
|
import { formatDate, formatDuration } from 'app/core/internationalization/dates';
|
||||||
import { PluginIconName } from 'app/features/plugins/admin/types';
|
import { PluginIconName } from 'app/features/plugins/admin/types';
|
||||||
import { ShowModalReactEvent } from 'app/types/events';
|
import { ShowModalReactEvent } from 'app/types/events';
|
||||||
|
|
||||||
@ -25,6 +27,7 @@ import { ExplainScorePopup } from './ExplainScorePopup';
|
|||||||
import { TableColumn } from './SearchResultsTable';
|
import { TableColumn } from './SearchResultsTable';
|
||||||
|
|
||||||
const TYPE_COLUMN_WIDTH = 175;
|
const TYPE_COLUMN_WIDTH = 175;
|
||||||
|
const DURATION_COLUMN_WIDTH = 200;
|
||||||
const DATASOURCE_COLUMN_WIDTH = 200;
|
const DATASOURCE_COLUMN_WIDTH = 200;
|
||||||
|
|
||||||
export const generateColumns = (
|
export const generateColumns = (
|
||||||
@ -112,15 +115,20 @@ export const generateColumns = (
|
|||||||
Cell: (p) => {
|
Cell: (p) => {
|
||||||
let classNames = cx(styles.nameCellStyle);
|
let classNames = cx(styles.nameCellStyle);
|
||||||
let name = access.name.values[p.row.index];
|
let name = access.name.values[p.row.index];
|
||||||
|
const isDeleted = access.isDeleted?.values[p.row.index];
|
||||||
|
|
||||||
if (!name?.length) {
|
if (!name?.length) {
|
||||||
const loading = p.row.index >= response.view.dataFrame.length;
|
const loading = p.row.index >= response.view.dataFrame.length;
|
||||||
name = loading ? 'Loading...' : 'Missing title'; // normal for panels
|
name = loading ? 'Loading...' : 'Missing title'; // normal for panels
|
||||||
classNames += ' ' + styles.missingTitleText;
|
classNames += ' ' + styles.missingTitleText;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.cell} {...p.cellProps}>
|
<div className={styles.cell} {...p.cellProps}>
|
||||||
{!response.isItemLoaded(p.row.index) ? (
|
{!response.isItemLoaded(p.row.index) ? (
|
||||||
<Skeleton width={200} />
|
<Skeleton width={200} />
|
||||||
|
) : isDeleted ? (
|
||||||
|
<span className={classNames}>{name}</span>
|
||||||
) : (
|
) : (
|
||||||
<a href={p.userProps.href} onClick={p.userProps.onClick} className={classNames} title={name}>
|
<a href={p.userProps.href} onClick={p.userProps.onClick} className={classNames} title={name}>
|
||||||
{name}
|
{name}
|
||||||
@ -136,9 +144,18 @@ export const generateColumns = (
|
|||||||
});
|
});
|
||||||
availableWidth -= width;
|
availableWidth -= width;
|
||||||
|
|
||||||
width = TYPE_COLUMN_WIDTH;
|
const showDeletedRemaining =
|
||||||
columns.push(makeTypeColumn(response, access.kind, access.panel_type, width, styles));
|
response.view.fields.permanentlyDeleteDate && hasValue(response.view.fields.permanentlyDeleteDate);
|
||||||
availableWidth -= width;
|
|
||||||
|
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
|
// Show datasources if we have any
|
||||||
if (access.ds_uid && onDatasourceChange) {
|
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(
|
function makeTypeColumn(
|
||||||
response: QueryResponse,
|
response: QueryResponse,
|
||||||
kindField: Field<string>,
|
kindField: Field<string>,
|
||||||
@ -442,3 +499,22 @@ function getDisplayValue({
|
|||||||
}
|
}
|
||||||
return formattedValueToString(getDisplay(value));
|
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 tags: string[][] = [];
|
||||||
const location: string[] = [];
|
const location: string[] = [];
|
||||||
const sortBy: number[] = [];
|
const sortBy: number[] = [];
|
||||||
|
const isDeleted: boolean[] = [];
|
||||||
|
const permanentlyDeleteDate: Array<Date | undefined> = [];
|
||||||
let sortMetaName: string | undefined;
|
let sortMetaName: string | undefined;
|
||||||
|
|
||||||
for (let hit of rsp) {
|
for (let hit of rsp) {
|
||||||
@ -168,6 +170,8 @@ export class SQLSearcher implements GrafanaSearcher {
|
|||||||
url.push(hit.url);
|
url.push(hit.url);
|
||||||
tags.push(hit.tags);
|
tags.push(hit.tags);
|
||||||
sortBy.push(hit.sortMeta!);
|
sortBy.push(hit.sortMeta!);
|
||||||
|
isDeleted.push(hit.isDeleted ?? false);
|
||||||
|
permanentlyDeleteDate.push(hit.permanentlyDeleteDate ? new Date(hit.permanentlyDeleteDate) : undefined);
|
||||||
|
|
||||||
let v = hit.folderUid;
|
let v = hit.folderUid;
|
||||||
if (!v && k === 'dashboard') {
|
if (!v && k === 'dashboard') {
|
||||||
@ -204,6 +208,8 @@ export class SQLSearcher implements GrafanaSearcher {
|
|||||||
{ name: 'url', type: FieldType.string, config: {}, values: url },
|
{ name: 'url', type: FieldType.string, config: {}, values: url },
|
||||||
{ name: 'tags', type: FieldType.other, config: {}, values: tags },
|
{ name: 'tags', type: FieldType.other, config: {}, values: tags },
|
||||||
{ name: 'location', type: FieldType.string, config: {}, values: location },
|
{ 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,
|
length: name.length,
|
||||||
meta: {
|
meta: {
|
||||||
|
@ -39,6 +39,8 @@ export interface DashboardQueryResult {
|
|||||||
tags: string[];
|
tags: string[];
|
||||||
location: string; // url that can be split
|
location: string; // url that can be split
|
||||||
ds_uid: string[];
|
ds_uid: string[];
|
||||||
|
isDeleted?: boolean;
|
||||||
|
permanentlyDeleteDate?: Date;
|
||||||
|
|
||||||
// debugging fields
|
// debugging fields
|
||||||
score: number;
|
score: number;
|
||||||
|
@ -30,6 +30,8 @@ export interface DashboardSearchHit extends WithAccessControlMetadata {
|
|||||||
url: string;
|
url: string;
|
||||||
sortMeta?: number;
|
sortMeta?: number;
|
||||||
sortMetaName?: string;
|
sortMetaName?: string;
|
||||||
|
isDeleted?: boolean;
|
||||||
|
permanentlyDeleteDate?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -1673,6 +1673,8 @@
|
|||||||
},
|
},
|
||||||
"results-table": {
|
"results-table": {
|
||||||
"datasource-header": "Data source",
|
"datasource-header": "Data source",
|
||||||
|
"deleted-less-than-1-min": "< 1 min",
|
||||||
|
"deleted-remaining-header": "Time remaining",
|
||||||
"location-header": "Location",
|
"location-header": "Location",
|
||||||
"name-header": "Name",
|
"name-header": "Name",
|
||||||
"tags-header": "Tags",
|
"tags-header": "Tags",
|
||||||
|
@ -1673,6 +1673,8 @@
|
|||||||
},
|
},
|
||||||
"results-table": {
|
"results-table": {
|
||||||
"datasource-header": "Đäŧä şőūřčę",
|
"datasource-header": "Đäŧä şőūřčę",
|
||||||
|
"deleted-less-than-1-min": "< 1 mįʼn",
|
||||||
|
"deleted-remaining-header": "Ŧįmę řęmäįʼnįʼnģ",
|
||||||
"location-header": "Ŀőčäŧįőʼn",
|
"location-header": "Ŀőčäŧįőʼn",
|
||||||
"name-header": "Ńämę",
|
"name-header": "Ńämę",
|
||||||
"tags-header": "Ŧäģş",
|
"tags-header": "Ŧäģş",
|
||||||
|
Loading…
Reference in New Issue
Block a user