Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
214200c
Refactor dot-usage-shell component for improved UI and structure
fmontes Dec 24, 2025
f3201ce
Update dot-usage-shell component for improved skeleton loading and st…
fmontes Dec 24, 2025
81bd166
Enhance dot-usage-shell component with last updated timestamp
fmontes Dec 24, 2025
66503e6
Refactor dot-usage component tests to use structured metrics format
fmontes Dec 24, 2025
879ce37
Refactor dot-usage service and shell component tests to utilize new H…
fmontes Dec 24, 2025
67e26d1
Add dot-usage service and tests for usage summary functionality
fmontes Dec 24, 2025
317f1ad
Enhance DotUsageShellComponent tests to improve error handling and lo…
fmontes Dec 24, 2025
daa6ae1
Add informational message to DotUsageShellComponent for analytics upd…
fmontes Dec 24, 2025
625dde3
Enhance DotUsageShellComponent with accessibility improvements and er…
fmontes Dec 24, 2025
d9eada7
Merge branch 'main' into issue-34166-usage-ui
fmontes Dec 26, 2025
1211afb
fix build
fmontes Dec 26, 2025
6d8a1ca
chore: clean up
fmontes Dec 27, 2025
d3b0590
Merge branch 'main' into issue-34166-usage-ui
fmontes Dec 29, 2025
dbac361
chore: fix lint
fmontes Dec 29, 2025
2603b96
chore: format
fmontes Dec 29, 2025
9b2f2b7
Merge branch 'main' into issue-34166-usage-ui
sfreudenthaler Dec 29, 2025
aa6b778
Merge branch 'main' into issue-34166-usage-ui
fmontes Dec 31, 2025
c8fbcce
Merge branch 'main' into issue-34166-usage-ui
sfreudenthaler Dec 31, 2025
dfb8d81
Merge branch 'main' into issue-34166-usage-ui
sfreudenthaler Jan 2, 2026
8ae2666
chore: trigger CI with timeout fixes from main
sfreudenthaler Jan 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions core-web/libs/data-access/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,4 @@ export * from './lib/push-publish/push-publish.service';
export * from './lib/dot-page-contenttype/dot-page-contenttype.service';
export * from './lib/dot-favorite-contenttype/dot-favorite-contenttype.service';
export * from './lib/dot-content-drive/dot-content-drive.service';
export * from './lib/dot-usage/dot-usage.service';
203 changes: 203 additions & 0 deletions core-web/libs/data-access/src/lib/dot-usage/dot-usage.service.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import { provideHttpClient, HttpErrorResponse } from '@angular/common/http';
import { provideHttpClientTesting, HttpTestingController } from '@angular/common/http/testing';
import { TestBed } from '@angular/core/testing';

import { DotUsageService, UsageApiResponse, UsageSummary } from './dot-usage.service';

describe('DotUsageService', () => {
let service: DotUsageService;
let httpMock: HttpTestingController;

const mockSummary: UsageSummary = {
metrics: {
content: {
COUNT_CONTENT: {
name: 'COUNT_CONTENT',
value: 1500,
displayLabel: 'usage.metric.COUNT_CONTENT'
}
},
site: {
COUNT_OF_SITES: {
name: 'COUNT_OF_SITES',
value: 5,
displayLabel: 'usage.metric.COUNT_OF_SITES'
}
},
user: {
COUNT_OF_USERS: {
name: 'COUNT_OF_USERS',
value: 60,
displayLabel: 'usage.metric.COUNT_OF_USERS'
}
},
system: {
COUNT_LANGUAGES: {
name: 'COUNT_LANGUAGES',
value: 3,
displayLabel: 'usage.metric.COUNT_LANGUAGES'
}
}
},
lastUpdated: '2024-01-15T15:30:00Z'
};

beforeEach(() => {
TestBed.configureTestingModule({
providers: [provideHttpClient(), provideHttpClientTesting(), DotUsageService]
});

service = TestBed.inject(DotUsageService);
httpMock = TestBed.inject(HttpTestingController);
});

afterEach(() => {
httpMock.verify();
});

it('should be created', () => {
expect(service).toBeTruthy();
});

it('should get summary successfully', (done) => {
const mockResponse: UsageApiResponse = { entity: mockSummary };

service.getSummary().subscribe((summary) => {
expect(summary).toEqual(mockSummary);
done();
});

const req = httpMock.expectOne('/api/v1/usage/summary');
expect(req.request.method).toBe('GET');
req.flush(mockResponse);
});

it('should handle HTTP errors', (done) => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation();

service.getSummary().subscribe({
next: () => fail('Should have failed'),
error: (error) => {
expect(error.status).toBe(401);
errorSpy.mockRestore();
done();
}
});

const req = httpMock.expectOne('/api/v1/usage/summary');
req.flush('Unauthorized', { status: 401, statusText: 'Unauthorized' });
});

it('should handle server errors', (done) => {
const errorSpy = jest.spyOn(console, 'error').mockImplementation();

service.getSummary().subscribe({
next: () => fail('Should have failed'),
error: (error) => {
expect(error.status).toBe(500);
errorSpy.mockRestore();
done();
}
});

const req = httpMock.expectOne('/api/v1/usage/summary');
req.flush('Internal Server Error', { status: 500, statusText: 'Internal Server Error' });
});

it('should get error message for 401', () => {
const error = { status: 401 } as HttpErrorResponse;
expect(service.getErrorMessage(error)).toBe('usage.dashboard.error.unauthorized');
});

it('should get error message for 403', () => {
const error = { status: 403 } as HttpErrorResponse;
expect(service.getErrorMessage(error)).toBe('usage.dashboard.error.forbidden');
});

it('should get error message for 404', () => {
const error = { status: 404 } as HttpErrorResponse;
expect(service.getErrorMessage(error)).toBe('usage.dashboard.error.notFound');
});

it('should get error message for 408', () => {
const error = { status: 408 } as HttpErrorResponse;
expect(service.getErrorMessage(error)).toBe('usage.dashboard.error.timeout');
});

it('should get error message for 500', () => {
const error = { status: 500 } as HttpErrorResponse;
expect(service.getErrorMessage(error)).toBe('usage.dashboard.error.serverError');
});

it('should get error message for 502', () => {
const error = { status: 502 } as HttpErrorResponse;
expect(service.getErrorMessage(error)).toBe('usage.dashboard.error.badGateway');
});

it('should get error message for 503', () => {
const error = { status: 503 } as HttpErrorResponse;
expect(service.getErrorMessage(error)).toBe('usage.dashboard.error.serviceUnavailable');
});

it('should get error message for unknown status', () => {
const error = { status: 418 } as HttpErrorResponse;
expect(service.getErrorMessage(error)).toBe('usage.dashboard.error.requestFailed');
});

it('should get error message from error.error.message', () => {
const error = {
error: { message: 'Custom error message' },
status: 400
} as HttpErrorResponse;
expect(service.getErrorMessage(error)).toBe('Custom error message');
});

it('should get generic error message when no status', () => {
const error = {} as HttpErrorResponse;
expect(service.getErrorMessage(error)).toBe('usage.dashboard.error.generic');
});

it('should refresh data', (done) => {
const mockResponse: UsageApiResponse = { entity: mockSummary };

service.refresh().subscribe((summary) => {
expect(summary).toEqual(mockSummary);
done();
});

const req = httpMock.expectOne('/api/v1/usage/summary');
req.flush(mockResponse);
});

it('should handle concurrent requests properly', () => {
const spy = jest.spyOn(console, 'error').mockImplementation();

// Start two requests simultaneously
service.getSummary().subscribe();
service.getSummary().subscribe();

const requests = httpMock.match('/api/v1/usage/summary');
expect(requests.length).toBe(2);

// Fulfill both requests
requests[0].flush({ entity: mockSummary });
requests[1].flush({ entity: mockSummary });

spy.mockRestore();
});

it('should validate response structure', (done) => {
const invalidResponse = { invalidProperty: 'test' };

service.getSummary().subscribe({
next: (summary) => {
// Should handle invalid response gracefully
expect(summary).toBeDefined();
done();
}
});

const req = httpMock.expectOne('/api/v1/usage/summary');
req.flush({ entity: invalidResponse });
});
});
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { Observable } from 'rxjs';

import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Injectable, inject, signal } from '@angular/core';
import { Injectable, inject } from '@angular/core';

import { catchError, map, tap } from 'rxjs/operators';
import { catchError, map } from 'rxjs/operators';

/**
* Metric metadata structure containing name, value, and display label.
Expand Down Expand Up @@ -68,46 +68,20 @@ export interface UsageErrorResponse {
readonly statusText?: string;
}

/**
* Service state interface for reactive state management
*/
export interface UsageServiceState {
readonly summary: UsageSummary | null;
readonly loading: boolean;
readonly error: string | null;
}

@Injectable({
providedIn: 'root'
})
export class DotUsageService {
#BASE_URL = '/api/v1/usage';
#http = inject(HttpClient);

// Reactive state
readonly summary = signal<UsageSummary | null>(null);
readonly loading = signal<boolean>(false);
readonly error = signal<string | null>(null);
readonly errorStatus = signal<number | null>(null);

/**
* Fetches usage summary from the backend API
*/
getSummary(): Observable<UsageSummary> {
this.loading.set(true);
this.error.set(null);

return this.#http.get<UsageApiResponse>(`${this.#BASE_URL}/summary`).pipe(
map((response) => response.entity),
tap((summary) => {
this.summary.set(summary);
this.loading.set(false);
}),
catchError((error) => {
const errorMessage = this.getErrorMessage(error);
this.error.set(errorMessage);
this.errorStatus.set(error.status || null);
this.loading.set(false);
console.error('Failed to fetch usage summary:', error);
throw error;
})
Expand All @@ -121,21 +95,11 @@ export class DotUsageService {
return this.getSummary();
}

/**
* Resets the service state
*/
reset(): void {
this.summary.set(null);
this.loading.set(false);
this.error.set(null);
this.errorStatus.set(null);
}

/**
* Extracts user-friendly error message i18n key from HTTP error
* Returns i18n keys that should be translated using the dm pipe in the component
*/
private getErrorMessage(error: HttpErrorResponse | UsageErrorResponse): string {
getErrorMessage(error: HttpErrorResponse | UsageErrorResponse): string {
if (error.error?.message) {
return error.error.message;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<ng-container *ngIf="vm$ | async as vm">
@if (vm$ | async; as vm) {
<p-card>
<ng-template pTemplate="title">
<h2 data-testId="targeting-card-name">
Expand All @@ -23,4 +23,4 @@ <h2 data-testId="targeting-card-name">
</div>
</ng-template>
</p-card>
</ng-container>
}
1 change: 0 additions & 1 deletion core-web/libs/portlets/dot-usage/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
export * from './lib/dot-usage-shell/dot-usage-shell.component';
export * from './lib/services/dot-usage.service';
export * from './lib.routes';
Loading