Skip to content

Commit 6a13521

Browse files
feat: relative timestamp table cell renderer and log events table in sheet (#818)
* feat: relative timestamp table cell renderer and log events table in sheet
1 parent afa5bd7 commit 6a13521

11 files changed

+340
-3
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
@import 'font';
2+
3+
.relative-timestamp {
4+
@include ellipsis-overflow();
5+
6+
&.first-column {
7+
@include body-1-medium($gray-9);
8+
}
9+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { DisplayDatePipe } from '@hypertrace/common';
2+
import {
3+
TableCellNoOpParser,
4+
tableCellProviders,
5+
tableCellRowDataProvider,
6+
TooltipDirective
7+
} from '@hypertrace/components';
8+
import { createComponentFactory } from '@ngneat/spectator/jest';
9+
import { MockComponent, MockPipe } from 'ng-mocks';
10+
import { tableCellDataProvider } from '../../test/cell-providers';
11+
import {
12+
RelativeTimestampTableCellRendererComponent,
13+
RowData
14+
} from './relative-timestamp-table-cell-renderer.component';
15+
16+
describe('relative timestamp table cell renderer component', () => {
17+
const buildComponent = createComponentFactory({
18+
component: RelativeTimestampTableCellRendererComponent,
19+
providers: [
20+
tableCellProviders(
21+
{
22+
id: 'test'
23+
},
24+
new TableCellNoOpParser(undefined!)
25+
)
26+
],
27+
declarations: [MockComponent(TooltipDirective), MockPipe(DisplayDatePipe)],
28+
shallow: true
29+
});
30+
31+
test('testing component properties', () => {
32+
const logEvent: RowData = {
33+
baseTimestamp: new Date(1619785437887)
34+
};
35+
const spectator = buildComponent({
36+
providers: [tableCellRowDataProvider(logEvent), tableCellDataProvider(new Date(1619785437887))]
37+
});
38+
39+
expect(spectator.queryAll('.relative-timestamp')[0]).toContainText('0 ms');
40+
expect(spectator.component.duration).toBe(0);
41+
});
42+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { ChangeDetectionStrategy, Component, Inject, OnInit } from '@angular/core';
2+
import { DateFormatMode, DateFormatOptions, Dictionary } from '@hypertrace/common';
3+
import { TableColumnConfig } from '../../../table-api';
4+
import {
5+
TABLE_CELL_DATA,
6+
TABLE_COLUMN_CONFIG,
7+
TABLE_COLUMN_INDEX,
8+
TABLE_DATA_PARSER,
9+
TABLE_ROW_DATA
10+
} from '../../table-cell-injection';
11+
import { TableCellParserBase } from '../../table-cell-parser-base';
12+
import { TableCellRenderer } from '../../table-cell-renderer';
13+
import { TableCellRendererBase } from '../../table-cell-renderer-base';
14+
import { CoreTableCellParserType } from '../../types/core-table-cell-parser-type';
15+
import { CoreTableCellRendererType } from '../../types/core-table-cell-renderer-type';
16+
import { TableCellAlignmentType } from '../../types/table-cell-alignment-type';
17+
18+
export interface RowData extends Dictionary<unknown> {
19+
baseTimestamp: Date;
20+
}
21+
@Component({
22+
selector: 'ht-relative-timestamp-table-cell-renderer',
23+
styleUrls: ['./relative-timestamp-table-cell-renderer.component.scss'],
24+
changeDetection: ChangeDetectionStrategy.OnPush,
25+
template: `
26+
<div
27+
class="relative-timestamp"
28+
[htTooltip]="this.value | htDisplayDate: this.dateFormat"
29+
[ngClass]="{ 'first-column': this.isFirstColumn }"
30+
>
31+
{{ this.duration }} ms
32+
</div>
33+
`
34+
})
35+
@TableCellRenderer({
36+
type: CoreTableCellRendererType.RelativeTimestamp,
37+
alignment: TableCellAlignmentType.Left,
38+
parser: CoreTableCellParserType.NoOp
39+
})
40+
export class RelativeTimestampTableCellRendererComponent extends TableCellRendererBase<Date> implements OnInit {
41+
public readonly dateFormat: DateFormatOptions = {
42+
mode: DateFormatMode.DateAndTimeWithSeconds
43+
};
44+
public readonly duration: number;
45+
46+
public constructor(
47+
@Inject(TABLE_COLUMN_CONFIG) columnConfig: TableColumnConfig,
48+
@Inject(TABLE_COLUMN_INDEX) index: number,
49+
@Inject(TABLE_DATA_PARSER)
50+
parser: TableCellParserBase<Date, Date, unknown>,
51+
@Inject(TABLE_CELL_DATA) cellData: Date,
52+
@Inject(TABLE_ROW_DATA) rowData: RowData
53+
) {
54+
super(columnConfig, index, parser, cellData, rowData);
55+
this.duration = cellData?.getTime() - rowData?.baseTimestamp?.getTime();
56+
}
57+
}

projects/components/src/table/cells/table-cells.module.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { CodeTableCellRendererComponent } from './data-renderers/code/code-table
2121
import { StringEnumTableCellRendererComponent } from './data-renderers/enum/string-enum-table-cell-renderer.component';
2222
import { IconTableCellRendererComponent } from './data-renderers/icon/icon-table-cell-renderer.component';
2323
import { NumericTableCellRendererComponent } from './data-renderers/numeric/numeric-table-cell-renderer.component';
24+
import { RelativeTimestampTableCellRendererComponent } from './data-renderers/relative-timestamp/relative-timestamp-table-cell-renderer.component';
2425
import { StringArrayTableCellRendererComponent } from './data-renderers/string-array/string-array-table-cell-renderer.component';
2526
import { TableDataCellRendererComponent } from './data-renderers/table-data-cell-renderer.component';
2627
import { TextWithCopyActionTableCellRendererComponent } from './data-renderers/text-with-copy/text-with-copy-table-cell-renderer.component';
@@ -72,7 +73,8 @@ export const TABLE_CELL_PARSERS = new InjectionToken<unknown[][]>('TABLE_CELL_PA
7273
CodeTableCellRendererComponent,
7374
StringArrayTableCellRendererComponent,
7475
StringEnumTableCellRendererComponent,
75-
TextWithCopyActionTableCellRendererComponent
76+
TextWithCopyActionTableCellRendererComponent,
77+
RelativeTimestampTableCellRendererComponent
7678
],
7779
providers: [
7880
{
@@ -88,7 +90,8 @@ export const TABLE_CELL_PARSERS = new InjectionToken<unknown[][]>('TABLE_CELL_PA
8890
CodeTableCellRendererComponent,
8991
StringArrayTableCellRendererComponent,
9092
StringEnumTableCellRendererComponent,
91-
TextWithCopyActionTableCellRendererComponent
93+
TextWithCopyActionTableCellRendererComponent,
94+
RelativeTimestampTableCellRendererComponent
9295
],
9396
multi: true
9497
},

projects/components/src/table/cells/types/core-table-cell-renderer-type.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export const enum CoreTableCellRendererType {
44
Icon = 'icon',
55
Number = 'number',
66
RowExpander = 'row-expander',
7+
RelativeTimestamp = 'relative-timestamp',
78
StringArray = 'string-array',
89
StringEnum = 'string-enum',
910
Text = 'text',
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
@import 'font';
2+
3+
.log-events-table {
4+
margin-top: 25px;
5+
}
6+
7+
.content {
8+
@include body-2-regular();
9+
margin-left: 175px;
10+
margin-top: 15px;
11+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { HttpClientTestingModule } from '@angular/common/http/testing';
2+
import { fakeAsync, flush } from '@angular/core/testing';
3+
import { ActivatedRoute } from '@angular/router';
4+
import { IconLibraryTestingModule } from '@hypertrace/assets-library';
5+
import { NavigationService } from '@hypertrace/common';
6+
import { TableComponent, TableModule } from '@hypertrace/components';
7+
import { runFakeRxjs } from '@hypertrace/test-utils';
8+
import { createHostFactory, mockProvider, Spectator } from '@ngneat/spectator/jest';
9+
import { EMPTY } from 'rxjs';
10+
import { LogEventsTableComponent, LogEventsTableViewType } from './log-events-table.component';
11+
import { LogEventsTableModule } from './log-events-table.module';
12+
13+
describe('LogEventsTableComponent', () => {
14+
let spectator: Spectator<LogEventsTableComponent>;
15+
16+
const createHost = createHostFactory({
17+
component: LogEventsTableComponent,
18+
imports: [LogEventsTableModule, TableModule, HttpClientTestingModule, IconLibraryTestingModule],
19+
declareComponent: false,
20+
providers: [
21+
mockProvider(ActivatedRoute, {
22+
queryParamMap: EMPTY
23+
}),
24+
mockProvider(NavigationService, {
25+
navigation$: EMPTY
26+
})
27+
]
28+
});
29+
30+
test('should render data correctly for sheet view', fakeAsync(() => {
31+
spectator = createHost(
32+
`<ht-log-events-table [logEvents]="logEvents" [logEventsTableViewType]="logEventsTableViewType" [spanStartTime]="spanStartTime"></ht-log-events-table>`,
33+
{
34+
hostProps: {
35+
logEvents: [
36+
{ attributes: { attr1: 1, attr2: 2 }, summary: 'test', timestamp: '2021-04-30T12:23:57.889149Z' }
37+
],
38+
logEventsTableViewType: LogEventsTableViewType.Condensed,
39+
spanStartTime: 1619785437887
40+
}
41+
}
42+
);
43+
44+
expect(spectator.query('.log-events-table')).toExist();
45+
expect(spectator.query(TableComponent)).toExist();
46+
expect(spectator.query(TableComponent)!.resizable).toBe(false);
47+
expect(spectator.query(TableComponent)!.columnConfigs).toMatchObject([
48+
expect.objectContaining({
49+
id: 'timestamp'
50+
}),
51+
expect.objectContaining({
52+
id: 'summary'
53+
})
54+
]);
55+
expect(spectator.query(TableComponent)!.pageable).toBe(false);
56+
expect(spectator.query(TableComponent)!.detailContent).not.toBeNull();
57+
58+
runFakeRxjs(({ expectObservable }) => {
59+
expect(spectator.component.dataSource).toBeDefined();
60+
expectObservable(spectator.component.dataSource!.getData(undefined!)).toBe('(x|)', {
61+
x: {
62+
data: [expect.objectContaining({ summary: 'test' })],
63+
totalCount: 1
64+
}
65+
});
66+
67+
flush();
68+
});
69+
}));
70+
});
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core';
2+
import { DateCoercer, Dictionary } from '@hypertrace/common';
3+
import {
4+
CoreTableCellRendererType,
5+
ListViewHeader,
6+
ListViewRecord,
7+
TableColumnConfig,
8+
TableDataResponse,
9+
TableDataSource,
10+
TableMode,
11+
TableRow
12+
} from '@hypertrace/components';
13+
import { LogEvent } from '@hypertrace/distributed-tracing';
14+
import { isEmpty } from 'lodash-es';
15+
import { Observable, of } from 'rxjs';
16+
17+
export const enum LogEventsTableViewType {
18+
Condensed = 'condensed',
19+
Detailed = 'detailed'
20+
}
21+
22+
@Component({
23+
selector: 'ht-log-events-table',
24+
styleUrls: ['./log-events-table.component.scss'],
25+
changeDetection: ChangeDetectionStrategy.OnPush,
26+
template: `
27+
<div class="log-events-table">
28+
<ht-table
29+
[columnConfigs]="this.columnConfigs"
30+
[data]="this.dataSource"
31+
[pageable]="false"
32+
[resizable]="false"
33+
mode=${TableMode.Detail}
34+
[detailContent]="detailContent"
35+
></ht-table>
36+
</div>
37+
<ng-template #detailContent let-row="row">
38+
<div class="content">
39+
<ht-list-view
40+
[records]="this.getLogEventAttributeRecords(row.attributes)"
41+
[header]="this.header"
42+
data-sensitive-pii
43+
></ht-list-view>
44+
</div>
45+
</ng-template>
46+
`
47+
})
48+
export class LogEventsTableComponent implements OnChanges {
49+
@Input()
50+
public logEvents: LogEvent[] = [];
51+
52+
@Input()
53+
public logEventsTableViewType: LogEventsTableViewType = LogEventsTableViewType.Condensed;
54+
55+
@Input()
56+
public spanStartTime?: number;
57+
58+
public readonly header: ListViewHeader = { keyLabel: 'key', valueLabel: 'value' };
59+
private readonly dateCoercer: DateCoercer = new DateCoercer();
60+
61+
public dataSource?: TableDataSource<TableRow>;
62+
public columnConfigs: TableColumnConfig[] = [];
63+
64+
public ngOnChanges(): void {
65+
this.buildDataSource();
66+
this.columnConfigs = this.getTableColumnConfigs();
67+
}
68+
69+
public getLogEventAttributeRecords(attributes: Dictionary<unknown>): ListViewRecord[] {
70+
if (!isEmpty(attributes)) {
71+
return Object.entries(attributes).map(([key, value]) => ({
72+
key: key,
73+
value: value as string | number
74+
}));
75+
}
76+
77+
return [];
78+
}
79+
80+
private buildDataSource(): void {
81+
this.dataSource = {
82+
getData: (): Observable<TableDataResponse<TableRow>> =>
83+
of({
84+
data: this.logEvents.map((logEvent: LogEvent) => ({
85+
...logEvent,
86+
timestamp: this.dateCoercer.coerce(logEvent.timestamp),
87+
baseTimestamp: this.dateCoercer.coerce(this.spanStartTime)
88+
})),
89+
totalCount: this.logEvents.length
90+
}),
91+
getScope: () => undefined
92+
};
93+
}
94+
95+
private getTableColumnConfigs(): TableColumnConfig[] {
96+
if (this.logEventsTableViewType === LogEventsTableViewType.Condensed) {
97+
return [
98+
{
99+
id: 'timestamp',
100+
name: 'timestamp',
101+
title: 'Timestamp',
102+
display: CoreTableCellRendererType.RelativeTimestamp,
103+
visible: true,
104+
width: '150px',
105+
sortable: false,
106+
filterable: false
107+
},
108+
{
109+
id: 'summary',
110+
name: 'summary',
111+
title: 'Summary',
112+
visible: true,
113+
sortable: false,
114+
filterable: false
115+
}
116+
];
117+
}
118+
119+
return [];
120+
}
121+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { CommonModule } from '@angular/common';
2+
import { NgModule } from '@angular/core';
3+
import { FormattingModule } from '@hypertrace/common';
4+
import { IconModule, ListViewModule, TableModule, TooltipModule } from '@hypertrace/components';
5+
import { LogEventsTableComponent } from './log-events-table.component';
6+
@NgModule({
7+
imports: [CommonModule, TableModule, IconModule, TooltipModule, FormattingModule, ListViewModule],
8+
declarations: [LogEventsTableComponent],
9+
exports: [LogEventsTableComponent]
10+
})
11+
export class LogEventsTableModule {}

projects/distributed-tracing/src/shared/components/span-detail/span-detail.component.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ import { SpanDetailLayoutStyle } from './span-detail-layout-style';
4646
<ht-tab label="Exit Calls" *ngIf="this.showExitCallsTab">
4747
<ht-span-exit-calls [exitCalls]="this.spanData.exitCallsBreakup"></ht-span-exit-calls>
4848
</ht-tab>
49+
<ht-tab *ngIf="this.showLogEventstab" label="Logs" [badge]="this.totalLogEvents">
50+
<ht-log-events-table
51+
[logEvents]="this.spanData?.logEvents"
52+
[spanStartTime]="this.spanData?.startTime"
53+
></ht-log-events-table>
54+
</ht-tab>
4955
</ht-tab-group>
5056
</div>
5157
`
@@ -66,12 +72,16 @@ export class SpanDetailComponent implements OnChanges {
6672
public showRequestTab?: boolean;
6773
public showResponseTab?: boolean;
6874
public showExitCallsTab?: boolean;
75+
public showLogEventstab?: boolean;
76+
public totalLogEvents?: number;
6977

7078
public ngOnChanges(changes: TypedSimpleChanges<this>): void {
7179
if (changes.spanData) {
7280
this.showRequestTab = !isEmpty(this.spanData?.requestHeaders) || !isEmpty(this.spanData?.requestBody);
7381
this.showResponseTab = !isEmpty(this.spanData?.responseHeaders) || !isEmpty(this.spanData?.responseBody);
7482
this.showExitCallsTab = !isEmpty(this.spanData?.exitCallsBreakup);
83+
this.showLogEventstab = !isEmpty(this.spanData?.logEvents);
84+
this.totalLogEvents = (this.spanData?.logEvents ?? []).length;
7585
}
7686
}
7787
}

0 commit comments

Comments
 (0)