diff --git a/console-webapp/src/app/app-routing.module.ts b/console-webapp/src/app/app-routing.module.ts index ebabe0d9c..ec9f46c6a 100644 --- a/console-webapp/src/app/app-routing.module.ts +++ b/console-webapp/src/app/app-routing.module.ts @@ -26,6 +26,7 @@ import SecurityComponent from './settings/security/security.component'; import { SettingsComponent } from './settings/settings.component'; import { SupportComponent } from './support/support.component'; import RdapComponent from './settings/rdap/rdap.component'; +import { HistoryComponent } from './history/history.component'; import { PasswordResetVerifyComponent } from './shared/components/passwordReset/passwordResetVerify.component'; export interface RouteWithIcon extends Route { @@ -64,13 +65,18 @@ export const routes: RouteWithIcon[] = [ title: 'Dashboard', iconName: 'view_comfy_alt', }, - // { path: 'tlds', component: TldsComponent, title: "TLDs", iconName: "event_list" }, { path: DomainListComponent.PATH, component: DomainListComponent, title: 'Domains', iconName: 'view_list', }, + { + path: HistoryComponent.PATH, + component: HistoryComponent, + // title: 'History', + // iconName: 'history', + }, { path: SettingsComponent.PATH, component: SettingsComponent, diff --git a/console-webapp/src/app/app.module.ts b/console-webapp/src/app/app.module.ts index 948e42b70..2aa9e2323 100644 --- a/console-webapp/src/app/app.module.ts +++ b/console-webapp/src/app/app.module.ts @@ -56,13 +56,14 @@ import { GlobalLoaderService } from './shared/services/globalLoader.service'; import { UserDataService } from './shared/services/userData.service'; import { SnackBarModule } from './snackbar.module'; import { SupportComponent } from './support/support.component'; -import { TldsComponent } from './tlds/tlds.component'; import { ForceFocusDirective } from './shared/directives/forceFocus.directive'; import RdapComponent from './settings/rdap/rdap.component'; import RdapEditComponent from './settings/rdap/rdapEdit.component'; import { PocReminderComponent } from './shared/components/pocReminder/pocReminder.component'; import { PasswordResetVerifyComponent } from './shared/components/passwordReset/passwordResetVerify.component'; import { PasswordInputForm } from './shared/components/passwordReset/passwordInputForm.component'; +import { HistoryComponent } from './history/history.component'; +import { HistoryListComponent } from './history/historyList.component'; @NgModule({ declarations: [SelectedRegistrarWrapper], @@ -81,6 +82,8 @@ export class SelectedRegistrarModule {} EppPasswordEditComponent, ForceFocusDirective, HeaderComponent, + HistoryComponent, + HistoryListComponent, HomeComponent, LocationBackDirective, NavigationComponent, @@ -104,7 +107,6 @@ export class SelectedRegistrarModule {} SettingsComponent, SettingsContactComponent, SupportComponent, - TldsComponent, UserLevelVisibility, ], bootstrap: [AppComponent], diff --git a/console-webapp/src/app/history/history.component.html b/console-webapp/src/app/history/history.component.html new file mode 100644 index 000000000..d0612ea2e --- /dev/null +++ b/console-webapp/src/app/history/history.component.html @@ -0,0 +1,62 @@ + +
+

+ Registrar Console Activity History +

+ + +
+ + +
+ +
+
+
+ + Console User Email: + + +
+
+ +
+
+ +
+
+
+ + +
diff --git a/console-webapp/src/app/tlds/tlds.component.scss b/console-webapp/src/app/history/history.component.scss similarity index 67% rename from console-webapp/src/app/tlds/tlds.component.scss rename to console-webapp/src/app/history/history.component.scss index 6753521df..03f522f8c 100644 --- a/console-webapp/src/app/tlds/tlds.component.scss +++ b/console-webapp/src/app/history/history.component.scss @@ -1,4 +1,4 @@ -// Copyright 2024 The Nomulus Authors. All Rights Reserved. +// Copyright 2025 The Nomulus Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,17 +12,11 @@ // See the License for the specific language governing permissions and // limitations under the License. -.console-tlds { - &__cards { - display: flex; - border-top: 1px solid #ddd; - padding: 1rem; - } - &__card { - max-width: 300px; - } - &__card-links { - display: flex; - flex-direction: column; +.history-log { + font-family: "Roboto", sans-serif; + max-width: 760px; + + .spacer { + margin: 20px 0; } } diff --git a/console-webapp/src/app/history/history.component.ts b/console-webapp/src/app/history/history.component.ts new file mode 100644 index 000000000..12ad58b51 --- /dev/null +++ b/console-webapp/src/app/history/history.component.ts @@ -0,0 +1,80 @@ +// Copyright 2025 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Component, effect } from '@angular/core'; +import { UserDataService } from '../shared/services/userData.service'; +import { BackendService } from '../shared/services/backend.service'; +import { RegistrarService } from '../registrar/registrar.service'; +import { HistoryService } from './history.service'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { + GlobalLoader, + GlobalLoaderService, +} from '../shared/services/globalLoader.service'; +import { HttpErrorResponse } from '@angular/common/http'; +import { RESTRICTED_ELEMENTS } from '../shared/directives/userLevelVisiblity.directive'; + +@Component({ + selector: 'app-history', + templateUrl: './history.component.html', + styleUrls: ['./history.component.scss'], + providers: [HistoryService], + standalone: false, +}) +export class HistoryComponent implements GlobalLoader { + public static PATH = 'history'; + + consoleUserEmail: string = ''; + isLoading: boolean = false; + + constructor( + private backendService: BackendService, + private registrarService: RegistrarService, + protected historyService: HistoryService, + protected globalLoader: GlobalLoaderService, + protected userDataService: UserDataService, + private _snackBar: MatSnackBar + ) { + effect(() => { + if (registrarService.registrarId()) { + this.loadHistory(); + } + }); + } + + getElementIdForUserLog() { + return RESTRICTED_ELEMENTS.ACTIVITY_PER_USER; + } + + loadingTimeout() { + this._snackBar.open('Timeout loading records history'); + } + + loadHistory() { + this.globalLoader.startGlobalLoader(this); + this.isLoading = true; + this.historyService + .getHistoryLog(this.registrarService.registrarId(), this.consoleUserEmail) + .subscribe({ + error: (err: HttpErrorResponse) => { + this._snackBar.open(err.error || err.message); + this.isLoading = false; + }, + next: () => { + this.globalLoader.stopGlobalLoader(this); + this.isLoading = false; + }, + }); + } +} diff --git a/console-webapp/src/app/history/history.service.ts b/console-webapp/src/app/history/history.service.ts new file mode 100644 index 000000000..41120c9f1 --- /dev/null +++ b/console-webapp/src/app/history/history.service.ts @@ -0,0 +1,46 @@ +// Copyright 2025 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { Injectable, signal } from '@angular/core'; +import { BackendService } from '../shared/services/backend.service'; +import { tap } from 'rxjs'; + +export interface HistoryRecord { + modificationTime: string; + type: string; + description: string; + actingUser: { + emailAddress: string; + }; +} + +@Injectable() +export class HistoryService { + historyRecordsRegistrar = signal([]); + historyRecordsUser = signal([]); + + constructor(private backendService: BackendService) {} + + getHistoryLog(registrarId: string, userEmail?: string) { + return this.backendService.getHistoryLog(registrarId, userEmail).pipe( + tap((historyRecords: HistoryRecord[]) => { + if (userEmail) { + this.historyRecordsUser.set(historyRecords); + } else { + this.historyRecordsRegistrar.set(historyRecords); + } + }) + ); + } +} diff --git a/console-webapp/src/app/history/historyList.component.html b/console-webapp/src/app/history/historyList.component.html new file mode 100644 index 000000000..3e746c33c --- /dev/null +++ b/console-webapp/src/app/history/historyList.component.html @@ -0,0 +1,50 @@ +@if (!isLoading && historyRecords.length == 0) { +
+ apps_outage +

No records found

+
+} @else { + + + + + + + {{ getIconForType(item.type) }} + + +
+
+ {{ + item.type + }} +
+ + {{ parseDescription(item.description).detail }} + +
+
+
+ User - {{ item.actingUser.emailAddress }} +
+
+ + + {{ item.modificationTime | date : "MMM d, y, h:mm a" }} + +
+ + +
+
+
+
+} diff --git a/console-webapp/src/app/history/historyList.component.scss b/console-webapp/src/app/history/historyList.component.scss new file mode 100644 index 000000000..439b89dd8 --- /dev/null +++ b/console-webapp/src/app/history/historyList.component.scss @@ -0,0 +1,81 @@ +// Copyright 2025 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +.history-list { + font-family: "Roboto", sans-serif; + &__item { + display: flex; + align-items: center; + // Override default mat-list-item height to fit content + height: auto !important; + padding: 16px 0; + } + + &__no-records { + display: flex; + justify-content: center; + align-items: center; + flex-direction: column; + } + &__no-records-icon { + width: 4rem; + height: 4rem; + font-size: 4rem; + margin-top: 1.5rem; + } + + &__icon { + margin-right: 16px; + + &--update { + color: #1976d2; + } + + &--security { + color: #d32f2f; + } + } + + &__description { + &--main { + font-size: 1rem; + font-weight: 500; + color: rgba(0, 0, 0, 0.87); + margin-bottom: 1em; + } + } + + &__content { + flex-grow: 1; + display: flex; + flex-direction: column; + gap: 4px; + margin-right: 16px; + } + + &__chip { + margin: 0.5rem 0; + } + + &__user { + font-size: 0.9rem; + color: rgba(0, 0, 0, 0.6); + } + + &__timestamp { + color: rgba(0, 0, 0, 0.6); + white-space: nowrap; + text-align: right; + } +} diff --git a/console-webapp/src/app/history/historyList.component.ts b/console-webapp/src/app/history/historyList.component.ts new file mode 100644 index 000000000..76ca96ad4 --- /dev/null +++ b/console-webapp/src/app/history/historyList.component.ts @@ -0,0 +1,66 @@ +// Copyright 2025 The Nomulus Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { ChangeDetectionStrategy, Component, Input } from '@angular/core'; +import { HistoryRecord } from './history.service'; + +@Component({ + selector: 'app-history-list', + templateUrl: './historyList.component.html', + styleUrls: ['./historyList.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + standalone: false, +}) +export class HistoryListComponent { + @Input() historyRecords: HistoryRecord[] = []; + @Input() isLoading: boolean = false; + + getIconForType(type: string): string { + switch (type) { + case 'REGISTRAR_UPDATE': + return 'edit'; + case 'REGISTRAR_SECURITY_UPDATE': + return 'security'; + default: + return 'history'; // A fallback icon + } + } + + getIconClass(type: string): string { + switch (type) { + case 'REGISTRAR_UPDATE': + return 'history-log__icon--update'; + case 'REGISTRAR_SECURITY_UPDATE': + return 'history-log__icon--security'; + default: + return ''; + } + } + + parseDescription(description: string): { + main: string; + detail: string | null; + } { + if (!description) { + return { main: 'N/A', detail: null }; + } + const parts = description.split('|'); + const detail = parts.length > 1 ? parts[1].replace(/_/g, ' ') : null; + + return { + main: parts[0], + detail: detail, + }; + } +} diff --git a/console-webapp/src/app/shared/directives/userLevelVisiblity.directive.ts b/console-webapp/src/app/shared/directives/userLevelVisiblity.directive.ts index 255099e27..fba4094f4 100644 --- a/console-webapp/src/app/shared/directives/userLevelVisiblity.directive.ts +++ b/console-webapp/src/app/shared/directives/userLevelVisiblity.directive.ts @@ -16,6 +16,7 @@ import { Directive, ElementRef, Input, effect } from '@angular/core'; import { UserDataService } from '../services/userData.service'; export enum RESTRICTED_ELEMENTS { + ACTIVITY_PER_USER, REGISTRAR_ELEMENT, OTE, USERS, @@ -28,9 +29,10 @@ export const DISABLED_ELEMENTS_PER_ROLE = { RESTRICTED_ELEMENTS.REGISTRAR_ELEMENT, RESTRICTED_ELEMENTS.OTE, RESTRICTED_ELEMENTS.SUSPEND, + RESTRICTED_ELEMENTS.ACTIVITY_PER_USER, ], SUPPORT_LEAD: [], - SUPPORT_AGENT: [], + SUPPORT_AGENT: [RESTRICTED_ELEMENTS.ACTIVITY_PER_USER], }; @Directive({ @@ -40,6 +42,8 @@ export const DISABLED_ELEMENTS_PER_ROLE = { export class UserLevelVisibility { @Input() elementId!: RESTRICTED_ELEMENTS | null; + @Input() isReverse: boolean = false; + constructor( private userDataService: UserDataService, private el: ElementRef @@ -56,9 +60,9 @@ export class UserLevelVisibility { // @ts-ignore (DISABLED_ELEMENTS_PER_ROLE[globalRole] || []).includes(this.elementId) ) { - this.el.nativeElement.style.display = 'none'; + this.el.nativeElement.style.display = this.isReverse ? '' : 'none'; } else { - this.el.nativeElement.style.display = ''; + this.el.nativeElement.style.display = this.isReverse ? 'none' : ''; } } } diff --git a/console-webapp/src/app/shared/services/backend.service.ts b/console-webapp/src/app/shared/services/backend.service.ts index 55d1bad71..3fe6e540d 100644 --- a/console-webapp/src/app/shared/services/backend.service.ts +++ b/console-webapp/src/app/shared/services/backend.service.ts @@ -31,6 +31,7 @@ import { Contact } from '../../settings/contact/contact.service'; import { EppPasswordBackendModel } from '../../settings/security/security.service'; import { UserData } from './userData.service'; import { PasswordResetVerifyResponse } from '../components/passwordReset/passwordResetVerify.component'; +import { HistoryRecord } from '../../history/history.service'; @Injectable() export class BackendService { @@ -123,6 +124,16 @@ export class BackendService { .pipe(catchError((err) => this.errorCatcher(err))); } + getHistoryLog(registrarId: string, userEmail?: string) { + return this.http + .get( + userEmail + ? `/console-api/history?registrarId=${registrarId}&consoleUserEmail=${userEmail}` + : `/console-api/history?registrarId=${registrarId}` + ) + .pipe(catchError((err) => this.errorCatcher(err))); + } + getRegistrars(): Observable { return this.http .get('/console-api/registrars') diff --git a/console-webapp/src/app/tlds/tlds.component.html b/console-webapp/src/app/tlds/tlds.component.html deleted file mode 100644 index fcb7066c3..000000000 --- a/console-webapp/src/app/tlds/tlds.component.html +++ /dev/null @@ -1 +0,0 @@ -
diff --git a/console-webapp/src/app/tlds/tlds.component.spec.ts b/console-webapp/src/app/tlds/tlds.component.spec.ts deleted file mode 100644 index e90adf08a..000000000 --- a/console-webapp/src/app/tlds/tlds.component.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2024 The Nomulus Authors. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { ComponentFixture, TestBed } from '@angular/core/testing'; - -import { TldsComponent } from './tlds.component'; -import { MaterialModule } from '../material.module'; - -describe('TldsComponent', () => { - let component: TldsComponent; - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [MaterialModule], - declarations: [TldsComponent], - }).compileComponents(); - - fixture = TestBed.createComponent(TldsComponent); - component = fixture.componentInstance; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); -}); diff --git a/console-webapp/src/app/tlds/tlds.component.ts b/console-webapp/src/app/tlds/tlds.component.ts deleted file mode 100644 index 6370182f9..000000000 --- a/console-webapp/src/app/tlds/tlds.component.ts +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2024 The Nomulus Authors. All Rights Reserved. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import { Component } from '@angular/core'; - -@Component({ - selector: 'app-tlds', - templateUrl: './tlds.component.html', - styleUrls: ['./tlds.component.scss'], - standalone: false, -}) -export class TldsComponent {}