mirror of
				https://github.com/grafana/grafana.git
				synced 2025-02-25 18:55:37 -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:
		| @@ -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": "Ŧäģş", | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user