1import { Row } from 'react-table'; 2import memoizeOne from 'memoize-one'; 3import { Property } from 'csstype'; 4import { 5 DataFrame, 6 Field, 7 FieldType, 8 formattedValueToString, 9 getFieldDisplayName, 10 SelectableValue, 11} from '@grafana/data'; 12 13import { DefaultCell } from './DefaultCell'; 14import { BarGaugeCell } from './BarGaugeCell'; 15import { CellComponent, TableCellDisplayMode, TableFieldOptions, FooterItem, GrafanaTableColumn } from './types'; 16import { JSONViewCell } from './JSONViewCell'; 17import { ImageCell } from './ImageCell'; 18import { getFooterValue } from './FooterRow'; 19 20export function getTextAlign(field?: Field): Property.JustifyContent { 21 if (!field) { 22 return 'flex-start'; 23 } 24 25 if (field.config.custom) { 26 const custom = field.config.custom as TableFieldOptions; 27 28 switch (custom.align) { 29 case 'right': 30 return 'flex-end'; 31 case 'left': 32 return 'flex-start'; 33 case 'center': 34 return 'center'; 35 } 36 } 37 38 if (field.type === FieldType.number) { 39 return 'flex-end'; 40 } 41 42 return 'flex-start'; 43} 44 45export function getColumns( 46 data: DataFrame, 47 availableWidth: number, 48 columnMinWidth: number, 49 footerValues?: FooterItem[] 50): GrafanaTableColumn[] { 51 const columns: GrafanaTableColumn[] = []; 52 let fieldCountWithoutWidth = 0; 53 54 for (const [fieldIndex, field] of data.fields.entries()) { 55 const fieldTableOptions = (field.config.custom || {}) as TableFieldOptions; 56 57 if (fieldTableOptions.hidden) { 58 continue; 59 } 60 61 if (fieldTableOptions.width) { 62 availableWidth -= fieldTableOptions.width; 63 } else { 64 fieldCountWithoutWidth++; 65 } 66 67 const selectSortType = (type: FieldType) => { 68 switch (type) { 69 case FieldType.number: 70 return 'number'; 71 case FieldType.time: 72 return 'basic'; 73 default: 74 return 'alphanumeric-insensitive'; 75 } 76 }; 77 78 const Cell = getCellComponent(fieldTableOptions.displayMode, field); 79 columns.push({ 80 Cell, 81 id: fieldIndex.toString(), 82 field: field, 83 Header: getFieldDisplayName(field, data), 84 accessor: (row: any, i: number) => { 85 return field.values.get(i); 86 }, 87 sortType: selectSortType(field.type), 88 width: fieldTableOptions.width, 89 minWidth: fieldTableOptions.minWidth ?? columnMinWidth, 90 filter: memoizeOne(filterByValue(field)), 91 justifyContent: getTextAlign(field), 92 Footer: getFooterValue(fieldIndex, footerValues), 93 }); 94 } 95 96 // set columns that are at minimum width 97 let sharedWidth = availableWidth / fieldCountWithoutWidth; 98 for (let i = fieldCountWithoutWidth; i > 0; i--) { 99 for (const column of columns) { 100 if (!column.width && column.minWidth > sharedWidth) { 101 column.width = column.minWidth; 102 availableWidth -= column.width; 103 fieldCountWithoutWidth -= 1; 104 sharedWidth = availableWidth / fieldCountWithoutWidth; 105 } 106 } 107 } 108 109 // divide up the rest of the space 110 for (const column of columns) { 111 if (!column.width) { 112 column.width = sharedWidth; 113 } 114 column.minWidth = 50; 115 } 116 117 return columns; 118} 119 120function getCellComponent(displayMode: TableCellDisplayMode, field: Field): CellComponent { 121 switch (displayMode) { 122 case TableCellDisplayMode.ColorText: 123 case TableCellDisplayMode.ColorBackground: 124 return DefaultCell; 125 case TableCellDisplayMode.Image: 126 return ImageCell; 127 case TableCellDisplayMode.LcdGauge: 128 case TableCellDisplayMode.BasicGauge: 129 case TableCellDisplayMode.GradientGauge: 130 return BarGaugeCell; 131 case TableCellDisplayMode.JSONView: 132 return JSONViewCell; 133 } 134 135 // Default or Auto 136 if (field.type === FieldType.other) { 137 return JSONViewCell; 138 } 139 return DefaultCell; 140} 141 142export function filterByValue(field?: Field) { 143 return function (rows: Row[], id: string, filterValues?: SelectableValue[]) { 144 if (rows.length === 0) { 145 return rows; 146 } 147 148 if (!filterValues) { 149 return rows; 150 } 151 152 if (!field) { 153 return rows; 154 } 155 156 return rows.filter((row) => { 157 if (!row.values.hasOwnProperty(id)) { 158 return false; 159 } 160 const value = rowToFieldValue(row, field); 161 return filterValues.find((filter) => filter.value === value) !== undefined; 162 }); 163 }; 164} 165 166export function calculateUniqueFieldValues(rows: any[], field?: Field) { 167 if (!field || rows.length === 0) { 168 return {}; 169 } 170 171 const set: Record<string, any> = {}; 172 173 for (let index = 0; index < rows.length; index++) { 174 const value = rowToFieldValue(rows[index], field); 175 set[value || '(Blanks)'] = value; 176 } 177 178 return set; 179} 180 181export function rowToFieldValue(row: any, field?: Field): string { 182 if (!field || !row) { 183 return ''; 184 } 185 186 const fieldValue = field.values.get(row.index); 187 const displayValue = field.display ? field.display(fieldValue) : fieldValue; 188 const value = field.display ? formattedValueToString(displayValue) : displayValue; 189 190 return value; 191} 192 193export function valuesToOptions(unique: Record<string, any>): SelectableValue[] { 194 return Object.keys(unique) 195 .reduce((all, key) => all.concat({ value: unique[key], label: key }), [] as SelectableValue[]) 196 .sort(sortOptions); 197} 198 199export function sortOptions(a: SelectableValue, b: SelectableValue): number { 200 if (a.label === undefined && b.label === undefined) { 201 return 0; 202 } 203 204 if (a.label === undefined && b.label !== undefined) { 205 return -1; 206 } 207 208 if (a.label !== undefined && b.label === undefined) { 209 return 1; 210 } 211 212 if (a.label! < b.label!) { 213 return -1; 214 } 215 216 if (a.label! > b.label!) { 217 return 1; 218 } 219 220 return 0; 221} 222 223export function getFilteredOptions(options: SelectableValue[], filterValues?: SelectableValue[]): SelectableValue[] { 224 if (!filterValues) { 225 return []; 226 } 227 228 return options.filter((option) => filterValues.some((filtered) => filtered.value === option.value)); 229} 230 231export function sortCaseInsensitive(a: Row<any>, b: Row<any>, id: string) { 232 return String(a.values[id]).localeCompare(String(b.values[id]), undefined, { sensitivity: 'base' }); 233} 234 235// sortNumber needs to have great performance as it is called a lot 236export function sortNumber(rowA: Row<any>, rowB: Row<any>, id: string) { 237 const a = toNumber(rowA.values[id]); 238 const b = toNumber(rowB.values[id]); 239 return a === b ? 0 : a > b ? 1 : -1; 240} 241 242function toNumber(value: any): number { 243 if (value === null || value === undefined || value === '' || isNaN(value)) { 244 return Number.NEGATIVE_INFINITY; 245 } 246 247 if (typeof value === 'number') { 248 return value; 249 } 250 251 return Number(value); 252} 253