Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 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
c90d04b
add layout tree
fmontes Dec 24, 2025
1c31dfb
Implement zoom and pan functionality in EditEmaEditor component
fmontes Dec 25, 2025
162a545
Refactor EditEmaEditor component to improve message handling and ifra…
fmontes Dec 25, 2025
4453853
Refactor EditEmaEditor component: rename zoom and pan method, update …
fmontes Dec 25, 2025
8615a44
Refactor EditEmaEditor component: rename zoom and pan method, update …
fmontes Dec 26, 2025
7fce1e8
Refactor DotUveBridgeService: streamline imports and simplify sendMes…
fmontes Dec 26, 2025
a5d3e22
Enhance EmaPageDropzone component: add zoomLevel input and adjust pos…
fmontes Dec 26, 2025
33fbe2b
Update DotUvePaletteComponent: change icon in tab header and remove u…
fmontes Dec 26, 2025
dfb4695
Refactor DotUvePaletteComponent: improve container label retrieval an…
fmontes Dec 26, 2025
25cb615
Enhance DotUvePaletteComponent: add contentlet nodes to layout tree s…
fmontes Dec 26, 2025
7f5076c
Update DotUvePaletteComponent: enhance tree display with overflow han…
fmontes Dec 26, 2025
d9f9ad9
DotUveDragDropService: clean up
fmontes Dec 26, 2025
b8c3ded
Refactor DotUveActionsHandlerService: streamline imports and remove u…
fmontes Dec 26, 2025
6f3469c
Enhance DotUvePaletteComponent and EditEmaEditor: add node selection …
fmontes Dec 26, 2025
db80e44
Adjust scroll position calculation in EditEmaEditor to account for zo…
fmontes Dec 26, 2025
7a4dd9b
Enhance DotUvePaletteComponent: implement drag-and-drop functionality…
fmontes Dec 26, 2025
9336d55
Integrate DotPageLayoutService into DotUvePaletteComponent and withSa…
fmontes Dec 26, 2025
bfeae75
Update _tree.scss and DotUvePaletteComponent: adjust tree node margin…
fmontes Dec 26, 2025
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';
204 changes: 204 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,204 @@
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 Expand Up @@ -165,3 +129,4 @@ export class DotUsageService {
return 'usage.dashboard.error.generic';
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@
.p-tree-container {
.p-treenode {
padding: 0;
margin: 0;
margin: 2px 0;
outline: 0;

.p-treenode-content {
border-radius: $border-radius-xs;
transition: none;
padding: $spacing-0 $spacing-1;
padding: 0 $spacing-0;

.p-tree-toggler {
margin-right: $spacing-1;
margin-right: $spacing-0;
width: $spacing-5;
height: $spacing-5;
color: $black;
Expand Down
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