120 lines
4.3 KiB
TypeScript
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;
|
|
};
|
|
}
|