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']
},
{