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