Skip to content

Commit

Permalink
chore(ux): message display in tables (#1254)
Browse files Browse the repository at this point in the history
  • Loading branch information
ngruelaneo authored Nov 18, 2024
2 parents 438764d + 046a71e commit 76e7ee9
Show file tree
Hide file tree
Showing 11 changed files with 385 additions and 0 deletions.
6 changes: 6 additions & 0 deletions src/app/components/table/table-cell.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@
@case ('object') {
<app-table-inspect-object [object]="handleNestedKeys(element)" [label]="column.name" />
}
@case ('message') {
<app-table-inspect-message [label]="column.name" [message]="string" />
}
@case ('output') {
<app-table-inspect-message [label]="column.name" [message]="string" />
}
@case ('link') {
<button mat-button (click)="navigate()">
{{ value | emptyCell }}
Expand Down
6 changes: 6 additions & 0 deletions src/app/components/table/table-cell.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { Duration, Timestamp } from '@ngx-grpc/well-known-types';
import { DurationPipe } from '@pipes/duration.pipe';
import { EmptyCellPipe } from '@pipes/empty-cell.pipe';
import { Subject } from 'rxjs';
import { TableInspectMessageComponent } from './table-inspect-message.component';
import { TableInspectObjectComponent } from './table-inspect-object.component';

@Component({
Expand All @@ -28,6 +29,7 @@ import { TableInspectObjectComponent } from './table-inspect-object.component';
MatButtonModule,
CountTasksByStatusComponent,
MatCheckboxModule,
TableInspectMessageComponent,
]
})
export class TableCellComponent<T extends DataRaw, S extends Status, O extends TaskOptions | null = null>{
Expand Down Expand Up @@ -78,6 +80,10 @@ export class TableCellComponent<T extends DataRaw, S extends Status, O extends T
return this._value;
}

get string() {
return this._value as string;
}

get durationValue() {
return this._value as Duration;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
mat-dialog-content {
width: 40rem;
}

section {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.25rem 1rem 0 1rem;
}

h2 {
margin: 0;
padding: 0 0 0.75rem 0;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<section>
<h2 mat-dialog-title>{{ label }}</h2>
<button mat-icon-button (click)="copy()">
<mat-icon [fontIcon]="getIcon('copy')" />
</button>
</section>

<mat-dialog-content>
{{ message }}
</mat-dialog-content>

<mat-dialog-actions align="end">
<button mat-flat-button (click)="onNoClick()" color="primary">Close</button>
</mat-dialog-actions>
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { Clipboard } from '@angular/cdk/clipboard';
import { TestBed } from '@angular/core/testing';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { IconsService } from '@services/icons.service';
import { NotificationService } from '@services/notification.service';
import { TableInspectMessageDialogComponent, TableInspectMessageDialogData } from './table-inspect-message-dialog.component';

describe('TableInspectMessageDialogComponent', () => {
let component: TableInspectMessageDialogComponent;

const label = 'Message';

const message = 'A long and complete message';

const mockMatDialogRef = {
close: jest.fn()
};

const mockData: TableInspectMessageDialogData = {
label: label,
message: message,
};

const mockNotificationService = {
success: jest.fn(),
};

const mockClipboard = {
copy: jest.fn()
};

beforeEach(() => {
component = TestBed.configureTestingModule({
providers: [
TableInspectMessageDialogComponent,
IconsService,
{ provide: Clipboard, useValue: mockClipboard },
{ provide: NotificationService, useValue: mockNotificationService },
{ provide: MatDialogRef, useValue: mockMatDialogRef },
{ provide: MAT_DIALOG_DATA, useValue: mockData }
]
}).inject(TableInspectMessageDialogComponent);
});

it('should create', () => {
expect(component).toBeTruthy();
});

it('should get label', () => {
expect(component.label).toEqual(label);
});

it('should get message', () => {
expect(component.message).toEqual(message);
});

describe('onCopy', () => {
beforeEach(() => {
component.copy();
});

it('should copy the message', () => {
expect(mockClipboard.copy).toHaveBeenCalledWith(message);
});

it('should notify the user', () => {
expect(mockNotificationService.success).toHaveBeenCalled();
});
});

it('should get icon', () => {
expect(component.getIcon('heart')).toEqual('favorite');
});

it('should close dialog', () => {
component.onNoClick();
expect(mockMatDialogRef.close).toHaveBeenCalled();
});
});
60 changes: 60 additions & 0 deletions src/app/components/table/table-inspect-message-dialog.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Clipboard } from '@angular/cdk/clipboard';
import { Component, Inject, inject } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { IconsService } from '@services/icons.service';
import { NotificationService } from '@services/notification.service';

export interface TableInspectMessageDialogData {
label: string;
message: string;
}

@Component({
selector: 'app-inspect-message-dialog',
templateUrl: 'table-inspect-message-dialog.component.html',
styleUrl: 'table-inspect-message-dialog.component.css',
standalone: true,
imports: [
MatCardModule,
MatButtonModule,
MatDialogModule,
MatIconModule,
],
providers: [
NotificationService,
]
})
export class TableInspectMessageDialogComponent {
get label() {
return this.data.label;
}

get message() {
return this.data.message;
}

constructor(
public dialogRef: MatDialogRef<TableInspectMessageDialogComponent>,
@Inject(MAT_DIALOG_DATA) private data: TableInspectMessageDialogData
) {}

private readonly iconsService = inject(IconsService);
private readonly clipboard = inject(Clipboard);
private readonly notificationService = inject(NotificationService);

copy() {
this.clipboard.copy(this.message);
this.notificationService.success('Message copied');
}

onNoClick(): void {
this.dialogRef.close();
}

getIcon(name: string) {
return this.iconsService.getIcon(name);
}
}
11 changes: 11 additions & 0 deletions src/app/components/table/table-inspect-message.component.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
section {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
width: fit-content;
}

mat-icon {
cursor: pointer;
}
11 changes: 11 additions & 0 deletions src/app/components/table/table-inspect-message.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<mat-chip disableRipple>
<section>
<span>{{ croppedMessage | emptyCell }}</span>
@if (croppedMessage) {
<mat-icon [fontIcon]="getIcon('copy')" (click)="copy()" />
}
@if (displayEye) {
<mat-icon [fontIcon]="getIcon('view')" (click)="onView()" />
}
</section>
</mat-chip>
99 changes: 99 additions & 0 deletions src/app/components/table/table-inspect-message.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { Clipboard } from '@angular/cdk/clipboard';
import { TestBed } from '@angular/core/testing';
import { MatDialog } from '@angular/material/dialog';
import { IconsService } from '@services/icons.service';
import { NotificationService } from '@services/notification.service';
import { TableInspectMessageDialogComponent } from './table-inspect-message-dialog.component';
import { TableInspectMessageComponent } from './table-inspect-message.component';

describe('TableInspectMessageComponent', () => {
let component: TableInspectMessageComponent;

const label = 'Message';

const message = 'A long and complete message';

const mockNotificationService = {
success: jest.fn(),
};

const mockDialog = {
open: jest.fn()
};

const mockClipboard = {
copy: jest.fn()
};

beforeEach(() => {
component = TestBed.configureTestingModule({
providers: [
TableInspectMessageComponent,
IconsService,
{ provide: NotificationService, useValue: mockNotificationService },
{ provide: MatDialog, useValue: mockDialog },
{ provide: Clipboard, useValue: mockClipboard }
]
}).inject(TableInspectMessageComponent);
component.label = label;
component.message = message;
});

it('should create', () => {
expect(component).toBeTruthy();
});

describe('setting message', () => {
it('should set the cropped message', () => {
expect(component.croppedMessage).toEqual('A long and com...');
});

it('should display the eye', () => {
expect(component.displayEye).toBeTruthy();
});

it('should set the full message if it not too long', () => {
const newMessage = 'message';
component.message = newMessage;
expect(component.croppedMessage).toEqual(newMessage);
});

it('should not display the eye if the message is too long', () => {
component.message = 'message';
expect(component.displayEye).toBeFalsy();
});
});

describe('onCopy', () => {
beforeEach(() => {
component.copy();
});

it('should copy the message', () => {
expect(mockClipboard.copy).toHaveBeenCalledWith(message);
});

it('should notify the user', () => {
expect(mockNotificationService.success).toHaveBeenCalled();
});
});

it('should get icon', () => {
expect(component.getIcon('heart')).toEqual('favorite');
});

describe('on View', () => {
beforeEach(() => {
component.onView();
});

it('should display pass the data to the dialog', () => {
expect(mockDialog.open).toHaveBeenCalledWith(TableInspectMessageDialogComponent, {
data: {
label: label,
message: message
}
});
});
});
});
Loading

1 comment on commit 76e7ee9

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lines Statements Branches Functions
Coverage: 99%
99.93% (4673/4676) 100% (876/876) 99.76% (1266/1269)

JUnit

Tests Skipped Failures Errors Time
1762 0 💤 0 ❌ 0 🔥 1m 17s ⏱️
Files coverage (99%)
File% Stmts% Branch% Funcs% LinesUncovered Line #s
All files99.9310099.7699.95 
applications100100100100 
   index.component.html100100100100 
   index.component.ts100100100100 
applications/components100100100100 
   table.component.html100100100100 
   table.component.ts100100100100 
applications/services100100100100 
   applications-filters.service.ts100100100100 
   applications-grpc.service.ts100100100100 
   applications-index.service.ts100100100100 
components99.8310099.4399.81 
   actions-toolbar-group.component.ts100100100100 
   actions-toolbar.component.ts100100100100 
   auto-complete.component.html100100100100 
   auto-complete.component.ts100100100100 
   auto-refresh-button.component.html100100100100 
   auto-refresh-button.component.ts100100100100 
   auto-refresh-dialog.component.html100100100100 
   auto-refresh-dialog.component.ts100100100100 
   columns-button.component.ts100100100100 
   columns-modify-dialog.component.html100100100100 
   columns-modify-dialog.component.ts100100100100 
   count-tasks-by-status.component.ts100100100100 
   icon-picker-dialog.component.html100100100100 
   icon-picker-dialog.component.ts100100100100 
   inspect-list.component.html100100100100 
   inspect-list.component.ts96.5510087.596.4268
   inspection-header.component.html100100100100 
   inspection-header.component.ts100100100100 
   inspection-toolbar.component.html100100100100 
   inspection-toolbar.component.ts100100100100 
   manage-custom-dialog.component.html100100100100 
   manage-custom-dialog.component.ts100100100100 
   page-header.component.html100100100100 
   page-header.component.ts100100100100 
   page-section-header.component.ts100100100100 
   page-section.component.ts100100100100 
   refresh-button.component.ts100100100100 
   share-url.component.ts100100100100 
   show-action-area.component.html100100100100 
   show-action-area.component.ts100100100100 
   show-actions.component.html100100100100 
   show-actions.component.ts100100100100 
   show-card-content.component.html100100100100 
   show-card-content.component.ts100100100100 
   show-card.component.html100100100100 
   show-card.component.ts100100100100 
   show-page.component.html100100100100 
   show-page.component.ts100100100100 
   spinner.component.ts100100100100 
   table-actions-toolbar.component.html100100100100 
   table-actions-toolbar.component.ts100100100100 
   table-container.component.ts100100100100 
   table-dashboard-actions-toolbar.component.html100100100100 
   table-dashboard-actions-toolbar.component.ts100100100100 
   table-index-actions-toolbar.component.html100100100100 
   table-index-actions-toolbar.component.ts100100100100 
   view-tasks-by-status.component.ts100100100100 
components/filters100100100100 
   filters-chips.component.html100100100100 
   filters-chips.component.ts100100100100 
   filters-dialog-and.component.html100100100100 
   filters-dialog-and.component.ts100100100100 
   filters-dialog-filter-field.component.html100100100100 
   filters-dialog-filter-field.component.ts100100100100 
   filters-dialog-input.component.html100100100100 
   filters-dialog-input.component.ts100100100100 
   filters-dialog-or.component.html100100100100 
   filters-dialog-or.component.ts100100100100 
   filters-dialog.component.html100100100100 
   filters-dialog.component.ts100100100100 
   filters-toolbar.component.html100100100100 
   filters-toolbar.component.ts100100100100 
components/inspection100100100100 
   field-content.component.html100100100100 
   field-content.component.ts100100100100 
   inspection-card.component.html100100100100 
   inspection-card.component.ts100100100100 
   inspection-json.component.html100100100100 
   inspection-json.component.ts100100100100 
   inspection-list-grid.component.html100100100100 
   inspection-list-grid.component.ts100100100100 
   inspection-object.component.html100100100100 
   inspection-object.component.ts100100100100 
   inspection.component.html100100100100 
   inspection.component.ts100100100100 
   json.component.html100100100100 
   json.component.ts100100100100 
   message.component.html100100100100 
   message.component.ts100100100100 
components/navigation100100100100 
   change-language-button.component.html100100100100 
   change-language-button.component.ts100100100100 
   navigation.component.html100100100100 
   navigation.component.ts100100100100 
   theme-selector.component.html100100100100 
   theme-selector.component.ts100100100100 
components/navigation/external-services100100100100 
   external-services.component.html100100100100 
   external-services.component.ts100100100100 
   form-external-service.component.html100100100100 
   form-external-service.component.ts100100100100 
   manage-external-services-dialog.component.html100100100100 
   manage-external-services-dialog.component.ts100100100100 
components/statuses100100100100 
   add-statuses-group-dialog.component.ts100100100100 
   edit-status-group-dialog.component.ts100100100100 
   form-statuses-group.component.html100100100100 
   form-statuses-group.component.ts100100100100 
   manage-groups-dialog.component.html100100100100 
   manage-groups-dialog.component.ts100100100100 
components/table99.610098.6499.57 
   table-actions.component.html100100100100 
   table-actions.component.ts100100100100 
   table-cell.component.html100100100100 
   table-cell.component.ts98.2710095.2398.2484
   table-column-header.component.html100100100100 
   table-column-header.component.ts100100100100 
   table-empty-data.component.ts100100100100 
   table-inspect-message-dialog.component.html100100100100 
   table-inspect-message-dialog.component.ts100100100100 
   table-inspect-message.component.html100100100100 
   table-inspect-message.component.ts100100100100 
   table-inspect-object-dialog.component.html100100100100 
   table-inspect-object-dialog.component.ts100100100100 
   table-inspect-object.component.html100100100100 
   table-inspect-object.component.ts100100100100 
   table.component.html100100100100 
   table.component.ts100100100100 
dashboard100100100100 
   index.component.html100100100100 
   index.component.ts100100100100 
dashboard/components100100100100 
   add-line-dialog.component.html100100100100 
   add-line-dialog.component.ts100100100100 
   edit-name-line-dialog.component.html100100100100 
   edit-name-line-dialog.component.ts100100100100 
   reorganize-lines-dialog.component.html100100100100 
   reorganize-lines-dialog.component.ts100100100100 
   split-lines-dialog.component.ts100100100100 
   statuses-group-card.component.html100100100100 
   statuses-group-card.component.ts100100100100 
dashboard/components/lines100100100100 
   applications-line.component.html100100100100 
   applications-line.component.ts100100100100 
   partitions-line.component.html100100100100 
   partitions-line.component.ts100100100100 
   results-line.component.html100100100100 
   results-line.component.ts100100100100 
   sessions-line.component.html100100100100 
   sessions-line.component.ts100100100100 
   task-by-status-line.component.html100100100100 
   task-by-status-line.component.ts100100100100 
   tasks-line.component.html100100100100 
   tasks-line.component.ts100100100100 
dashboard/services100100100100 
   dashboard-index.service.ts100100100100 
   dashboard-storage.service.ts100100100100 
healthcheck100100100100 
   healthcheck.component.html100100100100 
   healthcheck.component.ts100100100100 
healthcheck/services100100100100 
   healthcheck-grpc.service.ts100100100100 
partitions100100100100 
   index.component.html100100100100 
   index.component.ts100100100100 
   show.component.html100100100100 
   show.component.ts100100100100 
partitions/components100100100100 
   table.component.html100100100100 
   table.component.ts100100100100 
partitions/services100100100100 
   partitions-filters.service.ts100100100100 
   partitions-grpc.service.ts100100100100 
   partitions-index.service.ts100100100100 
   partitions-inspection.service.ts100100100100 
pipes100100100100 
   duration.pipe.ts100100100100 
   empty-cell.pipe.ts100100100100 
   pretty.pipe.ts100100100100 
profile100100100100 
   index.component.html100100100100 
   index.component.ts100100100100 
   types.ts100100100100 
results100100100100 
   index.component.html100100100100 
   index.component.ts100100100100 
   show.component.html100100100100 
   show.component.ts100100100100 
results/components100100100100 
   table.component.html100100100100 
   table.component.ts100100100100 
results/services100100100100 
   results-filters.service.ts100100100100 
   results-grpc.service.ts100100100100 
   results-index.service.ts100100100100 
   results-inspection.service.ts100100100100 
   results-statuses.service.ts100100100100 
services100100100100 
   auto-refresh.service.ts100100100100 
   cache.service.ts100100100100 
   default-config.service.ts100100100100 
   environment.service.ts100100100100 
   filters.service.ts100100100100 
   grpc-build-request.service.ts100100100100 
   grpc-sort-field.service.ts100100100100 
   icons.service.ts100100100100 
   navigation.service.ts100100100100 
   notification.service.ts100100100100 
   query-params.service.ts100100100100 
   share-url.service.ts100100100100 
   storage.service.ts100100100100 
   table-storage.service.ts100100100100 
   table-url.service.ts100100100100 
   table.service.ts100100100100 
   tasks-by-status.service.ts100100100100 
   user-grpc.service.ts100100100100 
   user.service.ts100100100100 
   utils.service.ts100100100100 
   versions-grpc.service.ts100100100100 
   versions.service.ts100100100100 
sessions100100100100 
   index.component.html100100100100 
   index.component.ts100100100100 
   show.component.html100100100100 
   show.component.ts100100100100 
sessions/components100100100100 
   table.component.html100100100100 
   table.component.ts100100100100 
sessions/services100100100100 
   sessions-filters.service.ts100100100100 
   sessions-grpc.service.ts100100100100 
   sessions-index.service.ts100100100100 
   sessions-inspection.service.ts100100100100 
   sessions-statuses.service.ts100100100100 
settings100100100100 
   index.component.html100100100100 
   index.component.ts100100100100 
settings/component100100100100 
   clear-all-dialog.component.html100100100100 
   clear-all-dialog.component.ts100100100100 
tasks100100100100 
   index.component.html100100100100 
   index.component.ts100100100100 
   show.component.html100100100100 
   show.component.ts100100100100 
tasks/components100100100100 
   manage-view-in-logs-dialog.component.html100100100100 
   manage-view-in-logs-dialog.component.ts100100100100 
   table.component.html100100100100 
   table.component.ts100100100100 
tasks/services100100100100 
   tasks-filters.service.ts100100100100 
   tasks-grpc.service.ts100100100100 
   tasks-index.service.ts100100100100 
   tasks-inspection.service.ts100100100100 
   tasks-statuses.service.ts100100100100 
tokens100100100100 
   filters.token.ts100100100100 
types100100100100 
   navigation.ts100100100100 
types/components99.7110099.13100 
   dashboard-line-table.ts100100100100 
   index.ts99.1510097.29100 
   show.ts100100100100 
   table.ts100100100100 
types/services100100100100 
   grpcService.ts100100100100 
   inspectionService.ts100100100100 

Please sign in to comment.