Skip to content

Commit

Permalink
NAS-129884: Improve directory services monitor (#10276)
Browse files Browse the repository at this point in the history
  • Loading branch information
undsoft authored Jul 3, 2024
1 parent 5842cb0 commit 6c304de
Show file tree
Hide file tree
Showing 101 changed files with 429 additions and 480 deletions.
7 changes: 0 additions & 7 deletions src/app/app.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,13 +140,6 @@ export const rootRouterConfig: Routes = [
path: 'system/audit',
loadChildren: () => import('./pages/audit/audit.module').then((module) => module.AuditModule),
},
{
path: 'errors',
children: [{
path: 'unauthorized',
loadChildren: () => import('./pages/unauthorized/unauthorized.module').then((module) => module.UnauthorizedModule),
}],
},
],
},
{
Expand Down
10 changes: 10 additions & 0 deletions src/app/enums/directory-service-state.enum.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,17 @@
import { marker as T } from '@biesbjerg/ngx-translate-extract-marker';

export enum DirectoryServiceState {
Disabled = 'DISABLED',
Healthy = 'HEALTHY',
Faulted = 'FAULTED',
Leaving = 'LEAVING',
Joining = 'JOINING',
}

export const directoryServiceStateLabels = new Map<DirectoryServiceState, string>([
[DirectoryServiceState.Disabled, T('Disabled')],
[DirectoryServiceState.Healthy, T('Healthy')],
[DirectoryServiceState.Faulted, T('Faulted')],
[DirectoryServiceState.Leaving, T('Leaving')],
[DirectoryServiceState.Joining, T('Joining')],
]);
Original file line number Diff line number Diff line change
@@ -1,69 +1,60 @@
<mat-dialog-content class="dir-service-monitor-dialog">
<div class="header" fxLayout="row" fxLayoutAlign="space-between center">
<mat-dialog-content class="dialog">
<div class="header">
<h3>{{ 'Directory Services' | translate }}</h3>

<div class="header-actions">
<!-- TODO: Use subscription instead. Move logic to a common component store/service -->
<button
*ixRequiresRoles="requiredRoles"
mat-icon-button
id="refresh-icon"
ixTest="refresh-directory-services"
[attr.aria-label]="'Refresh' | translate"
(click)="getStatus()"
>
<ix-icon name="refresh"></ix-icon>
</button>

<button
class="dir-services-monitor-action-button"
mat-icon-button
mat-dialog-close
ixTest="close-directory-services"
[attr.aria-label]="'Close' | translate"
>
<ix-icon name="clear"></ix-icon>
</button>
</div>
</div>
<div *ngIf="isLoading" class="spinner-wrapper">
<mat-spinner id="dir-service-monitor-spinner" [diameter]="40"></mat-spinner>
</div>
<mat-table
*ngIf="!isLoading"
class="mat-elevation-z8"
[dataSource]="dataSource"
>
<!-- Icon Column -->
<ng-container matColumnDef="icon">
<mat-header-cell *matHeaderCellDef fxFlex="74px"></mat-header-cell>
<mat-cell *matCellDef="let element" fxFlex="74px">
<ng-container [ngSwitch]="element.state">
<ix-icon *ngSwitchCase="DirectoryServiceState.Healthy" name="check_circle" class="state-healthy"></ix-icon>
<ix-icon *ngSwitchCase="DirectoryServiceState.Faulted" name="highlight_off" class="state-faulted"></ix-icon>
<ix-icon *ngSwitchCase="DirectoryServiceState.Leaving" name="arrow_back" class="state-leaving"></ix-icon>
<ix-icon *ngSwitchCase="DirectoryServiceState.Joining" name="arrow_forward" class="state-joining"></ix-icon>
<ix-icon *ngSwitchCase="DirectoryServiceState.Disabled" name="remove_circle" class="state-disabled"></ix-icon>
</ng-container>
</mat-cell>
</ng-container>

<ng-container matColumnDef="name">
<mat-header-cell *matHeaderCellDef>
{{ 'Name' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let element">{{ element.name }}</mat-cell>
</ng-container>
@if (isLoading()) {
<div class="spinner-wrapper">
<mat-spinner id="dir-service-monitor-spinner" [diameter]="40"></mat-spinner>
</div>
} @else {
<a
class="status-row"
[ixTest]="'go-to-directory-services'"
[routerLink]="['/credentials', 'directory-services']"
>
<div>{{ serviceName() }}</div>
<div class="state">
@switch (state()) {
@case (DirectoryServiceState.Healthy) {
<ix-icon name="check_circle" class="icon state-healthy"></ix-icon>
}
@case (DirectoryServiceState.Faulted) {
<ix-icon name="highlight_off" class="icon state-faulted"></ix-icon>
}
@case (DirectoryServiceState.Leaving) {
<ix-icon name="arrow_back" class="icon state-leaving"></ix-icon>
}
@case (DirectoryServiceState.Joining) {
<ix-icon name="arrow_forward" class="icon state-joining"></ix-icon>
}
@case (DirectoryServiceState.Disabled) {
<ix-icon name="remove_circle" class="icon state-disabled"></ix-icon>
}
}

<ng-container matColumnDef="state">
<mat-header-cell *matHeaderCellDef>
{{ 'State' | translate }}
</mat-header-cell>
<mat-cell *matCellDef="let element">{{ element.state }}</mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row
*matRowDef="let row; columns: displayedColumns"
class="table-row clickable"
[ixTest]="row.name"
(click)="goTo(row.id)"
></mat-row>
</mat-table>
{{ state() | mapValue: directoryServiceStateLabels | translate }}
</div>
</a>
}
</mat-dialog-content>
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
.dir-service-monitor-dialog {
.dialog {
padding: 0;
width: 350px;
}

.header {
background: var(--bg2);
border-bottom: 1px solid var(--lines);
height: 42px;
margin-left: -24px;
margin-right: -24px;
padding: 4px 8px 12px 24px;
display: flex;
justify-content: space-between;
padding: 11px 6px 7px 16px;
}

.header-actions {
Expand All @@ -17,48 +17,46 @@
justify-content: flex-end;
}

.mat-mdc-table {
background: var(--bg2);
margin-left: -24px;
margin-right: -24px;
.status-row {
align-items: center;
cursor: pointer;
display: flex;
font-size: 13px;
justify-content: space-between;
padding: 11px 18px;

.mat-header-row {
display: none !important;
&:hover {
background: var(--hover-bg);
}

.mat-cell,
.mat-footer-cell,
.mat-header-cell {
color: var(--fg1);
.state {
align-items: center;
display: flex;
font-size: 14px;
gap: 8px;
}
}

.mat-icon.state-healthy {
color: var(--green);
}

.mat-icon.state-faulted {
color: var(--red);
}

.mat-icon.state-joining {
color: var(--primary);
}
.icon {
&.state-healthy {
color: var(--green);
}

.mat-icon.state-leaving {
color: var(--accent);
}
&.state-faulted {
color: var(--red);
}

.mat-icon.state-disabled {
color: var(--grey);
}
&.state-joining {
color: var(--primary);
}

.table-row:hover {
background: var(--hover-bg);
}
&.state-leaving {
color: var(--accent);
}

.clickable {
cursor: pointer;
&.state-disabled {
color: var(--grey);
}
}
}

.spinner-wrapper {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { createComponentFactory, Spectator } from '@ngneat/spectator/jest';
import { mockCall, mockWebSocket } from 'app/core/testing/utils/mock-websocket.utils';
import { DirectoryServiceState } from 'app/enums/directory-service-state.enum';
import { IxIconHarness } from 'app/modules/ix-icon/ix-icon.harness';
import {
DirectoryServicesMonitorComponent,
} from 'app/modules/layout/components/topbar/directory-services-indicator/directory-services-monitor/directory-services-monitor.component';
import { MapValuePipe } from 'app/modules/pipes/map-value/map-value.pipe';
import { WebSocketService } from 'app/services/ws.service';

describe('DirectoryServicesMonitorComponent', () => {
let spectator: Spectator<DirectoryServicesMonitorComponent>;
let loader: HarnessLoader;
const createComponent = createComponentFactory({
component: DirectoryServicesMonitorComponent,
imports: [
MapValuePipe,
],
providers: [
mockWebSocket([
mockCall('directoryservices.get_state', {
activedirectory: DirectoryServiceState.Disabled,
ldap: DirectoryServiceState.Healthy,
}),
]),
],
});

beforeEach(() => {
spectator = createComponent();
loader = TestbedHarnessEnvironment.loader(spectator.fixture);
});

it('loads directory services status on component initialization', () => {
expect(spectator.inject(WebSocketService).call).toHaveBeenCalledWith('directoryservices.get_state');
});

it('shows status of a non-disabled directory service', () => {
expect(spectator.query('.status-row')).toHaveText('LDAP Healthy');

const statusIcon = spectator.query('.status-row .icon');
expect(statusIcon).toHaveClass('state-healthy');
});

it('updates directory services status when refresh button is pressed', async () => {
const refreshButton = await loader.getHarness(IxIconHarness.with({ name: 'refresh' }));
await refreshButton.click();

expect(spectator.inject(WebSocketService).call).toHaveBeenLastCalledWith('directoryservices.get_state');
});
});
Original file line number Diff line number Diff line change
@@ -1,20 +1,13 @@
import {
ChangeDetectionStrategy, ChangeDetectorRef, Component, OnInit,
ChangeDetectionStrategy, Component, OnInit,
signal,
} from '@angular/core';
import { MatDialogRef } from '@angular/material/dialog';
import { Router } from '@angular/router';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { take } from 'rxjs/operators';
import { DirectoryServiceState } from 'app/enums/directory-service-state.enum';
import { Role } from 'app/enums/role.enum';
import { finalize } from 'rxjs';
import { DirectoryServiceState, directoryServiceStateLabels } from 'app/enums/directory-service-state.enum';
import { ErrorHandlerService } from 'app/services/error-handler.service';
import { WebSocketService } from 'app/services/ws.service';

interface DirectoryServicesMonitorRow {
name: string;
state: DirectoryServiceState;
id: string;
}

@UntilDestroy()
@Component({
selector: 'ix-directory-services-monitor',
Expand All @@ -23,39 +16,38 @@ interface DirectoryServicesMonitorRow {
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class DirectoryServicesMonitorComponent implements OnInit {
readonly requiredRoles = [Role.FullAdmin];

displayedColumns: string[] = ['icon', 'name', 'state'];
dataSource: DirectoryServicesMonitorRow[] = [];
isLoading = false;
protected readonly isLoading = signal(false);
protected readonly serviceName = signal<string>('');
protected readonly state = signal<DirectoryServiceState | null>(null);

readonly DirectoryServiceState = DirectoryServiceState;
protected readonly DirectoryServiceState = DirectoryServiceState;
protected readonly directoryServiceStateLabels = directoryServiceStateLabels;

constructor(
private ws: WebSocketService,
private router: Router,
private dialogRef: MatDialogRef<DirectoryServicesMonitorComponent>,
private cdr: ChangeDetectorRef,
private errorHandler: ErrorHandlerService,
) {}

ngOnInit(): void {
this.getStatus();
}

getStatus(): void {
this.isLoading = true;
this.ws.call('directoryservices.get_state').pipe(take(1), untilDestroyed(this)).subscribe((state) => {
this.isLoading = false;
this.dataSource = [
{ name: 'Active Directory', state: state.activedirectory, id: 'activedirectory' },
{ name: 'LDAP', state: state.ldap, id: 'ldap' },
];
this.cdr.markForCheck();
});
}

goTo(el: string): void {
this.dialogRef.close();
this.router.navigate([`/directoryservice/${el}`]);
this.isLoading.set(true);
this.ws.call('directoryservices.get_state')
.pipe(
this.errorHandler.catchError(),
finalize(() => this.isLoading.set(false)),
untilDestroyed(this),
)
.subscribe((state) => {
if (state.ldap !== DirectoryServiceState.Disabled) {
this.serviceName.set('LDAP');
this.state.set(state.ldap);
} else {
this.serviceName.set('Active Directory');
this.state.set(state.activedirectory);
}
});
}
}
17 changes: 0 additions & 17 deletions src/app/pages/unauthorized/unauthorized.module.ts

This file was deleted.

Loading

0 comments on commit 6c304de

Please sign in to comment.