|
1 | 1 | import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core'; |
2 | 2 | import { TypedSimpleChanges } from '@hypertrace/common'; |
3 | | -import { SelectOption } from '@hypertrace/components'; |
4 | | -import { combineLatest, Observable, of, ReplaySubject, Subject } from 'rxjs'; |
5 | | -import { map, switchMap } from 'rxjs/operators'; |
| 3 | +import { InputAppearance, SelectOption } from '@hypertrace/components'; |
| 4 | +import { isEmpty, isNil } from 'lodash-es'; |
| 5 | +import { combineLatest, merge, Observable, of, ReplaySubject, Subject } from 'rxjs'; |
| 6 | +import { debounceTime, filter, map, switchMap } from 'rxjs/operators'; |
| 7 | +import { AttributeExpression } from '../../../graphql/model/attribute/attribute-expression'; |
| 8 | +import { AttributeMetadata, AttributeMetadataType } from '../../../graphql/model/metadata/attribute-metadata'; |
6 | 9 | import { TraceType } from '../../../graphql/model/schema/trace'; |
7 | 10 | import { MetadataService } from '../../../services/metadata/metadata.service'; |
8 | | - |
9 | 11 | @Component({ |
10 | 12 | selector: 'ht-explore-query-group-by-editor', |
11 | 13 | styleUrls: ['./explore-query-group-by-editor.component.scss'], |
12 | 14 | changeDetection: ChangeDetectionStrategy.OnPush, |
13 | 15 | template: ` |
14 | | - <div class="group-by-container"> |
15 | | - <span class="group-by-label"> Group By </span> |
16 | | - <ht-select |
17 | | - *ngIf="this.groupByKeyOptions$ | async as keyOptions" |
18 | | - [showBorder]="true" |
19 | | - class="group-by-selector" |
20 | | - [selected]="this.groupByKey$ | async" |
21 | | - (selectedChange)="this.onGroupByKeyChange($event)" |
22 | | - > |
23 | | - <ht-select-option |
24 | | - *ngFor="let option of keyOptions" |
25 | | - [value]="option.value" |
26 | | - [label]="option.label" |
27 | | - ></ht-select-option> |
28 | | - </ht-select> |
| 16 | + <div class="group-by-container" *htLetAsync="this.currentGroupByExpression$ as currentGroupByExpression"> |
| 17 | + <div class="group-by-input-container"> |
| 18 | + <span class="group-by-label"> Group By </span> |
| 19 | + <ht-select |
| 20 | + *ngIf="this.groupByAttributeOptions$ | async as attributeOptions" |
| 21 | + [showBorder]="true" |
| 22 | + class="group-by-selector" |
| 23 | + [selected]="currentGroupByExpression && currentGroupByExpression.metadata" |
| 24 | + (selectedChange)="this.onGroupByAttributeChange($event)" |
| 25 | + > |
| 26 | + <ht-select-option |
| 27 | + *ngFor="let option of attributeOptions" |
| 28 | + [value]="option.value" |
| 29 | + [label]="option.label" |
| 30 | + ></ht-select-option> |
| 31 | + </ht-select> |
| 32 | + </div> |
| 33 | + <div class="group-by-input-container" *ngIf="this.supportsSubpath(currentGroupByExpression?.metadata)"> |
| 34 | + <span class="group-by-label"> {{ currentGroupByExpression.metadata.displayName }} Key </span> |
| 35 | + <div class="group-by-path-wrapper"> |
| 36 | + <ht-form-field |
| 37 | + errorLabel="Key required" |
| 38 | + [showBorder]="true" |
| 39 | + [showFormError]="!currentGroupByExpression.subpath" |
| 40 | + > |
| 41 | + <ht-input |
| 42 | + type="string" |
| 43 | + class="group-by-path-input" |
| 44 | + appearance="${InputAppearance.Border}" |
| 45 | + [value]="currentGroupByExpression.subpath" |
| 46 | + (valueChange)="this.onGroupBySubpathChange(currentGroupByExpression, $event)" |
| 47 | + > |
| 48 | + </ht-input> |
| 49 | + </ht-form-field> |
| 50 | + </div> |
| 51 | + </div> |
29 | 52 | </div> |
30 | 53 | ` |
31 | 54 | }) |
32 | 55 | export class ExploreQueryGroupByEditorComponent implements OnChanges { |
33 | 56 | @Input() |
34 | | - public groupByKey?: string; |
| 57 | + public groupByExpression?: AttributeExpression; |
35 | 58 |
|
36 | 59 | @Input() |
37 | 60 | public context?: TraceType; |
38 | 61 |
|
39 | 62 | @Output() |
40 | | - public readonly groupByKeyChange: EventEmitter<string | undefined> = new EventEmitter(); |
| 63 | + public readonly groupByExpressionChange: EventEmitter<AttributeExpression | undefined> = new EventEmitter(); |
41 | 64 |
|
42 | 65 | private readonly contextSubject: Subject<TraceType | undefined> = new ReplaySubject(1); |
43 | | - private readonly incomingGroupByKeySubject: Subject<string | undefined> = new ReplaySubject(1); |
| 66 | + private readonly incomingGroupByExpressionSubject: Subject<AttributeExpression | undefined> = new ReplaySubject(1); |
| 67 | + private readonly groupByExpressionsToEmit: Subject<AttributeExpressionWithMetadata | undefined> = new Subject(); |
44 | 68 |
|
45 | | - public readonly groupByKey$: Observable<string | undefined>; |
46 | | - public readonly groupByKeyOptions$: Observable<SelectOption<string | undefined>[]>; |
| 69 | + public readonly currentGroupByExpression$: Observable<AttributeExpressionWithMetadata | undefined>; |
| 70 | + public readonly groupByAttributeOptions$: Observable<SelectOption<AttributeMetadata | undefined>[]>; |
47 | 71 |
|
48 | 72 | public constructor(private readonly metadataService: MetadataService) { |
49 | | - this.groupByKeyOptions$ = this.contextSubject.pipe(switchMap(context => this.getGroupByOptionsForContext(context))); |
50 | | - |
51 | | - this.groupByKey$ = combineLatest([this.groupByKeyOptions$, this.incomingGroupByKeySubject]).pipe( |
52 | | - map(optionsAndKey => this.resolveKeyFromOptions(...optionsAndKey)) |
| 73 | + this.groupByAttributeOptions$ = this.contextSubject.pipe( |
| 74 | + switchMap(context => this.getGroupByOptionsForContext(context)) |
53 | 75 | ); |
| 76 | + |
| 77 | + this.currentGroupByExpression$ = combineLatest([ |
| 78 | + this.groupByAttributeOptions$, |
| 79 | + merge(this.incomingGroupByExpressionSubject, this.groupByExpressionsToEmit) |
| 80 | + ]).pipe(map(optionsAndKey => this.resolveAttributeFromOptions(...optionsAndKey))); |
| 81 | + |
| 82 | + this.groupByExpressionsToEmit |
| 83 | + .pipe( |
| 84 | + filter(expression => this.isValidExpressionToEmit(expression)), |
| 85 | + debounceTime(500) |
| 86 | + ) |
| 87 | + .subscribe(this.groupByExpressionChange); |
54 | 88 | } |
55 | 89 |
|
56 | 90 | public ngOnChanges(changeObject: TypedSimpleChanges<this>): void { |
57 | 91 | if (changeObject.context) { |
58 | 92 | this.contextSubject.next(this.context); |
59 | 93 | } |
60 | 94 |
|
61 | | - if (changeObject.groupByKey) { |
62 | | - this.incomingGroupByKeySubject.next(this.groupByKey); |
| 95 | + if (changeObject.groupByExpression) { |
| 96 | + this.incomingGroupByExpressionSubject.next(this.groupByExpression); |
63 | 97 | } |
64 | 98 | } |
65 | 99 |
|
66 | | - public onGroupByKeyChange(newKey?: string): void { |
67 | | - this.groupByKeyChange.emit(newKey); |
| 100 | + public onGroupByAttributeChange(newAttribute?: AttributeMetadata): void { |
| 101 | + this.groupByExpressionsToEmit.next(newAttribute && { key: newAttribute.name, metadata: newAttribute }); |
68 | 102 | } |
69 | 103 |
|
70 | | - private resolveKeyFromOptions(options: SelectOption<string | undefined>[], key?: string): string | undefined { |
71 | | - if (key !== undefined && options.find(option => option.value === key)) { |
72 | | - return key; |
| 104 | + public onGroupBySubpathChange(previousExpression: AttributeExpressionWithMetadata, newPath: string): void { |
| 105 | + this.groupByExpressionsToEmit.next({ ...previousExpression, subpath: newPath }); |
| 106 | + } |
| 107 | + |
| 108 | + public supportsSubpath(attribute?: AttributeMetadata): boolean { |
| 109 | + return attribute?.type === AttributeMetadataType.StringMap; |
| 110 | + } |
| 111 | + |
| 112 | + private resolveAttributeFromOptions( |
| 113 | + options: SelectOption<AttributeMetadata | undefined>[], |
| 114 | + expression?: AttributeExpression |
| 115 | + ): AttributeExpressionWithMetadata | undefined { |
| 116 | + if (isNil(expression)) { |
| 117 | + return undefined; |
73 | 118 | } |
74 | 119 |
|
75 | | - return undefined; |
| 120 | + const metadata = options.find(option => option.value?.name === expression.key)?.value; |
| 121 | + |
| 122 | + return metadata && { ...expression, metadata: metadata }; |
76 | 123 | } |
77 | 124 |
|
78 | | - private getGroupByOptionsForContext(context?: TraceType): Observable<SelectOption<string | undefined>[]> { |
| 125 | + private getGroupByOptionsForContext(context?: TraceType): Observable<SelectOption<AttributeMetadata | undefined>[]> { |
79 | 126 | if (context === undefined) { |
80 | 127 | return of([this.getEmptyGroupByOption()]); |
81 | 128 | } |
82 | 129 |
|
83 | 130 | return this.metadataService.getGroupableAttributes(context).pipe( |
84 | 131 | map(attributes => |
85 | 132 | attributes.map(attribute => ({ |
86 | | - value: attribute.name, |
| 133 | + value: attribute, |
87 | 134 | label: this.metadataService.getAttributeDisplayName(attribute) |
88 | 135 | })) |
89 | 136 | ), |
90 | 137 | map(attributeOptions => [this.getEmptyGroupByOption(), ...attributeOptions]) |
91 | 138 | ); |
92 | 139 | } |
93 | 140 |
|
94 | | - private getEmptyGroupByOption(): SelectOption<string | undefined> { |
| 141 | + private getEmptyGroupByOption(): SelectOption<AttributeMetadata | undefined> { |
95 | 142 | return { |
96 | 143 | value: undefined, |
97 | 144 | label: 'None' |
98 | 145 | }; |
99 | 146 | } |
| 147 | + |
| 148 | + private isValidExpressionToEmit(expressionToTest?: AttributeExpressionWithMetadata): boolean { |
| 149 | + // Can't attept to group by a map attribute without a subpath, so we treat that state as invalid and don't emit |
| 150 | + return !(this.supportsSubpath(expressionToTest?.metadata) && isEmpty(expressionToTest?.subpath)); |
| 151 | + } |
| 152 | +} |
| 153 | + |
| 154 | +interface AttributeExpressionWithMetadata extends AttributeExpression { |
| 155 | + metadata: AttributeMetadata; |
100 | 156 | } |
0 commit comments