From 2e8ad023376ea95452a44cdf3731e6356880a470 Mon Sep 17 00:00:00 2001 From: Andrew Azores Date: Thu, 15 Aug 2024 16:04:17 -0400 Subject: [PATCH] feat(units): improve display of bytes and duration units (#1329) --- package.json | 2 + src/app/Recordings/ActiveRecordingsTable.tsx | 6 +-- src/app/Rules/Rules.tsx | 48 +++++++++---------- src/app/utils/utils.ts | 6 +++ .../Recordings/ActiveRecordingsTable.test.tsx | 2 +- yarn.lock | 16 +++++++ 6 files changed, 52 insertions(+), 28 deletions(-) diff --git a/package.json b/package.json index bc18ebc0f..5aa122009 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^12.1.5", "@testing-library/user-event": "^14.4.3", + "@types/humanize-duration": "^3.27.4", "@types/jest": "^27.0.2", "@types/js-base64": "3.3.1", "@types/react-test-renderer": "^17.0.7", @@ -101,6 +102,7 @@ "@types/lodash": "^4.14.202", "@types/react": "^17.0.69", "dayjs": "^1.11.7", + "humanize-duration": "^3.32.1", "i18next": "^22.4.10", "i18next-browser-languagedetector": "^7.0.1", "i18next-parser": "^8.12.0", diff --git a/src/app/Recordings/ActiveRecordingsTable.tsx b/src/app/Recordings/ActiveRecordingsTable.tsx index d5126a103..32854d6f8 100644 --- a/src/app/Recordings/ActiveRecordingsTable.tsx +++ b/src/app/Recordings/ActiveRecordingsTable.tsx @@ -46,7 +46,7 @@ import { ServiceContext } from '@app/Shared/Services/Services'; import { useDayjs } from '@app/utils/hooks/useDayjs'; import { useSort } from '@app/utils/hooks/useSort'; import { useSubscriptions } from '@app/utils/hooks/useSubscriptions'; -import { formatBytes, sortResources, TableColumn } from '@app/utils/utils'; +import { formatBytes, formatDuration, sortResources, TableColumn } from '@app/utils/utils'; import { Bullseye, Button, @@ -908,7 +908,7 @@ export const ActiveRecordingRow: React.FC = ({ const options: KeyValue[] = []; options.push({ key: 'toDisk', value: String(recording.toDisk) }); if (recording.maxAge) { - options.push({ key: 'maxAge', value: `${recording.maxAge / 1000}s` }); + options.push({ key: 'maxAge', value: formatDuration(recording.maxAge, 1) }); } if (recording.maxSize) { options.push({ key: 'maxSize', value: formatBytes(recording.maxSize) }); @@ -919,7 +919,7 @@ export const ActiveRecordingRow: React.FC = ({ const parentRow = React.useMemo(() => { const RecordingDuration = (props: { duration: number }) => { const str = React.useMemo( - () => (props.duration === 0 ? 'Continuous' : `${props.duration / 1000}s`), + () => (props.duration === 0 ? 'Continuous' : formatDuration(recording.duration, 1)), [props.duration], ); return {str}; diff --git a/src/app/Rules/Rules.tsx b/src/app/Rules/Rules.tsx index 5395b8c7b..0f499f180 100644 --- a/src/app/Rules/Rules.tsx +++ b/src/app/Rules/Rules.tsx @@ -19,7 +19,7 @@ import { LoadingView } from '@app/Shared/Components/LoadingView'; import { Rule, NotificationCategory } from '@app/Shared/Services/api.types'; import { ServiceContext } from '@app/Shared/Services/Services'; import { useSubscriptions } from '@app/utils/hooks/useSubscriptions'; -import { TableColumn, sortResources } from '@app/utils/utils'; +import { TableColumn, formatBytes, formatDuration, sortResources } from '@app/utils/utils'; import { Button, Card, @@ -83,17 +83,28 @@ const tableColumns: TableColumn[] = [ keyPaths: ['eventSpecifier'], tooltip: 'The name and location of the Event Template applied by this rule.', }, + { + title: 'Maximum age', + keyPaths: ['maxAgeSeconds'], + tooltip: + 'The maximum age for data kept in the JFR Recordings started by this rule. Values less than 1 indicate no limit.', + }, + { + title: 'Maximum size', + keyPaths: ['maxSizeBytes'], + tooltip: 'The maximum size for JFR Recordings started by this rule. Values less than 1 indicate no limit.', + }, { title: 'Archival period', keyPaths: ['archivalPeriodSeconds'], tooltip: - 'Period in seconds. Cryostat will connect to matching targets at this interval and copy the relevant Recording data into its archives. Values less than 1 prevent data from being repeatedly copied into archives - Recordings will be started and remain only in Target JVM memory.', + 'Cryostat will connect to matching targets at this interval and copy the relevant Recording data into its archives. Values less than 1 prevent data from being repeatedly copied into archives - Recordings will be started and remain only in Target JVM memory.', }, { title: 'Initial delay', keyPaths: ['initialDelaySeconds'], tooltip: - 'Initial delay in seconds. Cryostat will wait this amount of time before first copying Recording data into its archives. Values less than 0 default to equal to the Archival period. You can set a non-zero Initial delay with a zero Archival period, which will start a Recording and copy it into archives exactly once after a set delay.', + 'Cryostat will wait this amount of time before first copying Recording data into its archives. Values less than 0 default to equal to the Archival period. You can set a non-zero Initial delay with a zero Archival period, which will start a Recording and copy it into archives exactly once after a set delay.', }, { title: 'Preserved archives', @@ -101,17 +112,6 @@ const tableColumns: TableColumn[] = [ tooltip: 'The number of Recording copies to be maintained in the Cryostat archives. Cryostat will continue retrieving further archived copies and trimming the oldest copies from the archive to maintain this limit. Values less than 1 prevent data from being copied into archives - Recordings will be started and remain only in Target JVM memory.', }, - { - title: 'Maximum age', - keyPaths: ['maxAgeSeconds'], - tooltip: - 'The maximum age in seconds for data kept in the JFR Recordings started by this rule. Values less than 1 indicate no limit.', - }, - { - title: 'Maximum size', - keyPaths: ['maxSizeBytes'], - tooltip: 'The maximum size in bytes for JFR Recordings started by this rule. Values less than 1 indicate no limit.', - }, ]; export interface RulesTableProps {} @@ -335,20 +335,20 @@ export const RulesTable: React.FC = (_) => { {r.eventSpecifier} - - {r.archivalPeriodSeconds} + + {formatDuration(r.maxAgeSeconds)} - - {r.initialDelaySeconds} + + {formatBytes(r.maxSizeBytes)} - - {r.preservedArchives} + + {formatDuration(r.archivalPeriodSeconds)} - - {r.maxAgeSeconds} + + {formatDuration(r.initialDelaySeconds)} - - {r.maxSizeBytes} + + {r.preservedArchives} { return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizeUnits[i]}`; }; +/* scalar is the time unit. Default milliseconds. */ +export const formatDuration = (quantity: number, scalar = 1000): string => { + return humanizeDuration(quantity * scalar); +}; + export interface AutomatedAnalysisTimerObject { quantity: number; unit: string; diff --git a/src/test/Recordings/ActiveRecordingsTable.test.tsx b/src/test/Recordings/ActiveRecordingsTable.test.tsx index 0926bd1c1..14f8a4082 100644 --- a/src/test/Recordings/ActiveRecordingsTable.test.tsx +++ b/src/test/Recordings/ActiveRecordingsTable.test.tsx @@ -235,7 +235,7 @@ describe('', () => { expect(toolTip).toBeVisible(); const duration = screen.getByText( - mockRecording.continuous || mockRecording.duration === 0 ? 'Continuous' : `${mockRecording.duration / 1000}s`, + mockRecording.continuous || mockRecording.duration === 0 ? 'Continuous' : '1 second', ); expect(duration).toBeInTheDocument(); expect(duration).toBeVisible(); diff --git a/yarn.lock b/yarn.lock index ab3fa2ed5..8426a49da 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2231,6 +2231,13 @@ __metadata: languageName: node linkType: hard +"@types/humanize-duration@npm:^3.27.4": + version: 3.27.4 + resolution: "@types/humanize-duration@npm:3.27.4" + checksum: aa7fa41af839021c846a1728d2f097b7885f80acbc5050623a9804d23fd8fee19d2654eaae331f000a4e551b88d99cd0b5f6cc97aa99f869b9205f6248066827 + languageName: node + linkType: hard + "@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1": version: 2.0.4 resolution: "@types/istanbul-lib-coverage@npm:2.0.4" @@ -4361,6 +4368,7 @@ __metadata: "@testing-library/jest-dom": ^5.16.5 "@testing-library/react": ^12.1.5 "@testing-library/user-event": ^14.4.3 + "@types/humanize-duration": ^3.27.4 "@types/jest": ^27.0.2 "@types/js-base64": 3.3.1 "@types/lodash": ^4.14.202 @@ -4382,6 +4390,7 @@ __metadata: file-loader: ^6.2.0 fork-ts-checker-webpack-plugin: ^7.3.0 html-webpack-plugin: ^5.5.4 + humanize-duration: ^3.32.1 i18next: ^22.4.10 i18next-browser-languagedetector: ^7.0.1 i18next-parser: ^8.12.0 @@ -7461,6 +7470,13 @@ __metadata: languageName: node linkType: hard +"humanize-duration@npm:^3.32.1": + version: 3.32.1 + resolution: "humanize-duration@npm:3.32.1" + checksum: 17f6f2ec09a931eb0bf7de1fc8ac01f90174f366f60390289bd0797c6e4545255bd5d770dd18909c9b21685d76cc190b3a8ec880d2ecc088a1ad032e0d2f57cb + languageName: node + linkType: hard + "humanize-ms@npm:^1.2.1": version: 1.2.1 resolution: "humanize-ms@npm:1.2.1"