From ea4a65ed59f812e3116a803478d5f7a270f1d7e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Kope=C4=87?= <3338226+mkopec87@users.noreply.github.com> Date: Wed, 9 Oct 2024 14:17:11 +0200 Subject: [PATCH 1/4] - Add new memory units adaptive formatter to format bytes units into kB, MB, GB, etc. --- .../src/utils/D3Formatting.ts | 2 + .../factories/createMemoryFormatter.ts | 45 +++++++++++++++++++ .../src/number-format/index.ts | 1 + .../src/setup/setupFormatters.ts | 6 ++- 4 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 superset-frontend/packages/superset-ui-core/src/number-format/factories/createMemoryFormatter.ts diff --git a/superset-frontend/packages/superset-ui-chart-controls/src/utils/D3Formatting.ts b/superset-frontend/packages/superset-ui-chart-controls/src/utils/D3Formatting.ts index a8fd6312cbd1f..5541c4a4b4574 100644 --- a/superset-frontend/packages/superset-ui-chart-controls/src/utils/D3Formatting.ts +++ b/superset-frontend/packages/superset-ui-chart-controls/src/utils/D3Formatting.ts @@ -57,6 +57,8 @@ export const D3_FORMAT_OPTIONS: [string, string][] = [ ...d3Formatted, ['DURATION', t('Duration in ms (66000 => 1m 6s)')], ['DURATION_SUB', t('Duration in ms (1.40008 => 1ms 400µs 80ns)')], + ['MEMORY_DECIMAL', t('Memory in bytes - decimal (1024B => 1.024kB)')], + ['MEMORY_BINARY', t('Memory in bytes - binary (1024B => 1KiB)')], ]; export const D3_TIME_FORMAT_DOCS = t( diff --git a/superset-frontend/packages/superset-ui-core/src/number-format/factories/createMemoryFormatter.ts b/superset-frontend/packages/superset-ui-core/src/number-format/factories/createMemoryFormatter.ts new file mode 100644 index 0000000000000..2c0e88c7be1ec --- /dev/null +++ b/superset-frontend/packages/superset-ui-core/src/number-format/factories/createMemoryFormatter.ts @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import NumberFormatter from '../NumberFormatter'; + +export default function createMemoryFormatter(config: { + description?: string; + id?: string; + label?: string; + binary?: bool; +}) { + const { description, id, label, binary } = config; + + return new NumberFormatter({ + description, + formatFunc: value => { + if value == 0 return '0B'; + + const suffixes = binary ? ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'] : ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB', 'RB', 'QB'] + const base = binary ? 1024 : 1000; + const decimals = 2; + + const i = Math.floor(Math.log(value) / Math.log(base)) + return `${parseFloat((value / Math.pow(base, i)).toFixed(decimals))}${suffixes[i]}` + }, + id: id ?? 'memory_format', + label: label ?? `Memory formatter`, + }); +} diff --git a/superset-frontend/packages/superset-ui-core/src/number-format/index.ts b/superset-frontend/packages/superset-ui-core/src/number-format/index.ts index c65537552ee41..b9835d332d0e0 100644 --- a/superset-frontend/packages/superset-ui-core/src/number-format/index.ts +++ b/superset-frontend/packages/superset-ui-core/src/number-format/index.ts @@ -31,5 +31,6 @@ export { export { default as NumberFormatterRegistry } from './NumberFormatterRegistry'; export { default as createD3NumberFormatter } from './factories/createD3NumberFormatter'; export { default as createDurationFormatter } from './factories/createDurationFormatter'; +export { default as createMemoryFormatter } from './factories/createMemoryFormatter'; export { default as createSiAtMostNDigitFormatter } from './factories/createSiAtMostNDigitFormatter'; export { default as createSmartNumberFormatter } from './factories/createSmartNumberFormatter'; diff --git a/superset-frontend/src/setup/setupFormatters.ts b/superset-frontend/src/setup/setupFormatters.ts index e18aeba9dcb3e..d334eb9c19331 100644 --- a/superset-frontend/src/setup/setupFormatters.ts +++ b/superset-frontend/src/setup/setupFormatters.ts @@ -28,6 +28,7 @@ import { createSmartDateFormatter, createSmartDateVerboseFormatter, createSmartDateDetailedFormatter, + createMemoryFormatter, } from '@superset-ui/core'; import { FormatLocaleDefinition } from 'd3-format'; import { TimeLocaleDefinition } from 'd3-time-format'; @@ -76,7 +77,10 @@ export default function setupFormatters( .registerValue( 'DURATION_SUB', createDurationFormatter({ formatSubMilliseconds: true }), - ); + ) + .registerValue('MEMORY_DECIMAL', createMemoryFormatter({ binary: false })) + .registerValue('MEMORY_BINARY', createMemoryFormatter({ binary: true })) + ); const timeFormatterRegistry = getTimeFormatterRegistry(); From 20435db37bf0e65f3184869c5d43c5c45600cbc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Kope=C4=87?= <3338226+mkopec87@users.noreply.github.com> Date: Thu, 10 Oct 2024 09:52:53 +0200 Subject: [PATCH 2/4] - Fix boolean type declaration - Correct linter warnings --- .../number-format/factories/createMemoryFormatter.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/superset-frontend/packages/superset-ui-core/src/number-format/factories/createMemoryFormatter.ts b/superset-frontend/packages/superset-ui-core/src/number-format/factories/createMemoryFormatter.ts index 2c0e88c7be1ec..18e304cea809b 100644 --- a/superset-frontend/packages/superset-ui-core/src/number-format/factories/createMemoryFormatter.ts +++ b/superset-frontend/packages/superset-ui-core/src/number-format/factories/createMemoryFormatter.ts @@ -23,21 +23,23 @@ export default function createMemoryFormatter(config: { description?: string; id?: string; label?: string; - binary?: bool; + binary?: boolean; }) { const { description, id, label, binary } = config; return new NumberFormatter({ description, formatFunc: value => { - if value == 0 return '0B'; + if (value === 0) return '0B'; - const suffixes = binary ? ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'] : ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB', 'RB', 'QB'] + const suffixes = binary + ? ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'] + : ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB', 'RB', 'QB']; const base = binary ? 1024 : 1000; const decimals = 2; - const i = Math.floor(Math.log(value) / Math.log(base)) - return `${parseFloat((value / Math.pow(base, i)).toFixed(decimals))}${suffixes[i]}` + const i = Math.floor(Math.log(value) / Math.log(base)); + return `${parseFloat((value / Math.pow(base, i)).toFixed(decimals))}${suffixes[i]}`; }, id: id ?? 'memory_format', label: label ?? `Memory formatter`, From 7169a80a4eb3180b8d72b8339fc29273fb96af46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Kope=C4=87?= <3338226+mkopec87@users.noreply.github.com> Date: Thu, 10 Oct 2024 09:59:34 +0200 Subject: [PATCH 3/4] - Correct tsc error --- superset-frontend/src/setup/setupFormatters.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/superset-frontend/src/setup/setupFormatters.ts b/superset-frontend/src/setup/setupFormatters.ts index d334eb9c19331..384b1be9e30ae 100644 --- a/superset-frontend/src/setup/setupFormatters.ts +++ b/superset-frontend/src/setup/setupFormatters.ts @@ -79,8 +79,7 @@ export default function setupFormatters( createDurationFormatter({ formatSubMilliseconds: true }), ) .registerValue('MEMORY_DECIMAL', createMemoryFormatter({ binary: false })) - .registerValue('MEMORY_BINARY', createMemoryFormatter({ binary: true })) - ); + .registerValue('MEMORY_BINARY', createMemoryFormatter({ binary: true })); const timeFormatterRegistry = getTimeFormatterRegistry(); From 8bae4907f1e2ad62343f1ea2f5cb1f4db581fdef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Kope=C4=87?= <3338226+mkopec87@users.noreply.github.com> Date: Fri, 11 Oct 2024 10:24:12 +0200 Subject: [PATCH 4/4] - Add unit tests to MemoryFormatter - Make decimals option a parameter of MemoryFormatter - Handle negative values in MemoryFormatter - Handle values exceeding maximal unit value in MemoryFormatter --- .../factories/createMemoryFormatter.ts | 28 ++++-- .../factories/createMemoryFormatter.test.ts | 91 +++++++++++++++++++ .../test/number-format/index.test.ts | 2 + 3 files changed, 111 insertions(+), 10 deletions(-) create mode 100644 superset-frontend/packages/superset-ui-core/test/number-format/factories/createMemoryFormatter.test.ts diff --git a/superset-frontend/packages/superset-ui-core/src/number-format/factories/createMemoryFormatter.ts b/superset-frontend/packages/superset-ui-core/src/number-format/factories/createMemoryFormatter.ts index 18e304cea809b..193527b759dc4 100644 --- a/superset-frontend/packages/superset-ui-core/src/number-format/factories/createMemoryFormatter.ts +++ b/superset-frontend/packages/superset-ui-core/src/number-format/factories/createMemoryFormatter.ts @@ -19,27 +19,35 @@ import NumberFormatter from '../NumberFormatter'; -export default function createMemoryFormatter(config: { - description?: string; - id?: string; - label?: string; - binary?: boolean; -}) { - const { description, id, label, binary } = config; +export default function createMemoryFormatter( + config: { + description?: string; + id?: string; + label?: string; + binary?: boolean; + decimals?: number; + } = {}, +) { + const { description, id, label, binary, decimals = 2 } = config; return new NumberFormatter({ description, formatFunc: value => { if (value === 0) return '0B'; + const sign = value > 0 ? '' : '-'; + value = Math.abs(value); + const suffixes = binary ? ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'] : ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB', 'RB', 'QB']; const base = binary ? 1024 : 1000; - const decimals = 2; - const i = Math.floor(Math.log(value) / Math.log(base)); - return `${parseFloat((value / Math.pow(base, i)).toFixed(decimals))}${suffixes[i]}`; + const i = Math.min( + suffixes.length - 1, + Math.floor(Math.log(value) / Math.log(base)), + ); + return `${sign}${parseFloat((value / Math.pow(base, i)).toFixed(decimals))}${suffixes[i]}`; }, id: id ?? 'memory_format', label: label ?? `Memory formatter`, diff --git a/superset-frontend/packages/superset-ui-core/test/number-format/factories/createMemoryFormatter.test.ts b/superset-frontend/packages/superset-ui-core/test/number-format/factories/createMemoryFormatter.test.ts new file mode 100644 index 0000000000000..90a44845135fd --- /dev/null +++ b/superset-frontend/packages/superset-ui-core/test/number-format/factories/createMemoryFormatter.test.ts @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { NumberFormatter, createMemoryFormatter } from '@superset-ui/core'; + +describe('createMemoryFormatter()', () => { + it('creates an instance of MemoryFormatter', () => { + const formatter = createMemoryFormatter(); + expect(formatter).toBeInstanceOf(NumberFormatter); + }); + it('formats bytes in human readable format with default options', () => { + const formatter = createMemoryFormatter(); + expect(formatter(0)).toBe('0B'); + expect(formatter(50)).toBe('50B'); + expect(formatter(555)).toBe('555B'); + expect(formatter(1000)).toBe('1kB'); + expect(formatter(1111)).toBe('1.11kB'); + expect(formatter(1024)).toBe('1.02kB'); + expect(formatter(1337)).toBe('1.34kB'); + expect(formatter(1999)).toBe('2kB'); + expect(formatter(10 * 1000)).toBe('10kB'); + expect(formatter(100 * 1000)).toBe('100kB'); + expect(formatter(Math.pow(1000, 2))).toBe('1MB'); + expect(formatter(Math.pow(1000, 3))).toBe('1GB'); + expect(formatter(Math.pow(1000, 4))).toBe('1TB'); + expect(formatter(Math.pow(1000, 5))).toBe('1PB'); + expect(formatter(Math.pow(1000, 6))).toBe('1EB'); + expect(formatter(Math.pow(1000, 7))).toBe('1ZB'); + expect(formatter(Math.pow(1000, 8))).toBe('1YB'); + expect(formatter(Math.pow(1000, 9))).toBe('1RB'); + expect(formatter(Math.pow(1000, 10))).toBe('1QB'); + expect(formatter(Math.pow(1000, 11))).toBe('1000QB'); + expect(formatter(Math.pow(1000, 12))).toBe('1000000QB'); + }); + it('formats negative bytes in human readable format with default options', () => { + const formatter = createMemoryFormatter(); + expect(formatter(-50)).toBe('-50B'); + }); + it('formats float bytes in human readable format with default options', () => { + const formatter = createMemoryFormatter(); + expect(formatter(10.666)).toBe('10.67B'); + expect(formatter(1200.666)).toBe('1.2kB'); + }); + it('formats bytes in human readable format with additional binary option', () => { + const formatter = createMemoryFormatter({ binary: true }); + expect(formatter(0)).toBe('0B'); + expect(formatter(50)).toBe('50B'); + expect(formatter(555)).toBe('555B'); + expect(formatter(1000)).toBe('1000B'); + expect(formatter(1111)).toBe('1.08KiB'); + expect(formatter(1024)).toBe('1KiB'); + expect(formatter(1337)).toBe('1.31KiB'); + expect(formatter(2047)).toBe('2KiB'); + expect(formatter(10 * 1024)).toBe('10KiB'); + expect(formatter(100 * 1024)).toBe('100KiB'); + expect(formatter(Math.pow(1024, 2))).toBe('1MiB'); + expect(formatter(Math.pow(1024, 3))).toBe('1GiB'); + expect(formatter(Math.pow(1024, 4))).toBe('1TiB'); + expect(formatter(Math.pow(1024, 5))).toBe('1PiB'); + expect(formatter(Math.pow(1024, 6))).toBe('1EiB'); + expect(formatter(Math.pow(1024, 7))).toBe('1ZiB'); + expect(formatter(Math.pow(1024, 8))).toBe('1YiB'); + expect(formatter(Math.pow(1024, 9))).toBe('1024YiB'); + expect(formatter(Math.pow(1024, 10))).toBe('1048576YiB'); + }); + it('formats bytes in human readable format with additional decimals option', () => { + const formatter0decimals = createMemoryFormatter({ decimals: 0 }); + expect(formatter0decimals(0)).toBe('0B'); + expect(formatter0decimals(1111)).toBe('1kB'); + + const formatter3decimals = createMemoryFormatter({ decimals: 3 }); + expect(formatter3decimals(0)).toBe('0B'); + expect(formatter3decimals(1111)).toBe('1.111kB'); + }); +}); diff --git a/superset-frontend/packages/superset-ui-core/test/number-format/index.test.ts b/superset-frontend/packages/superset-ui-core/test/number-format/index.test.ts index 09395e722e6e2..103f5e44a9b7d 100644 --- a/superset-frontend/packages/superset-ui-core/test/number-format/index.test.ts +++ b/superset-frontend/packages/superset-ui-core/test/number-format/index.test.ts @@ -21,6 +21,7 @@ import { createD3NumberFormatter, createDurationFormatter, createSiAtMostNDigitFormatter, + createMemoryFormatter, formatNumber, getNumberFormatter, getNumberFormatterRegistry, @@ -35,6 +36,7 @@ describe('index', () => { createD3NumberFormatter, createDurationFormatter, createSiAtMostNDigitFormatter, + createMemoryFormatter, formatNumber, getNumberFormatter, getNumberFormatterRegistry,