diff --git a/projects/components/src/table/cells/data-renderers/relative-timestamp/relative-timestamp-table-cell-renderer.component.scss b/projects/components/src/table/cells/data-renderers/relative-timestamp/relative-timestamp-table-cell-renderer.component.scss new file mode 100644 index 000000000..9bea04de3 --- /dev/null +++ b/projects/components/src/table/cells/data-renderers/relative-timestamp/relative-timestamp-table-cell-renderer.component.scss @@ -0,0 +1,9 @@ +@import 'font'; + +.relative-timestamp { + @include ellipsis-overflow(); + + &.first-column { + @include body-1-medium($gray-9); + } +} diff --git a/projects/components/src/table/cells/data-renderers/relative-timestamp/relative-timestamp-table-cell-renderer.component.test.ts b/projects/components/src/table/cells/data-renderers/relative-timestamp/relative-timestamp-table-cell-renderer.component.test.ts new file mode 100644 index 000000000..45b76f090 --- /dev/null +++ b/projects/components/src/table/cells/data-renderers/relative-timestamp/relative-timestamp-table-cell-renderer.component.test.ts @@ -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); + }); +}); diff --git a/projects/components/src/table/cells/data-renderers/relative-timestamp/relative-timestamp-table-cell-renderer.component.ts b/projects/components/src/table/cells/data-renderers/relative-timestamp/relative-timestamp-table-cell-renderer.component.ts new file mode 100644 index 000000000..8e69fbbf1 --- /dev/null +++ b/projects/components/src/table/cells/data-renderers/relative-timestamp/relative-timestamp-table-cell-renderer.component.ts @@ -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 { + baseTimestamp: Date; +} +@Component({ + selector: 'ht-relative-timestamp-table-cell-renderer', + styleUrls: ['./relative-timestamp-table-cell-renderer.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +
+ {{ this.duration }} ms +
+ ` +}) +@TableCellRenderer({ + type: CoreTableCellRendererType.RelativeTimestamp, + alignment: TableCellAlignmentType.Left, + parser: CoreTableCellParserType.NoOp +}) +export class RelativeTimestampTableCellRendererComponent extends TableCellRendererBase 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, + @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(); + } +} diff --git a/projects/components/src/table/cells/table-cells.module.ts b/projects/components/src/table/cells/table-cells.module.ts index ad7a15e77..4dfed1580 100644 --- a/projects/components/src/table/cells/table-cells.module.ts +++ b/projects/components/src/table/cells/table-cells.module.ts @@ -21,6 +21,7 @@ import { CodeTableCellRendererComponent } from './data-renderers/code/code-table import { StringEnumTableCellRendererComponent } from './data-renderers/enum/string-enum-table-cell-renderer.component'; import { IconTableCellRendererComponent } from './data-renderers/icon/icon-table-cell-renderer.component'; import { NumericTableCellRendererComponent } from './data-renderers/numeric/numeric-table-cell-renderer.component'; +import { RelativeTimestampTableCellRendererComponent } from './data-renderers/relative-timestamp/relative-timestamp-table-cell-renderer.component'; import { StringArrayTableCellRendererComponent } from './data-renderers/string-array/string-array-table-cell-renderer.component'; import { TableDataCellRendererComponent } from './data-renderers/table-data-cell-renderer.component'; 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('TABLE_CELL_PA CodeTableCellRendererComponent, StringArrayTableCellRendererComponent, StringEnumTableCellRendererComponent, - TextWithCopyActionTableCellRendererComponent + TextWithCopyActionTableCellRendererComponent, + RelativeTimestampTableCellRendererComponent ], providers: [ { @@ -88,7 +90,8 @@ export const TABLE_CELL_PARSERS = new InjectionToken('TABLE_CELL_PA CodeTableCellRendererComponent, StringArrayTableCellRendererComponent, StringEnumTableCellRendererComponent, - TextWithCopyActionTableCellRendererComponent + TextWithCopyActionTableCellRendererComponent, + RelativeTimestampTableCellRendererComponent ], multi: true }, diff --git a/projects/components/src/table/cells/types/core-table-cell-renderer-type.ts b/projects/components/src/table/cells/types/core-table-cell-renderer-type.ts index 5c5d8a6e6..2917244bc 100644 --- a/projects/components/src/table/cells/types/core-table-cell-renderer-type.ts +++ b/projects/components/src/table/cells/types/core-table-cell-renderer-type.ts @@ -4,6 +4,7 @@ export const enum CoreTableCellRendererType { Icon = 'icon', Number = 'number', RowExpander = 'row-expander', + RelativeTimestamp = 'relative-timestamp', StringArray = 'string-array', StringEnum = 'string-enum', Text = 'text', diff --git a/projects/distributed-tracing/src/shared/components/log-events/log-events-table.component.scss b/projects/distributed-tracing/src/shared/components/log-events/log-events-table.component.scss new file mode 100644 index 000000000..c44b410dc --- /dev/null +++ b/projects/distributed-tracing/src/shared/components/log-events/log-events-table.component.scss @@ -0,0 +1,11 @@ +@import 'font'; + +.log-events-table { + margin-top: 25px; +} + +.content { + @include body-2-regular(); + margin-left: 175px; + margin-top: 15px; +} diff --git a/projects/distributed-tracing/src/shared/components/log-events/log-events-table.component.test.ts b/projects/distributed-tracing/src/shared/components/log-events/log-events-table.component.test.ts new file mode 100644 index 000000000..f8a5c53ab --- /dev/null +++ b/projects/distributed-tracing/src/shared/components/log-events/log-events-table.component.test.ts @@ -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; + + 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( + ``, + { + hostProps: { + logEvents: [ + { attributes: { attr1: 1, attr2: 2 }, summary: 'test', timestamp: '2021-04-30T12:23:57.889149Z' } + ], + logEventsTableViewType: LogEventsTableViewType.Condensed, + 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(); + }); + })); +}); diff --git a/projects/distributed-tracing/src/shared/components/log-events/log-events-table.component.ts b/projects/distributed-tracing/src/shared/components/log-events/log-events-table.component.ts new file mode 100644 index 000000000..0865353b2 --- /dev/null +++ b/projects/distributed-tracing/src/shared/components/log-events/log-events-table.component.ts @@ -0,0 +1,121 @@ +import { ChangeDetectionStrategy, Component, Input, OnChanges } from '@angular/core'; +import { DateCoercer, Dictionary } from '@hypertrace/common'; +import { + CoreTableCellRendererType, + ListViewHeader, + ListViewRecord, + TableColumnConfig, + TableDataResponse, + TableDataSource, + TableMode, + TableRow +} from '@hypertrace/components'; +import { LogEvent } from '@hypertrace/distributed-tracing'; +import { isEmpty } from 'lodash-es'; +import { Observable, of } from 'rxjs'; + +export const enum LogEventsTableViewType { + Condensed = 'condensed', + Detailed = 'detailed' +} + +@Component({ + selector: 'ht-log-events-table', + styleUrls: ['./log-events-table.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` +
+ +
+ +
+ +
+
+ ` +}) +export class LogEventsTableComponent implements OnChanges { + @Input() + public logEvents: LogEvent[] = []; + + @Input() + public logEventsTableViewType: LogEventsTableViewType = LogEventsTableViewType.Condensed; + + @Input() + public spanStartTime?: number; + + public readonly header: ListViewHeader = { keyLabel: 'key', valueLabel: 'value' }; + private readonly dateCoercer: DateCoercer = new DateCoercer(); + + public dataSource?: TableDataSource; + public columnConfigs: TableColumnConfig[] = []; + + public ngOnChanges(): void { + this.buildDataSource(); + this.columnConfigs = this.getTableColumnConfigs(); + } + + public getLogEventAttributeRecords(attributes: Dictionary): ListViewRecord[] { + if (!isEmpty(attributes)) { + return Object.entries(attributes).map(([key, value]) => ({ + key: key, + value: value as string | number + })); + } + + return []; + } + + private buildDataSource(): void { + this.dataSource = { + getData: (): Observable> => + of({ + data: this.logEvents.map((logEvent: LogEvent) => ({ + ...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.Condensed) { + 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 []; + } +} diff --git a/projects/distributed-tracing/src/shared/components/log-events/log-events-table.module.ts b/projects/distributed-tracing/src/shared/components/log-events/log-events-table.module.ts new file mode 100644 index 000000000..59327818f --- /dev/null +++ b/projects/distributed-tracing/src/shared/components/log-events/log-events-table.module.ts @@ -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 {} diff --git a/projects/distributed-tracing/src/shared/components/span-detail/span-detail.component.ts b/projects/distributed-tracing/src/shared/components/span-detail/span-detail.component.ts index 8c518c2fc..9f54a2901 100644 --- a/projects/distributed-tracing/src/shared/components/span-detail/span-detail.component.ts +++ b/projects/distributed-tracing/src/shared/components/span-detail/span-detail.component.ts @@ -46,6 +46,12 @@ import { SpanDetailLayoutStyle } from './span-detail-layout-style'; + + + ` @@ -66,12 +72,16 @@ export class SpanDetailComponent implements OnChanges { public showRequestTab?: boolean; public showResponseTab?: boolean; public showExitCallsTab?: boolean; + public showLogEventstab?: boolean; + public totalLogEvents?: number; public ngOnChanges(changes: TypedSimpleChanges): void { if (changes.spanData) { this.showRequestTab = !isEmpty(this.spanData?.requestHeaders) || !isEmpty(this.spanData?.requestBody); this.showResponseTab = !isEmpty(this.spanData?.responseHeaders) || !isEmpty(this.spanData?.responseBody); this.showExitCallsTab = !isEmpty(this.spanData?.exitCallsBreakup); + this.showLogEventstab = !isEmpty(this.spanData?.logEvents); + this.totalLogEvents = (this.spanData?.logEvents ?? []).length; } } } diff --git a/projects/distributed-tracing/src/shared/components/span-detail/span-detail.module.ts b/projects/distributed-tracing/src/shared/components/span-detail/span-detail.module.ts index 2bd41a72b..4312d4b1c 100644 --- a/projects/distributed-tracing/src/shared/components/span-detail/span-detail.module.ts +++ b/projects/distributed-tracing/src/shared/components/span-detail/span-detail.module.ts @@ -11,6 +11,7 @@ import { ToggleButtonModule, TooltipModule } from '@hypertrace/components'; +import { LogEventsTableModule } from '../log-events/log-events-table.module'; import { SpanExitCallsModule } from './exit-calls/span-exit-calls.module'; import { SpanDetailTitleHeaderModule } from './headers/title/span-detail-title-header.module'; import { SpanRequestDetailModule } from './request/span-request-detail.module'; @@ -34,7 +35,8 @@ import { SpanTagsDetailModule } from './tags/span-tags-detail.module'; LoadAsyncModule, ListViewModule, SpanDetailTitleHeaderModule, - SpanExitCallsModule + SpanExitCallsModule, + LogEventsTableModule ], declarations: [SpanDetailComponent], exports: [SpanDetailComponent]