-
Notifications
You must be signed in to change notification settings - Fork 11
feat: relative timestamp table cell renderer and log events table in sheet #818
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 35 commits
202b00c
2e7f58b
3cb722e
8f0b5c8
b03c83d
533c904
a1d101d
8033540
cd1214b
e51b716
e2aef98
552f7bc
2e642b8
2e90a3a
171dc8a
36e8d44
8886ae2
11bee55
77d168d
fe85d1c
129b6a4
8466606
10f3d37
929f90b
b45756d
01b9bbc
e25a66c
2874cca
88f233a
411ab62
5a4126a
08ba01b
c5b8a42
29f2bdd
bf4b7ae
dd1b677
e0b2ef4
2b87274
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| @import 'font'; | ||
|
|
||
| .relative-timestamp { | ||
| @include ellipsis-overflow(); | ||
|
|
||
| &.first-column { | ||
| @include body-1-medium($gray-9); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| import { DisplayDatePipe } from '@hypertrace/common'; | ||
| import { | ||
| TableCellNoOpParser, | ||
| tableCellProviders, | ||
| tableCellRowDataProvider, | ||
| TooltipDirective | ||
| } from '@hypertrace/components'; | ||
| import { createComponentFactory } from '@ngneat/spectator/jest'; | ||
| import { MockComponent, MockPipe } from 'ng-mocks'; | ||
| import { tableCellDataProvider } from '../../test/cell-providers'; | ||
| import { | ||
| RelativeTimestampTableCellRendererComponent, | ||
| RowData | ||
| } from './relative-timestamp-table-cell-renderer.component'; | ||
|
|
||
| describe('relative timestamp table cell renderer component', () => { | ||
| const buildComponent = createComponentFactory({ | ||
| component: RelativeTimestampTableCellRendererComponent, | ||
| providers: [ | ||
| tableCellProviders( | ||
| { | ||
| id: 'test' | ||
| }, | ||
| new TableCellNoOpParser(undefined!) | ||
| ) | ||
| ], | ||
| declarations: [MockComponent(TooltipDirective), MockPipe(DisplayDatePipe)], | ||
| shallow: true | ||
| }); | ||
|
|
||
| test('testing component properties', () => { | ||
| const logEvent: RowData = { | ||
| baseTimestamp: new Date(1619785437887) | ||
| }; | ||
| const spectator = buildComponent({ | ||
| providers: [tableCellRowDataProvider(logEvent), tableCellDataProvider(new Date(1619785437887))] | ||
| }); | ||
|
|
||
| expect(spectator.queryAll('.relative-timestamp')[0]).toContainText('0 ms'); | ||
| expect(spectator.component.duration).toBe(0); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| import { ChangeDetectionStrategy, Component, Inject, OnInit } from '@angular/core'; | ||
| import { DateFormatMode, DateFormatOptions, Dictionary } from '@hypertrace/common'; | ||
| import { TableColumnConfig } from '../../../table-api'; | ||
| import { | ||
| TABLE_CELL_DATA, | ||
| TABLE_COLUMN_CONFIG, | ||
| TABLE_COLUMN_INDEX, | ||
| TABLE_DATA_PARSER, | ||
| TABLE_ROW_DATA | ||
| } from '../../table-cell-injection'; | ||
| import { TableCellParserBase } from '../../table-cell-parser-base'; | ||
| import { TableCellRenderer } from '../../table-cell-renderer'; | ||
| import { TableCellRendererBase } from '../../table-cell-renderer-base'; | ||
| import { CoreTableCellParserType } from '../../types/core-table-cell-parser-type'; | ||
| import { CoreTableCellRendererType } from '../../types/core-table-cell-renderer-type'; | ||
| import { TableCellAlignmentType } from '../../types/table-cell-alignment-type'; | ||
|
|
||
| export interface RowData extends Dictionary<unknown> { | ||
| baseTimestamp: Date; | ||
| } | ||
| @Component({ | ||
| selector: 'ht-relative-timestamp-table-cell-renderer', | ||
| styleUrls: ['./relative-timestamp-table-cell-renderer.component.scss'], | ||
| changeDetection: ChangeDetectionStrategy.OnPush, | ||
| template: ` | ||
| <div | ||
| class="relative-timestamp" | ||
| [htTooltip]="this.value | htDisplayDate: this.dateFormat" | ||
| [ngClass]="{ 'first-column': this.isFirstColumn }" | ||
| > | ||
| {{ this.duration }} ms | ||
| </div> | ||
| ` | ||
| }) | ||
| @TableCellRenderer({ | ||
| type: CoreTableCellRendererType.RelativeTimestamp, | ||
| alignment: TableCellAlignmentType.Left, | ||
| parser: CoreTableCellParserType.NoOp | ||
| }) | ||
| export class RelativeTimestampTableCellRendererComponent extends TableCellRendererBase<Date> implements OnInit { | ||
| public readonly dateFormat: DateFormatOptions = { | ||
| mode: DateFormatMode.DateAndTimeWithSeconds | ||
| }; | ||
| public readonly duration: number; | ||
|
|
||
| public constructor( | ||
| @Inject(TABLE_COLUMN_CONFIG) columnConfig: TableColumnConfig, | ||
| @Inject(TABLE_COLUMN_INDEX) index: number, | ||
| @Inject(TABLE_DATA_PARSER) | ||
| parser: TableCellParserBase<Date, Date, unknown>, | ||
| @Inject(TABLE_CELL_DATA) cellData: Date, | ||
| @Inject(TABLE_ROW_DATA) rowData: RowData | ||
| ) { | ||
| super(columnConfig, index, parser, cellData, rowData); | ||
| this.duration = cellData.getTime() - rowData.baseTimestamp.getTime(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| @import 'font'; | ||
|
|
||
| .log-events-table { | ||
| margin-top: 25px; | ||
| } | ||
|
|
||
| .content { | ||
| @include body-2-regular(); | ||
| margin-left: 175px; | ||
| margin-top: 15px; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| import { HttpClientTestingModule } from '@angular/common/http/testing'; | ||
| import { fakeAsync, flush } from '@angular/core/testing'; | ||
| import { ActivatedRoute } from '@angular/router'; | ||
| import { IconLibraryTestingModule } from '@hypertrace/assets-library'; | ||
| import { NavigationService } from '@hypertrace/common'; | ||
| import { TableComponent, TableModule } from '@hypertrace/components'; | ||
| import { runFakeRxjs } from '@hypertrace/test-utils'; | ||
| import { createHostFactory, mockProvider, Spectator } from '@ngneat/spectator/jest'; | ||
| import { EMPTY } from 'rxjs'; | ||
| import { LogEventsTableComponent, LogEventsTableViewType } from './log-events-table.component'; | ||
| import { LogEventsTableModule } from './log-events-table.module'; | ||
|
|
||
| describe('LogEventsTableComponent', () => { | ||
| let spectator: Spectator<LogEventsTableComponent>; | ||
|
|
||
| const createHost = createHostFactory({ | ||
| component: LogEventsTableComponent, | ||
| imports: [LogEventsTableModule, TableModule, HttpClientTestingModule, IconLibraryTestingModule], | ||
| declareComponent: false, | ||
| providers: [ | ||
| mockProvider(ActivatedRoute, { | ||
| queryParamMap: EMPTY | ||
| }), | ||
| mockProvider(NavigationService, { | ||
| navigation$: EMPTY | ||
| }) | ||
| ] | ||
| }); | ||
|
|
||
| test('should render data correctly for sheet view', fakeAsync(() => { | ||
| spectator = createHost( | ||
| `<ht-log-events-table [logEvents]="logEvents" [logEventsTableViewType]="logEventsTableViewType" [spanStartTime]="spanStartTime"></ht-log-events-table>`, | ||
| { | ||
| hostProps: { | ||
| logEvents: [ | ||
| { attributes: { attr1: 1, attr2: 2 }, summary: 'test', timestamp: '2021-04-30T12:23:57.889149Z' } | ||
| ], | ||
| logEventsTableViewType: LogEventsTableViewType.Sheet, | ||
| spanStartTime: 1619785437887 | ||
| } | ||
| } | ||
| ); | ||
|
|
||
| expect(spectator.query('.log-events-table')).toExist(); | ||
| expect(spectator.query(TableComponent)).toExist(); | ||
| expect(spectator.query(TableComponent)!.resizable).toBe(false); | ||
| expect(spectator.query(TableComponent)!.columnConfigs).toMatchObject([ | ||
| expect.objectContaining({ | ||
| id: 'timestamp' | ||
| }), | ||
| expect.objectContaining({ | ||
| id: 'summary' | ||
| }) | ||
| ]); | ||
| expect(spectator.query(TableComponent)!.pageable).toBe(false); | ||
| expect(spectator.query(TableComponent)!.detailContent).not.toBeNull(); | ||
|
|
||
| runFakeRxjs(({ expectObservable }) => { | ||
| expect(spectator.component.dataSource).toBeDefined(); | ||
| expectObservable(spectator.component.dataSource!.getData(undefined!)).toBe('(x|)', { | ||
| x: { | ||
| data: [expect.objectContaining({ summary: 'test' })], | ||
| totalCount: 1 | ||
| } | ||
| }); | ||
|
|
||
| flush(); | ||
| }); | ||
| })); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| import { ChangeDetectionStrategy, Component, Input, OnInit } from '@angular/core'; | ||
| import { DateCoercer, Dictionary } from '@hypertrace/common'; | ||
| import { | ||
| CoreTableCellRendererType, | ||
| ListViewHeader, | ||
| ListViewRecord, | ||
| TableColumnConfig, | ||
| TableDataResponse, | ||
| TableDataSource, | ||
| TableMode, | ||
| TableRow | ||
| } from '@hypertrace/components'; | ||
| import { isEmpty } from 'lodash-es'; | ||
| import { Observable, of } from 'rxjs'; | ||
|
|
||
| export const enum LogEventsTableViewType { | ||
| Sheet = 'sheet', | ||
| Page = 'page' | ||
| } | ||
|
|
||
| @Component({ | ||
| selector: 'ht-log-events-table', | ||
| styleUrls: ['./log-events-table.component.scss'], | ||
| changeDetection: ChangeDetectionStrategy.OnPush, | ||
| template: ` | ||
| <div class="log-events-table"> | ||
| <ht-table | ||
| [columnConfigs]="this.columnConfigs" | ||
| [data]="this.dataSource" | ||
| [pageable]="false" | ||
| [resizable]="false" | ||
| mode=${TableMode.Detail} | ||
| [detailContent]="detailContent" | ||
| ></ht-table> | ||
| </div> | ||
| <ng-template #detailContent let-row="row"> | ||
| <div class="content"> | ||
| <ht-list-view | ||
| [records]="this.getLogEventAttributeRecords(row.attributes)" | ||
| [header]="this.header" | ||
| data-sensitive-pii | ||
| ></ht-list-view> | ||
| </div> | ||
| </ng-template> | ||
| ` | ||
| }) | ||
| export class LogEventsTableComponent implements OnInit { | ||
| @Input() | ||
| public logEvents: Dictionary<unknown>[] = []; | ||
|
|
||
| @Input() | ||
| public logEventsTableViewType: LogEventsTableViewType = LogEventsTableViewType.Sheet; | ||
|
|
||
| @Input() | ||
| public spanStartTime?: number; | ||
|
|
||
| public readonly header: ListViewHeader = { keyLabel: 'key', valueLabel: 'value' }; | ||
| private readonly dateCoercer: DateCoercer = new DateCoercer(); | ||
|
|
||
| public dataSource?: TableDataSource<TableRow>; | ||
| public columnConfigs: TableColumnConfig[] = []; | ||
|
|
||
| public ngOnInit(): void { | ||
| this.buildDataSource(); | ||
anandtiwary marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| this.columnConfigs = this.getTableColumnConfigs(); | ||
| } | ||
|
|
||
| public getLogEventAttributeRecords(attributes: Dictionary<unknown>): ListViewRecord[] { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: missing tests :) |
||
| if (!isEmpty(attributes)) { | ||
| return Object.entries(attributes).map((attribute: [string, unknown]) => ({ | ||
|
||
| key: attribute[0], | ||
| value: attribute[1] as string | number | ||
|
||
| })); | ||
| } | ||
|
|
||
| return []; | ||
| } | ||
|
|
||
| private buildDataSource(): void { | ||
| this.dataSource = { | ||
| getData: (): Observable<TableDataResponse<TableRow>> => | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit: we can define the type of TableRow returned from this method for clarity. |
||
| of({ | ||
| data: this.logEvents.map((logEvent: Dictionary<unknown>) => ({ | ||
| ...logEvent, | ||
| timestamp: this.dateCoercer.coerce(logEvent.timestamp), | ||
| baseTimestamp: this.dateCoercer.coerce(this.spanStartTime) | ||
| })), | ||
| totalCount: this.logEvents.length | ||
| }), | ||
| getScope: () => undefined | ||
| }; | ||
| } | ||
|
|
||
| private getTableColumnConfigs(): TableColumnConfig[] { | ||
| if (this.logEventsTableViewType === LogEventsTableViewType.Sheet) { | ||
| return [ | ||
| { | ||
| id: 'timestamp', | ||
| name: 'timestamp', | ||
| title: 'Timestamp', | ||
| display: CoreTableCellRendererType.RelativeTimestamp, | ||
| visible: true, | ||
| width: '150px', | ||
| sortable: false, | ||
| filterable: false | ||
| }, | ||
| { | ||
| id: 'summary', | ||
| name: 'summary', | ||
| title: 'Summary', | ||
| visible: true, | ||
| sortable: false, | ||
| filterable: false | ||
| } | ||
| ]; | ||
| } | ||
|
|
||
| return []; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| import { CommonModule } from '@angular/common'; | ||
| import { NgModule } from '@angular/core'; | ||
| import { FormattingModule } from '@hypertrace/common'; | ||
| import { IconModule, ListViewModule, TableModule, TooltipModule } from '@hypertrace/components'; | ||
| import { LogEventsTableComponent } from './log-events-table.component'; | ||
| @NgModule({ | ||
| imports: [CommonModule, TableModule, IconModule, TooltipModule, FormattingModule, ListViewModule], | ||
| declarations: [LogEventsTableComponent], | ||
| exports: [LogEventsTableComponent] | ||
| }) | ||
| export class LogEventsTableModule {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do need better name for this. Umm. How about
Condensedfor the sheet andDetailedfor the page? Any other suggestions anyone?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, these names look good to me, Changed!!