Skip to content

Commit cd76fed

Browse files
feat: persist explorer state in url
1 parent b1d8250 commit cd76fed

File tree

9 files changed

+332
-182
lines changed

9 files changed

+332
-182
lines changed

projects/common/src/navigation/navigation.service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,7 @@ export class NavigationService {
376376
}
377377

378378
export interface QueryParamObject extends Params {
379-
[key: string]: string | string[] | number | number[] | undefined;
379+
[key: string]: string | string[] | boolean | boolean[] | number | number[] | undefined;
380380
}
381381

382382
export type NavigationPath = string | (string | Dictionary<string>)[];

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

Lines changed: 105 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { HttpClientTestingModule } from '@angular/common/http/testing';
2-
import { discardPeriodicTasks, fakeAsync } from '@angular/core/testing';
2+
import { Provider } from '@angular/core';
3+
import { fakeAsync } from '@angular/core/testing';
34
import { ActivatedRoute, convertToParamMap } from '@angular/router';
45
import { RouterTestingModule } from '@angular/router/testing';
56
import { IconLibraryTestingModule } from '@hypertrace/assets-library';
@@ -16,7 +17,8 @@ import {
1617
FilterAttributeType,
1718
FilterBarComponent,
1819
FilterBuilderLookupService,
19-
FilterOperator
20+
FilterOperator,
21+
ToggleGroupComponent
2022
} from '@hypertrace/components';
2123
import { GraphQlRequestService } from '@hypertrace/graphql-client';
2224
import { getMockFlexLayoutProviders, patchRouterNavigateForTest } from '@hypertrace/test-utils';
@@ -25,6 +27,10 @@ import { EMPTY, NEVER, of } from 'rxjs';
2527
import { startWith } from 'rxjs/operators';
2628
import { CartesianSeriesVisualizationType } from '../../shared/components/cartesian/chart';
2729
import { ExploreQueryEditorComponent } from '../../shared/components/explore-query-editor/explore-query-editor.component';
30+
import { ExploreQueryGroupByEditorComponent } from '../../shared/components/explore-query-editor/group-by/explore-query-group-by-editor.component';
31+
import { ExploreQueryIntervalEditorComponent } from '../../shared/components/explore-query-editor/interval/explore-query-interval-editor.component';
32+
import { ExploreQueryLimitEditorComponent } from '../../shared/components/explore-query-editor/limit/explore-query-limit-editor.component';
33+
import { ExploreQuerySeriesEditorComponent } from '../../shared/components/explore-query-editor/series/explore-query-series-editor.component';
2834
import { MetricAggregationType } from '../../shared/graphql/model/metrics/metric-aggregation';
2935
import { GraphQlFieldFilter } from '../../shared/graphql/model/schema/filter/field/graphql-field-filter';
3036
import { GraphQlOperatorType } from '../../shared/graphql/model/schema/filter/graphql-filter';
@@ -109,27 +115,37 @@ describe('Explorer component', () => {
109115

110116
const detectQueryChange = () => {
111117
spectator.detectChanges(); // Detect whatever caused the change
112-
spectator.tick(200); // Query emits async, tick here triggers building the DOM for the query
113-
discardPeriodicTasks(); // Some of the newly instantiated components also uses async, need to wait for them to settle
114-
spectator.tick(200);
118+
spectator.tick(50); // Query emits async, tick here triggers building the DOM for the query
119+
// Break up the ticks into multiple to account for various async handoffs
120+
spectator.tick();
121+
spectator.tick(100);
115122
};
116123

117-
const init = (...params: Parameters<typeof createComponent>) => {
118-
spectator = createComponent(...params);
124+
const init = (...mockProviders: Provider[]) => {
125+
spectator = createComponent({
126+
providers: [
127+
{
128+
provide: ActivatedRoute,
129+
useValue: {
130+
queryParamMap: of(convertToParamMap({}))
131+
}
132+
},
133+
...mockProviders
134+
]
135+
});
119136
spectator.tick();
120137
patchRouterNavigateForTest(spectator);
121138
detectQueryChange();
122139
querySpy = spectator.inject(GraphQlRequestService).query;
123140
};
124141

125142
test('fires query on init for traces', fakeAsync(() => {
126-
init({
127-
providers: [
128-
mockProvider(GraphQlRequestService, {
129-
query: jest.fn().mockReturnValueOnce(of(mockAttributes)).mockReturnValue(EMPTY)
130-
})
131-
]
132-
});
143+
init(
144+
mockProvider(GraphQlRequestService, {
145+
query: jest.fn().mockReturnValueOnce(of(mockAttributes)).mockReturnValue(EMPTY)
146+
})
147+
);
148+
133149
// Traces tab is auto selected
134150
expect(querySpy).toHaveBeenNthCalledWith(
135151
2,
@@ -142,8 +158,10 @@ describe('Explorer component', () => {
142158
expect.objectContaining({})
143159
);
144160

145-
expect(querySpy).toHaveBeenNthCalledWith(
146-
3,
161+
// RunFakeRxjs(({ expectObservable }) => {
162+
// ExpectObservable(spectator.component.resultsDashboard$).toBe('x', { x: undefined });
163+
// });
164+
expect(querySpy).toHaveBeenCalledWith(
147165
expect.objectContaining({
148166
requestType: TRACES_GQL_REQUEST,
149167
filters: [],
@@ -154,13 +172,11 @@ describe('Explorer component', () => {
154172
}));
155173

156174
test('fires query on filter change for traces', fakeAsync(() => {
157-
init({
158-
providers: [
159-
mockProvider(GraphQlRequestService, {
160-
query: jest.fn().mockReturnValueOnce(of(mockAttributes)).mockReturnValue(EMPTY)
161-
})
162-
]
163-
});
175+
init(
176+
mockProvider(GraphQlRequestService, {
177+
query: jest.fn().mockReturnValueOnce(of(mockAttributes)).mockReturnValue(EMPTY)
178+
})
179+
);
164180
const filterBar = spectator.query(FilterBarComponent)!;
165181

166182
// tslint:disable-next-line: no-object-literal-type-assertion
@@ -202,13 +218,11 @@ describe('Explorer component', () => {
202218
}));
203219

204220
test('fires query on init for spans', fakeAsync(() => {
205-
init({
206-
providers: [
207-
mockProvider(GraphQlRequestService, {
208-
query: jest.fn().mockReturnValueOnce(of(mockAttributes)).mockReturnValue(EMPTY)
209-
})
210-
]
211-
});
221+
init(
222+
mockProvider(GraphQlRequestService, {
223+
query: jest.fn().mockReturnValueOnce(of(mockAttributes)).mockReturnValue(EMPTY)
224+
})
225+
);
212226
querySpy.mockClear();
213227

214228
// Select Spans tab
@@ -238,13 +252,11 @@ describe('Explorer component', () => {
238252
}));
239253

240254
test('fires query on init for traces', fakeAsync(() => {
241-
init({
242-
providers: [
243-
mockProvider(GraphQlRequestService, {
244-
query: jest.fn().mockReturnValueOnce(of(mockAttributes)).mockReturnValue(EMPTY)
245-
})
246-
]
247-
});
255+
init(
256+
mockProvider(GraphQlRequestService, {
257+
query: jest.fn().mockReturnValueOnce(of(mockAttributes)).mockReturnValue(EMPTY)
258+
})
259+
);
248260
// Select traces tab
249261
spectator.click(spectator.queryAll('ht-toggle-item')[1]);
250262
detectQueryChange();
@@ -291,13 +303,11 @@ describe('Explorer component', () => {
291303
}));
292304

293305
test('traces table fires query on series change', fakeAsync(() => {
294-
init({
295-
providers: [
296-
mockProvider(GraphQlRequestService, {
297-
query: jest.fn().mockReturnValueOnce(of(mockAttributes)).mockReturnValue(EMPTY)
298-
})
299-
]
300-
});
306+
init(
307+
mockProvider(GraphQlRequestService, {
308+
query: jest.fn().mockReturnValueOnce(of(mockAttributes)).mockReturnValue(EMPTY)
309+
})
310+
);
301311
spectator.query(ExploreQueryEditorComponent)!.setSeries([buildSeries('second', MetricAggregationType.Average)]);
302312

303313
detectQueryChange();
@@ -315,13 +325,11 @@ describe('Explorer component', () => {
315325
}));
316326

317327
test('visualization fires query on series change', fakeAsync(() => {
318-
init({
319-
providers: [
320-
mockProvider(GraphQlRequestService, {
321-
query: jest.fn().mockReturnValueOnce(of(mockAttributes)).mockReturnValue(EMPTY)
322-
})
323-
]
324-
});
328+
init(
329+
mockProvider(GraphQlRequestService, {
330+
query: jest.fn().mockReturnValueOnce(of(mockAttributes)).mockReturnValue(EMPTY)
331+
})
332+
);
325333
querySpy.mockClear();
326334

327335
spectator.query(ExploreQueryEditorComponent)!.setSeries([buildSeries('second', MetricAggregationType.Average)]);
@@ -340,46 +348,60 @@ describe('Explorer component', () => {
340348
);
341349
}));
342350

343-
test('updates URL with query param when context toggled', fakeAsync(() => {
351+
test('updates URL with query param when query updated', fakeAsync(() => {
344352
init();
345353
const queryParamChangeSpy = spyOn(spectator.inject(NavigationService), 'addQueryParametersToUrl');
346-
// Select Spans tab
347354
spectator.click(spectator.queryAll('ht-toggle-item')[1]);
355+
spectator.query(ExploreQueryEditorComponent)!.setSeries([buildSeries('second', MetricAggregationType.Average)]);
356+
spectator.query(ExploreQueryEditorComponent)!.setInterval(new TimeDuration(30, TimeUnit.Second));
357+
spectator.query(ExploreQueryEditorComponent)!.updateGroupByKey(
358+
{
359+
keys: ['apiName'],
360+
limit: 6,
361+
includeRest: true
362+
},
363+
'apiName'
364+
);
348365
detectQueryChange();
349-
expect(queryParamChangeSpy).toHaveBeenLastCalledWith(expect.objectContaining({ scope: 'spans' }));
350-
351-
// Select Endpoint traces tab
352-
spectator.click(spectator.queryAll('ht-toggle-item')[0]);
353-
detectQueryChange();
354-
expect(queryParamChangeSpy).toHaveBeenLastCalledWith(expect.objectContaining({ scope: 'endpoint-traces' }));
355-
}));
356-
357-
test('selects tab based on url', fakeAsync(() => {
358-
init({
359-
providers: [
360-
{
361-
provide: ActivatedRoute,
362-
useValue: {
363-
queryParamMap: of(convertToParamMap({ scope: 'spans' }))
364-
}
365-
}
366-
]
366+
expect(queryParamChangeSpy).toHaveBeenLastCalledWith({
367+
scope: 'spans',
368+
series: ['column:avg(second)'],
369+
group: 'apiName',
370+
limit: 6,
371+
other: true,
372+
interval: '30s'
367373
});
368-
expect(spectator.component.context).toBe(SPAN_SCOPE);
369374
}));
370375

371-
test('defaults to endpoints and sets url', fakeAsync(() => {
376+
test('sets state based on url', fakeAsync(() => {
372377
init({
373-
providers: [
374-
{
375-
provide: ActivatedRoute,
376-
useValue: {
377-
queryParamMap: of(convertToParamMap({}))
378-
}
379-
}
380-
]
378+
provide: ActivatedRoute,
379+
useValue: {
380+
queryParamMap: of(
381+
convertToParamMap({
382+
scope: 'spans',
383+
series: 'line:distinct_count(apiName)',
384+
group: 'apiName',
385+
limit: '6',
386+
other: 'true',
387+
interval: '30s'
388+
})
389+
)
390+
}
381391
});
382-
expect(spectator.component.context).toBe(ObservabilityTraceType.Api);
383-
expect(spectator.inject(NavigationService).getQueryParameter('scope', 'unset')).toEqual('endpoint-traces');
392+
expect(spectator.query(ToggleGroupComponent)?.activeItem?.label).toBe('Spans');
393+
expect(spectator.query(ExploreQueryGroupByEditorComponent)?.groupByKey).toBe('apiName');
394+
expect(spectator.query(ExploreQueryLimitEditorComponent)?.limit).toBe(6);
395+
expect(spectator.query(ExploreQueryLimitEditorComponent)?.includeRest).toBe(true);
396+
expect(spectator.query(ExploreQuerySeriesEditorComponent)?.series).toEqual({
397+
specification: expect.objectContaining({
398+
aggregation: MetricAggregationType.DistinctCount,
399+
name: 'apiName'
400+
}),
401+
visualizationOptions: { type: CartesianSeriesVisualizationType.Line }
402+
});
403+
expect(spectator.query(ExploreQueryIntervalEditorComponent)?.interval).toEqual(
404+
new TimeDuration(30, TimeUnit.Second)
405+
);
384406
}));
385407
});

0 commit comments

Comments
 (0)