From 02090f71d221157f18d177972f8e38419221a34b Mon Sep 17 00:00:00 2001 From: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> Date: Wed, 22 Nov 2023 13:15:29 +0100 Subject: [PATCH] `@grafana/data`: Add `serializeParams` (#78468) * Move serializeParams to @grafana/data * Update comment * Update solution in Tempo * Fix type assertions * Use toUrlParams in serializeParams * Update data sources * Update * Update packages/grafana-data/src/utils/url.ts --- .betterer.results | 8 ++--- packages/grafana-data/src/utils/url.test.ts | 30 ++++++++++++++++++ packages/grafana-data/src/utils/url.ts | 31 ++++++++++++++++--- public/app/core/utils/fetch.ts | 2 +- .../plugins/datasource/jaeger/datasource.ts | 4 +-- .../app/plugins/datasource/loki/datasource.ts | 4 +-- .../plugins/datasource/tempo/datasource.ts | 6 ++-- .../plugins/datasource/zipkin/datasource.ts | 5 ++- 8 files changed, 70 insertions(+), 20 deletions(-) diff --git a/.betterer.results b/.betterer.results index 0aa59dc2050..ae90f276b56 100644 --- a/.betterer.results +++ b/.betterer.results @@ -6486,11 +6486,9 @@ exports[`better eslint`] = { [0, 0, 0, "Do not use any type assertions.", "4"], [0, 0, 0, "Unexpected any. Specify a different type.", "5"], [0, 0, 0, "Do not use any type assertions.", "6"], - [0, 0, 0, "Do not use any type assertions.", "7"], - [0, 0, 0, "Do not use any type assertions.", "8"], - [0, 0, 0, "Unexpected any. Specify a different type.", "9"], - [0, 0, 0, "Unexpected any. Specify a different type.", "10"], - [0, 0, 0, "Unexpected any. Specify a different type.", "11"] + [0, 0, 0, "Unexpected any. Specify a different type.", "7"], + [0, 0, 0, "Unexpected any. Specify a different type.", "8"], + [0, 0, 0, "Unexpected any. Specify a different type.", "9"] ], "public/app/plugins/datasource/tempo/language_provider.ts:5381": [ [0, 0, 0, "Unexpected any. Specify a different type.", "0"] diff --git a/packages/grafana-data/src/utils/url.test.ts b/packages/grafana-data/src/utils/url.test.ts index b486564587f..58e20733593 100644 --- a/packages/grafana-data/src/utils/url.test.ts +++ b/packages/grafana-data/src/utils/url.test.ts @@ -33,6 +33,36 @@ describe('toUrlParams', () => { }); expect(url).toBe('datasource=testDs%5B%21%27%28%29%2A%5D'); }); + it('should encode object properties as url parameters', () => { + const params = urlUtil.serializeParams({ + server: 'backend-01', + hasSpace: 'has space', + many: ['1', '2', '3'], + true: true, + number: 20, + isNull: null, + isUndefined: undefined, + oneMore: false, + }); + expect(params).toBe( + 'server=backend-01&hasSpace=has%20space&many=1&many=2&many=3&true&number=20&isNull=&isUndefined=&oneMore=false' + ); + }); + + it('should not encode special character the same way as angular js', () => { + const params = urlUtil.serializeParams({ + server: ':@', + }); + expect(params).not.toBe('server=:@'); + }); + + it('should keep booleans', () => { + const url = urlUtil.serializeParams({ + bool1: true, + bool2: false, + }); + expect(url).toBe('bool1&bool2=false'); + }); }); describe('parseKeyValue', () => { diff --git a/packages/grafana-data/src/utils/url.ts b/packages/grafana-data/src/utils/url.ts index f3f6449e0d4..e3a602e0059 100644 --- a/packages/grafana-data/src/utils/url.ts +++ b/packages/grafana-data/src/utils/url.ts @@ -27,7 +27,7 @@ function renderUrl(path: string, query: UrlQueryMap | undefined): string { return path; } -function encodeURIComponentAsAngularJS(val: string, pctEncodeSpaces?: boolean) { +function encodeURIComponentAsAngularJS(val: EncodeURIComponentParams, pctEncodeSpaces?: boolean) { return encodeURIComponent(val) .replace(/%40/gi, '@') .replace(/%3A/gi, ':') @@ -40,10 +40,20 @@ function encodeURIComponentAsAngularJS(val: string, pctEncodeSpaces?: boolean) { }); } -function toUrlParams(a: any) { +type EncodeURIComponentParams = Parameters[0]; +/** + * Encodes URL parameters in the style of AngularJS. + * Use `serializeParams` to encode parameters using `encodeURIComponent` instead. + */ +function toUrlParams(a: any, encodeAsAngularJS = true) { const s: any[] = []; const rbracket = /\[\]$/; + const encodingFunction = encodeAsAngularJS + ? (value: EncodeURIComponentParams, pctEncodeSpaces?: boolean) => + encodeURIComponentAsAngularJS(value, pctEncodeSpaces) + : (value: EncodeURIComponentParams, _: boolean) => encodeURIComponent(value); + const isArray = (obj: any) => { return Object.prototype.toString.call(obj) === '[object Array]'; }; @@ -51,10 +61,10 @@ function toUrlParams(a: any) { const add = (k: string, v: any) => { v = typeof v === 'function' ? v() : v === null ? '' : v === undefined ? '' : v; if (typeof v !== 'boolean') { - s[s.length] = encodeURIComponentAsAngularJS(k, true) + '=' + encodeURIComponentAsAngularJS(v, true); + s[s.length] = encodingFunction(k, true) + '=' + encodingFunction(v, true); } else { - const valueQueryPart = v ? '' : '=' + encodeURIComponentAsAngularJS('false', true); - s[s.length] = encodeURIComponentAsAngularJS(k, true) + valueQueryPart; + const valueQueryPart = v ? '' : '=' + encodingFunction('false', true); + s[s.length] = encodingFunction(k, true) + valueQueryPart; } }; @@ -92,6 +102,16 @@ function toUrlParams(a: any) { return buildParams('', a).join('&'); } +/** + * Converts params into a URL-encoded query string. + * + * @param params data to serialize + * @returns A URL-encoded string representing the provided data. + */ +function serializeParams(params: unknown): string { + return toUrlParams(params, false); +} + function appendQueryToUrl(url: string, stringToAppend: string) { if (stringToAppend !== undefined && stringToAppend !== null && stringToAppend !== '') { const pos = url.indexOf('?'); @@ -198,6 +218,7 @@ export const urlUtil = { appendQueryToUrl, getUrlSearchParams, parseKeyValue, + serializeParams, }; /** diff --git a/public/app/core/utils/fetch.ts b/public/app/core/utils/fetch.ts index 03b5031d777..f011ae348b9 100644 --- a/public/app/core/utils/fetch.ts +++ b/public/app/core/utils/fetch.ts @@ -130,7 +130,7 @@ export async function parseResponseBody( return textData as any; } -export function serializeParams(data: Record): string { +function serializeParams(data: Record): string { return Object.keys(data) .map((key) => { const value = data[key]; diff --git a/public/app/plugins/datasource/jaeger/datasource.ts b/public/app/plugins/datasource/jaeger/datasource.ts index 5333b60f0c5..ab5c89e7407 100644 --- a/public/app/plugins/datasource/jaeger/datasource.ts +++ b/public/app/plugins/datasource/jaeger/datasource.ts @@ -13,10 +13,10 @@ import { FieldType, MutableDataFrame, ScopedVars, + urlUtil, } from '@grafana/data'; import { BackendSrvRequest, getBackendSrv, getTemplateSrv, TemplateSrv } from '@grafana/runtime'; import { NodeGraphOptions } from 'app/core/components/NodeGraphSettings'; -import { serializeParams } from 'app/core/utils/fetch'; import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { SpanBarOptions } from 'app/features/explore/TraceView/components'; @@ -235,7 +235,7 @@ export class JaegerDatasource extends DataSourceApi data?: Record, options?: Partial ): Observable> { - const params = data ? serializeParams(data) : ''; + const params = data ? urlUtil.serializeParams(data) : ''; const url = `${this.instanceSettings.url}${apiUrl}${params.length ? `?${params}` : ''}`; const req = { ...options, diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index 7657366504d..dfdb4028a56 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -36,6 +36,7 @@ import { renderLegendFormat, LegacyMetricFindQueryOptions, AdHocVariableFilter, + urlUtil, } from '@grafana/data'; import { Duration } from '@grafana/lezer-logql'; import { BackendSrvRequest, config, DataSourceWithBackend, getTemplateSrv, TemplateSrv } from '@grafana/runtime'; @@ -43,7 +44,6 @@ import { DataQuery } from '@grafana/schema'; import { convertToWebSocketUrl } from 'app/core/utils/explore'; import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv'; -import { serializeParams } from '../../../core/utils/fetch'; import { queryLogsSample, queryLogsVolume } from '../../../features/logs/logsModel'; import { getLogLevelFromKey } from '../../../features/logs/utils'; import { replaceVariables, returnVariables } from '../prometheus/querybuilder/shared/parsingUtils'; @@ -384,7 +384,7 @@ export class LokiDatasource private createLiveTarget(target: LokiQuery, maxDataPoints: number): LokiLiveTarget { const query = target.expr; const baseUrl = this.instanceSettings.url; - const params = serializeParams({ query }); + const params = urlUtil.serializeParams({ query }); return { query, diff --git a/public/app/plugins/datasource/tempo/datasource.ts b/public/app/plugins/datasource/tempo/datasource.ts index c276aab167e..22108f627ce 100644 --- a/public/app/plugins/datasource/tempo/datasource.ts +++ b/public/app/plugins/datasource/tempo/datasource.ts @@ -19,6 +19,7 @@ import { rangeUtil, ScopedVars, TestDataSourceResponse, + urlUtil, } from '@grafana/data'; import { BackendSrvRequest, @@ -32,7 +33,6 @@ import { import { BarGaugeDisplayMode, TableCellDisplayMode, VariableFormatID } from '@grafana/schema'; import { NodeGraphOptions } from 'app/core/components/NodeGraphSettings'; import { TraceToLogsOptions } from 'app/core/components/TraceToLogs/TraceToLogsSettings'; -import { serializeParams } from 'app/core/utils/fetch'; import { SpanBarOptions } from 'app/features/explore/TraceView/components'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; @@ -691,7 +691,7 @@ export class TempoDatasource extends DataSourceWithBackend ): Observable> { - const params = data ? serializeParams(data) : ''; + const params = data ? urlUtil.serializeParams(data) : ''; const url = `${this.instanceSettings.url}${apiUrl}${params.length ? `?${params}` : ''}`; const req = { ...options, url }; @@ -723,7 +723,9 @@ export class TempoDatasource extends DataSourceWithBackend data?: any, options?: Partial ): Observable> { - const params = data ? serializeParams(data) : ''; + const params = data ? urlUtil.serializeParams(data) : ''; const url = `${this.instanceSettings.url}${apiUrl}${params.length ? `?${params}` : ''}`; const req = { ...options,