Skip to content

Commit bd713d7

Browse files
feat: explorer group by subpath support
1 parent 2751bcd commit bd713d7

File tree

7 files changed

+146
-57
lines changed

7 files changed

+146
-57
lines changed

projects/observability/src/pages/explorer/explorer.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ export class ExplorerComponent {
308308
private tryDecodeAttributeExpression(expressionString: string): [AttributeExpression] | [] {
309309
const [key, subpath] = expressionString.split('__');
310310

311-
return [{ key: key, ...(isEmpty(subpath) ? { subpath: subpath } : {}) }];
311+
return [{ key: key, ...(!isEmpty(subpath) ? { subpath: subpath } : {}) }];
312312
}
313313
}
314314
interface ContextToggleItem extends ToggleItem<ExplorerContextScope> {

projects/observability/src/shared/components/explore-query-editor/explore-query-editor.component.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ import {
3939
<ht-explore-query-group-by-editor
4040
class="group-by"
4141
[context]="currentVisualization.context"
42-
[groupByKey]="(currentVisualization.groupBy?.keyExpressions)?.[0]?.key"
43-
(groupByKeyChange)="this.updateGroupByKey(currentVisualization.groupBy, $event)"
42+
[groupByExpression]="(currentVisualization.groupBy?.keyExpressions)[0]"
43+
(groupByExpressionChange)="this.updateGroupByExpression(currentVisualization.groupBy, $event)"
4444
></ht-explore-query-group-by-editor>
4545
4646
<ht-explore-query-limit-editor
@@ -104,16 +104,16 @@ export class ExploreQueryEditorComponent implements OnChanges, OnInit {
104104
}
105105

106106
if (changeObject.groupBy && this.groupBy?.keyExpressions.length) {
107-
this.updateGroupByKey(this.groupBy, this.groupBy.keyExpressions[0]?.key);
107+
this.updateGroupByExpression(this.groupBy, this.groupBy.keyExpressions[0]);
108108
}
109109
}
110110

111111
public setSeries(series: ExploreSeries[]): void {
112112
this.visualizationBuilder.setSeries(series);
113113
}
114114

115-
public updateGroupByKey(groupBy?: GraphQlGroupBy, key?: string): void {
116-
if (key === undefined) {
115+
public updateGroupByExpression(groupBy?: GraphQlGroupBy, keyExpression?: AttributeExpression): void {
116+
if (keyExpression === undefined) {
117117
this.visualizationBuilder.groupBy();
118118
} else {
119119
this.visualizationBuilder.groupBy(

projects/observability/src/shared/components/explore-query-editor/explore-query-editor.module.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
11
import { CommonModule } from '@angular/common';
22
import { NgModule } from '@angular/core';
3-
import { ButtonModule, InputModule, SelectModule, TooltipModule, TraceCheckboxModule } from '@hypertrace/components';
3+
import {
4+
ButtonModule,
5+
FormFieldModule,
6+
InputModule,
7+
LetAsyncModule,
8+
SelectModule,
9+
TooltipModule,
10+
TraceCheckboxModule
11+
} from '@hypertrace/components';
412
import { IntervalSelectModule } from '../interval-select/interval-select.module';
513
import { ExploreQueryEditorComponent } from './explore-query-editor.component';
614
import { ExploreQueryGroupByEditorComponent } from './group-by/explore-query-group-by-editor.component';
@@ -26,7 +34,9 @@ import { ExploreQuerySeriesGroupEditorComponent } from './series/explore-query-s
2634
TooltipModule,
2735
InputModule,
2836
IntervalSelectModule,
29-
TraceCheckboxModule
37+
TraceCheckboxModule,
38+
LetAsyncModule,
39+
FormFieldModule
3040
]
3141
})
3242
export class ExploreQueryEditorModule {}

projects/observability/src/shared/components/explore-query-editor/group-by/explore-query-group-by-editor.component.scss

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,29 @@
33

44
.group-by-container {
55
display: flex;
6-
flex-direction: column;
6+
flex-direction: row;
7+
gap: 24px;
78

8-
.group-by-label {
9-
@include body-1-medium($gray-9);
10-
height: 32px;
11-
line-height: 32px;
12-
margin-bottom: 12px;
9+
.group-by-input-container {
10+
display: flex;
11+
flex-direction: column;
12+
13+
.group-by-label {
14+
@include body-1-medium($gray-9);
15+
height: 32px;
16+
line-height: 32px;
17+
margin-bottom: 12px;
18+
}
19+
20+
.group-by-path-wrapper {
21+
width: 100px;
22+
23+
.group-by-path-input {
24+
@include body-2-regular($gray-9);
25+
width: 100%;
26+
height: 100%;
27+
line-height: 32px;
28+
}
29+
}
1330
}
1431
}
Lines changed: 95 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,156 @@
11
import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnChanges, Output } from '@angular/core';
22
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';
69
import { TraceType } from '../../../graphql/model/schema/trace';
710
import { MetadataService } from '../../../services/metadata/metadata.service';
8-
911
@Component({
1012
selector: 'ht-explore-query-group-by-editor',
1113
styleUrls: ['./explore-query-group-by-editor.component.scss'],
1214
changeDetection: ChangeDetectionStrategy.OnPush,
1315
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>
2952
</div>
3053
`
3154
})
3255
export class ExploreQueryGroupByEditorComponent implements OnChanges {
3356
@Input()
34-
public groupByKey?: string;
57+
public groupByExpression?: AttributeExpression;
3558

3659
@Input()
3760
public context?: TraceType;
3861

3962
@Output()
40-
public readonly groupByKeyChange: EventEmitter<string | undefined> = new EventEmitter();
63+
public readonly groupByExpressionChange: EventEmitter<AttributeExpression | undefined> = new EventEmitter();
4164

4265
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();
4468

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>[]>;
4771

4872
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))
5375
);
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);
5488
}
5589

5690
public ngOnChanges(changeObject: TypedSimpleChanges<this>): void {
5791
if (changeObject.context) {
5892
this.contextSubject.next(this.context);
5993
}
6094

61-
if (changeObject.groupByKey) {
62-
this.incomingGroupByKeySubject.next(this.groupByKey);
95+
if (changeObject.groupByExpression) {
96+
this.incomingGroupByExpressionSubject.next(this.groupByExpression);
6397
}
6498
}
6599

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 });
68102
}
69103

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;
73118
}
74119

75-
return undefined;
120+
const metadata = options.find(option => option.value?.name === expression.key)?.value;
121+
122+
return metadata && { ...expression, metadata: metadata };
76123
}
77124

78-
private getGroupByOptionsForContext(context?: TraceType): Observable<SelectOption<string | undefined>[]> {
125+
private getGroupByOptionsForContext(context?: TraceType): Observable<SelectOption<AttributeMetadata | undefined>[]> {
79126
if (context === undefined) {
80127
return of([this.getEmptyGroupByOption()]);
81128
}
82129

83130
return this.metadataService.getGroupableAttributes(context).pipe(
84131
map(attributes =>
85132
attributes.map(attribute => ({
86-
value: attribute.name,
133+
value: attribute,
87134
label: this.metadataService.getAttributeDisplayName(attribute)
88135
}))
89136
),
90137
map(attributeOptions => [this.getEmptyGroupByOption(), ...attributeOptions])
91138
);
92139
}
93140

94-
private getEmptyGroupByOption(): SelectOption<string | undefined> {
141+
private getEmptyGroupByOption(): SelectOption<AttributeMetadata | undefined> {
95142
return {
96143
value: undefined,
97144
label: 'None'
98145
};
99146
}
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;
100156
}

projects/observability/src/shared/graphql/request/builders/specification/explore/explore-specification-builder.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export class ExploreSpecificationBuilder {
4040
expression: AttributeExpression,
4141
aggregation?: MetricAggregationType
4242
): ExploreSpecification {
43-
const expressionString = this.attributeExpressionAsString(expression);
43+
const expressionString = this.attributeExpressionAsString(expression).replaceAll('.', '_');
4444
const queryAlias = aggregation === undefined ? expressionString : `${aggregation}_${expressionString}`;
4545

4646
return {

projects/observability/src/shared/services/metadata/metadata.service.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import { GraphQlRequestService } from '@hypertrace/graphql-client';
55
import { isEmpty, isNil } from 'lodash-es';
66
import { Observable, of } from 'rxjs';
77
import { catchError, defaultIfEmpty, filter, map, shareReplay, tap, throwIfEmpty } from 'rxjs/operators';
8-
import { AttributeMetadata, toFilterAttributeType } from '../../graphql/model/metadata/attribute-metadata';
8+
import {
9+
AttributeMetadata,
10+
AttributeMetadataType,
11+
toFilterAttributeType
12+
} from '../../graphql/model/metadata/attribute-metadata';
913
import {
1014
addAggregationToDisplayName,
1115
getAggregationDisplayName,
@@ -52,8 +56,10 @@ export class MetadataService {
5256

5357
public getGroupableAttributes(scope: string): ReplayObservable<AttributeMetadata[]> {
5458
return this.getServerDefinedAttributes(scope).pipe(
55-
// Can only group by strings right now
56-
map(attributes => attributes.filter(attribute => attribute.groupable))
59+
// Can only group by strings or string map subpaths right now
60+
map(attributes =>
61+
attributes.filter(attribute => attribute.groupable || attribute.type === AttributeMetadataType.StringMap)
62+
)
5763
);
5864
}
5965

0 commit comments

Comments
 (0)