mirror of https://github.com/grafana/grafana.git
				
				
				
			use react-table
This commit is contained in:
		
							parent
							
								
									f59ccdf59c
								
							
						
					
					
						commit
						372e892fab
					
				|  | @ -1,67 +1,376 @@ | |||
| // Libraries
 | ||||
| import _ from 'lodash'; | ||||
| import moment from 'moment'; | ||||
| import React, { PureComponent } from 'react'; | ||||
| 
 | ||||
| import ReactTable from 'react-table'; | ||||
| 
 | ||||
| import { sanitize } from 'app/core/utils/text'; | ||||
| 
 | ||||
| // Types
 | ||||
| import { PanelProps } from '@grafana/ui/src/types'; | ||||
| import { Options } from './types'; | ||||
| import { Options, Style, Column, CellFormatter } from './types'; | ||||
| import kbn from 'app/core/utils/kbn'; | ||||
| 
 | ||||
| import { Table, Index, Column } from 'react-virtualized'; | ||||
| import templateSrv from 'app/features/templating/template_srv'; | ||||
| 
 | ||||
| interface Props extends PanelProps<Options> {} | ||||
| 
 | ||||
| export class TablePanel extends PureComponent<Props> { | ||||
|   getRow = (index: Index): any => { | ||||
|     const { panelData } = this.props; | ||||
|     if (panelData.tableData) { | ||||
|       return panelData.tableData.rows[index.index]; | ||||
|   isUTC: false; // TODO? get UTC from props?
 | ||||
| 
 | ||||
|   columns: Column[]; | ||||
|   colorState: any; | ||||
| 
 | ||||
|   initColumns() { | ||||
|     this.colorState = {}; | ||||
| 
 | ||||
|     const { panelData, options } = this.props; | ||||
|     if (!panelData.tableData) { | ||||
|       this.columns = []; | ||||
|       return; | ||||
|     } | ||||
|     return null; | ||||
|     const { styles } = options; | ||||
| 
 | ||||
|     this.columns = panelData.tableData.columns.map((col, index) => { | ||||
|       let title = col.text; | ||||
|       let style: Style = null; | ||||
| 
 | ||||
|       for (let i = 0; i < styles.length; i++) { | ||||
|         const s = styles[i]; | ||||
|         const regex = kbn.stringToJsRegex(s.pattern); | ||||
|         if (title.match(regex)) { | ||||
|           style = s; | ||||
|           if (s.alias) { | ||||
|             title = title.replace(regex, s.alias); | ||||
|           } | ||||
|           break; | ||||
|         } | ||||
|       } | ||||
| 
 | ||||
|       return { | ||||
|         header: title, | ||||
|         accessor: col.text, // unique?
 | ||||
|         style: style, | ||||
|         formatter: this.createColumnFormatter(style, col), | ||||
|       }; | ||||
|     }); | ||||
|   } | ||||
| 
 | ||||
|   getColorForValue(value: any, style: Style) { | ||||
|     if (!style.thresholds) { | ||||
|       return null; | ||||
|     } | ||||
|     for (let i = style.thresholds.length; i > 0; i--) { | ||||
|       if (value >= style.thresholds[i - 1]) { | ||||
|         return style.colors[i]; | ||||
|       } | ||||
|     } | ||||
|     return _.first(style.colors); | ||||
|   } | ||||
| 
 | ||||
|   defaultCellFormatter(v: any, style: Style): string { | ||||
|     if (v === null || v === void 0 || v === undefined) { | ||||
|       return ''; | ||||
|     } | ||||
| 
 | ||||
|     if (_.isArray(v)) { | ||||
|       v = v.join(', '); | ||||
|     } | ||||
| 
 | ||||
|     if (style && style.sanitize) { | ||||
|       return sanitize(v); | ||||
|     } else { | ||||
|       return _.escape(v); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   createColumnFormatter(style: Style, header: any): CellFormatter { | ||||
|     if (!style) { | ||||
|       return this.defaultCellFormatter; | ||||
|     } | ||||
| 
 | ||||
|     if (style.type === 'hidden') { | ||||
|       return v => { | ||||
|         return undefined; | ||||
|       }; | ||||
|     } | ||||
| 
 | ||||
|     if (style.type === 'date') { | ||||
|       return v => { | ||||
|         if (v === undefined || v === null) { | ||||
|           return '-'; | ||||
|         } | ||||
| 
 | ||||
|         if (_.isArray(v)) { | ||||
|           v = v[0]; | ||||
|         } | ||||
|         let date = moment(v); | ||||
|         if (this.isUTC) { | ||||
|           date = date.utc(); | ||||
|         } | ||||
|         return date.format(style.dateFormat); | ||||
|       }; | ||||
|     } | ||||
| 
 | ||||
|     if (style.type === 'string') { | ||||
|       return v => { | ||||
|         if (_.isArray(v)) { | ||||
|           v = v.join(', '); | ||||
|         } | ||||
| 
 | ||||
|         const mappingType = style.mappingType || 0; | ||||
| 
 | ||||
|         if (mappingType === 1 && style.valueMaps) { | ||||
|           for (let i = 0; i < style.valueMaps.length; i++) { | ||||
|             const map = style.valueMaps[i]; | ||||
| 
 | ||||
|             if (v === null) { | ||||
|               if (map.value === 'null') { | ||||
|                 return map.text; | ||||
|               } | ||||
|               continue; | ||||
|             } | ||||
| 
 | ||||
|             // Allow both numeric and string values to be mapped
 | ||||
|             if ((!_.isString(v) && Number(map.value) === Number(v)) || map.value === v) { | ||||
|               this.setColorState(v, style); | ||||
|               return this.defaultCellFormatter(map.text, style); | ||||
|             } | ||||
|           } | ||||
|         } | ||||
| 
 | ||||
|         if (mappingType === 2 && style.rangeMaps) { | ||||
|           for (let i = 0; i < style.rangeMaps.length; i++) { | ||||
|             const map = style.rangeMaps[i]; | ||||
| 
 | ||||
|             if (v === null) { | ||||
|               if (map.from === 'null' && map.to === 'null') { | ||||
|                 return map.text; | ||||
|               } | ||||
|               continue; | ||||
|             } | ||||
| 
 | ||||
|             if (Number(map.from) <= Number(v) && Number(map.to) >= Number(v)) { | ||||
|               this.setColorState(v, style); | ||||
|               return this.defaultCellFormatter(map.text, style); | ||||
|             } | ||||
|           } | ||||
|         } | ||||
| 
 | ||||
|         if (v === null || v === void 0) { | ||||
|           return '-'; | ||||
|         } | ||||
| 
 | ||||
|         this.setColorState(v, style); | ||||
|         return this.defaultCellFormatter(v, style); | ||||
|       }; | ||||
|     } | ||||
| 
 | ||||
|     if (style.type === 'number') { | ||||
|       const valueFormatter = kbn.valueFormats[style.unit || header.unit]; | ||||
| 
 | ||||
|       return v => { | ||||
|         if (v === null || v === void 0) { | ||||
|           return '-'; | ||||
|         } | ||||
| 
 | ||||
|         if (_.isString(v) || _.isArray(v)) { | ||||
|           return this.defaultCellFormatter(v, style); | ||||
|         } | ||||
| 
 | ||||
|         this.setColorState(v, style); | ||||
|         return valueFormatter(v, style.decimals, null); | ||||
|       }; | ||||
|     } | ||||
| 
 | ||||
|     return value => { | ||||
|       return this.defaultCellFormatter(value, style); | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   setColorState(value: any, style: Style) { | ||||
|     if (!style.colorMode) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     if (value === null || value === void 0 || _.isArray(value)) { | ||||
|       return; | ||||
|     } | ||||
| 
 | ||||
|     if (_.isNaN(value)) { | ||||
|       return; | ||||
|     } | ||||
|     const numericValue = Number(value); | ||||
|     this.colorState[style.colorMode] = this.getColorForValue(numericValue, style); | ||||
|   } | ||||
| 
 | ||||
|   renderRowconstiables(rowIndex) { | ||||
|     const { panelData } = this.props; | ||||
| 
 | ||||
|     const scopedVars = {}; | ||||
|     const row = panelData.tableData.rows[rowIndex]; | ||||
|     for (let i = 0; i < row.length; i++) { | ||||
|       scopedVars[`__cell_${i}`] = { value: row[i] }; | ||||
|     } | ||||
|     return scopedVars; | ||||
|   } | ||||
| 
 | ||||
|   renderCell(columnIndex: number, rowIndex: number, value: any, addWidthHack = false) { | ||||
|     const column = this.columns[columnIndex]; | ||||
|     if (column.formatter) { | ||||
|       value = column.formatter(value, column.style); | ||||
|     } | ||||
| 
 | ||||
|     const style = {}; | ||||
|     const cellClasses = []; | ||||
|     let cellClass = ''; | ||||
| 
 | ||||
|     if (this.colorState.cell) { | ||||
|       style['backgroundColor'] = this.colorState.cell; | ||||
|       style['color'] = 'white'; | ||||
|       this.colorState.cell = null; | ||||
|     } else if (this.colorState.value) { | ||||
|       style['color'] = this.colorState.value; | ||||
|       this.colorState.value = null; | ||||
|     } | ||||
| 
 | ||||
|     if (value === undefined) { | ||||
|       style['display'] = 'none'; | ||||
|       column.hidden = true; | ||||
|     } else { | ||||
|       column.hidden = false; | ||||
|     } | ||||
| 
 | ||||
|     if (column.style && column.style.preserveFormat) { | ||||
|       cellClasses.push('table-panel-cell-pre'); | ||||
|     } | ||||
| 
 | ||||
|     let columnHtml; | ||||
|     if (column.style && column.style.link) { | ||||
|       // Render cell as link
 | ||||
|       const scopedconsts = this.renderRowconstiables(rowIndex); | ||||
|       scopedconsts['__cell'] = { value: value }; | ||||
| 
 | ||||
|       const cellLink = templateSrv.replace(column.style.linkUrl, scopedconsts, encodeURIComponent); | ||||
|       const cellLinkTooltip = templateSrv.replace(column.style.linkTooltip, scopedconsts); | ||||
|       const cellTarget = column.style.linkTargetBlank ? '_blank' : ''; | ||||
| 
 | ||||
|       cellClasses.push('table-panel-cell-link'); | ||||
|       columnHtml = ( | ||||
|         <a | ||||
|           href={cellLink} | ||||
|           target={cellTarget} | ||||
|           data-link-tooltip | ||||
|           data-original-title={cellLinkTooltip} | ||||
|           data-placement="right" | ||||
|         > | ||||
|           {value} | ||||
|         </a> | ||||
|       ); | ||||
|     } else { | ||||
|       columnHtml = <span>{value}</span>; | ||||
|     } | ||||
| 
 | ||||
|     let filterLink; | ||||
|     if (column.filterable) { | ||||
|       cellClasses.push('table-panel-cell-filterable'); | ||||
|       filterLink = ( | ||||
|         <span> | ||||
|           <a | ||||
|             className="table-panel-filter-link" | ||||
|             data-link-tooltip | ||||
|             data-original-title="Filter out value" | ||||
|             data-placement="bottom" | ||||
|             data-row={rowIndex} | ||||
|             data-column={columnIndex} | ||||
|             data-operator="!=" | ||||
|           > | ||||
|             <i className="fa fa-search-minus" /> | ||||
|           </a> | ||||
|           <a | ||||
|             className="table-panel-filter-link" | ||||
|             data-link-tooltip | ||||
|             data-original-title="Filter for value" | ||||
|             data-placement="bottom" | ||||
|             data-row={rowIndex} | ||||
|             data-column={columnIndex} | ||||
|             data-operator="=" | ||||
|           > | ||||
|             <i className="fa fa-search-plus" /> | ||||
|           </a> | ||||
|         </span> | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     if (cellClasses.length) { | ||||
|       cellClass = cellClasses.join(' '); | ||||
|     } | ||||
| 
 | ||||
|     style['width'] = '100%'; | ||||
|     style['height'] = '100%'; | ||||
|     columnHtml = ( | ||||
|       <div className={cellClass} style={style}> | ||||
|         {columnHtml} | ||||
|         {filterLink} | ||||
|       </div> | ||||
|     ); | ||||
|     return columnHtml; | ||||
|   } | ||||
| 
 | ||||
|   render() { | ||||
|     const { panelData, width, height, options } = this.props; | ||||
|     const { showHeader } = options; | ||||
|     const { panelData, height, options } = this.props; | ||||
|     const { pageSize } = options; | ||||
| 
 | ||||
|     const headerClassName = null; | ||||
|     const headerHeight = 30; | ||||
|     const rowHeight = 20; | ||||
| 
 | ||||
|     let rowCount = 0; | ||||
|     let rows = []; | ||||
|     let columns = []; | ||||
|     if (panelData.tableData) { | ||||
|       rowCount = panelData.tableData.rows.length; | ||||
|       this.initColumns(); | ||||
|       const fields = this.columns.map(c => { | ||||
|         return c.accessor; | ||||
|       }); | ||||
|       rows = panelData.tableData.rows.map(row => { | ||||
|         return _.zipObject(fields, row); | ||||
|       }); | ||||
|       columns = this.columns.map((c, columnIndex) => { | ||||
|         return { | ||||
|           Header: c.header, | ||||
|           accessor: c.accessor, | ||||
|           filterable: !!c.filterable, | ||||
|           Cell: row => { | ||||
|             return this.renderCell(columnIndex, row.index, row.value); | ||||
|           }, | ||||
|         }; | ||||
|       }); | ||||
|       console.log(templateSrv); | ||||
|       console.log(rows); | ||||
|     } else { | ||||
|       return <div>No Table Data...</div>; | ||||
|     } | ||||
| 
 | ||||
|     // Only show paging if necessary
 | ||||
|     const showPaginationBottom = pageSize && pageSize < panelData.tableData.rows.length; | ||||
| 
 | ||||
|     return ( | ||||
|       <div> | ||||
|         <Table | ||||
|           disableHeader={!showHeader} | ||||
|           headerClassName={headerClassName} | ||||
|           headerHeight={headerHeight} | ||||
|           height={height} | ||||
|           overscanRowCount={5} | ||||
|           rowHeight={rowHeight} | ||||
|           rowGetter={this.getRow} | ||||
|           rowCount={rowCount} | ||||
|           width={width} | ||||
|         > | ||||
|           {panelData.tableData.columns.map((col, index) => { | ||||
|             return ( | ||||
|               <Column | ||||
|                 label={col.text} | ||||
|                 cellDataGetter={({ rowData }) => { | ||||
|                   return rowData[index]; | ||||
|       <ReactTable | ||||
|         data={rows} | ||||
|         columns={columns} | ||||
|         defaultPageSize={pageSize} | ||||
|         style={{ | ||||
|           height: height - 20 + 'px', | ||||
|         }} | ||||
|         showPaginationBottom={showPaginationBottom} | ||||
|         getTdProps={(state, rowInfo, column, instance) => { | ||||
|           return { | ||||
|             onClick: (e, handleOriginal) => { | ||||
|               console.log('filter', rowInfo.row[column.id]); | ||||
|               if (handleOriginal) { | ||||
|                 handleOriginal(); | ||||
|               } | ||||
|             }, | ||||
|           }; | ||||
|         }} | ||||
|                 dataKey={index} | ||||
|                 disableSort={true} | ||||
|                 width={100} | ||||
|       /> | ||||
|     ); | ||||
|           })} | ||||
|         </Table> | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|  |  | |||
|  | @ -3,7 +3,7 @@ import _ from 'lodash'; | |||
| import React, { PureComponent } from 'react'; | ||||
| 
 | ||||
| // Types
 | ||||
| import { PanelEditorProps, Switch } from '@grafana/ui'; | ||||
| import { PanelEditorProps, Switch, FormField } from '@grafana/ui'; | ||||
| import { Options } from './types'; | ||||
| 
 | ||||
| export class TablePanelEditor extends PureComponent<PanelEditorProps<Options>> { | ||||
|  | @ -11,8 +11,10 @@ export class TablePanelEditor extends PureComponent<PanelEditorProps<Options>> { | |||
|     this.props.onOptionsChange({ ...this.props.options, showHeader: !this.props.options.showHeader }); | ||||
|   }; | ||||
| 
 | ||||
|   onRowsPerPageChange = ({ target }) => this.props.onOptionsChange({ ...this.props.options, pageSize: target.value }); | ||||
| 
 | ||||
|   render() { | ||||
|     const { showHeader } = this.props.options; | ||||
|     const { showHeader, pageSize } = this.props.options; | ||||
| 
 | ||||
|     return ( | ||||
|       <div> | ||||
|  | @ -20,6 +22,11 @@ export class TablePanelEditor extends PureComponent<PanelEditorProps<Options>> { | |||
|           <h5 className="section-heading">Header</h5> | ||||
|           <Switch label="Show" labelClass="width-5" checked={showHeader} onChange={this.onToggleShowHeader} /> | ||||
|         </div> | ||||
| 
 | ||||
|         <div className="section gf-form-group"> | ||||
|           <h5 className="section-heading">Paging</h5> | ||||
|           <FormField label="Rows per page" labelWidth={8} onChange={this.onRowsPerPageChange} value={pageSize} /> | ||||
|         </div> | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
|  |  | |||
|  | @ -1,7 +1,63 @@ | |||
| // Made to match the existing (untyped) settings in the angular table
 | ||||
| export interface Style { | ||||
|   alias?: string; | ||||
|   colorMode?: string; | ||||
|   colors?: any[]; | ||||
|   decimals?: number; | ||||
|   pattern?: string; | ||||
|   thresholds?: any[]; | ||||
|   type?: 'date' | 'number' | 'string' | 'hidden'; | ||||
|   unit?: string; | ||||
|   dateFormat?: string; | ||||
|   sanitize?: boolean; | ||||
|   mappingType?: any; | ||||
|   valueMaps?: any; | ||||
|   rangeMaps?: any; | ||||
| 
 | ||||
|   link?: any; | ||||
|   linkUrl?: any; | ||||
|   linkTooltip?: any; | ||||
|   linkTargetBlank?: boolean; | ||||
| 
 | ||||
|   preserveFormat?: boolean; | ||||
| } | ||||
| 
 | ||||
| export type CellFormatter = (v: any, style: Style) => string; | ||||
| 
 | ||||
| export interface Column { | ||||
|   header: string; | ||||
|   accessor: string; // the field name
 | ||||
|   style?: Style; | ||||
|   hidden?: boolean; | ||||
|   formatter: CellFormatter; | ||||
|   filterable?: boolean; | ||||
| } | ||||
| 
 | ||||
| export interface Options { | ||||
|   showHeader: boolean; | ||||
|   styles: Style[]; // TODO, just a copy from existing table
 | ||||
|   pageSize: number; | ||||
| } | ||||
| 
 | ||||
| export const defaults: Options = { | ||||
|   showHeader: true, | ||||
|   styles: [ | ||||
|     { | ||||
|       type: 'date', | ||||
|       pattern: 'Time', | ||||
|       alias: 'Time', | ||||
|       dateFormat: 'YYYY-MM-DD HH:mm:ss', | ||||
|     }, | ||||
|     { | ||||
|       unit: 'short', | ||||
|       type: 'number', | ||||
|       alias: '', | ||||
|       decimals: 2, | ||||
|       colors: ['rgba(245, 54, 54, 0.9)', 'rgba(237, 129, 40, 0.89)', 'rgba(50, 172, 45, 0.97)'], | ||||
|       colorMode: null, | ||||
|       pattern: '/.*/', | ||||
|       thresholds: [], | ||||
|     }, | ||||
|   ], | ||||
|   pageSize: 100, | ||||
| }; | ||||
|  |  | |||
|  | @ -98,7 +98,6 @@ | |||
| @import 'components/page_loader'; | ||||
| @import 'components/toggle_button_group'; | ||||
| @import 'components/popover-box'; | ||||
| @import 'components/react_virtualized'; | ||||
| 
 | ||||
| // LOAD @grafana/ui components | ||||
| @import '../../packages/grafana-ui/src/index'; | ||||
|  |  | |||
|  | @ -1,83 +0,0 @@ | |||
| /** | ||||
| COPIED FROM: | ||||
| https://raw.githubusercontent.com/bvaughn/react-virtualized/master/source/styles.css | ||||
| */ | ||||
| 
 | ||||
| /* Collection default theme */ | ||||
| 
 | ||||
| .ReactVirtualized__Collection { | ||||
| } | ||||
| 
 | ||||
| .ReactVirtualized__Collection__innerScrollContainer { | ||||
| } | ||||
| 
 | ||||
| /* Grid default theme */ | ||||
| 
 | ||||
| .ReactVirtualized__Grid { | ||||
| } | ||||
| 
 | ||||
| .ReactVirtualized__Grid__innerScrollContainer { | ||||
| } | ||||
| 
 | ||||
| /* Table default theme */ | ||||
| 
 | ||||
| .ReactVirtualized__Table { | ||||
| } | ||||
| 
 | ||||
| .ReactVirtualized__Table__Grid { | ||||
| } | ||||
| 
 | ||||
| .ReactVirtualized__Table__headerRow { | ||||
|   font-weight: 700; | ||||
|   text-transform: uppercase; | ||||
|   display: flex; | ||||
|   flex-direction: row; | ||||
|   align-items: center; | ||||
| } | ||||
| .ReactVirtualized__Table__row { | ||||
|   display: flex; | ||||
|   flex-direction: row; | ||||
|   align-items: center; | ||||
| } | ||||
| 
 | ||||
| .ReactVirtualized__Table__headerTruncatedText { | ||||
|   display: inline-block; | ||||
|   max-width: 100%; | ||||
|   white-space: nowrap; | ||||
|   text-overflow: ellipsis; | ||||
|   overflow: hidden; | ||||
| } | ||||
| 
 | ||||
| .ReactVirtualized__Table__headerColumn, | ||||
| .ReactVirtualized__Table__rowColumn { | ||||
|   margin-right: 10px; | ||||
|   min-width: 0px; | ||||
| } | ||||
| .ReactVirtualized__Table__rowColumn { | ||||
|   text-overflow: ellipsis; | ||||
|   white-space: nowrap; | ||||
| } | ||||
| 
 | ||||
| .ReactVirtualized__Table__headerColumn:first-of-type, | ||||
| .ReactVirtualized__Table__rowColumn:first-of-type { | ||||
|   margin-left: 10px; | ||||
| } | ||||
| .ReactVirtualized__Table__sortableHeaderColumn { | ||||
|   cursor: pointer; | ||||
| } | ||||
| 
 | ||||
| .ReactVirtualized__Table__sortableHeaderIconContainer { | ||||
|   display: flex; | ||||
|   align-items: center; | ||||
| } | ||||
| .ReactVirtualized__Table__sortableHeaderIcon { | ||||
|   flex: 0 0 24px; | ||||
|   height: 1em; | ||||
|   width: 1em; | ||||
|   fill: currentColor; | ||||
| } | ||||
| 
 | ||||
| /* List default theme */ | ||||
| 
 | ||||
| .ReactVirtualized__List { | ||||
| } | ||||
		Loading…
	
		Reference in New Issue