Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { IconComponent } from '../icon/icon.component';
import { DownloadJsonComponent } from './download-json.component';
import { DownloadJsonModule } from './download-json.module';

describe('Button Component', () => {
describe('Download Json Component', () => {
let spectator: Spectator<DownloadJsonComponent>;
const mockElement = document.createElement('a');
const createElementSpy = jest.fn().mockReturnValue(mockElement);
Expand Down Expand Up @@ -53,8 +53,7 @@ describe('Button Component', () => {
spyOn(spectator.component, 'triggerDownload');

expect(spectator.component.dataLoading).toBe(false);
expect(spectator.component.fileName).toBe('download');
expect(spectator.component.tooltip).toBe('Download Json');
expect(spectator.component.fileName).toBe('download.json');
const element = spectator.query('.download-json');
expect(element).toExist();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { NotificationService } from '../notification/notification.service';
changeDetection: ChangeDetectionStrategy.OnPush,
styleUrls: ['./download-json.component.scss'],
template: `
<div class="download-json" [htTooltip]="this.tooltip" (click)="this.triggerDownload()">
<div class="download-json" (click)="this.triggerDownload()">
<ht-button
*ngIf="!this.dataLoading"
class="download-button"
Expand All @@ -29,10 +29,7 @@ export class DownloadJsonComponent {
public dataSource!: Observable<unknown>;

@Input()
public fileName: string = 'download';

@Input()
public tooltip: string = 'Download Json';
public fileName: string = 'download.json';

public dataLoading: boolean = false;
private readonly dlJsonAnchorElement: HTMLAnchorElement;
Expand Down Expand Up @@ -72,7 +69,7 @@ export class DownloadJsonComponent {
'href',
`data:text/json;charset=utf-8,${encodeURIComponent(data)}`
);
this.renderer.setAttribute(this.dlJsonAnchorElement, 'download', `${this.fileName}.json`);
this.renderer.setAttribute(this.dlJsonAnchorElement, 'download', this.fileName);
this.renderer.setAttribute(this.dlJsonAnchorElement, 'display', 'none');
this.dlJsonAnchorElement.click();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,11 @@ import { NgModule } from '@angular/core';
import { ButtonModule } from '../button/button.module';
import { IconModule } from '../icon/icon.module';
import { NotificationModule } from '../notification/notification.module';
import { TooltipModule } from '../tooltip/tooltip.module';
import { DownloadJsonComponent } from './download-json.component';

@NgModule({
declarations: [DownloadJsonComponent],
imports: [CommonModule, ButtonModule, NotificationModule, IconModule, TooltipModule],
imports: [CommonModule, ButtonModule, NotificationModule, IconModule],
exports: [DownloadJsonComponent]
})
export class DownloadJsonModule {}
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,13 @@ import { TraceDetails, TraceDetailService } from './trace-detail.service';
<div class="separation"></div>

<ht-copy-shareable-link-to-clipboard class="share"></ht-copy-shareable-link-to-clipboard>

<ht-download-json
class="download"
[dataSource]="this.exportSpans$"
fileName="{{ traceDetails.id }}.json"
htTooltip="Download Trace as Json"
></ht-download-json>
</div>
</div>

Expand All @@ -60,13 +67,15 @@ export class TraceDetailPageComponent {
public static readonly TRACE_ID_PARAM_NAME: string = 'id';

public readonly traceDetails$: Observable<TraceDetails>;
public readonly exportSpans$: Observable<string>;

public constructor(
private readonly subscriptionLifecycle: SubscriptionLifecycle,
private readonly navigationService: NavigationService,
private readonly traceDetailService: TraceDetailService
) {
this.traceDetails$ = this.traceDetailService.fetchTraceDetails();
this.exportSpans$ = this.traceDetailService.fetchExportSpans();
}

public onDashboardReady(dashboard: Dashboard): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import { RouterModule } from '@angular/router';
import { FormattingModule, TraceRoute } from '@hypertrace/common';
import {
CopyShareableLinkToClipboardModule,
DownloadJsonModule,
IconModule,
LabelModule,
LoadAsyncModule,
SummaryValueModule
SummaryValueModule,
TooltipModule
} from '@hypertrace/components';
import { NavigableDashboardModule } from '../../shared/dashboard/dashboard-wrapper/navigable-dashboard.module';
import { TracingDashboardModule } from '../../shared/dashboard/tracing-dashboard.module';
Expand All @@ -30,9 +32,11 @@ const ROUTE_CONFIG: TraceRoute[] = [
TracingDashboardModule,
IconModule,
SummaryValueModule,
TooltipModule,
LoadAsyncModule,
FormattingModule,
CopyShareableLinkToClipboardModule,
DownloadJsonModule,
NavigableDashboardModule.withDefaultDashboards(traceDetailDashboard)
]
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import { Observable, Subject } from 'rxjs';
import { map, shareReplay, switchMap, takeUntil } from 'rxjs/operators';
import { Trace, traceIdKey, TraceType, traceTypeKey } from '../../shared/graphql/model/schema/trace';
import { SpecificationBuilder } from '../../shared/graphql/request/builders/specification/specification-builder';
import {
ExportSpansGraphQlQueryHandlerService,
EXPORT_SPANS_GQL_REQUEST
} from '../../shared/graphql/request/handlers/traces/export-spans-graphql-query-handler.service';
import {
TraceGraphQlQueryHandlerService,
TRACE_GQL_REQUEST
Expand Down Expand Up @@ -82,6 +86,21 @@ export class TraceDetailService implements OnDestroy {
);
}

public fetchExportSpans(): Observable<string> {
return this.routeIds$.pipe(
switchMap(routeIds =>
this.graphQlQueryService.query<ExportSpansGraphQlQueryHandlerService, string>({
requestType: EXPORT_SPANS_GQL_REQUEST,
traceId: routeIds.traceId,
timestamp: this.dateCoercer.coerce(routeIds.startTime),
limit: 1000
})
),
takeUntil(this.destroyed$),
shareReplay(1)
);
}

private fetchTrace(traceId: string, spanId?: string, startTime?: string | number): Observable<Trace> {
return this.graphQlQueryService.query<TraceGraphQlQueryHandlerService, Trace>({
requestType: TRACE_GQL_REQUEST,
Expand Down
1 change: 1 addition & 0 deletions projects/distributed-tracing/src/public-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export * from './shared/dashboard/widgets/table/table-widget-view-toggle.model';
export * from './shared/services/filter-builder/graphql-filter-builder.service';

// Handlers
export * from './shared/graphql/request/handlers/traces/export-spans-graphql-query-handler.service';
export * from './shared/graphql/request/handlers/traces/trace-graphql-query-handler.service';
export * from './shared/graphql/request/handlers/traces/traces-graphql-query-handler.service';
export * from './shared/graphql/request/handlers/spans/span-graphql-query-handler.service';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { SpanGraphQlQueryHandlerService } from '../../../graphql/request/handlers/spans/span-graphql-query-handler.service';
import { SpansGraphQlQueryHandlerService } from '../../../graphql/request/handlers/spans/spans-graphql-query-handler.service';
import { ExportSpansGraphQlQueryHandlerService } from '../../../graphql/request/handlers/traces/export-spans-graphql-query-handler.service';
import { TraceGraphQlQueryHandlerService } from '../../../graphql/request/handlers/traces/trace-graphql-query-handler.service';
import { TracesGraphQlQueryHandlerService } from '../../../graphql/request/handlers/traces/traces-graphql-query-handler.service';
import { MetadataGraphQlQueryHandlerService } from '../../../services/metadata/handler/metadata-graphql-query-handler.service';

export const GRAPHQL_DATA_SOURCE_HANDLER_PROVIDERS = [
ExportSpansGraphQlQueryHandlerService,
TracesGraphQlQueryHandlerService,
TraceGraphQlQueryHandlerService,
SpansGraphQlQueryHandlerService,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { FixedTimeRange, TimeDuration, TimeRangeService, TimeUnit } from '@hypertrace/common';
import { GraphQlEnumArgument } from '@hypertrace/graphql-client';
import { createServiceFactory, mockProvider } from '@ngneat/spectator/jest';
import { GraphQlFilterType } from '../../../model/schema/filter/graphql-filter';
import { GraphQlTimeRange } from '../../../model/schema/timerange/graphql-time-range';
import { TRACE_SCOPE } from '../../../model/schema/trace';
import {
ExportSpansGraphQlQueryHandlerService,
EXPORT_SPANS_GQL_REQUEST,
GraphQlExportSpansRequest
} from './export-spans-graphql-query-handler.service';

describe('ExportSpansGraphQlQueryHandlerService', () => {
const createService = createServiceFactory({
service: ExportSpansGraphQlQueryHandlerService,
providers: [
mockProvider(TimeRangeService, {
getCurrentTimeRange: jest
.fn()
.mockReturnValue(new FixedTimeRange(new Date(1568907645141), new Date(1568911245141)))
})
]
});

const testTimeRange = GraphQlTimeRange.fromTimeRange(
new FixedTimeRange(new Date(1568907645141), new Date(1568911245141))
);
const buildRequest = (timestamp?: Date): GraphQlExportSpansRequest => ({
requestType: EXPORT_SPANS_GQL_REQUEST,
traceId: 'test-id',
timestamp: timestamp,
limit: 1
});

test('matches request', () => {
const spectator = createService();
expect(spectator.service.matchesRequest(buildRequest())).toBe(true);
expect(spectator.service.matchesRequest({ requestType: 'other' })).toBe(false);
});

test('produces expected graphql', () => {
const spectator = createService();
const expected = spectator.service.convertRequest(buildRequest());
expect(expected).toEqual({
path: 'exportSpans',
arguments: [
{
name: 'limit',
value: 1
},
{
name: 'between',
value: {
startTime: new Date(testTimeRange.from),
endTime: new Date(testTimeRange.to)
}
},
{
name: 'filterBy',
value: [
{
operator: new GraphQlEnumArgument('EQUALS'),
value: 'test-id',
type: new GraphQlEnumArgument(GraphQlFilterType.Id),
idType: new GraphQlEnumArgument(TRACE_SCOPE)
}
]
}
],
children: [
{
path: 'result'
}
]
});
});

test('produces expected graphql with timestamp', () => {
const spectator = createService();
const traceTimestamp = new Date(new TimeDuration(30, TimeUnit.Minute).toMillis());
const expected = spectator.service.convertRequest(buildRequest(traceTimestamp));
expect(expected).toEqual({
path: 'exportSpans',
arguments: [
{
name: 'limit',
value: 1
},
{
name: 'between',
value: {
startTime: new Date(0),
endTime: new Date(traceTimestamp.getTime() * 2)
}
},
{
name: 'filterBy',
value: [
{
operator: new GraphQlEnumArgument('EQUALS'),
value: 'test-id',
type: new GraphQlEnumArgument(GraphQlFilterType.Id),
idType: new GraphQlEnumArgument(TRACE_SCOPE)
}
]
}
],
children: [
{
path: 'result'
}
]
});
});

test('converts response', () => {
const spectator = createService();
const exportSpansResponse = {
result: '{}'
};

expect(spectator.service.convertResponse(exportSpansResponse)).toEqual('{}');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { Injectable } from '@angular/core';
import { TimeDuration, TimeRangeService, TimeUnit } from '@hypertrace/common';
import { GraphQlHandlerType, GraphQlQueryHandler, GraphQlSelection } from '@hypertrace/graphql-client';
import { GlobalGraphQlFilterService } from '../../../model/schema/filter/global-graphql-filter.service';
import { GraphQlFilter } from '../../../model/schema/filter/graphql-filter';
import { GraphQlIdFilter } from '../../../model/schema/filter/id/graphql-id-filter';
import { GraphQlTimeRange } from '../../../model/schema/timerange/graphql-time-range';
import { resolveTraceType, TraceType } from '../../../model/schema/trace';
import { GraphQlArgumentBuilder } from '../../builders/argument/graphql-argument-builder';

@Injectable({ providedIn: 'root' })
export class ExportSpansGraphQlQueryHandlerService
implements GraphQlQueryHandler<GraphQlExportSpansRequest, string | undefined> {
public readonly type: GraphQlHandlerType.Query = GraphQlHandlerType.Query;
private readonly argBuilder: GraphQlArgumentBuilder = new GraphQlArgumentBuilder();

public constructor(
private readonly timeRangeService: TimeRangeService,
private readonly globalGraphQlFilterService: GlobalGraphQlFilterService
) {}

public matchesRequest(request: unknown): request is GraphQlExportSpansRequest {
return (
typeof request === 'object' &&
request !== null &&
(request as Partial<GraphQlExportSpansRequest>).requestType === EXPORT_SPANS_GQL_REQUEST
);
}

public convertRequest(request: GraphQlExportSpansRequest): GraphQlSelection {
const timeRange = this.buildTimeRange(request.timestamp);

return {
path: 'exportSpans',
arguments: [
this.argBuilder.forLimit(request.limit),
this.argBuilder.forTimeRange(timeRange),
...this.argBuilder.forFilters(
this.globalGraphQlFilterService.mergeGlobalFilters(resolveTraceType(request.traceType), [
this.buildTraceIdFilter(request)
])
)
],
children: [
{
path: 'result'
}
]
};
}

public convertResponse(response: ExportSpansResponse): string | undefined {
return response.result;
}

private buildTraceIdFilter(request: GraphQlExportSpansRequest): GraphQlFilter {
return new GraphQlIdFilter(request.traceId, resolveTraceType(request.traceType));
}

protected buildTimeRange(timestamp?: Date): GraphQlTimeRange {
const duration = new TimeDuration(30, TimeUnit.Minute);

return timestamp
? new GraphQlTimeRange(timestamp.getTime() - duration.toMillis(), timestamp.getTime() + duration.toMillis())
: GraphQlTimeRange.fromTimeRange(this.timeRangeService.getCurrentTimeRange());
}
}

export const EXPORT_SPANS_GQL_REQUEST = Symbol('GraphQL Export Spans Request');

export interface GraphQlExportSpansRequest {
requestType: typeof EXPORT_SPANS_GQL_REQUEST;
traceType?: TraceType;
traceId: string;
limit: number;
timestamp?: Date;
}

export interface ExportSpansResponse {
result: string;
}