From bf48b2458fbff8dd298642be385d7884ea5629ab Mon Sep 17 00:00:00 2001 From: Jake Bassett Date: Wed, 13 Jan 2021 12:51:56 -0800 Subject: [PATCH 1/6] feat: top level apis navigation --- projects/common/src/utilities/types/types.ts | 1 + .../src/select/select.component.scss | 1 + .../components/src/select/select.component.ts | 2 +- .../controls/table-controls.component.ts | 31 ++ .../table/controls/table-controls.module.ts | 3 +- .../widgets/table/table-widget-base.model.ts | 19 ++ .../table/table-widget-renderer.component.ts | 78 ++++- .../table/table-widget-select-filter.model.ts | 42 +++ .../widgets/table/table-widget.module.ts | 2 + .../apis/services/services-list.dashboard.ts | 307 +++++++----------- .../entities-attribute-data-source-model.ts | 38 +++ .../entities-values-data-source.model.ts | 32 ++ ...bservability-graphql-data-source.module.ts | 4 + .../secondary-entity-specification.model.ts | 46 +++ .../secondary-entity-specification-builder.ts | 54 +++ .../shared/navigation/navigation.component.ts | 4 +- 16 files changed, 470 insertions(+), 194 deletions(-) create mode 100644 projects/distributed-tracing/src/shared/dashboard/widgets/table/table-widget-select-filter.model.ts create mode 100644 projects/observability/src/shared/dashboard/data/graphql/entity/attribute/entities-attribute-data-source-model.ts create mode 100644 projects/observability/src/shared/dashboard/data/graphql/entity/entities-values-data-source.model.ts create mode 100644 projects/observability/src/shared/dashboard/data/graphql/specifiers/secondary-entity-specification.model.ts create mode 100644 projects/observability/src/shared/graphql/request/builders/specification/entity/secondary-entity-specification-builder.ts diff --git a/projects/common/src/utilities/types/types.ts b/projects/common/src/utilities/types/types.ts index 7cb9ad8a7..74a8cb7b6 100644 --- a/projects/common/src/utilities/types/types.ts +++ b/projects/common/src/utilities/types/types.ts @@ -31,3 +31,4 @@ export interface Json { } export type JsonValue = Json | Json[] | string | number | boolean | null; +export type PrimitiveValue = string | number | boolean; diff --git a/projects/components/src/select/select.component.scss b/projects/components/src/select/select.component.scss index a665cdb3d..baa92f2ea 100644 --- a/projects/components/src/select/select.component.scss +++ b/projects/components/src/select/select.component.scss @@ -78,6 +78,7 @@ .trigger-icon { padding: 0 8px; margin-left: auto; + color: $gray-7; } } } diff --git a/projects/components/src/select/select.component.ts b/projects/components/src/select/select.component.ts index 6a470fd9a..e75e28470 100644 --- a/projects/components/src/select/select.component.ts +++ b/projects/components/src/select/select.component.ts @@ -44,7 +44,7 @@ import { SelectSize } from './select-size'; - + diff --git a/projects/components/src/table/controls/table-controls.component.ts b/projects/components/src/table/controls/table-controls.component.ts index 3c1eee381..21c70e466 100644 --- a/projects/components/src/table/controls/table-controls.component.ts +++ b/projects/components/src/table/controls/table-controls.component.ts @@ -1,8 +1,10 @@ +import { KeyValue } from '@angular/common'; import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core'; import { SubscriptionLifecycle, TypedSimpleChanges } from '@hypertrace/common'; import { isEmpty } from 'lodash-es'; import { Subject } from 'rxjs'; import { debounceTime } from 'rxjs/operators'; +import { SelectOption } from '../../select/select-option'; import { ToggleItem } from '../../toggle-group/toggle-item'; import { TableMode } from '../table-api'; @@ -23,6 +25,20 @@ import { TableMode } from '../table-api'; (valueChange)="this.onSearchChange($event)" > + + + + + ; + @Input() public filterItems?: ToggleItem[] = []; @@ -79,6 +98,9 @@ export class TableControlsComponent implements OnChanges { @Input() public checkboxChecked?: boolean; + @Output() + public readonly selectChange: EventEmitter> = new EventEmitter>(); + @Output() public readonly checkboxCheckedChange: EventEmitter = new EventEmitter(); @@ -137,6 +159,10 @@ export class TableControlsComponent implements OnChanges { } } + public onSelectChange(key: string, value: unknown): void { + this.selectChange.emit({ key: key, value: value }); + } + public onFilterChange(item: ToggleItem): void { this.filterChange.emit(item); } @@ -149,3 +175,8 @@ export class TableControlsComponent implements OnChanges { this.modeChange.emit(item.value); } } + +export interface SelectFilter { + placeholder?: string; + options: SelectOption[]; +} diff --git a/projects/components/src/table/controls/table-controls.module.ts b/projects/components/src/table/controls/table-controls.module.ts index 149570ec8..f2259f15e 100644 --- a/projects/components/src/table/controls/table-controls.module.ts +++ b/projects/components/src/table/controls/table-controls.module.ts @@ -2,12 +2,13 @@ import { CommonModule } from '@angular/common'; import { NgModule } from '@angular/core'; import { TraceCheckboxModule } from '../../checkbox/checkbox.module'; import { TraceSearchBoxModule } from '../../search-box/search-box.module'; +import { SelectModule } from '../../select/select.module'; import { ToggleGroupModule } from '../../toggle-group/toggle-group.module'; import { TooltipModule } from '../../tooltip/tooltip.module'; import { TableControlsComponent } from './table-controls.component'; @NgModule({ - imports: [CommonModule, TooltipModule, TraceSearchBoxModule, ToggleGroupModule, TraceCheckboxModule], + imports: [CommonModule, TooltipModule, TraceSearchBoxModule, ToggleGroupModule, TraceCheckboxModule, SelectModule], declarations: [TableControlsComponent], exports: [TableControlsComponent] }) diff --git a/projects/distributed-tracing/src/shared/dashboard/widgets/table/table-widget-base.model.ts b/projects/distributed-tracing/src/shared/dashboard/widgets/table/table-widget-base.model.ts index e7d8bb367..dc344171a 100644 --- a/projects/distributed-tracing/src/shared/dashboard/widgets/table/table-widget-base.model.ts +++ b/projects/distributed-tracing/src/shared/dashboard/widgets/table/table-widget-base.model.ts @@ -21,6 +21,7 @@ import { TableWidgetRowSelectionModel } from './selections/table-widget-row-sele import { TableWidgetCheckboxFilterModel } from './table-widget-checkbox-filter-model'; import { SpecificationBackedTableColumnDef } from './table-widget-column.model'; import { TableWidgetFilterModel } from './table-widget-filter-model'; +import { TableWidgetSelectFilterModel } from './table-widget-select-filter.model'; export abstract class TableWidgetBaseModel extends BaseModel { @ModelProperty({ @@ -62,6 +63,20 @@ export abstract class TableWidgetBaseModel extends BaseModel { }) public filterOptions: TableWidgetFilterModel[] = []; + @ModelProperty({ + key: 'selectFilterOptions', + displayName: 'Filter Select Options', + // tslint:disable-next-line: no-object-literal-type-assertion + type: { + key: ARRAY_PROPERTY.type, + subtype: { + key: ModelPropertyType.TYPE, + defaultModelClass: TableWidgetSelectFilterModel + } + } as ArrayPropertyTypeInstance + }) + public selectFilterOptions: TableWidgetSelectFilterModel[] = []; + @ModelProperty({ key: 'checkbox-filter-option', displayName: 'Checkbox Filter Option', @@ -141,6 +156,10 @@ export abstract class TableWidgetBaseModel extends BaseModel { return this.checkboxFilterOption; } + public getSelectFilterOptions(): TableWidgetSelectFilterModel[] { + return this.selectFilterOptions; + } + public isPageable(): boolean { return this.pageable; } diff --git a/projects/distributed-tracing/src/shared/dashboard/widgets/table/table-widget-renderer.component.ts b/projects/distributed-tracing/src/shared/dashboard/widgets/table/table-widget-renderer.component.ts index 504a6f02e..fc0e63f19 100644 --- a/projects/distributed-tracing/src/shared/dashboard/widgets/table/table-widget-renderer.component.ts +++ b/projects/distributed-tracing/src/shared/dashboard/widgets/table/table-widget-renderer.component.ts @@ -1,8 +1,11 @@ +import { KeyValue } from '@angular/common'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, OnInit } from '@angular/core'; -import { isEqualIgnoreFunctions, isNonEmptyString, PreferenceService } from '@hypertrace/common'; +import { forkJoinSafeEmpty, isEqualIgnoreFunctions, isNonEmptyString, PreferenceService } from '@hypertrace/common'; import { FilterAttribute, FilterOperator, + SelectFilter, + SelectOption, StatefulTableRow, TableColumnConfig, TableDataSource, @@ -18,7 +21,7 @@ import { Renderer } from '@hypertrace/hyperdash'; import { RendererApi, RENDERER_API } from '@hypertrace/hyperdash-angular'; import { capitalize, isEmpty, pick } from 'lodash-es'; import { BehaviorSubject, combineLatest, Observable, of, Subject } from 'rxjs'; -import { filter, map, pairwise, share, startWith, switchMap, tap } from 'rxjs/operators'; +import { filter, first, map, pairwise, share, startWith, switchMap, tap } from 'rxjs/operators'; import { AttributeMetadata, toFilterAttributeType } from '../../../graphql/model/metadata/attribute-metadata'; import { MetadataService } from '../../../services/metadata/metadata.service'; import { InteractionHandler } from '../../interaction/interaction-handler'; @@ -44,11 +47,13 @@ import { TableWidgetModel } from './table-widget.model'; [] = []; public modeItems: ToggleItem[] = []; public activeMode!: TableMode; + public selectMap?: Map; public metadata$!: Observable; public columnConfigs$!: Observable; @@ -94,6 +100,7 @@ export class TableWidgetRendererComponent private readonly toggleFilterSubject: Subject = new BehaviorSubject([]); private readonly searchFilterSubject: Subject = new BehaviorSubject([]); private readonly checkboxFilterSubject: Subject = new BehaviorSubject([]); + private readonly selectFilterSubject: Subject = new BehaviorSubject([]); private selectedRowInteractionHandler?: InteractionHandler; @@ -120,9 +127,15 @@ export class TableWidgetRendererComponent this.combinedFilters$ = combineLatest([ this.toggleFilterSubject, this.searchFilterSubject, - this.checkboxFilterSubject + this.checkboxFilterSubject, + this.selectFilterSubject ]).pipe( - map(([toggleFilters, searchFilters, checkboxFilter]) => [...toggleFilters, ...searchFilters, ...checkboxFilter]) + map(([toggleFilters, searchFilters, checkboxFilter, selectFilters]) => [ + ...toggleFilters, + ...searchFilters, + ...checkboxFilter, + ...selectFilters + ]) ); this.filterItems = this.model.getFilterOptions().map(filterOption => ({ @@ -135,6 +148,7 @@ export class TableWidgetRendererComponent value: modeOption })); + this.fetchSelectFilterValues(); this.maybeEmitInitialCheckboxFilterChange(); } @@ -152,6 +166,41 @@ export class TableWidgetRendererComponent return this.data$!.pipe(map(data => data?.getScope())); } + private fetchSelectFilterValues(): void { + this.model.getSelectFilterOptions().forEach(option => { + forkJoinSafeEmpty( + option.getData().pipe( + first(), + map((resultOptions: unknown[]) => { + const options: SelectOption[] = resultOptions.map(value => ({ + label: String(value), + value: value + })); + + if (option.placeholder !== undefined) { + options.unshift({ + label: option.placeholder, + value: undefined + }); + } + + return { + attribute: option.attribute, + selectFilter: { + placeholder: option.placeholder, + options: options + } + }; + }) + ) + ).subscribe((selects: AttributeSelectFilter[]) => { + const m: Map = new Map(); + selects.forEach(select => m.set(select.attribute, select.selectFilter)); + this.selectMap = m; + }); + }); + } + private getColumnConfigs(persistedColumns: TableColumnConfig[] = []): Observable { return combineLatest([ this.getScope(), @@ -226,6 +275,22 @@ export class TableWidgetRendererComponent this.checkboxFilterSubject.next(tableFilter ? [tableFilter] : []); } + public onSelectChange(select: KeyValue): void { + const selectFilterModel = this.model.getSelectFilterOptions().find(option => option.attribute === select.key); + + if (selectFilterModel === undefined) { + return; // This shouldn't happen + } + + this.selectFilterSubject.pipe(first()).subscribe(filters => { + this.selectFilterSubject.next( + [...filters.filter(f => f.field !== select.key), selectFilterModel.getTableFilter(select.value)].filter( + f => f.value !== undefined + ) // Remove filters that are unset + ); + }); + } + public onSearchChange(text: string): void { const searchFilter: TableFilter = { field: this.api.model.searchAttribute!, @@ -289,3 +354,8 @@ export class TableWidgetRendererComponent return pick(column, ['id', 'visible']); } } + +interface AttributeSelectFilter { + attribute: string; + selectFilter: SelectFilter; +} diff --git a/projects/distributed-tracing/src/shared/dashboard/widgets/table/table-widget-select-filter.model.ts b/projects/distributed-tracing/src/shared/dashboard/widgets/table/table-widget-select-filter.model.ts new file mode 100644 index 000000000..c381dca24 --- /dev/null +++ b/projects/distributed-tracing/src/shared/dashboard/widgets/table/table-widget-select-filter.model.ts @@ -0,0 +1,42 @@ +import { Dictionary, PrimitiveValue } from '@hypertrace/common'; +import { FilterOperator, TableFilter } from '@hypertrace/components'; +import { Model, ModelApi, ModelProperty, STRING_PROPERTY } from '@hypertrace/hyperdash'; +import { ModelInject, MODEL_API } from '@hypertrace/hyperdash-angular'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; + +@Model({ + type: 'table-widget-select-filter', + displayName: 'Select Filter' +}) +export class TableWidgetSelectFilterModel { + @ModelProperty({ + key: 'attribute', + type: STRING_PROPERTY.type, + required: true + }) + public attribute!: string; + + @ModelProperty({ + key: 'placeholder', + type: STRING_PROPERTY.type + }) + public placeholder?: string; + + @ModelInject(MODEL_API) + protected readonly api!: ModelApi; + + public getData(): Observable { + return this.api + .getData[]>() + .pipe(map(results => results.map(result => result[this.attribute]))); + } + + public getTableFilter(value: unknown): TableFilter { + return { + field: this.attribute, + operator: FilterOperator.Equals, + value: value + }; + } +} diff --git a/projects/distributed-tracing/src/shared/dashboard/widgets/table/table-widget.module.ts b/projects/distributed-tracing/src/shared/dashboard/widgets/table/table-widget.module.ts index 52d75cb21..072e3f2fc 100644 --- a/projects/distributed-tracing/src/shared/dashboard/widgets/table/table-widget.module.ts +++ b/projects/distributed-tracing/src/shared/dashboard/widgets/table/table-widget.module.ts @@ -16,6 +16,7 @@ import { TableWidgetRowSelectionModel } from './selections/table-widget-row-sele import { TableWidgetCheckboxFilterModel } from './table-widget-checkbox-filter-model'; import { TableWidgetColumnModel } from './table-widget-column.model'; import { TableWidgetRendererComponent } from './table-widget-renderer.component'; +import { TableWidgetSelectFilterModel } from './table-widget-select-filter.model'; import { TableWidgetModel } from './table-widget.model'; @NgModule({ @@ -27,6 +28,7 @@ import { TableWidgetModel } from './table-widget.model'; TableWidgetColumnModel, TableWidgetRowSelectionModel, TableWidgetCheckboxFilterModel, + TableWidgetSelectFilterModel, WidgetHeaderModel, ModeToggleTableWidgetModel ], diff --git a/projects/observability/src/pages/apis/services/services-list.dashboard.ts b/projects/observability/src/pages/apis/services/services-list.dashboard.ts index 498770b99..c2dfa8dee 100644 --- a/projects/observability/src/pages/apis/services/services-list.dashboard.ts +++ b/projects/observability/src/pages/apis/services/services-list.dashboard.ts @@ -1,194 +1,129 @@ import { CoreTableCellRendererType, TableMode, TableSortDirection, TableStyle } from '@hypertrace/components'; -import { DashboardDefaultConfiguration, TracingTableCellType } from '@hypertrace/distributed-tracing'; -import { ModelJson } from '@hypertrace/hyperdash'; +import { + DashboardDefaultConfiguration, + MetricAggregationType, + TracingTableCellType +} from '@hypertrace/distributed-tracing'; import { ObservabilityTableCellType } from '../../../shared/components/table/observability-table-cell-type'; - -const treeTableWidget: ModelJson = { - type: 'table-widget', - mode: TableMode.Tree, - style: TableStyle.FullPage, - columns: [ - { - type: 'table-widget-column', - title: 'Name', - display: ObservabilityTableCellType.Entity, - width: '30%', - value: { - type: 'entity-specification' - } - }, - { - type: 'table-widget-column', - title: 'p99 Latency', - display: TracingTableCellType.Metric, - value: { - type: 'metric-aggregation', - metric: 'duration', - aggregation: 'p99' - }, - sort: TableSortDirection.Descending - }, - { - type: 'table-widget-column', - title: 'Avg Latency', - display: TracingTableCellType.Metric, - value: { - type: 'metric-aggregation', - metric: 'duration', - aggregation: 'avg' - } - }, - { - type: 'table-widget-column', - title: 'Errors/s', - display: CoreTableCellRendererType.Number, - value: { - type: 'metric-aggregation', - metric: 'errorCount', - aggregation: 'avgrate_sec' - } - }, - { - type: 'table-widget-column', - title: 'Errors', - display: CoreTableCellRendererType.Number, - value: { - type: 'metric-aggregation', - metric: 'errorCount', - aggregation: 'sum' - } - }, - { - type: 'table-widget-column', - title: 'Calls/s', - display: CoreTableCellRendererType.Number, - value: { - type: 'metric-aggregation', - metric: 'numCalls', - aggregation: 'avgrate_sec' - } - }, - { - type: 'table-widget-column', - title: 'Calls', - display: CoreTableCellRendererType.Number, - value: { - type: 'metric-aggregation', - metric: 'numCalls', - aggregation: 'sum' - } - } - ], - data: { - type: 'entity-table-data-source', - entity: 'SERVICE', - 'child-data-source': { - type: 'entity-table-data-source', - entity: 'API' - } - } -}; - -const flatTableWidget: ModelJson = { - type: 'table-widget', - mode: TableMode.Flat, - style: TableStyle.FullPage, - columns: [ - { - type: 'table-widget-column', - title: 'Name', - display: ObservabilityTableCellType.Entity, - width: '30%', - value: { - type: 'entity-specification' - } - }, - { - type: 'table-widget-column', - title: 'Service Name', - width: '30%', - value: { - type: 'attribute-specification', - attribute: 'serviceName' - } - }, - { - type: 'table-widget-column', - title: 'p99 Latency', - display: TracingTableCellType.Metric, - value: { - type: 'metric-aggregation', - metric: 'duration', - aggregation: 'p99' - }, - sort: TableSortDirection.Descending - }, - { - type: 'table-widget-column', - title: 'Avg Latency', - display: TracingTableCellType.Metric, - value: { - type: 'metric-aggregation', - metric: 'duration', - aggregation: 'avg' - } - }, - { - type: 'table-widget-column', - title: 'Errors/s', - display: CoreTableCellRendererType.Number, - value: { - type: 'metric-aggregation', - metric: 'errorCount', - aggregation: 'avgrate_sec' - } - }, - { - type: 'table-widget-column', - title: 'Errors', - display: CoreTableCellRendererType.Number, - value: { - type: 'metric-aggregation', - metric: 'errorCount', - aggregation: 'sum' - } - }, - { - type: 'table-widget-column', - title: 'Calls/s', - display: CoreTableCellRendererType.Number, - value: { - type: 'metric-aggregation', - metric: 'numCalls', - aggregation: 'avgrate_sec' - } - }, - { - type: 'table-widget-column', - title: 'Calls', - display: CoreTableCellRendererType.Number, - value: { - type: 'metric-aggregation', - metric: 'numCalls', - aggregation: 'sum' - } - } - ], - data: { - type: 'entity-table-data-source', - entity: 'API' - } -}; +import { ObservabilityEntityType } from '../../../shared/graphql/model/schema/entity'; export const servicesListDashboard: DashboardDefaultConfiguration = { location: 'SERVICE_LIST', json: { - type: 'mode-toggle-table-widget', - searchAttribute: 'name', - style: TableStyle.FullPage, - mode: TableMode.Tree, - modeOptions: [TableMode.Tree, TableMode.Flat], - flat: flatTableWidget, - tree: treeTableWidget + type: 'container-widget', + layout: { + type: 'auto-container-layout', + 'enable-style': false + }, + children: [ + { + type: 'table-widget', + id: 'services-list.table', + mode: TableMode.Flat, + style: TableStyle.FullPage, + searchAttribute: 'name', + selectFilterOptions: [ + { + type: 'table-widget-select-filter', + attribute: 'serviceName', + placeholder: 'All Services', + data: { + type: 'entities-attribute-data-source', + entity: 'API', + attribute: { + type: 'attribute-specification', + attribute: 'serviceName' + } + } + } + ], + columns: [ + { + type: 'table-widget-column', + title: 'Name', + display: ObservabilityTableCellType.Entity, + width: '20%', + value: { + type: 'entity-specification' + } + }, + { + type: 'table-widget-column', + title: 'Service', + display: ObservabilityTableCellType.Entity, + width: '20%', + value: { + type: 'secondary-entity-specification', + idAttribute: 'serviceId', + nameAttribute: 'serviceName', + entityType: ObservabilityEntityType.Service + } + }, + { + type: 'table-widget-column', + title: 'Service ID', + visible: false, // The secondary-entity-specification requires the primary entity to fetch the attributes + value: { + type: 'attribute-specification', + attribute: 'serviceId' + } + }, + { + type: 'table-widget-column', + title: 'Service Name', + visible: false, // The secondary-entity-specification requires the primary entity to fetch the attributes + value: { + type: 'attribute-specification', + attribute: 'serviceName' + } + }, + { + type: 'table-widget-column', + title: 'p99 Latency', + display: TracingTableCellType.Metric, + value: { + type: 'metric-aggregation', + metric: 'duration', + aggregation: 'p99' + }, + sort: TableSortDirection.Descending + }, + { + type: 'table-widget-column', + title: 'Errors/s', + display: CoreTableCellRendererType.Number, + value: { + type: 'metric-aggregation', + metric: 'errorCount', + aggregation: 'avgrate_sec' + } + }, + { + type: 'table-widget-column', + title: 'Calls/s', + display: CoreTableCellRendererType.Number, + value: { + type: 'metric-aggregation', + metric: 'numCalls', + aggregation: 'avgrate_sec' + } + }, + { + type: 'table-widget-column', + title: 'Last Called', + display: CoreTableCellRendererType.Timestamp, + value: { + type: 'metric-aggregation', + metric: 'endTime', + aggregation: MetricAggregationType.Max + } + } + ], + data: { + type: 'entity-table-data-source', + entity: 'API' + } + } + ] } }; diff --git a/projects/observability/src/shared/dashboard/data/graphql/entity/attribute/entities-attribute-data-source-model.ts b/projects/observability/src/shared/dashboard/data/graphql/entity/attribute/entities-attribute-data-source-model.ts new file mode 100644 index 000000000..30db55098 --- /dev/null +++ b/projects/observability/src/shared/dashboard/data/graphql/entity/attribute/entities-attribute-data-source-model.ts @@ -0,0 +1,38 @@ +import { AttributeSpecificationModel, Specification } from '@hypertrace/distributed-tracing'; +import { + Model, + ModelModelPropertyTypeInstance, + ModelProperty, + ModelPropertyType, + STRING_PROPERTY +} from '@hypertrace/hyperdash'; +import { Observable } from 'rxjs'; +import { Entity, EntityType } from '../../../../../graphql/model/schema/entity'; +import { EntitiesValuesDataSourceModel } from '../entities-values-data-source.model'; + +@Model({ + type: 'entities-attribute-data-source' +}) +export class EntitiesAttributeDataSourceModel extends EntitiesValuesDataSourceModel { + @ModelProperty({ + key: 'attribute', + // tslint:disable-next-line: no-object-literal-type-assertion + type: { + key: ModelPropertyType.TYPE, + defaultModelClass: AttributeSpecificationModel + } as ModelModelPropertyTypeInstance, + required: true + }) + public specification!: Specification; + + @ModelProperty({ + key: 'entity', + type: STRING_PROPERTY.type, + required: true + }) + public entityType!: EntityType; + + public getData(): Observable { + return this.fetchSpecificationData(); + } +} diff --git a/projects/observability/src/shared/dashboard/data/graphql/entity/entities-values-data-source.model.ts b/projects/observability/src/shared/dashboard/data/graphql/entity/entities-values-data-source.model.ts new file mode 100644 index 000000000..5ea3a9690 --- /dev/null +++ b/projects/observability/src/shared/dashboard/data/graphql/entity/entities-values-data-source.model.ts @@ -0,0 +1,32 @@ +import { GraphQlDataSourceModel, GraphQlFilter, Specification } from '@hypertrace/distributed-tracing'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { Entity, EntityType } from '../../../../graphql/model/schema/entity'; +import { EntitiesResponse } from '../../../../graphql/request/handlers/entities/query/entities-graphql-query-builder.service'; +import { + EntitiesGraphQlQueryHandlerService, + ENTITIES_GQL_REQUEST, + GraphQlEntitiesQueryRequest +} from '../../../../graphql/request/handlers/entities/query/entities-graphql-query-handler.service'; + +export abstract class EntitiesValuesDataSourceModel extends GraphQlDataSourceModel { + protected abstract specification: Specification; + protected abstract entityType: EntityType; + + protected fetchSpecificationData(): Observable { + return this.query(filters => + this.buildRequest(this.specification, filters) + ).pipe(map(response => response.results)); + } + + private buildRequest(specification: Specification, inheritedFilters: GraphQlFilter[]): GraphQlEntitiesQueryRequest { + return { + requestType: ENTITIES_GQL_REQUEST, + entityType: this.entityType, + limit: 100, + properties: [specification], + timeRange: this.getTimeRangeOrThrow(), + filters: inheritedFilters + }; + } +} diff --git a/projects/observability/src/shared/dashboard/data/graphql/observability-graphql-data-source.module.ts b/projects/observability/src/shared/dashboard/data/graphql/observability-graphql-data-source.module.ts index c55abc3f9..74133bfdf 100644 --- a/projects/observability/src/shared/dashboard/data/graphql/observability-graphql-data-source.module.ts +++ b/projects/observability/src/shared/dashboard/data/graphql/observability-graphql-data-source.module.ts @@ -9,6 +9,7 @@ import { EntityTopologyGraphQlQueryHandlerService } from '../../../graphql/reque import { ExploreGraphQlQueryHandlerService } from '../../../graphql/request/handlers/explore/explore-graphql-query-handler.service'; import { ApiCallsCountDataSourceModel } from './api-calls-count/api-calls-count-data-source-model'; import { EntityMetricAggregationDataSourceModel } from './entity/aggregation/entity-metric-aggregation-data-source.model'; +import { EntitiesAttributeDataSourceModel } from './entity/attribute/entities-attribute-data-source-model'; import { EntityAttributeDataSourceModel } from './entity/attribute/entity-attribute-data-source.model'; import { EntityErrorPercentageTimeseriesDataSourceModel } from './entity/timeseries/entity-error-percentage-timeseries-data-source.model'; import { EntityMetricTimeseriesDataSourceModel } from './entity/timeseries/entity-metric-timeseries-data-source.model'; @@ -20,6 +21,7 @@ import { ErrorPercentageMetricAggregationSpecificationModel } from './specifiers import { MetricTimeseriesSpecificationModel } from './specifiers/metric-timeseries-specification.model'; import { NeighborEntitySpecificationModel } from './specifiers/neighbor-entity-specification.model'; import { PercentileLatencyAggregationSpecificationModel } from './specifiers/percentile-latency-metric-aggregation.model'; +import { SecondaryEntitySpecificationModel } from './specifiers/secondary-entity-specification.model'; import { EntityTableDataSourceModel } from './table/entity/entity-table-data-source.model'; import { InteractionsTableDataSourceModel } from './table/interactions/interactions-table-data-source.model'; import { TopologyDataSourceModel } from './topology/topology-data-source.model'; @@ -47,7 +49,9 @@ import { ApiTraceWaterfallDataSourceModel } from './waterfall/api-trace-waterfal EntityTableDataSourceModel, InteractionsTableDataSourceModel, EntityAttributeDataSourceModel, + EntitiesAttributeDataSourceModel, EntitySpecificationModel, + SecondaryEntitySpecificationModel, NeighborEntitySpecificationModel, ExploreCartesianDataSourceModel, ApiCallsCountDataSourceModel, diff --git a/projects/observability/src/shared/dashboard/data/graphql/specifiers/secondary-entity-specification.model.ts b/projects/observability/src/shared/dashboard/data/graphql/specifiers/secondary-entity-specification.model.ts new file mode 100644 index 000000000..af0197bb6 --- /dev/null +++ b/projects/observability/src/shared/dashboard/data/graphql/specifiers/secondary-entity-specification.model.ts @@ -0,0 +1,46 @@ +import { Dictionary } from '@hypertrace/common'; +import { SpecificationModel } from '@hypertrace/distributed-tracing'; +import { Model, ModelProperty, STRING_PROPERTY } from '@hypertrace/hyperdash'; +import { Entity, EntityType } from '../../../../graphql/model/schema/entity'; +import { EntitySpecification } from '../../../../graphql/model/schema/specifications/entity-specification'; +import { SecondaryEntitySpecificationBuilder } from '../../../../graphql/request/builders/specification/entity/secondary-entity-specification-builder'; + +@Model({ + type: 'secondary-entity-specification' +}) +export class SecondaryEntitySpecificationModel extends SpecificationModel { + /* + * A secondary entity is composed entirely of attributes on the primary entity. A second fetch is not made, so + * the primary entity must provide the attributes to build the secondary entity. + */ + @ModelProperty({ + key: 'idAttribute', + displayName: 'ID Attribute', + type: STRING_PROPERTY.type, + required: true + }) + public idAttribute!: string; + + @ModelProperty({ + key: 'nameAttribute', + displayName: 'Name Attribute', + type: STRING_PROPERTY.type, + required: true + }) + public nameAttribute!: string; + + @ModelProperty({ + key: 'entityType', + type: STRING_PROPERTY.type, + required: true + }) + public entityType!: EntityType; + + protected buildInnerSpec(): EntitySpecification { + return new SecondaryEntitySpecificationBuilder().build(this.idAttribute, this.nameAttribute, this.entityType); + } + + public extractFromServerData(resultContainer: Dictionary): Entity { + return this.innerSpec.extractFromServerData(resultContainer); + } +} diff --git a/projects/observability/src/shared/graphql/request/builders/specification/entity/secondary-entity-specification-builder.ts b/projects/observability/src/shared/graphql/request/builders/specification/entity/secondary-entity-specification-builder.ts new file mode 100644 index 000000000..a962a0ed9 --- /dev/null +++ b/projects/observability/src/shared/graphql/request/builders/specification/entity/secondary-entity-specification-builder.ts @@ -0,0 +1,54 @@ +import { Dictionary } from '@hypertrace/common'; +import { GraphQlArgumentBuilder } from '@hypertrace/distributed-tracing'; +import { GraphQlSelection } from '@hypertrace/graphql-client'; +import { Entity, entityIdKey, EntityType, entityTypeKey } from '../../../../model/schema/entity'; +import { EntitySpecification } from '../../../../model/schema/specifications/entity-specification'; + +export class SecondaryEntitySpecificationBuilder { + /* + * A secondary entity is composed entirely of attributes on the primary entity. A second fetch is not made, so + * the primary entity must provide the attributes to build the secondary entity. + */ + + private readonly argBuilder: GraphQlArgumentBuilder = new GraphQlArgumentBuilder(); + + public build(idKey: string, nameKey: string, entityType: EntityType): EntitySpecification { + return { + resultAlias: () => '_secondaryEntity', + name: idKey, + asGraphQlSelections: () => this.buildGraphQlSelections(idKey, nameKey), + extractFromServerData: serverData => this.extractFromServerData(serverData, idKey, nameKey, entityType), + asGraphQlOrderByFragment: () => ({ + key: nameKey + }) + }; + } + + private buildGraphQlSelections(idKey: string, nameKey: string): GraphQlSelection[] { + return [ + { + path: 'attribute', + alias: idKey, + arguments: [this.argBuilder.forAttributeKey(idKey)] + }, + { + path: 'attribute', + alias: nameKey, + arguments: [this.argBuilder.forAttributeKey(nameKey)] + } + ]; + } + + private extractFromServerData( + serverData: Dictionary, + idKey: string, + nameKey: string, + entityType: EntityType + ): Entity { + return { + [entityIdKey]: serverData[idKey] as string, + [entityTypeKey]: entityType, + name: serverData[nameKey] as string + }; + } +} diff --git a/src/app/shared/navigation/navigation.component.ts b/src/app/shared/navigation/navigation.component.ts index 1362da2bf..df305e831 100644 --- a/src/app/shared/navigation/navigation.component.ts +++ b/src/app/shared/navigation/navigation.component.ts @@ -46,8 +46,8 @@ export class NavigationComponent { }, { type: NavItemType.Link, - label: 'Services', - icon: ObservabilityIconType.Service, + label: 'API Endpoints', + icon: ObservabilityIconType.Api, matchPaths: ['services'] }, { From a786afba8f553d8b73be19d06069a267d0252650 Mon Sep 17 00:00:00 2001 From: Jake Bassett Date: Wed, 13 Jan 2021 14:31:19 -0800 Subject: [PATCH 2/6] fix: linting --- .../widgets/table/table-widget-renderer.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/projects/distributed-tracing/src/shared/dashboard/widgets/table/table-widget-renderer.component.ts b/projects/distributed-tracing/src/shared/dashboard/widgets/table/table-widget-renderer.component.ts index fc0e63f19..046caea49 100644 --- a/projects/distributed-tracing/src/shared/dashboard/widgets/table/table-widget-renderer.component.ts +++ b/projects/distributed-tracing/src/shared/dashboard/widgets/table/table-widget-renderer.component.ts @@ -168,7 +168,7 @@ export class TableWidgetRendererComponent private fetchSelectFilterValues(): void { this.model.getSelectFilterOptions().forEach(option => { - forkJoinSafeEmpty( + forkJoinSafeEmpty([ option.getData().pipe( first(), map((resultOptions: unknown[]) => { @@ -193,7 +193,7 @@ export class TableWidgetRendererComponent }; }) ) - ).subscribe((selects: AttributeSelectFilter[]) => { + ]).subscribe((selects: AttributeSelectFilter[]) => { const m: Map = new Map(); selects.forEach(select => m.set(select.attribute, select.selectFilter)); this.selectMap = m; From 94ee8fd9917fed82202e2cd9763f9e23f854c949 Mon Sep 17 00:00:00 2001 From: Jake Bassett Date: Thu, 14 Jan 2021 13:35:12 -0800 Subject: [PATCH 3/6] fix: clean up the select filter input code in table widget renderer --- .../controls/table-controls.component.ts | 16 +-- .../table/table-widget-renderer.component.ts | 99 ++++++++++--------- .../table/table-widget-select-filter.model.ts | 4 +- .../apis/services/services-list.dashboard.ts | 4 +- 4 files changed, 67 insertions(+), 56 deletions(-) diff --git a/projects/components/src/table/controls/table-controls.component.ts b/projects/components/src/table/controls/table-controls.component.ts index 21c70e466..e989a0734 100644 --- a/projects/components/src/table/controls/table-controls.component.ts +++ b/projects/components/src/table/controls/table-controls.component.ts @@ -27,13 +27,13 @@ import { TableMode } from '../table-api'; @@ -77,7 +77,7 @@ export class TableControlsComponent implements OnChanges { public searchPlaceholder?: string = 'Search...'; @Input() - public selectMap?: Map; + public selectFilterItems?: SelectFilter[] = []; @Input() public filterItems?: ToggleItem[] = []; @@ -159,8 +159,8 @@ export class TableControlsComponent implements OnChanges { } } - public onSelectChange(key: string, value: unknown): void { - this.selectChange.emit({ key: key, value: value }); + public onSelectChange(keyValue: KeyValue): void { + this.selectChange.emit(keyValue); } public onFilterChange(item: ToggleItem): void { @@ -178,5 +178,5 @@ export class TableControlsComponent implements OnChanges { export interface SelectFilter { placeholder?: string; - options: SelectOption[]; + options: SelectOption>[]; } diff --git a/projects/distributed-tracing/src/shared/dashboard/widgets/table/table-widget-renderer.component.ts b/projects/distributed-tracing/src/shared/dashboard/widgets/table/table-widget-renderer.component.ts index 046caea49..6d8734797 100644 --- a/projects/distributed-tracing/src/shared/dashboard/widgets/table/table-widget-renderer.component.ts +++ b/projects/distributed-tracing/src/shared/dashboard/widgets/table/table-widget-renderer.component.ts @@ -1,6 +1,12 @@ import { KeyValue } from '@angular/common'; import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, OnInit } from '@angular/core'; -import { forkJoinSafeEmpty, isEqualIgnoreFunctions, isNonEmptyString, PreferenceService } from '@hypertrace/common'; +import { + forkJoinSafeEmpty, + isEqualIgnoreFunctions, + isNonEmptyString, + PreferenceService, + PrimitiveValue +} from '@hypertrace/common'; import { FilterAttribute, FilterOperator, @@ -47,7 +53,7 @@ import { TableWidgetModel } from './table-widget.model'; [] = []; public modeItems: ToggleItem[] = []; public activeMode!: TableMode; - public selectMap?: Map; + + public selectFilterItems$!: Observable; public metadata$!: Observable; public columnConfigs$!: Observable; @@ -148,7 +155,51 @@ export class TableWidgetRendererComponent value: modeOption })); - this.fetchSelectFilterValues(); + this.selectFilterItems$ = forkJoinSafeEmpty( + this.model.getSelectFilterOptions().map(selectFilterModel => + // Fetch the values for the selectFilter dropdown + selectFilterModel.getData().pipe( + first(), + map((values: PrimitiveValue[]) => { + /* + * Map the values to SelectOptions, but also include the attribute since there may be multiple select + * dropdowns and we need to be able to associate the selected values to the attribute so we can filter + * on them. + * + * The KeyValue typing requires the union with undefined only for the unsetOption below. + */ + const options: SelectOption>[] = values.map(value => ({ + label: String(value), + value: { + // The value is the only thing Outputted from select component, so we need both the attribute and value + key: selectFilterModel.attribute, + value: value + } + })); + + /* + * If there is an unsetOption, we want to add that as the first option as a way to set this select + * filter to undefined and remove it from our query. + */ + if (selectFilterModel.unsetOption !== undefined) { + options.unshift({ + label: selectFilterModel.unsetOption, + value: { + key: selectFilterModel.attribute, + value: undefined + } + }); + } + + return { + placeholder: selectFilterModel.unsetOption, + options: options + }; + }) + ) + ) + ); + this.maybeEmitInitialCheckboxFilterChange(); } @@ -166,41 +217,6 @@ export class TableWidgetRendererComponent return this.data$!.pipe(map(data => data?.getScope())); } - private fetchSelectFilterValues(): void { - this.model.getSelectFilterOptions().forEach(option => { - forkJoinSafeEmpty([ - option.getData().pipe( - first(), - map((resultOptions: unknown[]) => { - const options: SelectOption[] = resultOptions.map(value => ({ - label: String(value), - value: value - })); - - if (option.placeholder !== undefined) { - options.unshift({ - label: option.placeholder, - value: undefined - }); - } - - return { - attribute: option.attribute, - selectFilter: { - placeholder: option.placeholder, - options: options - } - }; - }) - ) - ]).subscribe((selects: AttributeSelectFilter[]) => { - const m: Map = new Map(); - selects.forEach(select => m.set(select.attribute, select.selectFilter)); - this.selectMap = m; - }); - }); - } - private getColumnConfigs(persistedColumns: TableColumnConfig[] = []): Observable { return combineLatest([ this.getScope(), @@ -354,8 +370,3 @@ export class TableWidgetRendererComponent return pick(column, ['id', 'visible']); } } - -interface AttributeSelectFilter { - attribute: string; - selectFilter: SelectFilter; -} diff --git a/projects/distributed-tracing/src/shared/dashboard/widgets/table/table-widget-select-filter.model.ts b/projects/distributed-tracing/src/shared/dashboard/widgets/table/table-widget-select-filter.model.ts index c381dca24..22507b02c 100644 --- a/projects/distributed-tracing/src/shared/dashboard/widgets/table/table-widget-select-filter.model.ts +++ b/projects/distributed-tracing/src/shared/dashboard/widgets/table/table-widget-select-filter.model.ts @@ -18,10 +18,10 @@ export class TableWidgetSelectFilterModel { public attribute!: string; @ModelProperty({ - key: 'placeholder', + key: 'unsetOption', type: STRING_PROPERTY.type }) - public placeholder?: string; + public unsetOption?: string; @ModelInject(MODEL_API) protected readonly api!: ModelApi; diff --git a/projects/observability/src/pages/apis/services/services-list.dashboard.ts b/projects/observability/src/pages/apis/services/services-list.dashboard.ts index c2dfa8dee..e84295217 100644 --- a/projects/observability/src/pages/apis/services/services-list.dashboard.ts +++ b/projects/observability/src/pages/apis/services/services-list.dashboard.ts @@ -26,10 +26,10 @@ export const servicesListDashboard: DashboardDefaultConfiguration = { { type: 'table-widget-select-filter', attribute: 'serviceName', - placeholder: 'All Services', + unsetOption: 'All Services', data: { type: 'entities-attribute-data-source', - entity: 'API', + entity: ObservabilityEntityType.Api, attribute: { type: 'attribute-specification', attribute: 'serviceName' From 224d5fbe03dccbcf950f788fac74b5ef667d82a6 Mon Sep 17 00:00:00 2001 From: Jake Bassett Date: Thu, 14 Jan 2021 13:47:20 -0800 Subject: [PATCH 4/6] fix: remove secondary entity spec and model --- .../apis/services/services-list.dashboard.ts | 8 +-- ...bservability-graphql-data-source.module.ts | 2 - .../secondary-entity-specification.model.ts | 46 ---------------- .../secondary-entity-specification-builder.ts | 54 ------------------- 4 files changed, 4 insertions(+), 106 deletions(-) delete mode 100644 projects/observability/src/shared/dashboard/data/graphql/specifiers/secondary-entity-specification.model.ts delete mode 100644 projects/observability/src/shared/graphql/request/builders/specification/entity/secondary-entity-specification-builder.ts diff --git a/projects/observability/src/pages/apis/services/services-list.dashboard.ts b/projects/observability/src/pages/apis/services/services-list.dashboard.ts index e84295217..8595008d2 100644 --- a/projects/observability/src/pages/apis/services/services-list.dashboard.ts +++ b/projects/observability/src/pages/apis/services/services-list.dashboard.ts @@ -53,10 +53,10 @@ export const servicesListDashboard: DashboardDefaultConfiguration = { display: ObservabilityTableCellType.Entity, width: '20%', value: { - type: 'secondary-entity-specification', - idAttribute: 'serviceId', - nameAttribute: 'serviceName', - entityType: ObservabilityEntityType.Service + type: 'entity-specification', + 'id-attribute': 'serviceId', + 'name-attribute': 'serviceName', + 'entity-type': ObservabilityEntityType.Service } }, { diff --git a/projects/observability/src/shared/dashboard/data/graphql/observability-graphql-data-source.module.ts b/projects/observability/src/shared/dashboard/data/graphql/observability-graphql-data-source.module.ts index 74133bfdf..f8398db57 100644 --- a/projects/observability/src/shared/dashboard/data/graphql/observability-graphql-data-source.module.ts +++ b/projects/observability/src/shared/dashboard/data/graphql/observability-graphql-data-source.module.ts @@ -21,7 +21,6 @@ import { ErrorPercentageMetricAggregationSpecificationModel } from './specifiers import { MetricTimeseriesSpecificationModel } from './specifiers/metric-timeseries-specification.model'; import { NeighborEntitySpecificationModel } from './specifiers/neighbor-entity-specification.model'; import { PercentileLatencyAggregationSpecificationModel } from './specifiers/percentile-latency-metric-aggregation.model'; -import { SecondaryEntitySpecificationModel } from './specifiers/secondary-entity-specification.model'; import { EntityTableDataSourceModel } from './table/entity/entity-table-data-source.model'; import { InteractionsTableDataSourceModel } from './table/interactions/interactions-table-data-source.model'; import { TopologyDataSourceModel } from './topology/topology-data-source.model'; @@ -51,7 +50,6 @@ import { ApiTraceWaterfallDataSourceModel } from './waterfall/api-trace-waterfal EntityAttributeDataSourceModel, EntitiesAttributeDataSourceModel, EntitySpecificationModel, - SecondaryEntitySpecificationModel, NeighborEntitySpecificationModel, ExploreCartesianDataSourceModel, ApiCallsCountDataSourceModel, diff --git a/projects/observability/src/shared/dashboard/data/graphql/specifiers/secondary-entity-specification.model.ts b/projects/observability/src/shared/dashboard/data/graphql/specifiers/secondary-entity-specification.model.ts deleted file mode 100644 index af0197bb6..000000000 --- a/projects/observability/src/shared/dashboard/data/graphql/specifiers/secondary-entity-specification.model.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { Dictionary } from '@hypertrace/common'; -import { SpecificationModel } from '@hypertrace/distributed-tracing'; -import { Model, ModelProperty, STRING_PROPERTY } from '@hypertrace/hyperdash'; -import { Entity, EntityType } from '../../../../graphql/model/schema/entity'; -import { EntitySpecification } from '../../../../graphql/model/schema/specifications/entity-specification'; -import { SecondaryEntitySpecificationBuilder } from '../../../../graphql/request/builders/specification/entity/secondary-entity-specification-builder'; - -@Model({ - type: 'secondary-entity-specification' -}) -export class SecondaryEntitySpecificationModel extends SpecificationModel { - /* - * A secondary entity is composed entirely of attributes on the primary entity. A second fetch is not made, so - * the primary entity must provide the attributes to build the secondary entity. - */ - @ModelProperty({ - key: 'idAttribute', - displayName: 'ID Attribute', - type: STRING_PROPERTY.type, - required: true - }) - public idAttribute!: string; - - @ModelProperty({ - key: 'nameAttribute', - displayName: 'Name Attribute', - type: STRING_PROPERTY.type, - required: true - }) - public nameAttribute!: string; - - @ModelProperty({ - key: 'entityType', - type: STRING_PROPERTY.type, - required: true - }) - public entityType!: EntityType; - - protected buildInnerSpec(): EntitySpecification { - return new SecondaryEntitySpecificationBuilder().build(this.idAttribute, this.nameAttribute, this.entityType); - } - - public extractFromServerData(resultContainer: Dictionary): Entity { - return this.innerSpec.extractFromServerData(resultContainer); - } -} diff --git a/projects/observability/src/shared/graphql/request/builders/specification/entity/secondary-entity-specification-builder.ts b/projects/observability/src/shared/graphql/request/builders/specification/entity/secondary-entity-specification-builder.ts deleted file mode 100644 index a962a0ed9..000000000 --- a/projects/observability/src/shared/graphql/request/builders/specification/entity/secondary-entity-specification-builder.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Dictionary } from '@hypertrace/common'; -import { GraphQlArgumentBuilder } from '@hypertrace/distributed-tracing'; -import { GraphQlSelection } from '@hypertrace/graphql-client'; -import { Entity, entityIdKey, EntityType, entityTypeKey } from '../../../../model/schema/entity'; -import { EntitySpecification } from '../../../../model/schema/specifications/entity-specification'; - -export class SecondaryEntitySpecificationBuilder { - /* - * A secondary entity is composed entirely of attributes on the primary entity. A second fetch is not made, so - * the primary entity must provide the attributes to build the secondary entity. - */ - - private readonly argBuilder: GraphQlArgumentBuilder = new GraphQlArgumentBuilder(); - - public build(idKey: string, nameKey: string, entityType: EntityType): EntitySpecification { - return { - resultAlias: () => '_secondaryEntity', - name: idKey, - asGraphQlSelections: () => this.buildGraphQlSelections(idKey, nameKey), - extractFromServerData: serverData => this.extractFromServerData(serverData, idKey, nameKey, entityType), - asGraphQlOrderByFragment: () => ({ - key: nameKey - }) - }; - } - - private buildGraphQlSelections(idKey: string, nameKey: string): GraphQlSelection[] { - return [ - { - path: 'attribute', - alias: idKey, - arguments: [this.argBuilder.forAttributeKey(idKey)] - }, - { - path: 'attribute', - alias: nameKey, - arguments: [this.argBuilder.forAttributeKey(nameKey)] - } - ]; - } - - private extractFromServerData( - serverData: Dictionary, - idKey: string, - nameKey: string, - entityType: EntityType - ): Entity { - return { - [entityIdKey]: serverData[idKey] as string, - [entityTypeKey]: entityType, - name: serverData[nameKey] as string - }; - } -} From 281235f0222a94802843dbe4339d757137e6e9af Mon Sep 17 00:00:00 2001 From: Jake Bassett Date: Fri, 15 Jan 2021 15:43:58 -0800 Subject: [PATCH 5/6] fix: change entities-attribute-data-source to return attributes array --- .../graphql/graphql-data-source.module.ts | 2 +- ...> mapped-attribute-specification.model.ts} | 0 .../table/table-widget-renderer.component.ts | 24 ++++++++++++------- .../table/table-widget-select-filter.model.ts | 9 +++---- .../apis/services/services-list.dashboard.ts | 2 +- ...> entities-attribute-data-source.model.ts} | 4 ++-- .../entities-values-data-source.model.ts | 11 +++++---- ...bservability-graphql-data-source.module.ts | 2 +- 8 files changed, 30 insertions(+), 24 deletions(-) rename projects/distributed-tracing/src/shared/dashboard/data/graphql/specifiers/{mapped-attribute-specification-model.ts => mapped-attribute-specification.model.ts} (100%) rename projects/observability/src/shared/dashboard/data/graphql/entity/attribute/{entities-attribute-data-source-model.ts => entities-attribute-data-source.model.ts} (88%) diff --git a/projects/distributed-tracing/src/shared/dashboard/data/graphql/graphql-data-source.module.ts b/projects/distributed-tracing/src/shared/dashboard/data/graphql/graphql-data-source.module.ts index 5c741879c..92f36c128 100644 --- a/projects/distributed-tracing/src/shared/dashboard/data/graphql/graphql-data-source.module.ts +++ b/projects/distributed-tracing/src/shared/dashboard/data/graphql/graphql-data-source.module.ts @@ -13,7 +13,7 @@ import { AttributeSpecificationModel } from './specifiers/attribute-specificatio import { CompositeSpecificationModel } from './specifiers/composite-specification.model'; import { EnrichedAttributeSpecificationModel } from './specifiers/enriched-attribute-specification.model'; import { FieldSpecificationModel } from './specifiers/field-specification.model'; -import { MappedAttributeSpecificationModel } from './specifiers/mapped-attribute-specification-model'; +import { MappedAttributeSpecificationModel } from './specifiers/mapped-attribute-specification.model'; import { TraceStatusSpecificationModel } from './specifiers/trace-status-specification.model'; import { SpansTableDataSourceModel } from './table/spans/spans-table-data-source.model'; import { TracesTableDataSourceModel } from './table/traces/traces-table-data-source.model'; diff --git a/projects/distributed-tracing/src/shared/dashboard/data/graphql/specifiers/mapped-attribute-specification-model.ts b/projects/distributed-tracing/src/shared/dashboard/data/graphql/specifiers/mapped-attribute-specification.model.ts similarity index 100% rename from projects/distributed-tracing/src/shared/dashboard/data/graphql/specifiers/mapped-attribute-specification-model.ts rename to projects/distributed-tracing/src/shared/dashboard/data/graphql/specifiers/mapped-attribute-specification.model.ts diff --git a/projects/distributed-tracing/src/shared/dashboard/widgets/table/table-widget-renderer.component.ts b/projects/distributed-tracing/src/shared/dashboard/widgets/table/table-widget-renderer.component.ts index 6d8734797..4c7381363 100644 --- a/projects/distributed-tracing/src/shared/dashboard/widgets/table/table-widget-renderer.component.ts +++ b/projects/distributed-tracing/src/shared/dashboard/widgets/table/table-widget-renderer.component.ts @@ -25,7 +25,7 @@ import { import { WidgetRenderer } from '@hypertrace/dashboards'; import { Renderer } from '@hypertrace/hyperdash'; import { RendererApi, RENDERER_API } from '@hypertrace/hyperdash-angular'; -import { capitalize, isEmpty, pick } from 'lodash-es'; +import { capitalize, isEmpty, pick, uniq } from 'lodash-es'; import { BehaviorSubject, combineLatest, Observable, of, Subject } from 'rxjs'; import { filter, first, map, pairwise, share, startWith, switchMap, tap } from 'rxjs/operators'; import { AttributeMetadata, toFilterAttributeType } from '../../../graphql/model/metadata/attribute-metadata'; @@ -155,11 +155,25 @@ export class TableWidgetRendererComponent value: modeOption })); + this.maybeEmitInitialCheckboxFilterChange(); + } + + public getChildModel = (row: TableRow): object | undefined => this.model.getChildModel(row); + + protected fetchData(): Observable | undefined> { + return this.model.getData().pipe( + startWith(undefined), + tap(() => this.fetchFilterValues()) + ); + } + + protected fetchFilterValues(): void { this.selectFilterItems$ = forkJoinSafeEmpty( this.model.getSelectFilterOptions().map(selectFilterModel => // Fetch the values for the selectFilter dropdown selectFilterModel.getData().pipe( first(), + map(uniq), map((values: PrimitiveValue[]) => { /* * Map the values to SelectOptions, but also include the attribute since there may be multiple select @@ -199,14 +213,6 @@ export class TableWidgetRendererComponent ) ) ); - - this.maybeEmitInitialCheckboxFilterChange(); - } - - public getChildModel = (row: TableRow): object | undefined => this.model.getChildModel(row); - - protected fetchData(): Observable | undefined> { - return this.model.getData().pipe(startWith(undefined)); } public get syncWithUrl(): boolean { diff --git a/projects/distributed-tracing/src/shared/dashboard/widgets/table/table-widget-select-filter.model.ts b/projects/distributed-tracing/src/shared/dashboard/widgets/table/table-widget-select-filter.model.ts index 22507b02c..89124c9e9 100644 --- a/projects/distributed-tracing/src/shared/dashboard/widgets/table/table-widget-select-filter.model.ts +++ b/projects/distributed-tracing/src/shared/dashboard/widgets/table/table-widget-select-filter.model.ts @@ -1,9 +1,8 @@ -import { Dictionary, PrimitiveValue } from '@hypertrace/common'; +import { PrimitiveValue } from '@hypertrace/common'; import { FilterOperator, TableFilter } from '@hypertrace/components'; import { Model, ModelApi, ModelProperty, STRING_PROPERTY } from '@hypertrace/hyperdash'; import { ModelInject, MODEL_API } from '@hypertrace/hyperdash-angular'; import { Observable } from 'rxjs'; -import { map } from 'rxjs/operators'; @Model({ type: 'table-widget-select-filter', @@ -18,7 +17,7 @@ export class TableWidgetSelectFilterModel { public attribute!: string; @ModelProperty({ - key: 'unsetOption', + key: 'unset-option', type: STRING_PROPERTY.type }) public unsetOption?: string; @@ -27,9 +26,7 @@ export class TableWidgetSelectFilterModel { protected readonly api!: ModelApi; public getData(): Observable { - return this.api - .getData[]>() - .pipe(map(results => results.map(result => result[this.attribute]))); + return this.api.getData(); } public getTableFilter(value: unknown): TableFilter { diff --git a/projects/observability/src/pages/apis/services/services-list.dashboard.ts b/projects/observability/src/pages/apis/services/services-list.dashboard.ts index 8595008d2..4138f6a9e 100644 --- a/projects/observability/src/pages/apis/services/services-list.dashboard.ts +++ b/projects/observability/src/pages/apis/services/services-list.dashboard.ts @@ -26,7 +26,7 @@ export const servicesListDashboard: DashboardDefaultConfiguration = { { type: 'table-widget-select-filter', attribute: 'serviceName', - unsetOption: 'All Services', + 'unset-option': 'All Services', data: { type: 'entities-attribute-data-source', entity: ObservabilityEntityType.Api, diff --git a/projects/observability/src/shared/dashboard/data/graphql/entity/attribute/entities-attribute-data-source-model.ts b/projects/observability/src/shared/dashboard/data/graphql/entity/attribute/entities-attribute-data-source.model.ts similarity index 88% rename from projects/observability/src/shared/dashboard/data/graphql/entity/attribute/entities-attribute-data-source-model.ts rename to projects/observability/src/shared/dashboard/data/graphql/entity/attribute/entities-attribute-data-source.model.ts index 30db55098..25f2e685a 100644 --- a/projects/observability/src/shared/dashboard/data/graphql/entity/attribute/entities-attribute-data-source-model.ts +++ b/projects/observability/src/shared/dashboard/data/graphql/entity/attribute/entities-attribute-data-source.model.ts @@ -7,7 +7,7 @@ import { STRING_PROPERTY } from '@hypertrace/hyperdash'; import { Observable } from 'rxjs'; -import { Entity, EntityType } from '../../../../../graphql/model/schema/entity'; +import { EntityType } from '../../../../../graphql/model/schema/entity'; import { EntitiesValuesDataSourceModel } from '../entities-values-data-source.model'; @Model({ @@ -32,7 +32,7 @@ export class EntitiesAttributeDataSourceModel extends EntitiesValuesDataSourceMo }) public entityType!: EntityType; - public getData(): Observable { + public getData(): Observable { return this.fetchSpecificationData(); } } diff --git a/projects/observability/src/shared/dashboard/data/graphql/entity/entities-values-data-source.model.ts b/projects/observability/src/shared/dashboard/data/graphql/entity/entities-values-data-source.model.ts index 5ea3a9690..4b66aa703 100644 --- a/projects/observability/src/shared/dashboard/data/graphql/entity/entities-values-data-source.model.ts +++ b/projects/observability/src/shared/dashboard/data/graphql/entity/entities-values-data-source.model.ts @@ -1,7 +1,7 @@ import { GraphQlDataSourceModel, GraphQlFilter, Specification } from '@hypertrace/distributed-tracing'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; -import { Entity, EntityType } from '../../../../graphql/model/schema/entity'; +import { EntityType } from '../../../../graphql/model/schema/entity'; import { EntitiesResponse } from '../../../../graphql/request/handlers/entities/query/entities-graphql-query-builder.service'; import { EntitiesGraphQlQueryHandlerService, @@ -9,14 +9,17 @@ import { GraphQlEntitiesQueryRequest } from '../../../../graphql/request/handlers/entities/query/entities-graphql-query-handler.service'; -export abstract class EntitiesValuesDataSourceModel extends GraphQlDataSourceModel { +export abstract class EntitiesValuesDataSourceModel extends GraphQlDataSourceModel { protected abstract specification: Specification; protected abstract entityType: EntityType; - protected fetchSpecificationData(): Observable { + protected fetchSpecificationData(): Observable { return this.query(filters => this.buildRequest(this.specification, filters) - ).pipe(map(response => response.results)); + ).pipe( + map(response => response.results), + map(results => results.map(result => result[this.specification.resultAlias()])) + ); } private buildRequest(specification: Specification, inheritedFilters: GraphQlFilter[]): GraphQlEntitiesQueryRequest { diff --git a/projects/observability/src/shared/dashboard/data/graphql/observability-graphql-data-source.module.ts b/projects/observability/src/shared/dashboard/data/graphql/observability-graphql-data-source.module.ts index f8398db57..5844a74c1 100644 --- a/projects/observability/src/shared/dashboard/data/graphql/observability-graphql-data-source.module.ts +++ b/projects/observability/src/shared/dashboard/data/graphql/observability-graphql-data-source.module.ts @@ -9,7 +9,7 @@ import { EntityTopologyGraphQlQueryHandlerService } from '../../../graphql/reque import { ExploreGraphQlQueryHandlerService } from '../../../graphql/request/handlers/explore/explore-graphql-query-handler.service'; import { ApiCallsCountDataSourceModel } from './api-calls-count/api-calls-count-data-source-model'; import { EntityMetricAggregationDataSourceModel } from './entity/aggregation/entity-metric-aggregation-data-source.model'; -import { EntitiesAttributeDataSourceModel } from './entity/attribute/entities-attribute-data-source-model'; +import { EntitiesAttributeDataSourceModel } from './entity/attribute/entities-attribute-data-source.model'; import { EntityAttributeDataSourceModel } from './entity/attribute/entity-attribute-data-source.model'; import { EntityErrorPercentageTimeseriesDataSourceModel } from './entity/timeseries/entity-error-percentage-timeseries-data-source.model'; import { EntityMetricTimeseriesDataSourceModel } from './entity/timeseries/entity-metric-timeseries-data-source.model'; From 16d27dc710c9fc302d1d217b858c661aec9f7841 Mon Sep 17 00:00:00 2001 From: Jake Bassett Date: Tue, 19 Jan 2021 15:25:20 -0800 Subject: [PATCH 6/6] fix: pr comments --- projects/components/src/public-api.ts | 1 + .../src/table/controls/table-controls-api.ts | 12 +++++++++ .../controls/table-controls.component.ts | 18 ++++++------- .../table/table-widget-renderer.component.ts | 26 ++++++++++++------- .../apis/services/services-list.dashboard.ts | 22 ++-------------- .../entities-values-data-source.model.ts | 9 ++++++- 6 files changed, 47 insertions(+), 41 deletions(-) create mode 100644 projects/components/src/table/controls/table-controls-api.ts diff --git a/projects/components/src/public-api.ts b/projects/components/src/public-api.ts index 919bcb5e8..1d2405f99 100644 --- a/projects/components/src/public-api.ts +++ b/projects/components/src/public-api.ts @@ -219,6 +219,7 @@ export * from './summary-value/summary-value.component'; export * from './summary-value/summary-value.module'; // Table +export * from './table/controls/table-controls-api'; export * from './table/data/table-data-source'; export * from './table/table.module'; export * from './table/table.component'; diff --git a/projects/components/src/table/controls/table-controls-api.ts b/projects/components/src/table/controls/table-controls-api.ts new file mode 100644 index 000000000..cfd2dc01f --- /dev/null +++ b/projects/components/src/table/controls/table-controls-api.ts @@ -0,0 +1,12 @@ +import { KeyValue } from '@angular/common'; +import { SelectOption } from '../../select/select-option'; + +export interface SelectFilter { + placeholder?: string; + options: SelectOption>[]; +} + +export interface SelectChange { + select: SelectFilter; + value: KeyValue; +} diff --git a/projects/components/src/table/controls/table-controls.component.ts b/projects/components/src/table/controls/table-controls.component.ts index e989a0734..c58e898c1 100644 --- a/projects/components/src/table/controls/table-controls.component.ts +++ b/projects/components/src/table/controls/table-controls.component.ts @@ -4,9 +4,9 @@ import { SubscriptionLifecycle, TypedSimpleChanges } from '@hypertrace/common'; import { isEmpty } from 'lodash-es'; import { Subject } from 'rxjs'; import { debounceTime } from 'rxjs/operators'; -import { SelectOption } from '../../select/select-option'; import { ToggleItem } from '../../toggle-group/toggle-item'; import { TableMode } from '../table-api'; +import { SelectChange, SelectFilter } from './table-controls-api'; @Component({ selector: 'ht-table-controls', @@ -30,7 +30,7 @@ import { TableMode } from '../table-api'; *ngFor="let selectFilterItem of this.selectFilterItems" [placeholder]="selectFilterItem.placeholder" class="control select" - (selectedChange)="this.onSelectChange($event)" + (selectedChange)="this.onSelectChange(selectFilterItem, $event)" > > = new EventEmitter>(); + public readonly selectChange: EventEmitter = new EventEmitter(); @Output() public readonly checkboxCheckedChange: EventEmitter = new EventEmitter(); @@ -159,8 +159,11 @@ export class TableControlsComponent implements OnChanges { } } - public onSelectChange(keyValue: KeyValue): void { - this.selectChange.emit(keyValue); + public onSelectChange(select: SelectFilter, keyValue: KeyValue): void { + this.selectChange.emit({ + select: select, + value: keyValue + }); } public onFilterChange(item: ToggleItem): void { @@ -175,8 +178,3 @@ export class TableControlsComponent implements OnChanges { this.modeChange.emit(item.value); } } - -export interface SelectFilter { - placeholder?: string; - options: SelectOption>[]; -} diff --git a/projects/distributed-tracing/src/shared/dashboard/widgets/table/table-widget-renderer.component.ts b/projects/distributed-tracing/src/shared/dashboard/widgets/table/table-widget-renderer.component.ts index 4c7381363..78521eeaa 100644 --- a/projects/distributed-tracing/src/shared/dashboard/widgets/table/table-widget-renderer.component.ts +++ b/projects/distributed-tracing/src/shared/dashboard/widgets/table/table-widget-renderer.component.ts @@ -10,6 +10,7 @@ import { import { FilterAttribute, FilterOperator, + SelectChange, SelectFilter, SelectOption, StatefulTableRow, @@ -107,7 +108,7 @@ export class TableWidgetRendererComponent private readonly toggleFilterSubject: Subject = new BehaviorSubject([]); private readonly searchFilterSubject: Subject = new BehaviorSubject([]); private readonly checkboxFilterSubject: Subject = new BehaviorSubject([]); - private readonly selectFilterSubject: Subject = new BehaviorSubject([]); + private readonly selectFilterSubject: BehaviorSubject = new BehaviorSubject([]); private selectedRowInteractionHandler?: InteractionHandler; @@ -297,20 +298,25 @@ export class TableWidgetRendererComponent this.checkboxFilterSubject.next(tableFilter ? [tableFilter] : []); } - public onSelectChange(select: KeyValue): void { - const selectFilterModel = this.model.getSelectFilterOptions().find(option => option.attribute === select.key); + public onSelectChange(changed: SelectChange): void { + const selectFilterModel = this.model + .getSelectFilterOptions() + .find(option => option.attribute === changed.value.key); if (selectFilterModel === undefined) { return; // This shouldn't happen } - this.selectFilterSubject.pipe(first()).subscribe(filters => { - this.selectFilterSubject.next( - [...filters.filter(f => f.field !== select.key), selectFilterModel.getTableFilter(select.value)].filter( - f => f.value !== undefined - ) // Remove filters that are unset - ); - }); + const existingSelectFiltersWithChangedRemoved = this.selectFilterSubject + .getValue() + .filter(f => f.field !== changed.value.key); + const changedSelectFilter = selectFilterModel.getTableFilter(changed.value.value); + + const selectFilters = [...existingSelectFiltersWithChangedRemoved, changedSelectFilter].filter( + f => f.value !== undefined + ); // Remove filters that are unset + + this.selectFilterSubject.next(selectFilters); } public onSearchChange(text: string): void { diff --git a/projects/observability/src/pages/apis/services/services-list.dashboard.ts b/projects/observability/src/pages/apis/services/services-list.dashboard.ts index 4138f6a9e..92a107e7c 100644 --- a/projects/observability/src/pages/apis/services/services-list.dashboard.ts +++ b/projects/observability/src/pages/apis/services/services-list.dashboard.ts @@ -29,10 +29,10 @@ export const servicesListDashboard: DashboardDefaultConfiguration = { 'unset-option': 'All Services', data: { type: 'entities-attribute-data-source', - entity: ObservabilityEntityType.Api, + entity: ObservabilityEntityType.Service, attribute: { type: 'attribute-specification', - attribute: 'serviceName' + attribute: 'name' } } } @@ -59,24 +59,6 @@ export const servicesListDashboard: DashboardDefaultConfiguration = { 'entity-type': ObservabilityEntityType.Service } }, - { - type: 'table-widget-column', - title: 'Service ID', - visible: false, // The secondary-entity-specification requires the primary entity to fetch the attributes - value: { - type: 'attribute-specification', - attribute: 'serviceId' - } - }, - { - type: 'table-widget-column', - title: 'Service Name', - visible: false, // The secondary-entity-specification requires the primary entity to fetch the attributes - value: { - type: 'attribute-specification', - attribute: 'serviceName' - } - }, { type: 'table-widget-column', title: 'p99 Latency', diff --git a/projects/observability/src/shared/dashboard/data/graphql/entity/entities-values-data-source.model.ts b/projects/observability/src/shared/dashboard/data/graphql/entity/entities-values-data-source.model.ts index 4b66aa703..afcec9379 100644 --- a/projects/observability/src/shared/dashboard/data/graphql/entity/entities-values-data-source.model.ts +++ b/projects/observability/src/shared/dashboard/data/graphql/entity/entities-values-data-source.model.ts @@ -1,4 +1,5 @@ import { GraphQlDataSourceModel, GraphQlFilter, Specification } from '@hypertrace/distributed-tracing'; +import { ModelProperty, NUMBER_PROPERTY } from '@hypertrace/hyperdash'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { EntityType } from '../../../../graphql/model/schema/entity'; @@ -13,6 +14,12 @@ export abstract class EntitiesValuesDataSourceModel extends GraphQlDataSourceMod protected abstract specification: Specification; protected abstract entityType: EntityType; + @ModelProperty({ + key: 'limit', + type: NUMBER_PROPERTY.type + }) + public limit: number = 100; + protected fetchSpecificationData(): Observable { return this.query(filters => this.buildRequest(this.specification, filters) @@ -26,7 +33,7 @@ export abstract class EntitiesValuesDataSourceModel extends GraphQlDataSourceMod return { requestType: ENTITIES_GQL_REQUEST, entityType: this.entityType, - limit: 100, + limit: this.limit, properties: [specification], timeRange: this.getTimeRangeOrThrow(), filters: inheritedFilters