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/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/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-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 3c1eee381..c58e898c1 100644 --- a/projects/components/src/table/controls/table-controls.component.ts +++ b/projects/components/src/table/controls/table-controls.component.ts @@ -1,3 +1,4 @@ +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'; @@ -5,6 +6,7 @@ import { Subject } from 'rxjs'; import { debounceTime } from 'rxjs/operators'; import { ToggleItem } from '../../toggle-group/toggle-item'; import { TableMode } from '../table-api'; +import { SelectChange, SelectFilter } from './table-controls-api'; @Component({ selector: 'ht-table-controls', @@ -23,6 +25,20 @@ import { TableMode } from '../table-api'; (valueChange)="this.onSearchChange($event)" > + + + + + = new EventEmitter(); + @Output() public readonly checkboxCheckedChange: EventEmitter = new EventEmitter(); @@ -137,6 +159,13 @@ export class TableControlsComponent implements OnChanges { } } + public onSelectChange(select: SelectFilter, keyValue: KeyValue): void { + this.selectChange.emit({ + select: select, + value: keyValue + }); + } + public onFilterChange(item: ToggleItem): void { this.filterChange.emit(item); } 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/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-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..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 @@ -1,8 +1,18 @@ +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, + PrimitiveValue +} from '@hypertrace/common'; import { FilterAttribute, FilterOperator, + SelectChange, + SelectFilter, + SelectOption, StatefulTableRow, TableColumnConfig, TableDataSource, @@ -16,9 +26,9 @@ 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, 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 +54,13 @@ import { TableWidgetModel } from './table-widget.model'; [] = []; public activeMode!: TableMode; + public selectFilterItems$!: Observable; + public metadata$!: Observable; public columnConfigs$!: Observable; public combinedFilters$!: Observable; @@ -94,6 +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: BehaviorSubject = new BehaviorSubject([]); private selectedRowInteractionHandler?: InteractionHandler; @@ -120,9 +135,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 => ({ @@ -141,7 +162,58 @@ export class TableWidgetRendererComponent public getChildModel = (row: TableRow): object | undefined => this.model.getChildModel(row); protected fetchData(): Observable | undefined> { - return this.model.getData().pipe(startWith(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 + * 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 + }; + }) + ) + ) + ); } public get syncWithUrl(): boolean { @@ -226,6 +298,27 @@ export class TableWidgetRendererComponent this.checkboxFilterSubject.next(tableFilter ? [tableFilter] : []); } + 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 + } + + 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 { const searchFilter: TableFilter = { field: this.api.model.searchAttribute!, 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..89124c9e9 --- /dev/null +++ b/projects/distributed-tracing/src/shared/dashboard/widgets/table/table-widget-select-filter.model.ts @@ -0,0 +1,39 @@ +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'; + +@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: 'unset-option', + type: STRING_PROPERTY.type + }) + public unsetOption?: string; + + @ModelInject(MODEL_API) + protected readonly api!: ModelApi; + + public getData(): Observable { + return this.api.getData(); + } + + 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..92a107e7c 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,111 @@ 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', + 'unset-option': 'All Services', + data: { + type: 'entities-attribute-data-source', + entity: ObservabilityEntityType.Service, + attribute: { + type: 'attribute-specification', + attribute: 'name' + } + } + } + ], + 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: 'entity-specification', + 'id-attribute': 'serviceId', + 'name-attribute': 'serviceName', + 'entity-type': ObservabilityEntityType.Service + } + }, + { + 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..25f2e685a --- /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 { 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..afcec9379 --- /dev/null +++ b/projects/observability/src/shared/dashboard/data/graphql/entity/entities-values-data-source.model.ts @@ -0,0 +1,42 @@ +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'; +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; + + @ModelProperty({ + key: 'limit', + type: NUMBER_PROPERTY.type + }) + public limit: number = 100; + + protected fetchSpecificationData(): Observable { + return this.query(filters => + this.buildRequest(this.specification, filters) + ).pipe( + map(response => response.results), + map(results => results.map(result => result[this.specification.resultAlias()])) + ); + } + + private buildRequest(specification: Specification, inheritedFilters: GraphQlFilter[]): GraphQlEntitiesQueryRequest { + return { + requestType: ENTITIES_GQL_REQUEST, + entityType: this.entityType, + limit: this.limit, + 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..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,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'; @@ -47,6 +48,7 @@ import { ApiTraceWaterfallDataSourceModel } from './waterfall/api-trace-waterfal EntityTableDataSourceModel, InteractionsTableDataSourceModel, EntityAttributeDataSourceModel, + EntitiesAttributeDataSourceModel, EntitySpecificationModel, NeighborEntitySpecificationModel, ExploreCartesianDataSourceModel, 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'] }, {