diff --git a/src/app/admin.routes.ts b/src/app/admin.routes.ts index 1585151331d..0c5dd7ebc60 100644 --- a/src/app/admin.routes.ts +++ b/src/app/admin.routes.ts @@ -3,6 +3,8 @@ import { marker as T } from '@biesbjerg/ngx-translate-extract-marker'; import { TranslationsLoadedGuard } from 'app/core/guards/translations-loaded.guard'; import { WebSocketConnectionGuard } from 'app/core/guards/websocket-connection.guard'; import { AdminLayoutComponent } from 'app/modules/layout/admin-layout/admin-layout.component'; +import { PlotterService } from 'app/pages/reports-dashboard/services/plotter.service'; +import { SmoothPlotterService } from 'app/pages/reports-dashboard/services/smooth-plotter.service'; import { AuthGuardService } from 'app/services/auth/auth-guard.service'; import { TwoFactorGuardService } from 'app/services/auth/two-factor-guard.service'; @@ -72,6 +74,12 @@ export const adminRoutes: Routes = [ path: 'reportsdashboard', loadChildren: () => import('app/pages/reports-dashboard/reports-dashboard.routes').then((module) => module.reportsDashboardRoutes), data: { title: T('Reporting'), breadcrumb: T('Reporting') }, + providers: [ + { + provide: PlotterService, + useClass: SmoothPlotterService, + }, + ], }, { path: 'shell', diff --git a/src/app/pages/reports-dashboard/reports-dashboard.component.spec.ts b/src/app/pages/reports-dashboard/reports-dashboard.component.spec.ts new file mode 100644 index 00000000000..d891916af68 --- /dev/null +++ b/src/app/pages/reports-dashboard/reports-dashboard.component.spec.ts @@ -0,0 +1,102 @@ +import { Spectator, createComponentFactory, mockProvider } from '@ngneat/spectator/jest'; +import { MockComponent } from 'ng-mocks'; +import { of } from 'rxjs'; +import { mockApi } from 'app/core/testing/utils/mock-api.utils'; +import { mockAuth } from 'app/core/testing/utils/mock-auth.utils'; +import { ReportingGraphName } from 'app/enums/reporting.enum'; +import { ReportingGraph } from 'app/interfaces/reporting-graph.interface'; +import { PageHeaderComponent } from 'app/modules/page-header/page-title-header/page-header.component'; +import { ReportsGlobalControlsComponent } from 'app/pages/reports-dashboard/components/reports-global-controls/reports-global-controls.component'; +import { ReportTab, ReportType } from 'app/pages/reports-dashboard/interfaces/report-tab.interface'; +import { Report } from 'app/pages/reports-dashboard/interfaces/report.interface'; +import { ReportsDashboardComponent } from 'app/pages/reports-dashboard/reports-dashboard.component'; +import { ReportsService } from 'app/pages/reports-dashboard/reports.service'; +import { LayoutService } from 'app/services/layout.service'; + +const fakeTabs: ReportTab[] = [ + { label: 'CPU', value: ReportType.Cpu }, + { label: 'Memory', value: ReportType.Memory }, + { label: 'Disk', value: ReportType.Disk }, +]; + +describe('ReportsDashboardComponent', () => { + let spectator: Spectator; + + const createComponent = createComponentFactory({ + component: ReportsDashboardComponent, + imports: [ + MockComponent(PageHeaderComponent), + MockComponent(ReportsGlobalControlsComponent), + ], + providers: [ + mockProvider(LayoutService, { + getContentContainer: jest.fn(() => document.createElement('div')), + }), + + mockProvider(ReportsService, { + getReportGraphs: jest.fn(() => of([ + { name: ReportingGraphName.Cpu, title: 'CPU', identifiers: [] }, + { name: ReportingGraphName.Memory, title: 'Memory', identifiers: [] }, + { + name: ReportingGraphName.Disk, + title: 'Disks', + identifiers: ['HDD | Model | test-sda-uuid', 'HDD | Model | test-sdb-uuid'], + }, + ] as ReportingGraph[])), + getReportTabs: jest.fn(() => fakeTabs), + }), + mockApi([]), + mockAuth(), + ], + }); + + beforeEach(() => { + spectator = createComponent(); + }); + + it('separates disk and other reports', () => { + const fakeReports = [ + { + identifiers: [], + name: ReportingGraphName.Cpu, + title: 'CPU', + isRendered: [true], + }, + { + identifiers: [], + name: ReportingGraphName.Memory, + title: 'Memory', + isRendered: [true], + }, + { + identifiers: ['HDD | Model | test-sda-uuid', 'HDD | Model | test-sdb-uuid'], + name: ReportingGraphName.Disk, + title: 'Disks', + isRendered: [true, true], + }, + ] as Report[]; + + expect(spectator.component.allReports).toEqual(fakeReports); + expect(spectator.component.diskReports).toEqual([fakeReports[2]]); + expect(spectator.component.otherReports).toEqual([fakeReports[0], fakeReports[1]]); + }); + + describe('buildDiskReport', () => { + it('rebuilds disk reports', () => { + spectator.component.updateActiveTab(fakeTabs[2]); + expect(spectator.component.activeReports).toHaveLength(2); + + spectator.component.buildDiskReport({ + devices: ['test-sdb-uuid'], + metrics: [ReportingGraphName.Disk], + }); + expect(spectator.component.visibleReports).toEqual([1]); + + spectator.component.buildDiskReport({ + devices: ['test-sda-uuid'], + metrics: [ReportingGraphName.Disk], + }); + expect(spectator.component.visibleReports).toEqual([0]); + }); + }); +}); diff --git a/src/app/pages/reports-dashboard/reports-dashboard.component.ts b/src/app/pages/reports-dashboard/reports-dashboard.component.ts index 54865afcdc7..9144445bd25 100644 --- a/src/app/pages/reports-dashboard/reports-dashboard.component.ts +++ b/src/app/pages/reports-dashboard/reports-dashboard.component.ts @@ -17,8 +17,6 @@ import { PageHeaderComponent } from 'app/modules/page-header/page-title-header/p import { ReportTab, ReportType } from 'app/pages/reports-dashboard/interfaces/report-tab.interface'; import { Report } from 'app/pages/reports-dashboard/interfaces/report.interface'; import { reportingElements } from 'app/pages/reports-dashboard/reports-dashboard.elements'; -import { PlotterService } from 'app/pages/reports-dashboard/services/plotter.service'; -import { SmoothPlotterService } from 'app/pages/reports-dashboard/services/smooth-plotter.service'; import { LayoutService } from 'app/services/layout.service'; import { ReportComponent } from './components/report/report.component'; import { ReportsGlobalControlsComponent } from './components/reports-global-controls/reports-global-controls.component'; @@ -41,12 +39,6 @@ import { ReportsService } from './reports.service'; ReportComponent, MatCard, ], - providers: [ - { - provide: PlotterService, - useClass: SmoothPlotterService, - }, - ], }) export class ReportsDashboardComponent implements OnInit, OnDestroy { readonly searchableElements = reportingElements; @@ -90,8 +82,8 @@ export class ReportsDashboardComponent implements OnInit, OnDestroy { }; }); - this.diskReports = this.allReports.filter((report) => report.name.startsWith('disk')); - this.otherReports = this.allReports.filter((report) => !report.name.startsWith('disk')); + this.diskReports = this.allReports.filter((report) => report.name === ReportingGraphName.Disk); + this.otherReports = this.allReports.filter((report) => report.name !== ReportingGraphName.Disk); this.activateTabFromUrl(); this.cdr.markForCheck(); @@ -240,7 +232,8 @@ export class ReportsDashboardComponent implements OnInit, OnDestroy { const visible: number[] = []; this.activeReports.forEach((item, index) => { - const deviceMatch = devices.includes(item.identifiers[0]); + const [,, identifier] = item.identifiers[0].split(' | '); + const deviceMatch = devices.includes(identifier); const metricMatch = metrics.includes(item.name); const condition = deviceMatch && metricMatch; if (condition) { diff --git a/src/app/pages/reports-dashboard/reports.service.spec.ts b/src/app/pages/reports-dashboard/reports.service.spec.ts new file mode 100644 index 00000000000..a120951a403 --- /dev/null +++ b/src/app/pages/reports-dashboard/reports.service.spec.ts @@ -0,0 +1,117 @@ +import { createServiceFactory, SpectatorService } from '@ngneat/spectator/jest'; +import { firstValueFrom } from 'rxjs'; +import { MockApiService } from 'app/core/testing/classes/mock-api.service'; +import { mockCall, mockApi } from 'app/core/testing/utils/mock-api.utils'; +import { ReportingGraphName } from 'app/enums/reporting.enum'; +import { Disk } from 'app/interfaces/disk.interface'; +import { ReportingGraph } from 'app/interfaces/reporting-graph.interface'; +import { ReportType } from 'app/pages/reports-dashboard/interfaces/report-tab.interface'; +import { Report } from 'app/pages/reports-dashboard/interfaces/report.interface'; +import { ReportsService } from 'app/pages/reports-dashboard/reports.service'; + +describe('ReportsService', () => { + let spectator: SpectatorService; + let api: MockApiService; + + const createService = createServiceFactory({ + service: ReportsService, + providers: [ + mockApi([ + mockCall('disk.query', [ + { devname: 'sda', identifier: '{uuid}test-sda-uuid' }, + { devname: 'sdb', identifier: '{uuid}test-sdb-uuid' }, + ] as Disk[]), + mockCall('reporting.netdata_graphs', [ + { name: ReportingGraphName.Cpu }, + { name: ReportingGraphName.Ups }, + ] as ReportingGraph[]), + mockCall('reporting.netdata_get_data', [{ + name: 'cpu', + identifier: 'cpu', + legend: ['time', 'active'], + start: 1735281261, + end: 1735281265, + data: [ + [1735281261, 382], + [1735281262, 381], + [1735281263, 380], + [1735281264, 384], + ], + aggregations: { min: [0], mean: [5], max: [10] }, + }]), + mockCall('disk.temperatures', {}), + ]), + ], + }); + + beforeEach(() => { + spectator = createService(); + api = spectator.inject(MockApiService); + }); + + describe('getDiskDevices', () => { + it('returns disk options', async () => { + const options = await firstValueFrom(spectator.service.getDiskDevices()); + expect(api.call).toHaveBeenCalledWith('disk.query'); + expect(options).toEqual([ + { label: 'sda', value: '{uuid}test-sda-uuid' }, + { label: 'sdb', value: '{uuid}test-sdb-uuid' }, + ]); + }); + }); + + describe('getNetData', () => { + it('returns report data', async () => { + const data = await firstValueFrom(spectator.service.getNetData({ + params: { name: 'cpu' }, + truncate: true, + timeFrame: { start: 1735284410000, end: 1735284411000 }, + report: { vertical_label: '%' } as Report, + })); + expect(api.call).toHaveBeenCalledWith( + 'reporting.netdata_get_data', + [[{ name: 'cpu' }], { end: 1735284411000, start: 1735284410000 }], + ); + expect(data).toEqual({ + name: 'cpu', + identifier: 'cpu', + legend: ['active'], + start: 1735281261, + end: 1735281265, + aggregations: { max: ['10'], mean: ['5'], min: ['0'] }, + data: [[1735281261, 382], [1735281262, 381], [1735281263, 380], [1735281264, 384]], + }); + }); + }); + + describe('getReportTabs', () => { + it('returns report tabs', () => { + const tabs = spectator.service.getReportTabs(); + expect(tabs).toEqual([ + { label: 'CPU', value: ReportType.Cpu }, + { label: 'Disk', value: ReportType.Disk }, + { label: 'Memory', value: ReportType.Memory }, + { label: 'Network', value: ReportType.Network }, + { label: 'NFS', value: ReportType.Nfs }, + { label: 'Partition', value: ReportType.Partition }, + { label: 'System', value: ReportType.System }, + { label: 'UPS', value: ReportType.Ups }, + { label: 'Target', value: ReportType.Target }, + { label: 'ZFS', value: ReportType.Zfs }, + ]); + }); + }); + + describe('getDiskMetrics', () => { + it('returns disk metrics', async () => { + const fakeMetrics = [ + { label: 'sda', value: 'uuid_sda' }, + { label: 'sdb', value: 'uuid_sdb' }, + ]; + spectator.service.setDiskMetrics(fakeMetrics); + + const metrics = await firstValueFrom(spectator.service.getDiskMetrics()); + expect(metrics).toEqual(fakeMetrics); + }); + }); +}); diff --git a/src/app/pages/reports-dashboard/reports.service.ts b/src/app/pages/reports-dashboard/reports.service.ts index f6514231fe2..01407af4829 100644 --- a/src/app/pages/reports-dashboard/reports.service.ts +++ b/src/app/pages/reports-dashboard/reports.service.ts @@ -107,8 +107,7 @@ export class ReportsService { return disks .filter((disk) => !disk.devname.includes('multipath')) .map((disk) => { - const [value] = disk.devname.split(' '); - return { label: disk.devname, value }; + return { label: disk.devname, value: disk.identifier }; }) .sort((a, b) => a.label.localeCompare(b.label)); }),