Skip to content
1 change: 1 addition & 0 deletions projects/common/src/utilities/types/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,4 @@ export interface Json {
}

export type JsonValue = Json | Json[] | string | number | boolean | null;
export type PrimitiveValue = string | number | boolean;
1 change: 1 addition & 0 deletions projects/components/src/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
1 change: 1 addition & 0 deletions projects/components/src/select/select.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
.trigger-icon {
padding: 0 8px;
margin-left: auto;
color: $gray-7;
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion projects/components/src/select/select.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ import { SelectSize } from './select-size';
<ht-icon *ngIf="this.icon" class="trigger-prefix-icon" [icon]="this.icon" size="${IconSize.Small}">
</ht-icon>
<ht-label class="trigger-label" [label]="selected?.label || this.placeholder"> </ht-label>
<ht-icon class="trigger-icon" icon="${IconType.ChevronDown}" size="${IconSize.Small}"> </ht-icon>
<ht-icon class="trigger-icon" icon="${IconType.ChevronDown}" size="${IconSize.ExtraSmall}"> </ht-icon>
</div>
</ht-popover-trigger>
<ht-popover-content>
Expand Down
12 changes: 12 additions & 0 deletions projects/components/src/table/controls/table-controls-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { KeyValue } from '@angular/common';
import { SelectOption } from '../../select/select-option';

export interface SelectFilter {
placeholder?: string;
options: SelectOption<KeyValue<string, unknown>>[];
}

export interface SelectChange {
select: SelectFilter;
value: KeyValue<string, unknown>;
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
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 { ToggleItem } from '../../toggle-group/toggle-item';
import { TableMode } from '../table-api';
import { SelectChange, SelectFilter } from './table-controls-api';

@Component({
selector: 'ht-table-controls',
Expand All @@ -23,6 +25,20 @@ import { TableMode } from '../table-api';
(valueChange)="this.onSearchChange($event)"
></ht-search-box>

<!-- Selects -->
<ht-select
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't the select dropdown be a multi select dropdown? For example: Show me all the APIs which lies in Service A or Service B or Service C

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually good feedback. I think multi select makes sense. That said, let me get the single select version out and demoed. I'll make the switch to IN filtering once everyone is onboard with the new changes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes. that makes sense

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is actually good feedback

What are you saying?!

*ngFor="let selectFilterItem of this.selectFilterItems"
[placeholder]="selectFilterItem.placeholder"
class="control select"
(selectedChange)="this.onSelectChange(selectFilterItem, $event)"
>
<ht-select-option
*ngFor="let option of selectFilterItem.options"
[label]="option.label"
[value]="option.value"
></ht-select-option>
</ht-select>

<!-- Filter Toggle -->
<ht-toggle-group
*ngIf="this.filterItemsEnabled"
Expand Down Expand Up @@ -60,6 +76,9 @@ export class TableControlsComponent implements OnChanges {
@Input()
public searchPlaceholder?: string = 'Search...';

@Input()
public selectFilterItems?: SelectFilter[] = [];

@Input()
public filterItems?: ToggleItem[] = [];

Expand All @@ -79,6 +98,9 @@ export class TableControlsComponent implements OnChanges {
@Input()
public checkboxChecked?: boolean;

@Output()
public readonly selectChange: EventEmitter<SelectChange> = new EventEmitter<SelectChange>();

@Output()
public readonly checkboxCheckedChange: EventEmitter<boolean> = new EventEmitter<boolean>();

Expand Down Expand Up @@ -137,6 +159,13 @@ export class TableControlsComponent implements OnChanges {
}
}

public onSelectChange(select: SelectFilter, keyValue: KeyValue<string, unknown>): void {
this.selectChange.emit({
select: select,
value: keyValue
});
}

public onFilterChange(item: ToggleItem): void {
this.filterChange.emit(item);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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';
Expand All @@ -44,11 +54,13 @@ import { TableWidgetModel } from './table-widget.model';
<ht-table-controls
class="table-controls"
[searchEnabled]="!!this.api.model.searchAttribute"
[selectFilterItems]="this.selectFilterItems$ | async"
[filterItems]="this.filterItems"
[modeItems]="this.modeItems"
[checkboxLabel]="this.model.getCheckboxFilterOption()?.label"
[checkboxChecked]="this.model.getCheckboxFilterOption()?.checked"
(checkboxCheckedChange)="this.onCheckboxCheckedChange($event)"
(selectChange)="this.onSelectChange($event)"
(searchChange)="this.onSearchChange($event)"
(filterChange)="this.onFilterChange($event)"
(modeChange)="this.onModeChange($event)"
Expand Down Expand Up @@ -87,13 +99,16 @@ export class TableWidgetRendererComponent
public modeItems: ToggleItem<TableMode>[] = [];
public activeMode!: TableMode;

public selectFilterItems$!: Observable<SelectFilter[]>;

public metadata$!: Observable<FilterAttribute[]>;
public columnConfigs$!: Observable<TableColumnConfig[]>;
public combinedFilters$!: Observable<TableFilter[]>;

private readonly toggleFilterSubject: Subject<TableFilter[]> = new BehaviorSubject<TableFilter[]>([]);
private readonly searchFilterSubject: Subject<TableFilter[]> = new BehaviorSubject<TableFilter[]>([]);
private readonly checkboxFilterSubject: Subject<TableFilter[]> = new BehaviorSubject<TableFilter[]>([]);
private readonly selectFilterSubject: BehaviorSubject<TableFilter[]> = new BehaviorSubject<TableFilter[]>([]);

private selectedRowInteractionHandler?: InteractionHandler;

Expand All @@ -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 => ({
Expand All @@ -141,7 +162,58 @@ export class TableWidgetRendererComponent
public getChildModel = (row: TableRow): object | undefined => this.model.getChildModel(row);

protected fetchData(): Observable<TableDataSource<TableRow> | 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<KeyValue<string, PrimitiveValue | undefined>>[] = 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 {
Expand Down Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does the undefined filtering mean we can't switch back to unfiltered (isn't undefined the "All Services" option) ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct. The select option returning a value of undefined signals to the widget to drop that filter, so we remove it from the request all together.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm confused - I would expect to not use that filter in the request, but it should still remain in the dropdown list if I want to return to see all apis across services. The behavior I read/you describe sounds like it is removed from the selection list once an option is chosen, never to return.

); // Remove filters that are unset

this.selectFilterSubject.next(selectFilters);
}

public onSearchChange(text: string): void {
const searchFilter: TableFilter = {
field: this.api.model.searchAttribute!,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<PrimitiveValue[]> {
return this.api.getData<PrimitiveValue[]>();
}

public getTableFilter(value: unknown): TableFilter {
return {
field: this.attribute,
operator: FilterOperator.Equals,
value: value
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -27,6 +28,7 @@ import { TableWidgetModel } from './table-widget.model';
TableWidgetColumnModel,
TableWidgetRowSelectionModel,
TableWidgetCheckboxFilterModel,
TableWidgetSelectFilterModel,
WidgetHeaderModel,
ModeToggleTableWidgetModel
],
Expand Down
Loading