Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0ac4d0f99d | ||
|
|
7503cbf902 | ||
|
|
13bf14474d | ||
|
|
c7ea032298 | ||
|
|
30776d6179 | ||
|
|
66564c807d | ||
|
|
c40120dd3a | ||
|
|
c1309072de | ||
|
|
3278af0327 | ||
|
|
ccfcec73e9 | ||
|
|
5ffd717953 | ||
|
|
8cd9f5f25a |
@@ -1,8 +1,12 @@
|
||||
/* eslint-disable sonarjs/cognitive-complexity */
|
||||
import { formattedValueToString, getValueFormat } from '@grafana/data';
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { UniversalYAxisUnit } from 'components/YAxisUnitSelector/types';
|
||||
import { isUniversalUnit } from 'components/YAxisUnitSelector/utils';
|
||||
import { isNaN } from 'lodash-es';
|
||||
|
||||
import { formatUniversalUnit } from '../YAxisUnitSelector/formatter';
|
||||
|
||||
const DEFAULT_SIGNIFICANT_DIGITS = 15;
|
||||
// max decimals to keep should not exceed 15 decimal places to avoid floating point precision issues
|
||||
const MAX_DECIMALS = 15;
|
||||
@@ -138,6 +142,12 @@ export const getYAxisFormattedValue = (
|
||||
precision,
|
||||
);
|
||||
}
|
||||
|
||||
// Separate logic for universal units
|
||||
if (format && isUniversalUnit(format)) {
|
||||
return formatUniversalUnit(parseFloat(value), format as UniversalYAxisUnit);
|
||||
}
|
||||
|
||||
return formattedValueToString(formattedValue);
|
||||
} catch (error) {
|
||||
Sentry.captureEvent({
|
||||
|
||||
@@ -0,0 +1,327 @@
|
||||
import { UniversalYAxisUnit } from 'components/YAxisUnitSelector/types';
|
||||
|
||||
import {
|
||||
AdditionalLabelsMappingForGrafanaUnits,
|
||||
UniversalUnitToGrafanaUnit,
|
||||
} from '../constants';
|
||||
import { formatUniversalUnit } from '../formatter';
|
||||
|
||||
const VALUE_BELOW_THRESHOLD = 900;
|
||||
|
||||
describe('formatUniversalUnit', () => {
|
||||
describe('Time', () => {
|
||||
it('formats time values below conversion threshold', () => {
|
||||
expect(formatUniversalUnit(31, UniversalYAxisUnit.DAYS)).toBe('4.43 weeks');
|
||||
expect(formatUniversalUnit(25, UniversalYAxisUnit.HOURS)).toBe('1.04 days');
|
||||
expect(formatUniversalUnit(61, UniversalYAxisUnit.MINUTES)).toBe(
|
||||
'1.02 hours',
|
||||
);
|
||||
expect(formatUniversalUnit(61, UniversalYAxisUnit.SECONDS)).toBe(
|
||||
'1.02 mins',
|
||||
);
|
||||
expect(formatUniversalUnit(1006, UniversalYAxisUnit.MILLISECONDS)).toBe(
|
||||
'1.01 s',
|
||||
);
|
||||
expect(formatUniversalUnit(100006, UniversalYAxisUnit.MICROSECONDS)).toBe(
|
||||
'100 ms',
|
||||
);
|
||||
expect(formatUniversalUnit(1006, UniversalYAxisUnit.NANOSECONDS)).toBe(
|
||||
'1.01 µs',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Data', () => {
|
||||
it('formats data values below conversion threshold', () => {
|
||||
expect(
|
||||
formatUniversalUnit(VALUE_BELOW_THRESHOLD, UniversalYAxisUnit.BYTES),
|
||||
).toBe('900 B');
|
||||
expect(formatUniversalUnit(900, UniversalYAxisUnit.KILOBYTES)).toBe(
|
||||
'900 kB',
|
||||
);
|
||||
expect(formatUniversalUnit(900, UniversalYAxisUnit.MEGABYTES)).toBe(
|
||||
'900 MB',
|
||||
);
|
||||
expect(formatUniversalUnit(900, UniversalYAxisUnit.GIGABYTES)).toBe(
|
||||
'900 GB',
|
||||
);
|
||||
expect(formatUniversalUnit(900, UniversalYAxisUnit.TERABYTES)).toBe(
|
||||
'900 TB',
|
||||
);
|
||||
expect(formatUniversalUnit(900, UniversalYAxisUnit.PETABYTES)).toBe(
|
||||
'900 PB',
|
||||
);
|
||||
expect(formatUniversalUnit(900, UniversalYAxisUnit.EXABYTES)).toBe('900 EB');
|
||||
expect(formatUniversalUnit(900, UniversalYAxisUnit.ZETTABYTES)).toBe(
|
||||
'900 ZB',
|
||||
);
|
||||
expect(formatUniversalUnit(900, UniversalYAxisUnit.YOTTABYTES)).toBe(
|
||||
'900 YB',
|
||||
);
|
||||
});
|
||||
|
||||
it('formats data values above conversion threshold with scaling', () => {
|
||||
expect(formatUniversalUnit(1000, UniversalYAxisUnit.BYTES)).toBe('1 kB');
|
||||
expect(formatUniversalUnit(1000, UniversalYAxisUnit.KILOBYTES)).toBe('1 MB');
|
||||
expect(formatUniversalUnit(1000, UniversalYAxisUnit.MEGABYTES)).toBe('1 GB');
|
||||
expect(formatUniversalUnit(1000, UniversalYAxisUnit.GIGABYTES)).toBe('1 TB');
|
||||
expect(formatUniversalUnit(1000, UniversalYAxisUnit.TERABYTES)).toBe('1 PB');
|
||||
expect(formatUniversalUnit(1000, UniversalYAxisUnit.PETABYTES)).toBe('1 EB');
|
||||
expect(formatUniversalUnit(1000, UniversalYAxisUnit.EXABYTES)).toBe('1 ZB');
|
||||
expect(formatUniversalUnit(1000, UniversalYAxisUnit.ZETTABYTES)).toBe(
|
||||
'1 YB',
|
||||
);
|
||||
});
|
||||
|
||||
it('formats data values above conversion threshold with decimals', () => {
|
||||
expect(formatUniversalUnit(1034, UniversalYAxisUnit.BYTES)).toBe('1.03 kB');
|
||||
expect(formatUniversalUnit(1034, UniversalYAxisUnit.KILOBYTES)).toBe(
|
||||
'1.03 MB',
|
||||
);
|
||||
expect(formatUniversalUnit(1034, UniversalYAxisUnit.MEGABYTES)).toBe(
|
||||
'1.03 GB',
|
||||
);
|
||||
expect(formatUniversalUnit(1034, UniversalYAxisUnit.TERABYTES)).toBe(
|
||||
'1.03 PB',
|
||||
);
|
||||
expect(formatUniversalUnit(1034, UniversalYAxisUnit.PETABYTES)).toBe(
|
||||
'1.03 EB',
|
||||
);
|
||||
expect(formatUniversalUnit(1034, UniversalYAxisUnit.EXABYTES)).toBe(
|
||||
'1.03 ZB',
|
||||
);
|
||||
expect(formatUniversalUnit(1034, UniversalYAxisUnit.ZETTABYTES)).toBe(
|
||||
'1.03 YB',
|
||||
);
|
||||
expect(formatUniversalUnit(1034, UniversalYAxisUnit.YOTTABYTES)).toBe(
|
||||
'1034 YB',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Data rate', () => {
|
||||
it('formats data rate values below conversion threshold', () => {
|
||||
expect(formatUniversalUnit(900, UniversalYAxisUnit.BYTES_SECOND)).toBe(
|
||||
'900 B/s',
|
||||
);
|
||||
expect(formatUniversalUnit(900, UniversalYAxisUnit.KILOBYTES_SECOND)).toBe(
|
||||
'900 kB/s',
|
||||
);
|
||||
expect(formatUniversalUnit(900, UniversalYAxisUnit.MEGABYTES_SECOND)).toBe(
|
||||
'900 MB/s',
|
||||
);
|
||||
expect(formatUniversalUnit(900, UniversalYAxisUnit.GIGABYTES_SECOND)).toBe(
|
||||
'900 GB/s',
|
||||
);
|
||||
expect(formatUniversalUnit(900, UniversalYAxisUnit.TERABYTES_SECOND)).toBe(
|
||||
'900 TB/s',
|
||||
);
|
||||
expect(formatUniversalUnit(900, UniversalYAxisUnit.PETABYTES_SECOND)).toBe(
|
||||
'900 PB/s',
|
||||
);
|
||||
expect(formatUniversalUnit(900, UniversalYAxisUnit.EXABYTES_SECOND)).toBe(
|
||||
'900 EB/s',
|
||||
);
|
||||
expect(formatUniversalUnit(900, UniversalYAxisUnit.ZETTABYTES_SECOND)).toBe(
|
||||
'900 ZB/s',
|
||||
);
|
||||
expect(formatUniversalUnit(900, UniversalYAxisUnit.YOTTABYTES_SECOND)).toBe(
|
||||
'900 YB/s',
|
||||
);
|
||||
});
|
||||
|
||||
it('formats data rate values above conversion threshold with scaling (1000)', () => {
|
||||
expect(formatUniversalUnit(1000, UniversalYAxisUnit.BYTES_SECOND)).toBe(
|
||||
'1 kB/s',
|
||||
);
|
||||
expect(formatUniversalUnit(1000, UniversalYAxisUnit.KILOBYTES_SECOND)).toBe(
|
||||
'1 MB/s',
|
||||
);
|
||||
expect(formatUniversalUnit(1000, UniversalYAxisUnit.MEGABYTES_SECOND)).toBe(
|
||||
'1 GB/s',
|
||||
);
|
||||
expect(formatUniversalUnit(1000, UniversalYAxisUnit.GIGABYTES_SECOND)).toBe(
|
||||
'1 TB/s',
|
||||
);
|
||||
expect(formatUniversalUnit(1000, UniversalYAxisUnit.TERABYTES_SECOND)).toBe(
|
||||
'1 PB/s',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Bit', () => {
|
||||
it('formats bit values below conversion threshold', () => {
|
||||
expect(formatUniversalUnit(900, UniversalYAxisUnit.BITS)).toBe('900 b');
|
||||
expect(formatUniversalUnit(900, UniversalYAxisUnit.KILOBITS)).toBe('900 kb');
|
||||
expect(formatUniversalUnit(900, UniversalYAxisUnit.MEGABITS)).toBe('900 Mb');
|
||||
expect(formatUniversalUnit(900, UniversalYAxisUnit.GIGABITS)).toBe('900 Gb');
|
||||
expect(formatUniversalUnit(900, UniversalYAxisUnit.TERABITS)).toBe('900 Tb');
|
||||
expect(formatUniversalUnit(900, UniversalYAxisUnit.PETABITS)).toBe('900 Pb');
|
||||
expect(formatUniversalUnit(900, UniversalYAxisUnit.EXABITS)).toBe('900 Eb');
|
||||
expect(formatUniversalUnit(900, UniversalYAxisUnit.ZETTABITS)).toBe(
|
||||
'900 Zb',
|
||||
);
|
||||
expect(formatUniversalUnit(900, UniversalYAxisUnit.YOTTABITS)).toBe(
|
||||
'900 Yb',
|
||||
);
|
||||
});
|
||||
|
||||
it('formats bit values above conversion threshold with scaling (1000)', () => {
|
||||
expect(formatUniversalUnit(1000, UniversalYAxisUnit.BITS)).toBe('1 kb');
|
||||
expect(formatUniversalUnit(1000, UniversalYAxisUnit.KILOBITS)).toBe('1 Mb');
|
||||
expect(formatUniversalUnit(1000, UniversalYAxisUnit.MEGABITS)).toBe('1 Gb');
|
||||
expect(formatUniversalUnit(1000, UniversalYAxisUnit.GIGABITS)).toBe('1 Tb');
|
||||
expect(formatUniversalUnit(1000, UniversalYAxisUnit.TERABITS)).toBe('1 Pb');
|
||||
expect(formatUniversalUnit(1000, UniversalYAxisUnit.PETABITS)).toBe('1 Eb');
|
||||
expect(formatUniversalUnit(1000, UniversalYAxisUnit.EXABITS)).toBe('1 Zb');
|
||||
expect(formatUniversalUnit(1000, UniversalYAxisUnit.ZETTABITS)).toBe('1 Yb');
|
||||
});
|
||||
|
||||
it('formats bit values below conversion threshold with decimals (0.5)', () => {
|
||||
expect(formatUniversalUnit(0.5, UniversalYAxisUnit.KILOBITS)).toBe('500 b');
|
||||
expect(formatUniversalUnit(0.001, UniversalYAxisUnit.MEGABITS)).toBe('1 kb');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Bit rate', () => {
|
||||
it('formats bit rate values below conversion threshold', () => {
|
||||
expect(formatUniversalUnit(900, UniversalYAxisUnit.BITS_SECOND)).toBe(
|
||||
'900 b/s',
|
||||
);
|
||||
});
|
||||
|
||||
it('formats bit rate values above conversion threshold with scaling (1000)', () => {
|
||||
expect(formatUniversalUnit(1000, UniversalYAxisUnit.BITS_SECOND)).toBe(
|
||||
'1 kb/s',
|
||||
);
|
||||
expect(formatUniversalUnit(1000, UniversalYAxisUnit.KILOBITS_SECOND)).toBe(
|
||||
'1 Mb/s',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Count', () => {
|
||||
it('formats small values without abbreviation', () => {
|
||||
expect(formatUniversalUnit(100, UniversalYAxisUnit.COUNT)).toBe('100');
|
||||
expect(formatUniversalUnit(900, UniversalYAxisUnit.COUNT)).toBe('900');
|
||||
});
|
||||
|
||||
it('formats count values above conversion threshold with scaling (1000)', () => {
|
||||
expect(formatUniversalUnit(1000, UniversalYAxisUnit.COUNT)).toBe('1 K');
|
||||
expect(formatUniversalUnit(10000, UniversalYAxisUnit.COUNT)).toBe('10 K');
|
||||
expect(formatUniversalUnit(100000, UniversalYAxisUnit.COUNT)).toBe('100 K');
|
||||
expect(formatUniversalUnit(1000000, UniversalYAxisUnit.COUNT)).toBe('1 Mil');
|
||||
expect(formatUniversalUnit(10000000, UniversalYAxisUnit.COUNT)).toBe(
|
||||
'10 Mil',
|
||||
);
|
||||
expect(formatUniversalUnit(100000000, UniversalYAxisUnit.COUNT)).toBe(
|
||||
'100 Mil',
|
||||
);
|
||||
expect(formatUniversalUnit(1000000000, UniversalYAxisUnit.COUNT)).toBe(
|
||||
'1 Bil',
|
||||
);
|
||||
expect(formatUniversalUnit(1000000000000, UniversalYAxisUnit.COUNT)).toBe(
|
||||
'1 Tri',
|
||||
);
|
||||
});
|
||||
|
||||
it('formats count per time units', () => {
|
||||
expect(formatUniversalUnit(900, UniversalYAxisUnit.COUNT_SECOND)).toBe(
|
||||
'900 c/s',
|
||||
);
|
||||
expect(formatUniversalUnit(900, UniversalYAxisUnit.COUNT_MINUTE)).toBe(
|
||||
'900 c/m',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Operations units', () => {
|
||||
it('formats operations per time', () => {
|
||||
expect(formatUniversalUnit(900, UniversalYAxisUnit.OPS_SECOND)).toBe(
|
||||
'900 ops/s',
|
||||
);
|
||||
expect(formatUniversalUnit(900, UniversalYAxisUnit.OPS_MINUTE)).toBe(
|
||||
'900 ops/m',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Request units', () => {
|
||||
it('formats requests per time', () => {
|
||||
expect(formatUniversalUnit(900, UniversalYAxisUnit.REQUESTS_SECOND)).toBe(
|
||||
'900 req/s',
|
||||
);
|
||||
expect(formatUniversalUnit(900, UniversalYAxisUnit.REQUESTS_MINUTE)).toBe(
|
||||
'900 req/m',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Read/Write units', () => {
|
||||
it('formats reads and writes per time', () => {
|
||||
expect(formatUniversalUnit(900, UniversalYAxisUnit.READS_SECOND)).toBe(
|
||||
'900 rd/s',
|
||||
);
|
||||
expect(formatUniversalUnit(900, UniversalYAxisUnit.WRITES_SECOND)).toBe(
|
||||
'900 wr/s',
|
||||
);
|
||||
expect(formatUniversalUnit(900, UniversalYAxisUnit.READS_MINUTE)).toBe(
|
||||
'900 rd/m',
|
||||
);
|
||||
expect(formatUniversalUnit(900, UniversalYAxisUnit.WRITES_MINUTE)).toBe(
|
||||
'900 wr/m',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('IO Operations units', () => {
|
||||
it('formats IOPS', () => {
|
||||
expect(formatUniversalUnit(900, UniversalYAxisUnit.IOOPS_SECOND)).toBe(
|
||||
'900 io/s',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Percent units', () => {
|
||||
it('formats percent as-is', () => {
|
||||
expect(formatUniversalUnit(900, UniversalYAxisUnit.PERCENT)).toBe('900%');
|
||||
});
|
||||
|
||||
it('multiplies percent_unit by 100', () => {
|
||||
expect(formatUniversalUnit(9, UniversalYAxisUnit.PERCENT_UNIT)).toBe('900%');
|
||||
});
|
||||
});
|
||||
|
||||
describe('None unit', () => {
|
||||
it('formats as plain number', () => {
|
||||
expect(formatUniversalUnit(900, UniversalYAxisUnit.NONE)).toBe('900');
|
||||
});
|
||||
});
|
||||
|
||||
describe('High-order byte scaling', () => {
|
||||
it('scales between EB, ZB, YB', () => {
|
||||
expect(formatUniversalUnit(1000, UniversalYAxisUnit.EXABYTES)).toBe('1 ZB');
|
||||
expect(formatUniversalUnit(1000, UniversalYAxisUnit.ZETTABYTES)).toBe(
|
||||
'1 YB',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Mapping Validator', () => {
|
||||
it('validates that all units have a mapping', () => {
|
||||
// Each universal unit should have a mapping to a 1:1 Grafana unit in UniversalUnitToGrafanaUnit or an additional mapping in AdditionalLabelsMappingForGrafanaUnits
|
||||
const units = Object.values(UniversalYAxisUnit);
|
||||
expect(
|
||||
units.every((unit) => {
|
||||
const hasBaseMapping = unit in UniversalUnitToGrafanaUnit;
|
||||
const hasAdditionalMapping = unit in AdditionalLabelsMappingForGrafanaUnits;
|
||||
const hasMapping = hasBaseMapping || hasAdditionalMapping;
|
||||
if (!hasMapping) {
|
||||
console.error(`Unit ${unit} does not have a mapping`);
|
||||
}
|
||||
return hasMapping;
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { UniversalYAxisUnit, YAxisUnit } from './types';
|
||||
import { UnitFamilyConfig, UniversalYAxisUnit, YAxisUnit } from './types';
|
||||
|
||||
// Mapping of universal y-axis units to their AWS, UCUM, and OpenMetrics equivalents
|
||||
export const UniversalYAxisUnitMappings: Record<
|
||||
@@ -625,3 +625,152 @@ export const Y_AXIS_CATEGORIES = [
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const UniversalUnitToGrafanaUnit: Partial<
|
||||
Record<UniversalYAxisUnit, string>
|
||||
> = {
|
||||
// Time
|
||||
[UniversalYAxisUnit.WEEKS]: 'wk',
|
||||
[UniversalYAxisUnit.DAYS]: 'd',
|
||||
[UniversalYAxisUnit.HOURS]: 'h',
|
||||
[UniversalYAxisUnit.MINUTES]: 'm',
|
||||
[UniversalYAxisUnit.SECONDS]: 's',
|
||||
[UniversalYAxisUnit.MILLISECONDS]: 'ms',
|
||||
[UniversalYAxisUnit.MICROSECONDS]: 'µs',
|
||||
[UniversalYAxisUnit.NANOSECONDS]: 'ns',
|
||||
|
||||
// Data (Grafana uses 1024-based IEC format)
|
||||
[UniversalYAxisUnit.BYTES]: 'decbytes',
|
||||
[UniversalYAxisUnit.KILOBYTES]: 'deckbytes',
|
||||
[UniversalYAxisUnit.MEGABYTES]: 'decmbytes',
|
||||
[UniversalYAxisUnit.GIGABYTES]: 'decgbytes',
|
||||
[UniversalYAxisUnit.TERABYTES]: 'dectbytes',
|
||||
[UniversalYAxisUnit.PETABYTES]: 'decpbytes',
|
||||
|
||||
// Data Rate
|
||||
[UniversalYAxisUnit.BYTES_SECOND]: 'Bps',
|
||||
[UniversalYAxisUnit.KILOBYTES_SECOND]: 'KBs',
|
||||
[UniversalYAxisUnit.MEGABYTES_SECOND]: 'MBs',
|
||||
[UniversalYAxisUnit.GIGABYTES_SECOND]: 'GBs',
|
||||
[UniversalYAxisUnit.TERABYTES_SECOND]: 'TBs',
|
||||
[UniversalYAxisUnit.PETABYTES_SECOND]: 'PBs',
|
||||
|
||||
// Bits
|
||||
[UniversalYAxisUnit.BITS]: 'bits',
|
||||
|
||||
// Bit Rate
|
||||
[UniversalYAxisUnit.BITS_SECOND]: 'bps',
|
||||
[UniversalYAxisUnit.KILOBITS_SECOND]: 'Kbits',
|
||||
[UniversalYAxisUnit.MEGABITS_SECOND]: 'Mbits',
|
||||
[UniversalYAxisUnit.GIGABITS_SECOND]: 'Gbits',
|
||||
[UniversalYAxisUnit.TERABITS_SECOND]: 'Tbits',
|
||||
[UniversalYAxisUnit.PETABITS_SECOND]: 'Pbits',
|
||||
|
||||
// Count
|
||||
[UniversalYAxisUnit.COUNT]: 'short',
|
||||
[UniversalYAxisUnit.COUNT_SECOND]: 'cps',
|
||||
[UniversalYAxisUnit.COUNT_MINUTE]: 'cpm',
|
||||
|
||||
// Operations
|
||||
[UniversalYAxisUnit.OPS_SECOND]: 'ops',
|
||||
[UniversalYAxisUnit.OPS_MINUTE]: 'opm',
|
||||
|
||||
// Requests
|
||||
[UniversalYAxisUnit.REQUESTS_SECOND]: 'reqps',
|
||||
[UniversalYAxisUnit.REQUESTS_MINUTE]: 'reqpm',
|
||||
|
||||
// Reads/Writes
|
||||
[UniversalYAxisUnit.READS_SECOND]: 'rps',
|
||||
[UniversalYAxisUnit.WRITES_SECOND]: 'wps',
|
||||
[UniversalYAxisUnit.READS_MINUTE]: 'rpm',
|
||||
[UniversalYAxisUnit.WRITES_MINUTE]: 'wpm',
|
||||
|
||||
// IO Operations
|
||||
[UniversalYAxisUnit.IOOPS_SECOND]: 'iops',
|
||||
|
||||
// Percent
|
||||
[UniversalYAxisUnit.PERCENT]: 'percent',
|
||||
[UniversalYAxisUnit.PERCENT_UNIT]: 'percentunit',
|
||||
|
||||
// None
|
||||
[UniversalYAxisUnit.NONE]: 'none',
|
||||
};
|
||||
|
||||
export const AdditionalLabelsMappingForGrafanaUnits: Partial<
|
||||
Record<UniversalYAxisUnit, string>
|
||||
> = {
|
||||
// Data
|
||||
[UniversalYAxisUnit.EXABYTES]: 'EB',
|
||||
[UniversalYAxisUnit.ZETTABYTES]: 'ZB',
|
||||
[UniversalYAxisUnit.YOTTABYTES]: 'YB',
|
||||
|
||||
// Data Rate
|
||||
[UniversalYAxisUnit.EXABYTES_SECOND]: 'EB/s',
|
||||
[UniversalYAxisUnit.ZETTABYTES_SECOND]: 'ZB/s',
|
||||
[UniversalYAxisUnit.YOTTABYTES_SECOND]: 'YB/s',
|
||||
|
||||
// Bits
|
||||
[UniversalYAxisUnit.BITS]: 'b',
|
||||
[UniversalYAxisUnit.KILOBITS]: 'kb',
|
||||
[UniversalYAxisUnit.MEGABITS]: 'Mb',
|
||||
[UniversalYAxisUnit.GIGABITS]: 'Gb',
|
||||
[UniversalYAxisUnit.TERABITS]: 'Tb',
|
||||
[UniversalYAxisUnit.PETABITS]: 'Pb',
|
||||
[UniversalYAxisUnit.EXABITS]: 'Eb',
|
||||
[UniversalYAxisUnit.ZETTABITS]: 'Zb',
|
||||
[UniversalYAxisUnit.YOTTABITS]: 'Yb',
|
||||
|
||||
// Bit Rate
|
||||
[UniversalYAxisUnit.EXABITS_SECOND]: 'Eb/s',
|
||||
[UniversalYAxisUnit.ZETTABITS_SECOND]: 'Zb/s',
|
||||
[UniversalYAxisUnit.YOTTABITS_SECOND]: 'Yb/s',
|
||||
};
|
||||
|
||||
/**
|
||||
* Configuration for unit families that need custom scaling
|
||||
* These are units where Grafana doesn't auto-scale between levels
|
||||
*/
|
||||
export const CUSTOM_SCALING_FAMILIES: UnitFamilyConfig[] = [
|
||||
// Bits (b → kb → Mb → Gb → Tb → Pb → Eb → Zb → Yb)
|
||||
{
|
||||
units: [
|
||||
UniversalYAxisUnit.BITS,
|
||||
UniversalYAxisUnit.KILOBITS,
|
||||
UniversalYAxisUnit.MEGABITS,
|
||||
UniversalYAxisUnit.GIGABITS,
|
||||
UniversalYAxisUnit.TERABITS,
|
||||
UniversalYAxisUnit.PETABITS,
|
||||
UniversalYAxisUnit.EXABITS,
|
||||
UniversalYAxisUnit.ZETTABITS,
|
||||
UniversalYAxisUnit.YOTTABITS,
|
||||
],
|
||||
scaleFactor: 1000,
|
||||
},
|
||||
// High-order bit rates (Eb/s → Zb/s → Yb/s)
|
||||
{
|
||||
units: [
|
||||
UniversalYAxisUnit.EXABITS_SECOND,
|
||||
UniversalYAxisUnit.ZETTABITS_SECOND,
|
||||
UniversalYAxisUnit.YOTTABITS_SECOND,
|
||||
],
|
||||
scaleFactor: 1000,
|
||||
},
|
||||
// High-order bytes (EB → ZB → YB)
|
||||
{
|
||||
units: [
|
||||
UniversalYAxisUnit.EXABYTES,
|
||||
UniversalYAxisUnit.ZETTABYTES,
|
||||
UniversalYAxisUnit.YOTTABYTES,
|
||||
],
|
||||
scaleFactor: 1000,
|
||||
},
|
||||
// High-order byte rates (EB/s → ZB/s → YB/s)
|
||||
{
|
||||
units: [
|
||||
UniversalYAxisUnit.EXABYTES_SECOND,
|
||||
UniversalYAxisUnit.ZETTABYTES_SECOND,
|
||||
UniversalYAxisUnit.YOTTABYTES_SECOND,
|
||||
],
|
||||
scaleFactor: 1000,
|
||||
},
|
||||
];
|
||||
|
||||
70
frontend/src/components/YAxisUnitSelector/formatter.ts
Normal file
70
frontend/src/components/YAxisUnitSelector/formatter.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { formattedValueToString, getValueFormat } from '@grafana/data';
|
||||
import {
|
||||
AdditionalLabelsMappingForGrafanaUnits,
|
||||
CUSTOM_SCALING_FAMILIES,
|
||||
UniversalUnitToGrafanaUnit,
|
||||
} from 'components/YAxisUnitSelector/constants';
|
||||
import { UniversalYAxisUnit } from 'components/YAxisUnitSelector/types';
|
||||
|
||||
const format = (
|
||||
formatStr: string,
|
||||
value: number,
|
||||
): ReturnType<ReturnType<typeof getValueFormat>> =>
|
||||
getValueFormat(formatStr)(value, undefined, undefined, undefined);
|
||||
|
||||
function scaleValue(
|
||||
value: number,
|
||||
unit: UniversalYAxisUnit,
|
||||
family: UniversalYAxisUnit[],
|
||||
factor: number,
|
||||
): { value: number; label: string } {
|
||||
let idx = family.indexOf(unit);
|
||||
// If the unit is not in the family, return the unit with the additional label
|
||||
if (idx === -1) {
|
||||
return { value, label: AdditionalLabelsMappingForGrafanaUnits[unit] || '' };
|
||||
}
|
||||
|
||||
// Scale the value up or down to the nearest unit in the family
|
||||
let scaled = value;
|
||||
// Scale up
|
||||
while (scaled >= factor && idx < family.length - 1) {
|
||||
scaled /= factor;
|
||||
idx += 1;
|
||||
}
|
||||
// Scale down
|
||||
while (scaled < 1 && idx > 0) {
|
||||
scaled *= factor;
|
||||
idx -= 1;
|
||||
}
|
||||
|
||||
// Return the scaled value and the label of the nearest unit in the family
|
||||
return {
|
||||
value: scaled,
|
||||
label: AdditionalLabelsMappingForGrafanaUnits[family[idx]] || '',
|
||||
};
|
||||
}
|
||||
|
||||
export function formatUniversalUnit(
|
||||
value: number,
|
||||
unit: UniversalYAxisUnit,
|
||||
): string {
|
||||
// Check if this unit belongs to a family that needs custom scaling
|
||||
const family = CUSTOM_SCALING_FAMILIES.find((family) =>
|
||||
family.units.includes(unit),
|
||||
);
|
||||
if (family) {
|
||||
const scaled = scaleValue(value, unit, family.units, family.scaleFactor);
|
||||
const formatted = format('none', scaled.value);
|
||||
return `${formatted.text} ${scaled.label}`;
|
||||
}
|
||||
|
||||
// Use Grafana formatting with custom label mappings
|
||||
const grafanaFormat = UniversalUnitToGrafanaUnit[unit];
|
||||
if (grafanaFormat) {
|
||||
const formatted = format(grafanaFormat, value);
|
||||
return formattedValueToString(formatted);
|
||||
}
|
||||
|
||||
// Fallback to short format for other units
|
||||
return `${format('short', value).text} ${unit}`;
|
||||
}
|
||||
@@ -14,7 +14,7 @@ export enum UniversalYAxisUnit {
|
||||
HOURS = 'h',
|
||||
MINUTES = 'min',
|
||||
SECONDS = 's',
|
||||
MICROSECONDS = 'us',
|
||||
MICROSECONDS = 'µs',
|
||||
MILLISECONDS = 'ms',
|
||||
NANOSECONDS = 'ns',
|
||||
|
||||
@@ -364,3 +364,13 @@ export enum YAxisUnit {
|
||||
|
||||
OPEN_METRICS_PERCENT_UNIT = 'percentunit',
|
||||
}
|
||||
|
||||
export interface ScaledValue {
|
||||
value: number;
|
||||
label: string;
|
||||
}
|
||||
|
||||
export interface UnitFamilyConfig {
|
||||
units: UniversalYAxisUnit[];
|
||||
scaleFactor: number;
|
||||
}
|
||||
|
||||
@@ -31,3 +31,9 @@ export const getUniversalNameFromMetricUnit = (
|
||||
|
||||
return universalName || unit || '-';
|
||||
};
|
||||
|
||||
export function isUniversalUnit(format: string): boolean {
|
||||
return Object.values(UniversalYAxisUnit).includes(
|
||||
format as UniversalYAxisUnit,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -58,6 +58,20 @@
|
||||
.explore-content {
|
||||
padding: 0 8px;
|
||||
|
||||
.y-axis-unit-selector-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
|
||||
.save-unit-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-space {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 20px;
|
||||
@@ -75,6 +89,14 @@
|
||||
.time-series-view {
|
||||
min-width: 100%;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
.no-unit-warning {
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
right: 40px;
|
||||
z-index: 1000;
|
||||
}
|
||||
}
|
||||
|
||||
.time-series-container {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import './Explorer.styles.scss';
|
||||
|
||||
import * as Sentry from '@sentry/react';
|
||||
import { Switch } from 'antd';
|
||||
import { Switch, Tooltip } from 'antd';
|
||||
import logEvent from 'api/common/logEvent';
|
||||
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
|
||||
import WarningPopover from 'components/WarningPopover/WarningPopover';
|
||||
@@ -25,10 +25,10 @@ import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToD
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { MetricsExplorerEventKeys, MetricsExplorerEvents } from '../events';
|
||||
// import QuerySection from './QuerySection';
|
||||
import MetricDetails from '../MetricDetails/MetricDetails';
|
||||
import TimeSeries from './TimeSeries';
|
||||
import { ExplorerTabs } from './types';
|
||||
import { splitQueryIntoOneChartPerQuery } from './utils';
|
||||
import { splitQueryIntoOneChartPerQuery, useGetMetricUnits } from './utils';
|
||||
|
||||
const ONE_CHART_PER_QUERY_ENABLED_KEY = 'isOneChartPerQueryEnabled';
|
||||
|
||||
@@ -40,6 +40,31 @@ function Explorer(): JSX.Element {
|
||||
currentQuery,
|
||||
} = useQueryBuilder();
|
||||
const { safeNavigate } = useSafeNavigate();
|
||||
const [isMetricDetailsOpen, setIsMetricDetailsOpen] = useState(false);
|
||||
|
||||
const metricNames = useMemo(
|
||||
() =>
|
||||
stagedQuery?.builder.queryData.map(
|
||||
(query) => query.aggregateAttribute?.key ?? '',
|
||||
) ?? [],
|
||||
[stagedQuery],
|
||||
);
|
||||
|
||||
const {
|
||||
units,
|
||||
metrics,
|
||||
isLoading: isMetricUnitsLoading,
|
||||
isError: isMetricUnitsError,
|
||||
} = useGetMetricUnits(metricNames);
|
||||
|
||||
const areAllMetricUnitsSame = useMemo(
|
||||
() =>
|
||||
!isMetricUnitsLoading &&
|
||||
!isMetricUnitsError &&
|
||||
units.length > 0 &&
|
||||
units.every((unit) => unit === units[0]),
|
||||
[units, isMetricUnitsLoading, isMetricUnitsError],
|
||||
);
|
||||
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const isOneChartPerQueryEnabled =
|
||||
@@ -48,7 +73,31 @@ function Explorer(): JSX.Element {
|
||||
const [showOneChartPerQuery, toggleShowOneChartPerQuery] = useState(
|
||||
isOneChartPerQueryEnabled,
|
||||
);
|
||||
const [disableOneChartPerQuery, toggleDisableOneChartPerQuery] = useState(
|
||||
false,
|
||||
);
|
||||
const [selectedTab] = useState<ExplorerTabs>(ExplorerTabs.TIME_SERIES);
|
||||
const [yAxisUnit, setYAxisUnit] = useState<string>('');
|
||||
|
||||
useEffect(() => {
|
||||
if (units.length === 0) {
|
||||
setYAxisUnit('');
|
||||
} else if (units.length === 1 && units[0] !== '') {
|
||||
setYAxisUnit(units[0]);
|
||||
} else if (areAllMetricUnitsSame && units[0] !== '') {
|
||||
setYAxisUnit(units[0]);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [JSON.stringify(units), areAllMetricUnitsSame]);
|
||||
|
||||
useEffect(() => {
|
||||
if (units.length > 1 && !areAllMetricUnitsSame) {
|
||||
toggleShowOneChartPerQuery(true);
|
||||
toggleDisableOneChartPerQuery(true);
|
||||
} else {
|
||||
toggleDisableOneChartPerQuery(false);
|
||||
}
|
||||
}, [units, areAllMetricUnitsSame]);
|
||||
|
||||
const handleToggleShowOneChartPerQuery = (): void => {
|
||||
toggleShowOneChartPerQuery(!showOneChartPerQuery);
|
||||
@@ -68,15 +117,20 @@ function Explorer(): JSX.Element {
|
||||
[updateAllQueriesOperators],
|
||||
);
|
||||
|
||||
const exportDefaultQuery = useMemo(
|
||||
() =>
|
||||
updateAllQueriesOperators(
|
||||
currentQuery || initialQueriesMap[DataSource.METRICS],
|
||||
PANEL_TYPES.TIME_SERIES,
|
||||
DataSource.METRICS,
|
||||
),
|
||||
[currentQuery, updateAllQueriesOperators],
|
||||
);
|
||||
const exportDefaultQuery = useMemo(() => {
|
||||
const query = updateAllQueriesOperators(
|
||||
currentQuery || initialQueriesMap[DataSource.METRICS],
|
||||
PANEL_TYPES.TIME_SERIES,
|
||||
DataSource.METRICS,
|
||||
);
|
||||
if (yAxisUnit) {
|
||||
return {
|
||||
...query,
|
||||
unit: yAxisUnit,
|
||||
};
|
||||
}
|
||||
return query;
|
||||
}, [currentQuery, updateAllQueriesOperators, yAxisUnit]);
|
||||
|
||||
useShareBuilderUrl({ defaultValue: defaultQuery });
|
||||
|
||||
@@ -90,8 +144,16 @@ function Explorer(): JSX.Element {
|
||||
|
||||
const widgetId = uuid();
|
||||
|
||||
let query = queryToExport || exportDefaultQuery;
|
||||
if (yAxisUnit) {
|
||||
query = {
|
||||
...query,
|
||||
unit: yAxisUnit,
|
||||
};
|
||||
}
|
||||
|
||||
const dashboardEditView = generateExportToDashboardLink({
|
||||
query: queryToExport || exportDefaultQuery,
|
||||
query,
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
dashboardId: dashboard.id,
|
||||
widgetId,
|
||||
@@ -99,7 +161,7 @@ function Explorer(): JSX.Element {
|
||||
|
||||
safeNavigate(dashboardEditView);
|
||||
},
|
||||
[exportDefaultQuery, safeNavigate],
|
||||
[exportDefaultQuery, safeNavigate, yAxisUnit],
|
||||
);
|
||||
|
||||
const splitedQueries = useMemo(
|
||||
@@ -129,11 +191,17 @@ function Explorer(): JSX.Element {
|
||||
<div className="explore-header">
|
||||
<div className="explore-header-left-actions">
|
||||
<span>1 chart/query</span>
|
||||
<Switch
|
||||
checked={showOneChartPerQuery}
|
||||
onChange={handleToggleShowOneChartPerQuery}
|
||||
size="small"
|
||||
/>
|
||||
<Tooltip
|
||||
open={disableOneChartPerQuery ? undefined : false}
|
||||
title="One chart per query cannot be disabled for multiple queries with different units."
|
||||
>
|
||||
<Switch
|
||||
checked={showOneChartPerQuery}
|
||||
onChange={handleToggleShowOneChartPerQuery}
|
||||
disabled={disableOneChartPerQuery}
|
||||
size="small"
|
||||
/>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<div className="explore-header-right-actions">
|
||||
{!isEmpty(warning) && <WarningPopover warningData={warning} />}
|
||||
@@ -174,6 +242,15 @@ function Explorer(): JSX.Element {
|
||||
<TimeSeries
|
||||
showOneChartPerQuery={showOneChartPerQuery}
|
||||
setWarning={setWarning}
|
||||
areAllMetricUnitsSame={areAllMetricUnitsSame}
|
||||
isMetricUnitsLoading={isMetricUnitsLoading}
|
||||
isMetricUnitsError={isMetricUnitsError}
|
||||
metricUnits={units}
|
||||
metricNames={metricNames}
|
||||
metrics={metrics}
|
||||
setIsMetricDetailsOpen={setIsMetricDetailsOpen}
|
||||
yAxisUnit={yAxisUnit}
|
||||
setYAxisUnit={setYAxisUnit}
|
||||
/>
|
||||
)}
|
||||
{/* TODO: Enable once we have resolved all related metrics issues */}
|
||||
@@ -190,6 +267,14 @@ function Explorer(): JSX.Element {
|
||||
isOneChartPerQuery={false}
|
||||
splitedQueries={splitedQueries}
|
||||
/>
|
||||
{isMetricDetailsOpen && (
|
||||
<MetricDetails
|
||||
metricName={metricNames[0]}
|
||||
isOpen={isMetricDetailsOpen}
|
||||
onClose={(): void => setIsMetricDetailsOpen(false)}
|
||||
isModalTimeSelection={false}
|
||||
/>
|
||||
)}
|
||||
</Sentry.ErrorBoundary>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Button, Tooltip, Typography } from 'antd';
|
||||
import { MetricType } from 'api/metricsExplorer/getMetricsList';
|
||||
import { isAxiosError } from 'axios';
|
||||
import classNames from 'classnames';
|
||||
import YAxisUnitSelector from 'components/YAxisUnitSelector';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { BuilderUnitsFilter } from 'container/QueryBuilder/filters/BuilderUnitsFilter/BuilderUnits';
|
||||
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
|
||||
import { convertDataValueToMs } from 'container/TimeSeriesView/utils';
|
||||
import { useUpdateMetricMetadata } from 'hooks/metricsExplorer/useUpdateMetricMetadata';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useNotifications } from 'hooks/useNotifications';
|
||||
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useQueries } from 'react-query';
|
||||
import { AlertTriangle } from 'lucide-react';
|
||||
import { useMemo } from 'react';
|
||||
import { useQueries, useQueryClient } from 'react-query';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { SuccessResponse } from 'types/api';
|
||||
@@ -24,8 +30,19 @@ import { splitQueryIntoOneChartPerQuery } from './utils';
|
||||
function TimeSeries({
|
||||
showOneChartPerQuery,
|
||||
setWarning,
|
||||
areAllMetricUnitsSame,
|
||||
isMetricUnitsLoading,
|
||||
isMetricUnitsError,
|
||||
metricUnits,
|
||||
metricNames,
|
||||
metrics,
|
||||
setIsMetricDetailsOpen,
|
||||
yAxisUnit,
|
||||
setYAxisUnit,
|
||||
}: TimeSeriesProps): JSX.Element {
|
||||
const { stagedQuery, currentQuery } = useQueryBuilder();
|
||||
const { notifications } = useNotifications();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const { selectedTime: globalSelectedTime, maxTime, minTime } = useSelector<
|
||||
AppState,
|
||||
@@ -61,8 +78,6 @@ function TimeSeries({
|
||||
[showOneChartPerQuery, stagedQuery],
|
||||
);
|
||||
|
||||
const [yAxisUnit, setYAxisUnit] = useState<string>('');
|
||||
|
||||
const queries = useQueries(
|
||||
queryPayloads.map((payload, index) => ({
|
||||
queryKey: [
|
||||
@@ -83,6 +98,7 @@ function TimeSeries({
|
||||
globalSelectedInterval: globalSelectedTime,
|
||||
params: {
|
||||
dataSource: DataSource.METRICS,
|
||||
source: 'metrics-explorer',
|
||||
},
|
||||
},
|
||||
// ENTITY_VERSION_V4,
|
||||
@@ -126,32 +142,141 @@ function TimeSeries({
|
||||
setYAxisUnit(value);
|
||||
};
|
||||
|
||||
const goToMetricDetails = (): void => {
|
||||
setIsMetricDetailsOpen(true);
|
||||
};
|
||||
|
||||
const showYAxisUnitSelector = useMemo(() => {
|
||||
if (metricUnits.length <= 1) {
|
||||
return true;
|
||||
}
|
||||
if (areAllMetricUnitsSame) {
|
||||
return metricUnits[0] !== '';
|
||||
}
|
||||
return false;
|
||||
}, [metricUnits, areAllMetricUnitsSame]);
|
||||
|
||||
const showSaveUnitButton = useMemo(
|
||||
() =>
|
||||
metricUnits.length === 1 &&
|
||||
Boolean(metrics?.[0]) &&
|
||||
metricUnits[0] === '' &&
|
||||
yAxisUnit !== '',
|
||||
[metricUnits, metrics, yAxisUnit],
|
||||
);
|
||||
|
||||
const {
|
||||
mutate: updateMetricMetadata,
|
||||
isLoading: isUpdatingMetricMetadata,
|
||||
} = useUpdateMetricMetadata();
|
||||
|
||||
const handleSaveUnit = (): void => {
|
||||
updateMetricMetadata(
|
||||
{
|
||||
metricName: metricNames[0],
|
||||
payload: {
|
||||
unit: yAxisUnit,
|
||||
description: metrics[0]?.metadata?.description ?? '',
|
||||
metricType: metrics[0]?.metadata?.metric_type as MetricType,
|
||||
temporality: metrics[0]?.metadata?.temporality,
|
||||
},
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
notifications.success({
|
||||
message: 'Unit saved successfully',
|
||||
});
|
||||
queryClient.invalidateQueries([
|
||||
REACT_QUERY_KEY.GET_METRIC_DETAILS,
|
||||
metricNames[0],
|
||||
]);
|
||||
},
|
||||
onError: () => {
|
||||
notifications.error({
|
||||
message: 'Failed to save unit',
|
||||
});
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<BuilderUnitsFilter onChange={onUnitChangeHandler} yAxisUnit={yAxisUnit} />
|
||||
<div className="y-axis-unit-selector-container">
|
||||
{showYAxisUnitSelector && (
|
||||
<>
|
||||
<YAxisUnitSelector
|
||||
value={yAxisUnit}
|
||||
onChange={onUnitChangeHandler}
|
||||
loading={isMetricUnitsLoading}
|
||||
disabled={isMetricUnitsLoading || isMetricUnitsError}
|
||||
data-testid="metrics-explorer-y-axis-unit-selector"
|
||||
/>
|
||||
{showSaveUnitButton && (
|
||||
<div className="save-unit-container">
|
||||
<Typography.Text>
|
||||
Save the selected unit for this metric?
|
||||
</Typography.Text>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
loading={isUpdatingMetricMetadata}
|
||||
onClick={handleSaveUnit}
|
||||
>
|
||||
Yes
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className={classNames({
|
||||
'time-series-container': changeLayoutForOneChartPerQuery,
|
||||
})}
|
||||
>
|
||||
{responseData.map((datapoint, index) => (
|
||||
<div
|
||||
className="time-series-view"
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={index}
|
||||
>
|
||||
<TimeSeriesView
|
||||
isFilterApplied={false}
|
||||
isError={queries[index].isError}
|
||||
isLoading={queries[index].isLoading}
|
||||
data={datapoint}
|
||||
yAxisUnit={yAxisUnit}
|
||||
dataSource={DataSource.METRICS}
|
||||
error={queries[index].error as APIError}
|
||||
setWarning={setWarning}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
{responseData.map((datapoint, index) => {
|
||||
const isMetricUnitEmpty =
|
||||
!queries[index].isLoading &&
|
||||
!isMetricUnitsLoading &&
|
||||
metricUnits.length > 1 &&
|
||||
metricUnits[index] === '';
|
||||
|
||||
return (
|
||||
<div
|
||||
className="time-series-view"
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
key={index}
|
||||
>
|
||||
{isMetricUnitEmpty && (
|
||||
<Tooltip
|
||||
className="no-unit-warning"
|
||||
title={
|
||||
<Typography.Text>
|
||||
This metric does not have a unit. Please set one for it in the{' '}
|
||||
<Typography.Link onClick={goToMetricDetails}>
|
||||
metric details
|
||||
</Typography.Link>{' '}
|
||||
drawer.
|
||||
</Typography.Text>
|
||||
}
|
||||
>
|
||||
<AlertTriangle size={16} color={Color.BG_AMBER_400} />
|
||||
</Tooltip>
|
||||
)}
|
||||
<TimeSeriesView
|
||||
isFilterApplied={false}
|
||||
isError={queries[index].isError}
|
||||
isLoading={queries[index].isLoading}
|
||||
data={datapoint}
|
||||
yAxisUnit={yAxisUnit}
|
||||
dataSource={DataSource.METRICS}
|
||||
error={queries[index].error as APIError}
|
||||
setWarning={setWarning}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -15,6 +15,7 @@ import { LicenseEvent } from 'types/api/licensesV3/getActive';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import Explorer from '../Explorer';
|
||||
import * as useGetMetricUnitsHooks from '../utils';
|
||||
|
||||
const mockSetSearchParams = jest.fn();
|
||||
const queryClient = new QueryClient();
|
||||
@@ -126,6 +127,23 @@ jest.spyOn(useQueryBuilderHooks, 'useQueryBuilder').mockReturnValue({
|
||||
...mockUseQueryBuilderData,
|
||||
} as any);
|
||||
|
||||
const Y_AXIS_UNIT_SELECTOR_TEST_ID = 'metrics-explorer-y-axis-unit-selector';
|
||||
const SECONDS_UNIT_LABEL = 'Seconds (s)';
|
||||
|
||||
function renderExplorer(): void {
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<ErrorModalProvider>
|
||||
<Explorer />
|
||||
</ErrorModalProvider>
|
||||
</Provider>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
describe('Explorer', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
@@ -142,17 +160,7 @@ describe('Explorer', () => {
|
||||
mockSetSearchParams,
|
||||
]);
|
||||
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<ErrorModalProvider>
|
||||
<Explorer />
|
||||
</ErrorModalProvider>
|
||||
</Provider>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
renderExplorer();
|
||||
|
||||
expect(mockUpdateAllQueriesOperators).toHaveBeenCalledWith(
|
||||
initialQueriesMap[DataSource.METRICS],
|
||||
@@ -167,17 +175,7 @@ describe('Explorer', () => {
|
||||
mockSetSearchParams,
|
||||
]);
|
||||
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<ErrorModalProvider>
|
||||
<Explorer />
|
||||
</ErrorModalProvider>
|
||||
</Provider>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
renderExplorer();
|
||||
|
||||
const toggle = screen.getByRole('switch');
|
||||
expect(toggle).toBeChecked();
|
||||
@@ -189,19 +187,68 @@ describe('Explorer', () => {
|
||||
mockSetSearchParams,
|
||||
]);
|
||||
|
||||
render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<MemoryRouter>
|
||||
<Provider store={store}>
|
||||
<ErrorModalProvider>
|
||||
<Explorer />
|
||||
</ErrorModalProvider>
|
||||
</Provider>
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>,
|
||||
);
|
||||
renderExplorer();
|
||||
|
||||
const toggle = screen.getByRole('switch');
|
||||
expect(toggle).not.toBeChecked();
|
||||
});
|
||||
|
||||
it('should render pre-populated y axis unit for single metric', () => {
|
||||
jest.spyOn(useGetMetricUnitsHooks, 'useGetMetricUnits').mockReturnValue({
|
||||
units: ['seconds'],
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
metrics: [],
|
||||
} as any);
|
||||
|
||||
renderExplorer();
|
||||
|
||||
const yAxisUnitSelector = screen.getByTestId(Y_AXIS_UNIT_SELECTOR_TEST_ID);
|
||||
expect(yAxisUnitSelector).toBeInTheDocument();
|
||||
expect(yAxisUnitSelector).toHaveTextContent(SECONDS_UNIT_LABEL);
|
||||
});
|
||||
|
||||
it('should render pre-populated y axis unit for mutliple metrics with same unit', () => {
|
||||
jest.spyOn(useGetMetricUnitsHooks, 'useGetMetricUnits').mockReturnValue({
|
||||
units: ['seconds', 'seconds'],
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
metrics: [],
|
||||
} as any);
|
||||
|
||||
renderExplorer();
|
||||
|
||||
const yAxisUnitSelector = screen.getByTestId(Y_AXIS_UNIT_SELECTOR_TEST_ID);
|
||||
expect(yAxisUnitSelector).toBeInTheDocument();
|
||||
expect(yAxisUnitSelector).toHaveTextContent(SECONDS_UNIT_LABEL);
|
||||
});
|
||||
|
||||
it('should hide y axis unit selector for multiple metrics with different units', () => {
|
||||
jest.spyOn(useGetMetricUnitsHooks, 'useGetMetricUnits').mockReturnValue({
|
||||
units: ['seconds', 'milliseconds'],
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
metrics: [],
|
||||
} as any);
|
||||
|
||||
renderExplorer();
|
||||
|
||||
const yAxisUnitSelector = screen.queryByTestId(Y_AXIS_UNIT_SELECTOR_TEST_ID);
|
||||
expect(yAxisUnitSelector).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render empty y axis unit selector for a single metric with no unit', () => {
|
||||
jest.spyOn(useGetMetricUnitsHooks, 'useGetMetricUnits').mockReturnValue({
|
||||
units: [],
|
||||
isLoading: false,
|
||||
isError: false,
|
||||
metrics: [],
|
||||
} as any);
|
||||
|
||||
renderExplorer();
|
||||
|
||||
const yAxisUnitSelector = screen.queryByTestId(Y_AXIS_UNIT_SELECTOR_TEST_ID);
|
||||
expect(yAxisUnitSelector).toBeInTheDocument();
|
||||
expect(yAxisUnitSelector).toHaveTextContent('Please select a unit');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { MetricDetails } from 'api/metricsExplorer/getMetricDetails';
|
||||
import { RelatedMetric } from 'api/metricsExplorer/getRelatedMetrics';
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { UseQueryResult } from 'react-query';
|
||||
@@ -12,6 +13,15 @@ export enum ExplorerTabs {
|
||||
export interface TimeSeriesProps {
|
||||
showOneChartPerQuery: boolean;
|
||||
setWarning: Dispatch<SetStateAction<Warning | undefined>>;
|
||||
areAllMetricUnitsSame: boolean;
|
||||
isMetricUnitsLoading: boolean;
|
||||
isMetricUnitsError: boolean;
|
||||
metricUnits: string[];
|
||||
metricNames: string[];
|
||||
metrics: (MetricDetails | undefined)[];
|
||||
setIsMetricDetailsOpen: (isOpen: boolean) => void;
|
||||
yAxisUnit: string;
|
||||
setYAxisUnit: (unit: string) => void;
|
||||
}
|
||||
|
||||
export interface RelatedMetricsProps {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { MetricDetails } from 'api/metricsExplorer/getMetricDetails';
|
||||
import { useGetMultipleMetrics } from 'hooks/metricsExplorer/useGetMultipleMetrics';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
@@ -35,3 +37,25 @@ export const splitQueryIntoOneChartPerQuery = (query: Query): Query[] => {
|
||||
|
||||
return queries;
|
||||
};
|
||||
|
||||
export function useGetMetricUnits(
|
||||
metricNames: string[],
|
||||
isEnabled = true,
|
||||
): {
|
||||
isLoading: boolean;
|
||||
units: string[];
|
||||
isError: boolean;
|
||||
metrics: (MetricDetails | undefined)[];
|
||||
} {
|
||||
const metricsData = useGetMultipleMetrics(metricNames, {
|
||||
enabled: metricNames.length > 0 && isEnabled,
|
||||
});
|
||||
return {
|
||||
isLoading: metricsData.some((metric) => metric.isLoading),
|
||||
units: metricsData.map(
|
||||
(metric) => metric.data?.payload?.data?.metadata?.unit ?? '',
|
||||
),
|
||||
metrics: metricsData.map((metric) => metric.data?.payload?.data),
|
||||
isError: metricsData.some((metric) => metric.isError),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -132,7 +132,7 @@ function MetricDetails({
|
||||
Open in Explorer
|
||||
</Button>
|
||||
{/* Show the based on the feature flag. Will remove before releasing the feature */}
|
||||
{showInspectFeature && (
|
||||
{showInspectFeature && openInspectModal && (
|
||||
<Button
|
||||
className="inspect-metrics-button"
|
||||
aria-label="Inspect Metric"
|
||||
|
||||
@@ -11,7 +11,7 @@ export interface MetricDetailsProps {
|
||||
isOpen: boolean;
|
||||
metricName: string | null;
|
||||
isModalTimeSelection: boolean;
|
||||
openInspectModal: (metricName: string) => void;
|
||||
openInspectModal?: (metricName: string) => void;
|
||||
}
|
||||
|
||||
export interface DashboardsAndAlertsPopoverProps {
|
||||
|
||||
35
frontend/src/hooks/metricsExplorer/useGetMultipleMetrics.ts
Normal file
35
frontend/src/hooks/metricsExplorer/useGetMultipleMetrics.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import {
|
||||
getMetricDetails,
|
||||
MetricDetailsResponse,
|
||||
} from 'api/metricsExplorer/getMetricDetails';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useQueries, UseQueryOptions, UseQueryResult } from 'react-query';
|
||||
import { ErrorResponse, SuccessResponse } from 'types/api';
|
||||
|
||||
type QueryData = SuccessResponse<MetricDetailsResponse> | ErrorResponse;
|
||||
type QueryResult = UseQueryResult<QueryData, Error>;
|
||||
|
||||
type UseGetMultipleMetrics = (
|
||||
metricNames: string[],
|
||||
options?: UseQueryOptions<QueryData, Error>,
|
||||
headers?: Record<string, string>,
|
||||
) => QueryResult[];
|
||||
|
||||
export const useGetMultipleMetrics: UseGetMultipleMetrics = (
|
||||
metricNames,
|
||||
options,
|
||||
headers,
|
||||
) => {
|
||||
const queries = useQueries(
|
||||
metricNames.map(
|
||||
(metricName) =>
|
||||
({
|
||||
queryKey: [REACT_QUERY_KEY.GET_METRIC_DETAILS, metricName],
|
||||
queryFn: ({ signal }) => getMetricDetails(metricName, signal, headers),
|
||||
...options,
|
||||
} as UseQueryOptions<QueryData, Error>),
|
||||
),
|
||||
);
|
||||
|
||||
return queries as QueryResult[];
|
||||
};
|
||||
Reference in New Issue
Block a user