mirror of https://github.com/grafana/grafana
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 => { |
||||
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; |
||||
} |
||||
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; |
||||
if (panelData.tableData) { |
||||
return panelData.tableData.rows[index.index]; |
||||
|
||||
const scopedVars = {}; |
||||
const row = panelData.tableData.rows[rowIndex]; |
||||
for (let i = 0; i < row.length; i++) { |
||||
scopedVars[`__cell_${i}`] = { value: row[i] }; |
||||
} |
||||
return null; |
||||
}; |
||||
return scopedVars; |
||||
} |
||||
|
||||
render() { |
||||
const { panelData, width, height, options } = this.props; |
||||
const { showHeader } = options; |
||||
renderCell(columnIndex: number, rowIndex: number, value: any, addWidthHack = false) { |
||||
const column = this.columns[columnIndex]; |
||||
if (column.formatter) { |
||||
value = column.formatter(value, column.style); |
||||
} |
||||
|
||||
const headerClassName = null; |
||||
const headerHeight = 30; |
||||
const rowHeight = 20; |
||||
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; |
||||
} |
||||
|
||||
let rowCount = 0; |
||||
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, height, options } = this.props; |
||||
const { pageSize } = options; |
||||
|
||||
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]; |
||||
}} |
||||
dataKey={index} |
||||
disableSort={true} |
||||
width={100} |
||||
/> |
||||
); |
||||
})} |
||||
</Table> |
||||
</div> |
||||
<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(); |
||||
} |
||||
}, |
||||
}; |
||||
}} |
||||
/> |
||||
); |
||||
} |
||||
} |
||||
|
@ -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, |
||||
}; |
||||
|
@ -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