kibana/x-pack/plugins/lens/public/datatable_visualization/sorting.tsx

120 lines
4.3 KiB
TypeScript

/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import ipaddr from 'ipaddr.js';
import type { IPv4, IPv6 } from 'ipaddr.js';
import { FieldFormat } from 'src/plugins/data/public';
function isIPv6Address(ip: IPv4 | IPv6): ip is IPv6 {
return ip.kind() === 'ipv6';
}
function getSafeIpAddress(ip: string, directionFactor: number) {
if (!ipaddr.isValid(ip)) {
// for non valid IPs have the same behaviour as for now (we assume it's only the "Other" string)
// create a mock object which has all a special value to keep them always at the bottom of the list
return { parts: Array(8).fill(directionFactor * Infinity) };
}
const parsedIp = ipaddr.parse(ip);
return isIPv6Address(parsedIp) ? parsedIp : parsedIp.toIPv4MappedAddress();
}
function getIPCriteria(sortBy: string, directionFactor: number) {
// Create a set of 8 function to sort based on the 8 IPv6 slots of an address
// For IPv4 bring them to the IPv6 "mapped" format and then sort
return (rowA: Record<string, unknown>, rowB: Record<string, unknown>) => {
const ipAString = rowA[sortBy] as string;
const ipBString = rowB[sortBy] as string;
const ipA = getSafeIpAddress(ipAString, directionFactor);
const ipB = getSafeIpAddress(ipBString, directionFactor);
// Now compare each part of the IPv6 address and exit when a value != 0 is found
let i = 0;
let diff = ipA.parts[i] - ipB.parts[i];
while (!diff && i < 7) {
i++;
diff = ipA.parts[i] - ipB.parts[i];
}
// in case of same address but written in different styles, sort by string length
if (diff === 0) {
return directionFactor * (ipAString.length - ipBString.length);
}
return directionFactor * diff;
};
}
function getRangeCriteria(sortBy: string, directionFactor: number) {
// fill missing fields with these open bounds to perform number sorting
const openRange = { gte: -Infinity, lt: Infinity };
return (rowA: Record<string, unknown>, rowB: Record<string, unknown>) => {
const rangeA = { ...openRange, ...(rowA[sortBy] as Omit<Range, 'type'>) };
const rangeB = { ...openRange, ...(rowB[sortBy] as Omit<Range, 'type'>) };
const fromComparison = rangeA.gte - rangeB.gte;
const toComparison = rangeA.lt - rangeB.lt;
return directionFactor * (fromComparison || toComparison);
};
}
type CompareFn = (rowA: Record<string, unknown>, rowB: Record<string, unknown>) => number;
export function getSortingCriteria(
type: string | undefined,
sortBy: string,
formatter: FieldFormat,
direction: string
) {
// handle the direction with a multiply factor.
const directionFactor = direction === 'asc' ? 1 : -1;
let criteria: CompareFn;
if (['number', 'date'].includes(type || '')) {
criteria = (rowA: Record<string, unknown>, rowB: Record<string, unknown>) =>
directionFactor * ((rowA[sortBy] as number) - (rowB[sortBy] as number));
}
// this is a custom type, and can safely assume the gte and lt fields are all numbers or undefined
else if (type === 'range') {
criteria = getRangeCriteria(sortBy, directionFactor);
}
// IP have a special sorting
else if (type === 'ip') {
criteria = getIPCriteria(sortBy, directionFactor);
} else {
// use a string sorter for the rest
criteria = (rowA: Record<string, unknown>, rowB: Record<string, unknown>) => {
const aString = formatter.convert(rowA[sortBy]);
const bString = formatter.convert(rowB[sortBy]);
return directionFactor * aString.localeCompare(bString);
};
}
return getUndefinedHandler(sortBy, criteria);
}
function getUndefinedHandler(
sortBy: string,
sortingCriteria: (rowA: Record<string, unknown>, rowB: Record<string, unknown>) => number
) {
return (rowA: Record<string, unknown>, rowB: Record<string, unknown>) => {
const valueA = rowA[sortBy];
const valueB = rowB[sortBy];
if (valueA != null && valueB != null && !Number.isNaN(valueA) && !Number.isNaN(valueB)) {
return sortingCriteria(rowA, rowB);
}
if (valueA == null || Number.isNaN(valueA)) {
return 1;
}
if (valueB == null || Number.isNaN(valueB)) {
return -1;
}
return 0;
};
}