mirror of
https://github.com/grafana/grafana.git
synced 2025-08-01 21:22:12 +08:00

* Table: Fixes sorting for number fields * Refactor: changes after PR comments * Refactor: found a bug
476 lines
16 KiB
TypeScript
476 lines
16 KiB
TypeScript
import { ArrayVector, Field, FieldType, MutableDataFrame, SelectableValue } from '@grafana/data';
|
|
import {
|
|
calculateUniqueFieldValues,
|
|
filterByValue,
|
|
getColumns,
|
|
getFilteredOptions,
|
|
getTextAlign,
|
|
rowToFieldValue,
|
|
sortNumber,
|
|
sortOptions,
|
|
valuesToOptions,
|
|
} from './utils';
|
|
|
|
function getData() {
|
|
const data = new MutableDataFrame({
|
|
fields: [
|
|
{ name: 'Time', type: FieldType.time, values: [] },
|
|
{
|
|
name: 'Value',
|
|
type: FieldType.number,
|
|
values: [],
|
|
config: {
|
|
custom: {
|
|
width: 100,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: 'Message',
|
|
type: FieldType.string,
|
|
values: [],
|
|
config: {
|
|
custom: {
|
|
align: 'center',
|
|
},
|
|
},
|
|
},
|
|
],
|
|
});
|
|
return data;
|
|
}
|
|
|
|
describe('Table utils', () => {
|
|
describe('getColumns', () => {
|
|
it('Should build columns from DataFrame', () => {
|
|
const columns = getColumns(getData(), 1000, 120);
|
|
|
|
expect(columns[0].Header).toBe('Time');
|
|
expect(columns[1].Header).toBe('Value');
|
|
});
|
|
|
|
it('Should distribute width and use field config width', () => {
|
|
const columns = getColumns(getData(), 1000, 120);
|
|
|
|
expect(columns[0].width).toBe(450);
|
|
expect(columns[1].width).toBe(100);
|
|
});
|
|
});
|
|
|
|
describe('getTextAlign', () => {
|
|
it('Should use textAlign from custom', () => {
|
|
const data = getData();
|
|
const textAlign = getTextAlign(data.fields[2]);
|
|
|
|
expect(textAlign).toBe('center');
|
|
});
|
|
|
|
it('Should set textAlign to right for number values', () => {
|
|
const data = getData();
|
|
const textAlign = getTextAlign(data.fields[1]);
|
|
expect(textAlign).toBe('flex-end');
|
|
});
|
|
});
|
|
|
|
describe('filterByValue', () => {
|
|
describe('happy path', () => {
|
|
const field: any = { values: new ArrayVector(['a', 'aa', 'ab', 'b', 'ba', 'bb', 'c']) };
|
|
const rows: any = [
|
|
{ index: 0, values: { 0: 'a' } },
|
|
{ index: 1, values: { 0: 'aa' } },
|
|
{ index: 2, values: { 0: 'ab' } },
|
|
{ index: 3, values: { 0: 'b' } },
|
|
{ index: 4, values: { 0: 'ba' } },
|
|
{ index: 5, values: { 0: 'bb' } },
|
|
{ index: 6, values: { 0: 'c' } },
|
|
];
|
|
const filterValues = [{ value: 'a' }, { value: 'b' }, { value: 'c' }];
|
|
|
|
const result = filterByValue(field)(rows, '0', filterValues);
|
|
|
|
expect(result).toEqual([
|
|
{ index: 0, values: { 0: 'a' } },
|
|
{ index: 3, values: { 0: 'b' } },
|
|
{ index: 6, values: { 0: 'c' } },
|
|
]);
|
|
});
|
|
|
|
describe('fast exit cases', () => {
|
|
describe('no rows', () => {
|
|
it('should return empty array', () => {
|
|
const field: any = { values: new ArrayVector(['a']) };
|
|
const rows: any = [];
|
|
const filterValues = [{ value: 'a' }];
|
|
|
|
const result = filterByValue(field)(rows, '', filterValues);
|
|
|
|
expect(result).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('no filterValues', () => {
|
|
it('should return rows', () => {
|
|
const field: any = { values: new ArrayVector(['a']) };
|
|
const rows: any = [{}];
|
|
const filterValues = undefined;
|
|
|
|
const result = filterByValue(field)(rows, '', filterValues);
|
|
|
|
expect(result).toEqual([{}]);
|
|
});
|
|
});
|
|
|
|
describe('no field', () => {
|
|
it('should return rows', () => {
|
|
const field = undefined;
|
|
const rows: any = [{}];
|
|
const filterValues = [{ value: 'a' }];
|
|
|
|
const result = filterByValue(field)(rows, '', filterValues);
|
|
|
|
expect(result).toEqual([{}]);
|
|
});
|
|
});
|
|
|
|
describe('missing id in values', () => {
|
|
it('should return rows', () => {
|
|
const field: any = { values: new ArrayVector(['a', 'b', 'c']) };
|
|
const rows: any = [
|
|
{ index: 0, values: { 0: 'a' } },
|
|
{ index: 1, values: { 0: 'b' } },
|
|
{ index: 2, values: { 0: 'c' } },
|
|
];
|
|
const filterValues = [{ value: 'a' }, { value: 'b' }, { value: 'c' }];
|
|
|
|
const result = filterByValue(field)(rows, '1', filterValues);
|
|
|
|
expect(result).toEqual([]);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('calculateUniqueFieldValues', () => {
|
|
describe('when called without field', () => {
|
|
it('then it should return an empty object', () => {
|
|
const field = undefined;
|
|
const rows = [{ index: 0 }];
|
|
|
|
const result = calculateUniqueFieldValues(rows, field);
|
|
|
|
expect(result).toEqual({});
|
|
});
|
|
});
|
|
|
|
describe('when called with no rows', () => {
|
|
it('then it should return an empty object', () => {
|
|
const field: Field = {
|
|
config: {},
|
|
labels: {},
|
|
values: new ArrayVector([1]),
|
|
name: 'value',
|
|
type: FieldType.number,
|
|
getLinks: () => [],
|
|
state: null,
|
|
display: (value: any) => ({
|
|
numeric: 1,
|
|
percent: 0.01,
|
|
color: '',
|
|
title: '1.0',
|
|
text: '1.0',
|
|
}),
|
|
parse: (value: any) => '1.0',
|
|
};
|
|
const rows: any[] = [];
|
|
|
|
const result = calculateUniqueFieldValues(rows, field);
|
|
|
|
expect(result).toEqual({});
|
|
});
|
|
});
|
|
|
|
describe('when called with rows and field with display processor', () => {
|
|
it('then it should return an array with unique values', () => {
|
|
const field: Field = {
|
|
config: {},
|
|
values: new ArrayVector([1, 2, 2, 1, 3, 5, 6]),
|
|
name: 'value',
|
|
type: FieldType.number,
|
|
display: jest.fn((value: any) => ({
|
|
numeric: 1,
|
|
percent: 0.01,
|
|
color: '',
|
|
title: `${value}.0`,
|
|
text: `${value}.0`,
|
|
})),
|
|
};
|
|
const rows: any[] = [{ index: 0 }, { index: 1 }, { index: 2 }, { index: 3 }, { index: 4 }];
|
|
|
|
const result = calculateUniqueFieldValues(rows, field);
|
|
|
|
expect(field.display).toHaveBeenCalledTimes(5);
|
|
expect(result).toEqual({
|
|
'1.0': '1.0',
|
|
'2.0': '2.0',
|
|
'3.0': '3.0',
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('when called with rows and field without display processor', () => {
|
|
it('then it should return an array with unique values', () => {
|
|
const field: Field = {
|
|
config: {},
|
|
values: new ArrayVector([1, 2, 2, 1, 3, 5, 6]),
|
|
name: 'value',
|
|
type: FieldType.number,
|
|
};
|
|
const rows: any[] = [{ index: 0 }, { index: 1 }, { index: 2 }, { index: 3 }, { index: 4 }];
|
|
|
|
const result = calculateUniqueFieldValues(rows, field);
|
|
|
|
expect(result).toEqual({
|
|
'1': 1,
|
|
'2': 2,
|
|
'3': 3,
|
|
});
|
|
});
|
|
|
|
describe('when called with rows with blanks and field', () => {
|
|
it('then it should return an array with unique values and (Blanks)', () => {
|
|
const field: Field = {
|
|
config: {},
|
|
values: new ArrayVector([1, null, null, 1, 3, 5, 6]),
|
|
name: 'value',
|
|
type: FieldType.number,
|
|
};
|
|
const rows: any[] = [{ index: 0 }, { index: 1 }, { index: 2 }, { index: 3 }, { index: 4 }];
|
|
|
|
const result = calculateUniqueFieldValues(rows, field);
|
|
|
|
expect(result).toEqual({
|
|
'(Blanks)': null,
|
|
'1': 1,
|
|
'3': 3,
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('rowToFieldValue', () => {
|
|
describe('happy paths', () => {
|
|
describe('field without field display', () => {
|
|
const field: any = { values: new ArrayVector(['a', 'b', 'c']) };
|
|
const row = { index: 1 };
|
|
|
|
const result = rowToFieldValue(row, field);
|
|
|
|
expect(result).toEqual('b');
|
|
});
|
|
|
|
describe('field with display processor', () => {
|
|
const field: Field = {
|
|
config: {},
|
|
values: new ArrayVector([1, 2, 2, 1, 3, 5, 6]),
|
|
name: 'value',
|
|
type: FieldType.number,
|
|
display: jest.fn((value: any) => ({
|
|
numeric: 1,
|
|
percent: 0.01,
|
|
color: '',
|
|
title: `${value}.0`,
|
|
text: `${value}.0`,
|
|
})),
|
|
};
|
|
const row = { index: 4 };
|
|
|
|
const result = rowToFieldValue(row, field);
|
|
|
|
expect(result).toEqual('3.0');
|
|
});
|
|
});
|
|
|
|
describe('quick exist paths', () => {
|
|
describe('field is missing', () => {
|
|
const field = undefined;
|
|
const row = { index: 0 };
|
|
|
|
const result = rowToFieldValue(row, field);
|
|
|
|
expect(result).toEqual('');
|
|
});
|
|
describe('row is missing', () => {
|
|
const field: any = { values: new ArrayVector(['a', 'b', 'c']) };
|
|
const row = undefined;
|
|
|
|
const result = rowToFieldValue(row, field);
|
|
|
|
expect(result).toEqual('');
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('valuesToOptions', () => {
|
|
describe('when called with a record object', () => {
|
|
it('then it should return sorted options from that object', () => {
|
|
const date = new Date();
|
|
const unique = {
|
|
string: 'string',
|
|
numeric: 1,
|
|
date: date,
|
|
boolean: true,
|
|
};
|
|
|
|
const result = valuesToOptions(unique);
|
|
|
|
expect(result).toEqual([
|
|
{ label: 'boolean', value: true },
|
|
{ label: 'date', value: date },
|
|
{ label: 'numeric', value: 1 },
|
|
{ label: 'string', value: 'string' },
|
|
]);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('sortOptions', () => {
|
|
it.each`
|
|
a | b | expected
|
|
${{ label: undefined }} | ${{ label: undefined }} | ${0}
|
|
${{ label: undefined }} | ${{ label: 'b' }} | ${-1}
|
|
${{ label: 'a' }} | ${{ label: undefined }} | ${1}
|
|
${{ label: 'a' }} | ${{ label: 'b' }} | ${-1}
|
|
${{ label: 'b' }} | ${{ label: 'a' }} | ${1}
|
|
${{ label: 'a' }} | ${{ label: 'a' }} | ${0}
|
|
`("when called with a: '$a.toString', b: '$b.toString' then result should be '$expected'", ({ a, b, expected }) => {
|
|
expect(sortOptions(a, b)).toEqual(expected);
|
|
});
|
|
});
|
|
|
|
describe('getFilteredOptions', () => {
|
|
describe('when called without filterValues', () => {
|
|
it('then it should return an empty array', () => {
|
|
const options = [
|
|
{ label: 'a', value: 'a' },
|
|
{ label: 'b', value: 'b' },
|
|
{ label: 'c', value: 'c' },
|
|
];
|
|
const filterValues = undefined;
|
|
|
|
const result = getFilteredOptions(options, filterValues);
|
|
|
|
expect(result).toEqual([]);
|
|
});
|
|
});
|
|
|
|
describe('when called with no options', () => {
|
|
it('then it should return an empty array', () => {
|
|
const options: SelectableValue[] = [];
|
|
const filterValues = [
|
|
{ label: 'a', value: 'a' },
|
|
{ label: 'b', value: 'b' },
|
|
{ label: 'c', value: 'c' },
|
|
];
|
|
|
|
const result = getFilteredOptions(options, filterValues);
|
|
|
|
expect(result).toEqual(options);
|
|
});
|
|
});
|
|
|
|
describe('when called with options and matching filterValues', () => {
|
|
it('then it should return an empty array', () => {
|
|
const options: SelectableValue[] = [
|
|
{ label: 'a', value: 'a' },
|
|
{ label: 'b', value: 'b' },
|
|
{ label: 'c', value: 'c' },
|
|
];
|
|
const filterValues = [
|
|
{ label: 'a', value: 'a' },
|
|
{ label: 'b', value: 'b' },
|
|
];
|
|
|
|
const result = getFilteredOptions(options, filterValues);
|
|
|
|
expect(result).toEqual([
|
|
{ label: 'a', value: 'a' },
|
|
{ label: 'b', value: 'b' },
|
|
]);
|
|
});
|
|
});
|
|
|
|
describe('when called with options and non matching filterValues', () => {
|
|
it('then it should return an empty array', () => {
|
|
const options: SelectableValue[] = [
|
|
{ label: 'a', value: 'a' },
|
|
{ label: 'b', value: 'b' },
|
|
{ label: 'c', value: 'c' },
|
|
];
|
|
const filterValues = [{ label: 'q', value: 'q' }];
|
|
|
|
const result = getFilteredOptions(options, filterValues);
|
|
|
|
expect(result).toEqual([]);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('sortNumber', () => {
|
|
it.each`
|
|
a | b | expected
|
|
${{ values: [] }} | ${{ values: [] }} | ${0}
|
|
${{ values: [undefined] }} | ${{ values: [undefined] }} | ${0}
|
|
${{ values: [null] }} | ${{ values: [null] }} | ${0}
|
|
${{ values: [Number.POSITIVE_INFINITY] }} | ${{ values: [Number.POSITIVE_INFINITY] }} | ${0}
|
|
${{ values: [Number.NEGATIVE_INFINITY] }} | ${{ values: [Number.NEGATIVE_INFINITY] }} | ${0}
|
|
${{ values: [Number.POSITIVE_INFINITY] }} | ${{ values: [Number.NEGATIVE_INFINITY] }} | ${1}
|
|
${{ values: [Number.NEGATIVE_INFINITY] }} | ${{ values: [Number.POSITIVE_INFINITY] }} | ${-1}
|
|
${{ values: ['infinIty'] }} | ${{ values: ['infinIty'] }} | ${0}
|
|
${{ values: ['infinity'] }} | ${{ values: ['not infinity'] }} | ${0}
|
|
${{ values: [1] }} | ${{ values: [1] }} | ${0}
|
|
${{ values: [1.5] }} | ${{ values: [1.5] }} | ${0}
|
|
${{ values: [2] }} | ${{ values: [1] }} | ${1}
|
|
${{ values: [25] }} | ${{ values: [2.5] }} | ${1}
|
|
${{ values: [2.5] }} | ${{ values: [1.5] }} | ${1}
|
|
${{ values: [1] }} | ${{ values: [2] }} | ${-1}
|
|
${{ values: [2.5] }} | ${{ values: [25] }} | ${-1}
|
|
${{ values: [1.5] }} | ${{ values: [2.5] }} | ${-1}
|
|
${{ values: [1] }} | ${{ values: [] }} | ${1}
|
|
${{ values: [1] }} | ${{ values: [undefined] }} | ${1}
|
|
${{ values: [1] }} | ${{ values: [null] }} | ${1}
|
|
${{ values: [1] }} | ${{ values: [Number.POSITIVE_INFINITY] }} | ${-1}
|
|
${{ values: [1] }} | ${{ values: [Number.NEGATIVE_INFINITY] }} | ${1}
|
|
${{ values: [1] }} | ${{ values: ['infinIty'] }} | ${1}
|
|
${{ values: [-1] }} | ${{ values: ['infinIty'] }} | ${1}
|
|
${{ values: [] }} | ${{ values: [1] }} | ${-1}
|
|
${{ values: [undefined] }} | ${{ values: [1] }} | ${-1}
|
|
${{ values: [null] }} | ${{ values: [1] }} | ${-1}
|
|
${{ values: [Number.POSITIVE_INFINITY] }} | ${{ values: [1] }} | ${1}
|
|
${{ values: [Number.NEGATIVE_INFINITY] }} | ${{ values: [1] }} | ${-1}
|
|
${{ values: ['infinIty'] }} | ${{ values: [1] }} | ${-1}
|
|
${{ values: ['infinIty'] }} | ${{ values: [-1] }} | ${-1}
|
|
`("when called with a: '$a.toString', b: '$b.toString' then result should be '$expected'", ({ a, b, expected }) => {
|
|
expect(sortNumber(a, b, '0')).toEqual(expected);
|
|
});
|
|
|
|
it.skip('should have good performance', () => {
|
|
const ITERATIONS = 100000;
|
|
const a: any = { values: Array(ITERATIONS) };
|
|
const b: any = { values: Array(ITERATIONS) };
|
|
for (let i = 0; i < ITERATIONS; i++) {
|
|
a.values[i] = Math.random() * Date.now();
|
|
b.values[i] = Math.random() * Date.now();
|
|
}
|
|
|
|
const start = performance.now();
|
|
for (let i = 0; i < ITERATIONS; i++) {
|
|
sortNumber(a, b, i.toString(10));
|
|
}
|
|
const stop = performance.now();
|
|
const diff = stop - start;
|
|
expect(diff).toBeLessThanOrEqual(20);
|
|
});
|
|
});
|
|
});
|