From 8132764e706b3caeb0350a20e8b8387ce9a70307 Mon Sep 17 00:00:00 2001 From: Volker Theile Date: Thu, 22 Oct 2020 11:17:31 +0200 Subject: [PATCH] mgr/dashboard: displaying time in human-readable format Fixes: https://tracker.ceph.com/issues/47884 Signed-off-by: Volker Theile --- .../app/ceph/block/iscsi/iscsi.component.html | 2 +- .../ceph/block/iscsi/iscsi.component.spec.ts | 2 - .../cluster/monitor/monitor.component.html | 2 +- .../cluster/monitor/monitor.component.spec.ts | 3 +- .../service-daemon-list.component.ts | 5 +- .../cluster/services/services.component.ts | 3 ++ .../notifications-sidebar.component.html | 2 +- .../app/shared/pipes/duration.pipe.spec.ts | 7 --- .../src/app/shared/pipes/duration.pipe.ts | 12 +---- .../shared/pipes/not-available.pipe.spec.ts | 12 ++++- .../app/shared/pipes/not-available.pipe.ts | 6 ++- .../shared/pipes/relative-date.pipe.spec.ts | 35 ++++++++++--- .../app/shared/pipes/relative-date.pipe.ts | 52 +++++++++++++++++-- 13 files changed, 101 insertions(+), 42 deletions(-) diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.html index 5ccdbf03807b..eccb79514c1b 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.html @@ -40,7 +40,7 @@ let-row="row" let-value="value"> - {{ value | relativeDate }} + {{ value | relativeDate | notAvailable }} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.spec.ts index 4ac569c77a7b..c081b7c31e8a 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/block/iscsi/iscsi.component.spec.ts @@ -9,7 +9,6 @@ import { IscsiService } from '../../../shared/api/iscsi.service'; import { CephShortVersionPipe } from '../../../shared/pipes/ceph-short-version.pipe'; import { DimlessPipe } from '../../../shared/pipes/dimless.pipe'; import { IscsiBackstorePipe } from '../../../shared/pipes/iscsi-backstore.pipe'; -import { RelativeDatePipe } from '../../../shared/pipes/relative-date.pipe'; import { FormatterService } from '../../../shared/services/formatter.service'; import { SharedModule } from '../../../shared/shared.module'; import { IscsiComponent } from './iscsi.component'; @@ -36,7 +35,6 @@ describe('IscsiComponent', () => { CephShortVersionPipe, DimlessPipe, FormatterService, - RelativeDatePipe, IscsiBackstorePipe, { provide: IscsiService, useValue: fakeService } ] diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor/monitor.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor/monitor.component.html index 70d8dc5befe9..ca9ac82212c7 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor/monitor.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/monitor/monitor.component.html @@ -13,7 +13,7 @@ monmap modified - {{ mon_status.monmap.modified }} + {{ mon_status.monmap.modified | relativeDate }} { @@ -14,7 +15,7 @@ describe('MonitorComponent', () => { let getMonitorSpy: jasmine.Spy; configureTestBed({ - imports: [HttpClientTestingModule], + imports: [HttpClientTestingModule, SharedModule], declarations: [MonitorComponent], schemas: [NO_ERRORS_SCHEMA], providers: [MonitorService] diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.ts index e0df084e2990..f4589d2da120 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-daemon-list/service-daemon-list.component.ts @@ -22,6 +22,7 @@ import { CellTemplate } from '../../../../shared/enum/cell-template.enum'; import { CdTableColumn } from '../../../../shared/models/cd-table-column'; import { CdTableFetchDataContext } from '../../../../shared/models/cd-table-fetch-data-context'; import { Daemon } from '../../../../shared/models/daemon.interface'; +import { RelativeDatePipe } from '../../../../shared/pipes/relative-date.pipe'; @Component({ selector: 'cd-service-daemon-list', @@ -52,7 +53,8 @@ export class ServiceDaemonListComponent implements OnInit, OnChanges, AfterViewI constructor( private hostService: HostService, private cephServiceService: CephServiceService, - private orchService: OrchestratorService + private orchService: OrchestratorService, + private relativeDatePipe: RelativeDatePipe ) {} ngOnInit() { @@ -117,6 +119,7 @@ export class ServiceDaemonListComponent implements OnInit, OnChanges, AfterViewI { name: $localize`Last Refreshed`, prop: 'last_refresh', + pipe: this.relativeDatePipe, flexGrow: 2 } ]; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.ts index b92e39bb31bc..5f065704c995 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/services.component.ts @@ -19,6 +19,7 @@ import { OrchestratorFeature } from '../../../shared/models/orchestrator.enum'; import { OrchestratorStatus } from '../../../shared/models/orchestrator.interface'; import { Permissions } from '../../../shared/models/permissions'; import { CephServiceSpec } from '../../../shared/models/service.interface'; +import { RelativeDatePipe } from '../../../shared/pipes/relative-date.pipe'; import { AuthStorageService } from '../../../shared/services/auth-storage.service'; import { ModalService } from '../../../shared/services/modal.service'; import { TaskWrapperService } from '../../../shared/services/task-wrapper.service'; @@ -61,6 +62,7 @@ export class ServicesComponent extends ListWithDetails implements OnChanges, OnI private modalService: ModalService, private orchService: OrchestratorService, private cephServiceService: CephServiceService, + private relativeDatePipe: RelativeDatePipe, private taskWrapperService: TaskWrapperService, private urlBuilder: URLBuilderService ) { @@ -119,6 +121,7 @@ export class ServicesComponent extends ListWithDetails implements OnChanges, OnI { name: $localize`Last Refreshed`, prop: 'status.last_refresh', + pipe: this.relativeDatePipe, flexGrow: 1 } ]; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/notifications-sidebar/notifications-sidebar.component.html b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/notifications-sidebar/notifications-sidebar.component.html index 6f55a882a3d1..bba23747b01d 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/components/notifications-sidebar/notifications-sidebar.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/components/notifications-sidebar/notifications-sidebar.component.html @@ -83,7 +83,7 @@
{{ notification.timestamp | duration: true }} + [title]="notification.timestamp | cdDate">{{ notification.timestamp | relativeDate }} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/duration.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/duration.pipe.spec.ts index a97f8a751666..1b0e22578aaf 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/duration.pipe.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/duration.pipe.spec.ts @@ -1,5 +1,3 @@ -import moment from 'moment'; - import { DurationPipe } from './duration.pipe'; describe('DurationPipe', () => { @@ -16,9 +14,4 @@ describe('DurationPipe', () => { expect(pipe.transform(600)).toBe('10 minutes'); expect(pipe.transform(6000)).toBe('1 hour 40 minutes'); }); - - it('transforms date into a human readable relative duration', () => { - const date = moment().subtract(130, 'seconds'); - expect(pipe.transform(date, true)).toBe('2 minutes ago'); - }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/duration.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/duration.pipe.ts index 407de929154e..4675fc0f6c6d 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/duration.pipe.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/duration.pipe.ts @@ -1,20 +1,10 @@ import { Pipe, PipeTransform } from '@angular/core'; -import moment from 'moment'; - @Pipe({ name: 'duration', pure: false }) export class DurationPipe implements PipeTransform { - transform(date: any, isRelative = false): string { - if (isRelative) { - return moment(date).fromNow(); - } else { - return this._forHumans(date); - } - } - /** * Translates seconds into human readable format of seconds, minutes, hours, days, and years * source: https://stackoverflow.com/a/34270811 @@ -22,7 +12,7 @@ export class DurationPipe implements PipeTransform { * @param {number} seconds The number of seconds to be processed * @return {string} The phrase describing the the amount of time */ - _forHumans(seconds: number): string { + transform(seconds: number): string { const levels = [ [`${Math.floor(seconds / 31536000)}`, 'years'], [`${Math.floor((seconds % 31536000) / 86400)}`, 'days'], diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/not-available.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/not-available.pipe.spec.ts index 9d36defb937f..06279a5ea96e 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/not-available.pipe.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/not-available.pipe.spec.ts @@ -11,12 +11,20 @@ describe('NotAvailablePipe', () => { expect(pipe).toBeTruthy(); }); - it('transforms not available', () => { + it('transforms not available (1)', () => { expect(pipe.transform('')).toBe('n/a'); }); - it('transforms number', () => { + it('transforms not available (2)', () => { + expect(pipe.transform('', 'Unknown')).toBe('Unknown'); + }); + + it('transform not necessary (1)', () => { expect(pipe.transform(0)).toBe(0); expect(pipe.transform(1)).toBe(1); }); + + it('transform not necessary (2)', () => { + expect(pipe.transform('foo')).toBe('foo'); + }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/not-available.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/not-available.pipe.ts index 17184c38ae10..9d0222724782 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/not-available.pipe.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/not-available.pipe.ts @@ -1,12 +1,14 @@ import { Pipe, PipeTransform } from '@angular/core'; +import _ from 'lodash'; + @Pipe({ name: 'notAvailable' }) export class NotAvailablePipe implements PipeTransform { - transform(value: any): any { + transform(value: any, text?: string): any { if (value === '') { - return $localize`n/a`; + return _.defaultTo(text, $localize`n/a`); } return value; } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/relative-date.pipe.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/relative-date.pipe.spec.ts index 97859d106462..a12d3c2a11d1 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/relative-date.pipe.spec.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/relative-date.pipe.spec.ts @@ -9,17 +9,36 @@ describe('RelativeDatePipe', () => { expect(pipe).toBeTruthy(); }); - it('transforms without value', () => { - expect(pipe.transform(undefined)).toBe('unknown'); + it('transforms date into a human readable relative time (1)', () => { + const date: Date = moment().subtract(130, 'seconds').toDate(); + expect(pipe.transform(date)).toBe('2 minutes ago'); }); - it('transforms "in 7 days"', () => { - const value = moment().add(7, 'days').unix(); - expect(pipe.transform(value)).toBe('in 7 days'); + it('transforms date into a human readable relative time (2)', () => { + const date: Date = moment().subtract(65, 'minutes').toDate(); + expect(pipe.transform(date)).toBe('An hour ago'); }); - it('transforms "7 days ago"', () => { - const value = moment().subtract(7, 'days').unix(); - expect(pipe.transform(value)).toBe('7 days ago'); + it('transforms date into a human readable relative time (3)', () => { + const date: string = moment().subtract(130, 'minutes').toISOString(); + expect(pipe.transform(date)).toBe('2 hours ago'); + }); + + it('transforms date into a human readable relative time (4)', () => { + const date: string = moment().subtract(30, 'seconds').toISOString(); + expect(pipe.transform(date, false)).toBe('a few seconds ago'); + }); + + it('transforms date into a human readable relative time (5)', () => { + const date: number = moment().subtract(3, 'days').unix(); + expect(pipe.transform(date)).toBe('3 days ago'); + }); + + it('invalid input (1)', () => { + expect(pipe.transform('')).toBe(''); + }); + + it('invalid input (2)', () => { + expect(pipe.transform('2011-10-10T10:20:90')).toBe(''); }); }); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/relative-date.pipe.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/relative-date.pipe.ts index 1718ee258b30..f802b6b2add1 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/relative-date.pipe.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/pipes/relative-date.pipe.ts @@ -1,15 +1,57 @@ import { Pipe, PipeTransform } from '@angular/core'; +import _ from 'lodash'; import moment from 'moment'; +moment.updateLocale('en', { + relativeTime: { + future: $localize`in %s`, + past: $localize`%s ago`, + s: $localize`a few seconds`, + ss: $localize`%d seconds`, + m: $localize`a minute`, + mm: $localize`%d minutes`, + h: $localize`an hour`, + hh: $localize`%d hours`, + d: $localize`a day`, + dd: $localize`%d days`, + w: $localize`a week`, + ww: $localize`%d weeks`, + M: $localize`a month`, + MM: $localize`%d months`, + y: $localize`a year`, + yy: $localize`%d years` + } +}); + @Pipe({ - name: 'relativeDate' + name: 'relativeDate', + pure: false }) export class RelativeDatePipe implements PipeTransform { - transform(value: any): any { - if (!value) { - return 'unknown'; + /** + * Convert a time into a human readable form, e.g. '2 minutes ago'. + * @param {Date | string | number} value The date to convert, should be + * an ISO8601 string, an Unix timestamp (seconds) or Date object. + * @param {boolean} upperFirst Set to `true` to start the sentence + * upper case. Defaults to `true`. + * @return {string} The time in human readable form or an empty string + * on failure (e.g. invalid input). + */ + transform(value: Date | string | number, upperFirst = true): string { + let date: moment.Moment; + if (_.isNumber(value)) { + date = moment.unix(value); + } else { + date = moment(value); + } + if (!date.isValid()) { + return ''; + } + let relativeDate: string = date.fromNow(); + if (upperFirst) { + relativeDate = _.upperFirst(relativeDate); } - return moment(value * 1000).fromNow(); + return relativeDate; } } -- 2.47.3