diff --git a/package.json b/package.json index 69418b946fa..ef01f3e4b21 100644 --- a/package.json +++ b/package.json @@ -203,7 +203,6 @@ "zone.js": "0.7.8" }, "dependencies": { - "@braintree/sanitize-url": "4.0.0", "@grafana/slate-react": "0.22.9-grafana", "@reduxjs/toolkit": "1.3.4", "@torkelo/react-select": "3.0.8", @@ -274,8 +273,7 @@ "tether-drop": "https://github.com/torkelo/drop/tarball/master", "tinycolor2": "1.4.1", "tti-polyfill": "0.2.2", - "whatwg-fetch": "3.0.0", - "xss": "1.0.6" + "whatwg-fetch": "3.0.0" }, "resolutions": { "caniuse-db": "1.0.30000772" diff --git a/packages/grafana-data/package.json b/packages/grafana-data/package.json index 671ab799159..456f6cc1af6 100644 --- a/packages/grafana-data/package.json +++ b/packages/grafana-data/package.json @@ -25,7 +25,9 @@ "dependencies": { "apache-arrow": "0.16.0", "lodash": "4.17.15", - "rxjs": "6.5.5" + "rxjs": "6.5.5", + "xss": "1.0.6", + "@braintree/sanitize-url": "4.0.0" }, "devDependencies": { "@grafana/eslint-config": "^1.0.0-rc1", diff --git a/packages/grafana-data/src/field/fieldDisplay.ts b/packages/grafana-data/src/field/fieldDisplay.ts index e98f0278b74..daaed05e7d1 100644 --- a/packages/grafana-data/src/field/fieldDisplay.ts +++ b/packages/grafana-data/src/field/fieldDisplay.ts @@ -11,6 +11,7 @@ import { FieldConfigSource, FieldType, InterpolateFunction, + LinkModel, } from '../types'; import { DataFrameView } from '../dataframe/DataFrameView'; import { GraphSeriesValue } from '../types/graph'; @@ -77,6 +78,7 @@ export interface FieldDisplay { view?: DataFrameView; colIndex?: number; // The field column index rowIndex?: number; // only filled in when the value is from a row (ie, not a reduction) + getLinks?: () => LinkModel[]; } export interface GetFieldDisplayValuesOptions { @@ -113,7 +115,7 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi for (let i = 0; i < series.fields.length && !hitLimit; i++) { const field = series.fields[i]; - + const fieldLinksSupplier = field.getLinks; // Show all number fields if (field.type !== FieldType.number) { continue; @@ -157,6 +159,12 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi view, colIndex: i, rowIndex: j, + getLinks: fieldLinksSupplier + ? () => + fieldLinksSupplier({ + valueRowIndex: j, + }) + : () => [], }); if (values.length >= limit) { @@ -193,6 +201,12 @@ export const getFieldDisplayValues = (options: GetFieldDisplayValuesOptions): Fi sparkline, view, colIndex: i, + getLinks: fieldLinksSupplier + ? () => + fieldLinksSupplier({ + calculatedValue: displayValue, + }) + : () => [], }); } } diff --git a/packages/grafana-data/src/field/fieldOverrides.ts b/packages/grafana-data/src/field/fieldOverrides.ts index 51bafa9fa80..d6c0b543017 100644 --- a/packages/grafana-data/src/field/fieldOverrides.ts +++ b/packages/grafana-data/src/field/fieldOverrides.ts @@ -11,6 +11,10 @@ import { ScopedVars, ApplyFieldOverrideOptions, FieldConfigPropertyItem, + LinkModel, + InterpolateFunction, + ValueLinkConfig, + GrafanaTheme, } from '../types'; import { fieldMatchers, ReducerID, reduceField } from '../transformations'; import { FieldMatcher } from '../types/transformations'; @@ -19,9 +23,12 @@ import set from 'lodash/set'; import unset from 'lodash/unset'; import get from 'lodash/get'; import { getDisplayProcessor } from './displayProcessor'; -import { guessFieldTypeForField } from '../dataframe'; +import { getTimeField, guessFieldTypeForField } from '../dataframe'; import { standardFieldConfigEditorRegistry } from './standardFieldConfigEditorRegistry'; import { FieldConfigOptionsRegistry } from './FieldConfigOptionsRegistry'; +import { DataLinkBuiltInVars, locationUtil } from '../utils'; +import { formattedValueToString } from '../valueFormats'; +import { getFieldDisplayValuesProxy } from './getFieldDisplayValuesProxy'; interface OverrideProps { match: FieldMatcher; @@ -102,10 +109,10 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra if (!fieldName) { fieldName = `Field[${fieldIndex}]`; } + const fieldScopedVars = { ...scopedVars }; + fieldScopedVars['__field'] = { text: 'Field', value: { name: fieldName } }; - scopedVars['__field'] = { text: 'Field', value: { name: fieldName } }; - - const config: FieldConfig = { ...field.config, scopedVars } || {}; + const config: FieldConfig = { ...field.config, scopedVars: fieldScopedVars } || {}; const context = { field, data: options.data!, @@ -182,6 +189,12 @@ export function applyFieldOverrides(options: ApplyFieldOverrideOptions): DataFra theme: options.theme, timeZone: options.timeZone, }); + + // Attach data links supplier + f.getLinks = getLinksSupplier(frame, f, fieldScopedVars, context.replaceVariables, { + theme: options.theme, + }); + return f; }); @@ -314,3 +327,84 @@ export function validateFieldConfig(config: FieldConfig) { config.min = tmp; } } + +const getLinksSupplier = ( + frame: DataFrame, + field: Field, + fieldScopedVars: ScopedVars, + replaceVariables: InterpolateFunction, + options: { + theme: GrafanaTheme; + } +) => (config: ValueLinkConfig): Array> => { + if (!field.config.links || field.config.links.length === 0) { + return []; + } + const timeRangeUrl = locationUtil.getTimeRangeUrlParams(); + const { timeField } = getTimeField(frame); + + return field.config.links.map(link => { + let href = link.url; + let dataFrameVars = {}; + let valueVars = {}; + + const info: LinkModel = { + href: locationUtil.assureBaseUrl(href.replace(/\n/g, '')), + title: replaceVariables(link.title || ''), + target: link.targetBlank ? '_blank' : '_self', + origin: field, + }; + + const variablesQuery = locationUtil.getVariablesUrlParams(); + + // We are not displaying reduction result + if (config.valueRowIndex !== undefined && !isNaN(config.valueRowIndex)) { + const fieldsProxy = getFieldDisplayValuesProxy(frame, config.valueRowIndex, options); + valueVars = { + raw: field.values.get(config.valueRowIndex), + numeric: fieldsProxy[field.name].numeric, + text: fieldsProxy[field.name].text, + time: timeField ? timeField.values.get(config.valueRowIndex) : undefined, + }; + dataFrameVars = { + __data: { + value: { + name: frame.name, + refId: frame.refId, + fields: fieldsProxy, + }, + text: 'Data', + }, + }; + } else { + if (config.calculatedValue) { + valueVars = { + raw: config.calculatedValue.numeric, + numeric: config.calculatedValue.numeric, + text: formattedValueToString(config.calculatedValue), + }; + } + } + + info.href = replaceVariables(info.href, { + ...fieldScopedVars, + __value: { + text: 'Value', + value: valueVars, + }, + ...dataFrameVars, + [DataLinkBuiltInVars.keepTime]: { + text: timeRangeUrl, + value: timeRangeUrl, + }, + [DataLinkBuiltInVars.includeVars]: { + text: variablesQuery, + value: variablesQuery, + }, + }); + + info.href = locationUtil.processUrl(info.href); + + return info; + }); +}; diff --git a/public/app/features/panel/panellinks/fieldDisplayValuesProxy.test.ts b/packages/grafana-data/src/field/getFieldDisplayValuesProxy.test.tsx similarity index 75% rename from public/app/features/panel/panellinks/fieldDisplayValuesProxy.test.ts rename to packages/grafana-data/src/field/getFieldDisplayValuesProxy.test.tsx index f69de4cd36f..eb9aed61e0e 100644 --- a/public/app/features/panel/panellinks/fieldDisplayValuesProxy.test.ts +++ b/packages/grafana-data/src/field/getFieldDisplayValuesProxy.test.tsx @@ -1,5 +1,7 @@ -import { toDataFrame, applyFieldOverrides, GrafanaTheme } from '@grafana/data'; -import { getFieldDisplayValuesProxy } from './fieldDisplayValuesProxy'; +import { getFieldDisplayValuesProxy } from './getFieldDisplayValuesProxy'; +import { applyFieldOverrides } from './fieldOverrides'; +import { toDataFrame } from '../dataframe'; +import { GrafanaTheme } from '../types'; describe('getFieldDisplayValuesProxy', () => { const data = applyFieldOverrides({ @@ -40,7 +42,9 @@ describe('getFieldDisplayValuesProxy', () => { it('should format the time values in UTC', () => { // Test Proxies in general - const p = getFieldDisplayValuesProxy(data, 0); + const p = getFieldDisplayValuesProxy(data, 0, { + theme: {} as GrafanaTheme, + }); const time = p.Time; expect(time.numeric).toEqual(1); expect(time.text).toEqual('1970-01-01 00:00:00'); @@ -51,7 +55,9 @@ describe('getFieldDisplayValuesProxy', () => { }); it('Lookup by name, index, or title', () => { - const p = getFieldDisplayValuesProxy(data, 2); + const p = getFieldDisplayValuesProxy(data, 2, { + theme: {} as GrafanaTheme, + }); expect(p.power.numeric).toEqual(300); expect(p['power'].numeric).toEqual(300); expect(p['The Power'].numeric).toEqual(300); @@ -59,7 +65,9 @@ describe('getFieldDisplayValuesProxy', () => { }); it('should return undefined when missing', () => { - const p = getFieldDisplayValuesProxy(data, 0); + const p = getFieldDisplayValuesProxy(data, 0, { + theme: {} as GrafanaTheme, + }); expect(p.xyz).toBeUndefined(); expect(p[100]).toBeUndefined(); }); diff --git a/public/app/features/panel/panellinks/fieldDisplayValuesProxy.ts b/packages/grafana-data/src/field/getFieldDisplayValuesProxy.ts similarity index 65% rename from public/app/features/panel/panellinks/fieldDisplayValuesProxy.ts rename to packages/grafana-data/src/field/getFieldDisplayValuesProxy.ts index ec7f06dea8e..bfc70aec755 100644 --- a/public/app/features/panel/panellinks/fieldDisplayValuesProxy.ts +++ b/packages/grafana-data/src/field/getFieldDisplayValuesProxy.ts @@ -1,8 +1,22 @@ -import { DisplayValue, DataFrame, formattedValueToString, getDisplayProcessor } from '@grafana/data'; -import { config } from '@grafana/runtime'; import toNumber from 'lodash/toNumber'; +import { DataFrame, DisplayValue, GrafanaTheme } from '../types'; +import { getDisplayProcessor } from './displayProcessor'; +import { formattedValueToString } from '../valueFormats'; -export function getFieldDisplayValuesProxy(frame: DataFrame, rowIndex: number): Record { +/** + * + * @param frame + * @param rowIndex + * @param options + * @internal + */ +export function getFieldDisplayValuesProxy( + frame: DataFrame, + rowIndex: number, + options: { + theme: GrafanaTheme; + } +): Record { return new Proxy({} as Record, { get: (obj: any, key: string) => { // 1. Match the name @@ -23,7 +37,7 @@ export function getFieldDisplayValuesProxy(frame: DataFrame, rowIndex: number): // Lazy load the display processor field.display = getDisplayProcessor({ field, - theme: config.theme, + theme: options.theme, }); } const raw = field.values.get(rowIndex); diff --git a/packages/grafana-data/src/field/index.ts b/packages/grafana-data/src/field/index.ts index 548a9ab98e7..4a92bd90bd7 100644 --- a/packages/grafana-data/src/field/index.ts +++ b/packages/grafana-data/src/field/index.ts @@ -6,3 +6,4 @@ export * from './overrides/processors'; export { FieldConfigOptionsRegistry } from './FieldConfigOptionsRegistry'; export { applyFieldOverrides, validateFieldConfig } from './fieldOverrides'; +export { getFieldDisplayValuesProxy } from './getFieldDisplayValuesProxy'; diff --git a/packages/grafana-data/src/text/index.ts b/packages/grafana-data/src/text/index.ts index b59a1d992d5..bece68920ac 100644 --- a/packages/grafana-data/src/text/index.ts +++ b/packages/grafana-data/src/text/index.ts @@ -1,3 +1,11 @@ export * from './string'; export * from './markdown'; export * from './text'; +import { escapeHtml, hasAnsiCodes, sanitize, sanitizeUrl } from './sanitize'; + +export const textUtil = { + escapeHtml, + hasAnsiCodes, + sanitize, + sanitizeUrl, +}; diff --git a/public/app/core/utils/text.ts b/packages/grafana-data/src/text/sanitize.ts similarity index 100% rename from public/app/core/utils/text.ts rename to packages/grafana-data/src/text/sanitize.ts diff --git a/packages/grafana-data/src/types/config.ts b/packages/grafana-data/src/types/config.ts new file mode 100644 index 00000000000..706231a3abb --- /dev/null +++ b/packages/grafana-data/src/types/config.ts @@ -0,0 +1,100 @@ +import { DataSourceInstanceSettings } from './datasource'; +import { PanelPluginMeta } from './panel'; +import { GrafanaTheme } from './theme'; + +/** + * Describes the build information that will be available via the Grafana cofiguration. + * + * @public + */ +export interface BuildInfo { + version: string; + commit: string; + /** + * Is set to true when running Grafana Enterprise edition. + * + * @deprecated use `licenseInfo.hasLicense` instead + */ + isEnterprise: boolean; + env: string; + edition: string; + latestVersion: string; + hasUpdate: boolean; +} + +/** + * Describes available feature toggles in Grafana. These can be configured via the + * `conf/custom.ini` to enable features under development or not yet available in + * stable version. + * + * @public + */ +export interface FeatureToggles { + transformations: boolean; + expressions: boolean; + newEdit: boolean; + /** + * @remarks + * Available only in Grafana Enterprise + */ + meta: boolean; + newVariables: boolean; + tracingIntegration: boolean; +} + +/** + * Describes the license information about the current running instance of Grafana. + * + * @public + */ +export interface LicenseInfo { + hasLicense: boolean; + expiry: number; + licenseUrl: string; + stateInfo: string; +} + +/** + * Describes all the different Grafana configuration values available for an instance. + * + * @public + */ +export interface GrafanaConfig { + datasources: { [str: string]: DataSourceInstanceSettings }; + panels: { [key: string]: PanelPluginMeta }; + minRefreshInterval: string; + appSubUrl: string; + windowTitlePrefix: string; + buildInfo: BuildInfo; + newPanelTitle: string; + bootData: any; + externalUserMngLinkUrl: string; + externalUserMngLinkName: string; + externalUserMngInfo: string; + allowOrgCreate: boolean; + disableLoginForm: boolean; + defaultDatasource: string; + alertingEnabled: boolean; + alertingErrorOrTimeout: string; + alertingNoDataOrNullValues: string; + alertingMinInterval: number; + authProxyEnabled: boolean; + exploreEnabled: boolean; + ldapEnabled: boolean; + samlEnabled: boolean; + autoAssignOrg: boolean; + verifyEmailEnabled: boolean; + oauth: any; + disableUserSignUp: boolean; + loginHint: any; + passwordHint: any; + loginError: any; + navTree: any; + viewersCanEdit: boolean; + editorsCanAdmin: boolean; + disableSanitizeHtml: boolean; + theme: GrafanaTheme; + pluginsToPreload: string[]; + featureToggles: FeatureToggles; + licenseInfo: LicenseInfo; +} diff --git a/packages/grafana-data/src/types/dataFrame.ts b/packages/grafana-data/src/types/dataFrame.ts index 7fa46f6a3d6..0b6e6371f3b 100644 --- a/packages/grafana-data/src/types/dataFrame.ts +++ b/packages/grafana-data/src/types/dataFrame.ts @@ -1,8 +1,8 @@ import { ThresholdsConfig } from './thresholds'; import { ValueMapping } from './valueMapping'; import { QueryResultBase, Labels, NullValueMode } from './data'; -import { DisplayProcessor } from './displayValue'; -import { DataLink } from './dataLink'; +import { DisplayProcessor, DisplayValue } from './displayValue'; +import { DataLink, LinkModel } from './dataLink'; import { Vector } from './vector'; import { FieldCalcs } from '../transformations/fieldReducer'; import { FieldColor } from './fieldColor'; @@ -57,6 +57,17 @@ export interface FieldConfig { scopedVars?: ScopedVars; } +export interface ValueLinkConfig { + /** + * Result of field reduction + */ + calculatedValue?: DisplayValue; + /** + * Index of the value row within Field. Should be provided only when value is not a result of a reduction + */ + valueRowIndex?: number; +} + export interface Field> { /** * Name of the field (column) @@ -87,6 +98,11 @@ export interface Field> { * Convert a value for display */ display?: DisplayProcessor; + + /** + * Get value data links with variables interpolated + */ + getLinks?: (config: ValueLinkConfig) => Array>; } export interface DataFrame extends QueryResultBase { diff --git a/packages/grafana-data/src/types/dataLink.ts b/packages/grafana-data/src/types/dataLink.ts index e399e5c55eb..d77adf5ce9e 100644 --- a/packages/grafana-data/src/types/dataLink.ts +++ b/packages/grafana-data/src/types/dataLink.ts @@ -37,7 +37,7 @@ export type LinkTarget = '_blank' | '_self'; /** * Processed Link Model. The values are ready to use */ -export interface LinkModel { +export interface LinkModel { href: string; title: string; target: LinkTarget; diff --git a/packages/grafana-data/src/types/index.ts b/packages/grafana-data/src/types/index.ts index 66af316e931..00e6547812f 100644 --- a/packages/grafana-data/src/types/index.ts +++ b/packages/grafana-data/src/types/index.ts @@ -31,3 +31,4 @@ export { AppEvent, AppEvents }; import * as PanelEvents from './panelEvents'; export { PanelEvents }; +export { GrafanaConfig, BuildInfo, FeatureToggles, LicenseInfo } from './config'; diff --git a/packages/grafana-data/src/utils/dataLinks.ts b/packages/grafana-data/src/utils/dataLinks.ts new file mode 100644 index 00000000000..70a9b72df10 --- /dev/null +++ b/packages/grafana-data/src/utils/dataLinks.ts @@ -0,0 +1,14 @@ +export const DataLinkBuiltInVars = { + keepTime: '__url_time_range', + timeRangeFrom: '__from', + timeRangeTo: '__to', + includeVars: '__all_variables', + seriesName: '__series.name', + fieldName: '__field.name', + valueTime: '__value.time', + valueNumeric: '__value.numeric', + valueText: '__value.text', + valueRaw: '__value.raw', + // name of the calculation represented by the value + valueCalc: '__value.calc', +}; diff --git a/packages/grafana-data/src/utils/index.ts b/packages/grafana-data/src/utils/index.ts index 1f1d7e3e8af..1a5011ca81b 100644 --- a/packages/grafana-data/src/utils/index.ts +++ b/packages/grafana-data/src/utils/index.ts @@ -12,3 +12,6 @@ export { PanelOptionsEditorBuilder, FieldConfigEditorBuilder } from './OptionsUI export { getMappedValue } from './valueMappings'; export { getFlotPairs, getFlotPairsConstant } from './flotPairs'; +export { locationUtil } from './location'; +export { urlUtil, UrlQueryMap, UrlQueryValue } from './url'; +export { DataLinkBuiltInVars } from './dataLinks'; diff --git a/packages/grafana-data/src/utils/location.test.ts b/packages/grafana-data/src/utils/location.test.ts new file mode 100644 index 00000000000..dfedfa712a9 --- /dev/null +++ b/packages/grafana-data/src/utils/location.test.ts @@ -0,0 +1,21 @@ +import { locationUtil } from './location'; + +describe('locationUtil', () => { + beforeAll(() => { + locationUtil.initialize({ + getConfig: () => { + return { appSubUrl: '/subUrl' } as any; + }, + // @ts-ignore + buildParamsFromVariables: () => {}, + // @ts-ignore + getTimeRangeForUrl: () => {}, + }); + }); + describe('With /subUrl as appSubUrl', () => { + it('/subUrl should be stripped', () => { + const urlWithoutMaster = locationUtil.stripBaseFromUrl('/subUrl/grafana/'); + expect(urlWithoutMaster).toBe('/grafana/'); + }); + }); +}); diff --git a/packages/grafana-data/src/utils/location.ts b/packages/grafana-data/src/utils/location.ts new file mode 100644 index 00000000000..dc1f8925088 --- /dev/null +++ b/packages/grafana-data/src/utils/location.ts @@ -0,0 +1,73 @@ +import { GrafanaConfig, RawTimeRange, ScopedVars } from '../types'; +import { urlUtil } from './url'; +import { textUtil } from '../text'; + +let grafanaConfig: () => GrafanaConfig; +let getTimeRangeUrlParams: () => RawTimeRange; +let getVariablesUrlParams: (params?: Record, scopedVars?: ScopedVars) => string; + +/** + * + * @param url + * @internal + */ +const stripBaseFromUrl = (url: string): string => { + const appSubUrl = grafanaConfig ? grafanaConfig().appSubUrl : ''; + const stripExtraChars = appSubUrl.endsWith('/') ? 1 : 0; + const urlWithoutBase = + url.length > 0 && url.indexOf(appSubUrl) === 0 ? url.slice(appSubUrl.length - stripExtraChars) : url; + + return urlWithoutBase; +}; + +/** + * + * @param url + * @internal + */ +const assureBaseUrl = (url: string) => { + if (url.startsWith('/')) { + return `${grafanaConfig ? grafanaConfig().appSubUrl : ''}${stripBaseFromUrl(url)}`; + } + return url; +}; + +interface LocationUtilDependencies { + getConfig: () => GrafanaConfig; + getTimeRangeForUrl: () => RawTimeRange; + buildParamsFromVariables: (params: any, scopedVars?: ScopedVars) => string; +} + +export const locationUtil = { + /** + * + * @param getConfig + * @param buildParamsFromVariables + * @param getTimeRangeForUrl + * @internal + */ + initialize: ({ getConfig, buildParamsFromVariables, getTimeRangeForUrl }: LocationUtilDependencies) => { + grafanaConfig = getConfig; + getTimeRangeUrlParams = getTimeRangeForUrl; + getVariablesUrlParams = buildParamsFromVariables; + }, + stripBaseFromUrl, + assureBaseUrl, + getTimeRangeUrlParams: () => { + if (!getTimeRangeUrlParams) { + return null; + } + return urlUtil.toUrlParams(getTimeRangeUrlParams()); + }, + getVariablesUrlParams: (scopedVars?: ScopedVars) => { + if (!getVariablesUrlParams) { + return null; + } + const params = {}; + getVariablesUrlParams(params, scopedVars); + return urlUtil.toUrlParams(params); + }, + processUrl: (url: string) => { + return grafanaConfig().disableSanitizeHtml ? url : textUtil.sanitizeUrl(url); + }, +}; diff --git a/public/app/core/specs/url.test.ts b/packages/grafana-data/src/utils/url.test.ts similarity index 83% rename from public/app/core/specs/url.test.ts rename to packages/grafana-data/src/utils/url.test.ts index 3b7f81494f9..ca876d10f01 100644 --- a/public/app/core/specs/url.test.ts +++ b/packages/grafana-data/src/utils/url.test.ts @@ -1,8 +1,8 @@ -import { toUrlParams } from '../utils/url'; +import { urlUtil } from './url'; describe('toUrlParams', () => { it('should encode object properties as url parameters', () => { - const url = toUrlParams({ + const url = urlUtil.toUrlParams({ server: 'backend-01', hasSpace: 'has space', many: ['1', '2', '3'], @@ -17,7 +17,7 @@ describe('toUrlParams', () => { describe('toUrlParams', () => { it('should encode the same way as angularjs', () => { - const url = toUrlParams({ + const url = urlUtil.toUrlParams({ server: ':@', }); expect(url).toBe('server=:@'); diff --git a/public/app/core/utils/url.ts b/packages/grafana-data/src/utils/url.ts similarity index 78% rename from public/app/core/utils/url.ts rename to packages/grafana-data/src/utils/url.ts index 2bfa3f2ddf0..9e7999c829b 100644 --- a/public/app/core/utils/url.ts +++ b/packages/grafana-data/src/utils/url.ts @@ -2,16 +2,28 @@ * @preserve jquery-param (c) 2015 KNOWLEDGECODE | MIT */ -import { UrlQueryMap } from '@grafana/runtime'; +/** + * Type to represent the value of a single query variable. + * + * @public + */ +export type UrlQueryValue = string | number | boolean | string[] | number[] | boolean[] | undefined | null; -export function renderUrl(path: string, query: UrlQueryMap | undefined): string { +/** + * Type to represent the values parsed from the query string. + * + * @public + */ +export type UrlQueryMap = Record; + +function renderUrl(path: string, query: UrlQueryMap | undefined): string { if (query && Object.keys(query).length > 0) { path += '?' + toUrlParams(query); } return path; } -export function encodeURIComponentAsAngularJS(val: string, pctEncodeSpaces?: boolean) { +function encodeURIComponentAsAngularJS(val: string, pctEncodeSpaces?: boolean) { return encodeURIComponent(val) .replace(/%40/gi, '@') .replace(/%3A/gi, ':') @@ -21,7 +33,7 @@ export function encodeURIComponentAsAngularJS(val: string, pctEncodeSpaces?: boo .replace(/%20/g, pctEncodeSpaces ? '%20' : '+'); } -export function toUrlParams(a: any) { +function toUrlParams(a: any) { const s: any[] = []; const rbracket = /\[\]$/; @@ -72,7 +84,7 @@ export function toUrlParams(a: any) { return buildParams('', a).join('&'); } -export function appendQueryToUrl(url: string, stringToAppend: string) { +function appendQueryToUrl(url: string, stringToAppend: string) { if (stringToAppend !== undefined && stringToAppend !== null && stringToAppend !== '') { const pos = url.indexOf('?'); if (pos !== -1) { @@ -91,7 +103,7 @@ export function appendQueryToUrl(url: string, stringToAppend: string) { /** * Return search part (as object) of current url */ -export function getUrlSearchParams() { +function getUrlSearchParams() { const search = window.location.search.substring(1); const searchParamsSegments = search.split('&'); const params: any = {}; @@ -110,3 +122,10 @@ export function getUrlSearchParams() { } return params; } + +export const urlUtil = { + renderUrl, + toUrlParams, + appendQueryToUrl, + getUrlSearchParams, +}; diff --git a/packages/grafana-data/tsconfig.json b/packages/grafana-data/tsconfig.json index 99a0e5ed841..ec47e7b5a9b 100644 --- a/packages/grafana-data/tsconfig.json +++ b/packages/grafana-data/tsconfig.json @@ -3,9 +3,9 @@ "declarationDir": "dist", "outDir": "compiled", "rootDirs": ["."], - "typeRoots": ["node_modules/@types"] + "typeRoots": ["node_modules/@types", "types"] }, "exclude": ["dist", "node_modules"], "extends": "@grafana/tsconfig", - "include": ["src/**/*.ts*", "../../public/app/types/jquery/*.ts"] + "include": ["src/**/*.ts*", "../../public/app/types/jquery/*.ts", "../../public/app/types/sanitize-url.d.ts"] } diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts index f7201f35fff..00395036b2f 100644 --- a/packages/grafana-runtime/src/config.ts +++ b/packages/grafana-runtime/src/config.ts @@ -1,65 +1,17 @@ import merge from 'lodash/merge'; import { getTheme } from '@grafana/ui'; -import { DataSourceInstanceSettings, GrafanaTheme, GrafanaThemeType, PanelPluginMeta } from '@grafana/data'; +import { + DataSourceInstanceSettings, + GrafanaTheme, + GrafanaThemeType, + PanelPluginMeta, + GrafanaConfig, + LicenseInfo, + BuildInfo, + FeatureToggles, +} from '@grafana/data'; -/** - * Describes the build information that will be available via the Grafana cofiguration. - * - * @public - */ -export interface BuildInfo { - version: string; - commit: string; - /** - * Is set to true when running Grafana Enterprise edition. - * - * @deprecated use `licenseInfo.hasLicense` instead - */ - isEnterprise: boolean; - env: string; - edition: string; - latestVersion: string; - hasUpdate: boolean; -} - -/** - * Describes available feature toggles in Grafana. These can be configured via the - * `conf/custom.ini` to enable features under development or not yet available in - * stable version. - * - * @public - */ -export interface FeatureToggles { - transformations: boolean; - expressions: boolean; - newEdit: boolean; - /** - * @remarks - * Available only in Grafana Enterprise - */ - meta: boolean; - newVariables: boolean; - tracingIntegration: boolean; -} - -/** - * Describes the license information about the current running instance of Grafana. - * - * @public - */ -export interface LicenseInfo { - hasLicense: boolean; - expiry: number; - licenseUrl: string; - stateInfo: string; -} - -/** - * Describes all the different Grafana configuration values available for an instance. - * - * @public - */ -export class GrafanaBootConfig { +export class GrafanaBootConfig implements GrafanaConfig { datasources: { [str: string]: DataSourceInstanceSettings } = {}; panels: { [key: string]: PanelPluginMeta } = {}; minRefreshInterval = ''; diff --git a/packages/grafana-runtime/src/services/LocationSrv.ts b/packages/grafana-runtime/src/services/LocationSrv.ts index 12dec43eaac..720f5b8d3e1 100644 --- a/packages/grafana-runtime/src/services/LocationSrv.ts +++ b/packages/grafana-runtime/src/services/LocationSrv.ts @@ -4,6 +4,8 @@ * * @public */ +import { UrlQueryMap } from '@grafana/data'; + export interface LocationUpdate { /** * Target path where you automatically wants to navigate the user. @@ -37,20 +39,6 @@ export interface LocationUpdate { replace?: boolean; } -/** - * Type to represent the value of a single query variable. - * - * @public - */ -export type UrlQueryValue = string | number | boolean | string[] | number[] | boolean[] | undefined | null; - -/** - * Type to represent the values parsed from the query string. - * - * @public - */ -export type UrlQueryMap = Record; - /** * If you need to automatically navigate the user to a new place in the application this should * be done via the LocationSrv and it will make sure to update the application state accordingly. diff --git a/packages/grafana-runtime/tsconfig.json b/packages/grafana-runtime/tsconfig.json index 8bb070b0c58..80ce0289af2 100644 --- a/packages/grafana-runtime/tsconfig.json +++ b/packages/grafana-runtime/tsconfig.json @@ -11,5 +11,5 @@ }, "exclude": ["dist", "node_modules"], "extends": "@grafana/tsconfig", - "include": ["src/**/*.ts*", "../../public/app/types/jquery/*.ts"] + "include": ["src/**/*.ts*", "../../public/app/types/jquery/*.ts", "../../public/app/types/sanitize-url.d.ts"] } diff --git a/packages/grafana-toolkit/tsconfig.json b/packages/grafana-toolkit/tsconfig.json index 2ec311265de..b6c80bdbb7f 100644 --- a/packages/grafana-toolkit/tsconfig.json +++ b/packages/grafana-toolkit/tsconfig.json @@ -8,5 +8,5 @@ }, "exclude": ["dist", "node_modules"], "extends": "@grafana/tsconfig", - "include": ["src/**/*.ts", "../../public/app/types/jquery/*.ts"] + "include": ["src/**/*.ts", "../../public/app/types/jquery/*.ts", "../../public/app/types/sanitize-url.d.ts"] } diff --git a/packages/grafana-ui/.storybook/tsconfig.json b/packages/grafana-ui/.storybook/tsconfig.json index 9243161aba5..807d02f32b3 100644 --- a/packages/grafana-ui/.storybook/tsconfig.json +++ b/packages/grafana-ui/.storybook/tsconfig.json @@ -6,5 +6,5 @@ }, "exclude": ["../dist", "../node_modules"], "extends": "../tsconfig.json", - "include": ["../src/**/*.ts", "../src/**/*.tsx"] + "include": ["../src/**/*.ts", "../src/**/*.tsx", "../../../public/app/types/sanitize-url.d.ts"] } diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinkInput.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinkInput.tsx index abb6cbd66c6..9109ce4d210 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinkInput.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinkInput.tsx @@ -1,7 +1,7 @@ import React, { useState, useMemo, useContext, useRef, RefObject, memo, useEffect } from 'react'; import usePrevious from 'react-use/lib/usePrevious'; import { DataLinkSuggestions } from './DataLinkSuggestions'; -import { ThemeContext, DataLinkBuiltInVars, makeValue } from '../../index'; +import { ThemeContext, makeValue } from '../../index'; import { SelectionReference } from './SelectionReference'; import { Portal } from '../index'; @@ -14,7 +14,7 @@ import { css } from 'emotion'; import { SlatePrism } from '../../slate-plugins'; import { SCHEMA } from '../../utils/slate'; import { stylesFactory } from '../../themes'; -import { GrafanaTheme, VariableSuggestion, VariableOrigin } from '@grafana/data'; +import { GrafanaTheme, VariableSuggestion, VariableOrigin, DataLinkBuiltInVars } from '@grafana/data'; const modulo = (a: number, n: number) => a - n * Math.floor(a / n); diff --git a/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.tsx b/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.tsx index ed9b910b8a6..8fb639f9f34 100644 --- a/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.tsx +++ b/packages/grafana-ui/src/components/DataLinks/DataLinksContextMenu.tsx @@ -1,12 +1,12 @@ import React from 'react'; import { WithContextMenu } from '../ContextMenu/WithContextMenu'; -import { LinkModelSupplier } from '@grafana/data'; +import { LinkModel } from '@grafana/data'; import { linkModelToContextMenuItems } from '../../utils/dataLinks'; import { css } from 'emotion'; interface DataLinksContextMenuProps { children: (props: { openMenu?: React.MouseEventHandler; targetClassName?: string }) => JSX.Element; - links?: LinkModelSupplier; + links?: () => LinkModel[]; } export const DataLinksContextMenu: React.FC = ({ children, links }) => { diff --git a/packages/grafana-ui/src/components/OptionsUI/links.tsx b/packages/grafana-ui/src/components/OptionsUI/links.tsx index 09d5e23e59d..768f14ff556 100644 --- a/packages/grafana-ui/src/components/OptionsUI/links.tsx +++ b/packages/grafana-ui/src/components/OptionsUI/links.tsx @@ -1,5 +1,10 @@ import React from 'react'; -import { FieldConfigEditorProps, DataLink, DataLinksFieldConfigSettings } from '@grafana/data'; +import { + DataLink, + DataLinksFieldConfigSettings, + FieldConfigEditorProps, + VariableSuggestionsScope, +} from '@grafana/data'; import { DataLinksInlineEditor } from '../DataLinks/DataLinksInlineEditor/DataLinksInlineEditor'; export const DataLinksValueEditor: React.FC> = ({ @@ -12,7 +17,7 @@ export const DataLinksValueEditor: React.FC ); }; diff --git a/packages/grafana-ui/src/components/Table/DefaultCell.tsx b/packages/grafana-ui/src/components/Table/DefaultCell.tsx index 3d174444da0..35342cb653b 100644 --- a/packages/grafana-ui/src/components/Table/DefaultCell.tsx +++ b/packages/grafana-ui/src/components/Table/DefaultCell.tsx @@ -1,14 +1,33 @@ import React, { FC } from 'react'; import { TableCellProps } from './types'; -import { formattedValueToString } from '@grafana/data'; +import { formattedValueToString, LinkModel } from '@grafana/data'; export const DefaultCell: FC = props => { - const { field, cell, tableStyles } = props; + const { field, cell, tableStyles, row } = props; + let link: LinkModel | undefined; if (!field.display) { return null; } const displayValue = field.display(cell.value); - return
{formattedValueToString(displayValue)}
; + + if (field.getLinks) { + link = field.getLinks({ + valueRowIndex: row.index, + })[0]; + } + const value = formattedValueToString(displayValue); + + return ( +
+ {link ? ( + + {value} + + ) : ( + value + )} +
+ ); }; diff --git a/packages/grafana-ui/src/utils/dataLinks.ts b/packages/grafana-ui/src/utils/dataLinks.ts index 9939816784d..36ebd9e0f77 100644 --- a/packages/grafana-ui/src/utils/dataLinks.ts +++ b/packages/grafana-ui/src/utils/dataLinks.ts @@ -1,26 +1,11 @@ import { ContextMenuItem } from '../components/ContextMenu/ContextMenu'; -import { LinkModelSupplier } from '@grafana/data'; - -export const DataLinkBuiltInVars = { - keepTime: '__url_time_range', - timeRangeFrom: '__from', - timeRangeTo: '__to', - includeVars: '__all_variables', - seriesName: '__series.name', - fieldName: '__field.name', - valueTime: '__value.time', - valueNumeric: '__value.numeric', - valueText: '__value.text', - valueRaw: '__value.raw', - // name of the calculation represented by the value - valueCalc: '__value.calc', -}; +import { LinkModel } from '@grafana/data'; /** * Delays creating links until we need to open the ContextMenu */ -export const linkModelToContextMenuItems: (links: LinkModelSupplier) => ContextMenuItem[] = links => { - return links.getLinks().map(link => { +export const linkModelToContextMenuItems: (links: () => LinkModel[]) => ContextMenuItem[] = links => { + return links().map(link => { return { label: link.title, // TODO: rename to href diff --git a/packages/grafana-ui/tsconfig.json b/packages/grafana-ui/tsconfig.json index c688a5e1b1c..5de540f417b 100644 --- a/packages/grafana-ui/tsconfig.json +++ b/packages/grafana-ui/tsconfig.json @@ -11,5 +11,5 @@ }, "exclude": ["dist", "node_modules"], "extends": "@grafana/tsconfig", - "include": ["src/**/*.ts*"] + "include": ["src/**/*.ts*", "../../public/app/types/sanitize-url.d.ts"] } diff --git a/public/app/core/logs_model.ts b/public/app/core/logs_model.ts index 7288aaac8d0..5f12338edc8 100644 --- a/public/app/core/logs_model.ts +++ b/public/app/core/logs_model.ts @@ -25,9 +25,10 @@ import { getFlotPairs, TimeZone, getDisplayProcessor, + textUtil, } from '@grafana/data'; import { getThemeColor } from 'app/core/utils/colors'; -import { hasAnsiCodes } from 'app/core/utils/text'; + import { sortInAscendingOrder, deduplicateLogRowsById } from 'app/core/utils/explore'; import { getGraphSeriesModel } from 'app/plugins/panel/graph2/getGraphSeriesModel'; @@ -313,7 +314,7 @@ export function logSeriesToLogsModel(logSeries: DataFrame[]): LogsModel | undefi // This should be string but sometimes isn't (eg elastic) because the dataFrame is not strongly typed. const message: string = typeof messageValue === 'string' ? messageValue : JSON.stringify(messageValue); - const hasAnsi = hasAnsiCodes(message); + const hasAnsi = textUtil.hasAnsiCodes(message); const searchWords = series.meta && series.meta.searchWords ? series.meta.searchWords : []; let logLevel = LogLevel.unknown; diff --git a/public/app/core/reducers/location.ts b/public/app/core/reducers/location.ts index da67a7fdb99..b42f466f91a 100644 --- a/public/app/core/reducers/location.ts +++ b/public/app/core/reducers/location.ts @@ -3,7 +3,7 @@ import { Action, createAction } from '@reduxjs/toolkit'; import { LocationUpdate } from '@grafana/runtime'; import { LocationState } from 'app/types'; -import { renderUrl } from 'app/core/utils/url'; +import { urlUtil } from '@grafana/data'; export const initialState: LocationState = { url: '', @@ -33,7 +33,7 @@ export const locationReducer = (state: LocationState = initialState, action: Act } return { - url: renderUrl(path || state.path, query), + url: urlUtil.renderUrl(path || state.path, query), path: path || state.path, query: { ...query }, routeParams: routeParams || state.routeParams, diff --git a/public/app/core/services/bridge_srv.test.ts b/public/app/core/services/bridge_srv.test.ts index 7c141bdb2a1..00767d2cdb0 100644 --- a/public/app/core/services/bridge_srv.test.ts +++ b/public/app/core/services/bridge_srv.test.ts @@ -1,4 +1,4 @@ -import { UrlQueryMap } from '@grafana/runtime'; +import { UrlQueryMap } from '@grafana/data'; import { findTemplateVarChanges } from './bridge_srv'; describe('when checking template variables', () => { diff --git a/public/app/core/services/bridge_srv.ts b/public/app/core/services/bridge_srv.ts index 4c2657a7703..38448cea7da 100644 --- a/public/app/core/services/bridge_srv.ts +++ b/public/app/core/services/bridge_srv.ts @@ -1,12 +1,11 @@ import coreModule from 'app/core/core_module'; import appEvents from 'app/core/app_events'; import { store } from 'app/store/store'; -import locationUtil from 'app/core/utils/location_util'; import { updateLocation } from 'app/core/actions'; -import { ITimeoutService, ILocationService, IWindowService } from 'angular'; +import { ILocationService, ITimeoutService, IWindowService } from 'angular'; import { CoreEvents } from 'app/types'; import { GrafanaRootScope } from 'app/routes/GrafanaCtrl'; -import { UrlQueryMap } from '@grafana/runtime'; +import { locationUtil, UrlQueryMap } from '@grafana/data'; import { getDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { VariableSrv } from 'app/features/templating/all'; diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index de01483c274..d31f7edeff4 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -3,7 +3,6 @@ import _ from 'lodash'; import coreModule from 'app/core/core_module'; import appEvents from 'app/core/app_events'; import { getExploreUrl } from 'app/core/utils/explore'; -import locationUtil from 'app/core/utils/location_util'; import { store } from 'app/store/store'; import { AppEventEmitter, CoreEvents } from 'app/types'; @@ -15,6 +14,7 @@ import { GrafanaRootScope } from 'app/routes/GrafanaCtrl'; import { DashboardModel } from '../../features/dashboard/state'; import { ShareModal } from 'app/features/dashboard/components/ShareModal'; import { SaveDashboardModalProxy } from '../../features/dashboard/components/SaveDashboard/SaveDashboardModalProxy'; +import { locationUtil } from '@grafana/data'; export class KeybindingSrv { helpModal: boolean; diff --git a/public/app/core/specs/location_util.test.ts b/public/app/core/specs/location_util.test.ts deleted file mode 100644 index 8aee29d9a87..00000000000 --- a/public/app/core/specs/location_util.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import locationUtil from 'app/core/utils/location_util'; - -jest.mock('app/core/config', () => { - return { - getConfig: () => ({ appSubUrl: '/subUrl' }), - }; -}); - -describe('locationUtil', () => { - describe('With /subUrl as appSubUrl', () => { - it('/subUrl should be stripped', () => { - const urlWithoutMaster = locationUtil.stripBaseFromUrl('/subUrl/grafana/'); - expect(urlWithoutMaster).toBe('/grafana/'); - }); - }); -}); diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index 90518506908..a8c9107c03b 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -21,8 +21,8 @@ import { TimeZone, toUtc, ExploreMode, + urlUtil, } from '@grafana/data'; -import { renderUrl } from 'app/core/utils/url'; import store from 'app/core/store'; import kbn from 'app/core/utils/kbn'; import { getNextRefIdChar } from './query'; @@ -106,7 +106,7 @@ export async function getExploreUrl(args: GetExploreUrlArguments): Promise { - const appSubUrl = getConfig().appSubUrl; - const stripExtraChars = appSubUrl.endsWith('/') ? 1 : 0; - const urlWithoutBase = - url.length > 0 && url.indexOf(appSubUrl) === 0 ? url.slice(appSubUrl.length - stripExtraChars) : url; - - return urlWithoutBase; -}; - -export const assureBaseUrl = (url: string) => { - if (url.startsWith('/')) { - return `${getConfig().appSubUrl}${stripBaseFromUrl(url)}`; - } - return url; -}; - -export default { stripBaseFromUrl, assureBaseUrl }; diff --git a/public/app/core/utils/richHistory.ts b/public/app/core/utils/richHistory.ts index 0249b9b8b40..7c51c9b86e4 100644 --- a/public/app/core/utils/richHistory.ts +++ b/public/app/core/utils/richHistory.ts @@ -2,8 +2,7 @@ import _ from 'lodash'; // Services & Utils -import { DataQuery, ExploreMode, dateTime } from '@grafana/data'; -import { renderUrl } from 'app/core/utils/url'; +import { DataQuery, ExploreMode, dateTime, urlUtil } from '@grafana/data'; import store from 'app/core/store'; import { serializeStateToUrlParam, SortOrder } from './explore'; import { getExploreDatasources } from '../../features/explore/state/selectors'; @@ -172,7 +171,7 @@ export const createUrlFromRichHistory = (query: RichHistoryQuery) => { const serializedState = serializeStateToUrlParam(exploreState, true); const baseUrl = /.*(?=\/explore)/.exec(`${window.location.href}`)[0]; - const url = renderUrl(`${baseUrl}/explore`, { left: serializedState }); + const url = urlUtil.renderUrl(`${baseUrl}/explore`, { left: serializedState }); return url; }; diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx index 4ed75a2b825..424855eb643 100644 --- a/public/app/features/dashboard/components/DashNav/DashNav.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx @@ -1,5 +1,5 @@ // Libaries -import React, { PureComponent, FC } from 'react'; +import React, { FC, PureComponent } from 'react'; import { connect } from 'react-redux'; import { css } from 'emotion'; // Utils & Services @@ -8,7 +8,8 @@ import { PlaylistSrv } from 'app/features/playlist/playlist_srv'; // Components import { DashNavButton } from './DashNavButton'; import { DashNavTimeControls } from './DashNavTimeControls'; -import { ModalsController, Icon } from '@grafana/ui'; +import { Icon, ModalsController } from '@grafana/ui'; +import { textUtil } from '@grafana/data'; import { BackButton } from 'app/core/components/BackButton/BackButton'; // State import { updateLocation } from 'app/core/actions'; @@ -17,7 +18,6 @@ import { DashboardModel } from '../../state'; import { CoreEvents, StoreState } from 'app/types'; import { ShareModal } from 'app/features/dashboard/components/ShareModal'; import { SaveDashboardModalProxy } from 'app/features/dashboard/components/SaveDashboard/SaveDashboardModalProxy'; -import { sanitizeUrl } from 'app/core/utils/text'; export interface OwnProps { dashboard: DashboardModel; @@ -244,8 +244,8 @@ class DashNav extends PureComponent { )} diff --git a/public/app/features/dashboard/components/DashboardSettings/SettingsCtrl.ts b/public/app/features/dashboard/components/DashboardSettings/SettingsCtrl.ts index 6f4fe077272..606e5e68f08 100644 --- a/public/app/features/dashboard/components/DashboardSettings/SettingsCtrl.ts +++ b/public/app/features/dashboard/components/DashboardSettings/SettingsCtrl.ts @@ -10,9 +10,8 @@ import { backendSrv } from 'app/core/services/backend_srv'; import { DashboardSrv } from '../../services/DashboardSrv'; import { CoreEvents } from 'app/types'; import { GrafanaRootScope } from 'app/routes/GrafanaCtrl'; -import { AppEvents } from '@grafana/data'; +import { AppEvents, locationUtil } from '@grafana/data'; import { promiseToDigest } from '../../../../core/utils/promiseToDigest'; -import locationUtil from 'app/core/utils/location_util'; export class SettingsCtrl { dashboard: DashboardModel; diff --git a/public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx b/public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx index 96b7e38db6b..5519d37a36d 100644 --- a/public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx +++ b/public/app/features/dashboard/components/SaveDashboard/useDashboardSave.tsx @@ -1,11 +1,10 @@ import { useEffect } from 'react'; import useAsyncFn from 'react-use/lib/useAsyncFn'; -import { AppEvents } from '@grafana/data'; +import { AppEvents, locationUtil } from '@grafana/data'; import { useDispatch, useSelector } from 'react-redux'; import { SaveDashboardOptions } from './types'; import { CoreEvents, StoreState } from 'app/types'; import appEvents from 'app/core/app_events'; -import locationUtil from 'app/core/utils/location_util'; import { updateLocation } from 'app/core/reducers/location'; import { DashboardModel } from 'app/features/dashboard/state'; import { getBackendSrv } from 'app/core/services/backend_srv'; diff --git a/public/app/features/dashboard/components/ShareModal/utils.ts b/public/app/features/dashboard/components/ShareModal/utils.ts index 34281c7c577..dacc19d530b 100644 --- a/public/app/features/dashboard/components/ShareModal/utils.ts +++ b/public/app/features/dashboard/components/ShareModal/utils.ts @@ -1,8 +1,7 @@ import { config } from '@grafana/runtime'; -import { appendQueryToUrl, toUrlParams, getUrlSearchParams } from 'app/core/utils/url'; import { getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; import templateSrv from 'app/features/templating/template_srv'; -import { PanelModel, dateTime } from '@grafana/data'; +import { PanelModel, dateTime, urlUtil } from '@grafana/data'; export function buildParams( useCurrentTimeRange: boolean, @@ -10,7 +9,7 @@ export function buildParams( selectedTheme?: string, panel?: PanelModel ) { - const params = getUrlSearchParams(); + const params = urlUtil.getUrlSearchParams(); const range = getTimeSrv().timeRange(); params.from = range.from.valueOf(); @@ -61,7 +60,7 @@ export function buildShareUrl( const baseUrl = buildBaseUrl(); const params = buildParams(useCurrentTimeRange, includeTemplateVars, selectedTheme, panel); - return appendQueryToUrl(baseUrl, toUrlParams(params)); + return urlUtil.appendQueryToUrl(baseUrl, urlUtil.toUrlParams(params)); } export function buildSoloUrl( @@ -77,7 +76,7 @@ export function buildSoloUrl( soloUrl = soloUrl.replace(config.appSubUrl + '/d/', config.appSubUrl + '/d-solo/'); delete params.fullscreen; delete params.edit; - return appendQueryToUrl(soloUrl, toUrlParams(params)); + return urlUtil.appendQueryToUrl(soloUrl, urlUtil.toUrlParams(params)); } export function buildImageUrl( diff --git a/public/app/features/dashboard/components/VersionHistory/HistoryListCtrl.ts b/public/app/features/dashboard/components/VersionHistory/HistoryListCtrl.ts index 25fb73882a4..859006d5682 100644 --- a/public/app/features/dashboard/components/VersionHistory/HistoryListCtrl.ts +++ b/public/app/features/dashboard/components/VersionHistory/HistoryListCtrl.ts @@ -1,10 +1,9 @@ import _ from 'lodash'; import angular, { ILocationService, IScope } from 'angular'; -import locationUtil from 'app/core/utils/location_util'; import { DashboardModel } from '../../state/DashboardModel'; import { CalculateDiffOptions, HistoryListOpts, HistorySrv, RevisionsModel } from './HistorySrv'; -import { AppEvents, dateTime, DateTimeInput, toUtc } from '@grafana/data'; +import { AppEvents, dateTime, DateTimeInput, locationUtil, toUtc } from '@grafana/data'; import { GrafanaRootScope } from 'app/routes/GrafanaCtrl'; import { CoreEvents } from 'app/types'; import { promiseToDigest } from '../../../../core/utils/promiseToDigest'; diff --git a/public/app/features/dashboard/services/DashboardLoaderSrv.ts b/public/app/features/dashboard/services/DashboardLoaderSrv.ts index fcae0302de9..9a22fd1fe0b 100644 --- a/public/app/features/dashboard/services/DashboardLoaderSrv.ts +++ b/public/app/features/dashboard/services/DashboardLoaderSrv.ts @@ -3,12 +3,11 @@ import moment from 'moment'; // eslint-disable-line no-restricted-imports import _ from 'lodash'; import $ from 'jquery'; import kbn from 'app/core/utils/kbn'; -import { dateMath, AppEvents } from '@grafana/data'; +import { AppEvents, dateMath, UrlQueryValue } from '@grafana/data'; import impressionSrv from 'app/core/services/impression_srv'; import { backendSrv } from 'app/core/services/backend_srv'; import { DashboardSrv } from './DashboardSrv'; import DatasourceSrv from 'app/features/plugins/datasource_srv'; -import { UrlQueryValue } from '@grafana/runtime'; import { GrafanaRootScope } from 'app/routes/GrafanaCtrl'; export class DashboardLoaderSrv { diff --git a/public/app/features/dashboard/services/TimeSrv.ts b/public/app/features/dashboard/services/TimeSrv.ts index dbc5b176941..b261d5ba076 100644 --- a/public/app/features/dashboard/services/TimeSrv.ts +++ b/public/app/features/dashboard/services/TimeSrv.ts @@ -264,7 +264,7 @@ export class TimeSrv { this.$timeout(this.refreshDashboard.bind(this), 0); } - timeRangeForUrl() { + timeRangeForUrl = () => { const range = this.timeRange().raw; if (isDateTime(range.from)) { @@ -275,7 +275,7 @@ export class TimeSrv { } return range; - } + }; timeRange(): TimeRange { // make copies if they are moment (do not want to return out internal moment, because they are mutable!) diff --git a/public/app/features/dashboard/state/DashboardMigrator.test.ts b/public/app/features/dashboard/state/DashboardMigrator.test.ts index a0174d293b0..e1457cba853 100644 --- a/public/app/features/dashboard/state/DashboardMigrator.test.ts +++ b/public/app/features/dashboard/state/DashboardMigrator.test.ts @@ -3,7 +3,7 @@ import { DashboardModel } from '../state/DashboardModel'; import { PanelModel } from '../state/PanelModel'; import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants'; import { expect } from 'test/lib/common'; -import { DataLinkBuiltInVars } from '@grafana/ui'; +import { DataLinkBuiltInVars } from '@grafana/data'; jest.mock('app/core/services/context_srv', () => ({})); diff --git a/public/app/features/dashboard/state/DashboardMigrator.ts b/public/app/features/dashboard/state/DashboardMigrator.ts index 352b4ecfa8f..61fd242f0c8 100644 --- a/public/app/features/dashboard/state/DashboardMigrator.ts +++ b/public/app/features/dashboard/state/DashboardMigrator.ts @@ -2,12 +2,11 @@ import _ from 'lodash'; // Utils import getFactors from 'app/core/utils/factors'; -import { appendQueryToUrl } from 'app/core/utils/url'; import kbn from 'app/core/utils/kbn'; // Types import { PanelModel } from './PanelModel'; import { DashboardModel } from './DashboardModel'; -import { DataLink } from '@grafana/data'; +import { DataLink, urlUtil, DataLinkBuiltInVars } from '@grafana/data'; // Constants import { DEFAULT_PANEL_SPAN, @@ -17,7 +16,6 @@ import { GRID_COLUMN_COUNT, MIN_PANEL_HEIGHT, } from 'app/core/constants'; -import { DataLinkBuiltInVars } from '@grafana/ui'; import { isMulti } from 'app/features/variables/guard'; import { alignCurrentWithMulti } from 'app/features/variables/shared/multiOptions'; @@ -737,15 +735,15 @@ function upgradePanelLink(link: any): DataLink { } if (link.keepTime) { - url = appendQueryToUrl(url, `$${DataLinkBuiltInVars.keepTime}`); + url = urlUtil.appendQueryToUrl(url, `$${DataLinkBuiltInVars.keepTime}`); } if (link.includeVars) { - url = appendQueryToUrl(url, `$${DataLinkBuiltInVars.includeVars}`); + url = urlUtil.appendQueryToUrl(url, `$${DataLinkBuiltInVars.includeVars}`); } if (link.params) { - url = appendQueryToUrl(url, link.params); + url = urlUtil.appendQueryToUrl(url, link.params); } return { diff --git a/public/app/features/dashboard/state/DashboardModel.ts b/public/app/features/dashboard/state/DashboardModel.ts index a069196754e..ceb347f7021 100644 --- a/public/app/features/dashboard/state/DashboardModel.ts +++ b/public/app/features/dashboard/state/DashboardModel.ts @@ -10,8 +10,17 @@ import sortByKeys from 'app/core/utils/sort_by_keys'; // Types import { GridPos, panelAdded, PanelModel, panelRemoved } from './PanelModel'; import { DashboardMigrator } from './DashboardMigrator'; -import { AppEvent, dateTime, DateTimeInput, isDateTime, PanelEvents, TimeRange, TimeZone, toUtc } from '@grafana/data'; -import { UrlQueryValue } from '@grafana/runtime'; +import { + AppEvent, + dateTime, + DateTimeInput, + isDateTime, + PanelEvents, + TimeRange, + TimeZone, + toUtc, + UrlQueryValue, +} from '@grafana/data'; import { CoreEvents, DashboardMeta, KIOSK_MODE_TV } from 'app/types'; import { getConfig } from '../../../core/config'; import { GetVariables, getVariables } from 'app/features/variables/state/selectors'; diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index 9dc010a2cca..9862d32c579 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -9,7 +9,6 @@ import { VariableSrv } from 'app/features/templating/variable_srv'; import { KeybindingSrv } from 'app/core/services/keybindingSrv'; // Actions import { notifyApp, updateLocation } from 'app/core/actions'; -import locationUtil from 'app/core/utils/location_util'; import { clearDashboardQueriesToUpdateOnLoad, dashboardInitCompleted, @@ -21,7 +20,7 @@ import { // Types import { DashboardDTO, DashboardRouteInfo, StoreState, ThunkDispatch, ThunkResult } from 'app/types'; import { DashboardModel } from './DashboardModel'; -import { DataQuery } from '@grafana/data'; +import { DataQuery, locationUtil } from '@grafana/data'; import { getConfig } from '../../../core/config'; import { initDashboardTemplating, processVariables, completeDashboardTemplating } from '../../variables/state/actions'; import { emitDashboardViewEvent } from './analyticsProcessor'; diff --git a/public/app/features/datasources/settings/DataSourceSettingsPage.tsx b/public/app/features/datasources/settings/DataSourceSettingsPage.tsx index 5bf9cb8bd84..52dc1e93101 100644 --- a/public/app/features/datasources/settings/DataSourceSettingsPage.tsx +++ b/public/app/features/datasources/settings/DataSourceSettingsPage.tsx @@ -14,18 +14,17 @@ import appEvents from 'app/core/app_events'; import { getDataSource, getDataSourceMeta } from '../state/selectors'; import { deleteDataSource, - loadDataSource, - updateDataSource, initDataSourceSettings, + loadDataSource, testDataSource, + updateDataSource, } from '../state/actions'; import { getNavModel } from 'app/core/selectors/navModel'; import { getRouteParamsId } from 'app/core/selectors/location'; // Types import { CoreEvents, StoreState } from 'app/types/'; -import { UrlQueryMap } from '@grafana/runtime'; +import { DataSourcePluginMeta, DataSourceSettings, NavModel, UrlQueryMap } from '@grafana/data'; import { Icon } from '@grafana/ui'; -import { DataSourcePluginMeta, DataSourceSettings, NavModel } from '@grafana/data'; import { getDataSourceLoadingNav } from '../state/navModel'; import PluginStateinfo from 'app/features/plugins/PluginStateInfo'; import { dataSourceLoaded, setDataSourceName, setIsDefault } from '../state/reducers'; diff --git a/public/app/features/datasources/state/selectors.ts b/public/app/features/datasources/state/selectors.ts index 9a0a6a0ef42..7837a65ef9e 100644 --- a/public/app/features/datasources/state/selectors.ts +++ b/public/app/features/datasources/state/selectors.ts @@ -1,6 +1,5 @@ -import { DataSourceSettings, DataSourcePluginMeta } from '@grafana/data'; +import { DataSourcePluginMeta, DataSourceSettings, UrlQueryValue } from '@grafana/data'; import { DataSourcesState } from '../../../types/datasources'; -import { UrlQueryValue } from '@grafana/runtime'; export const getDataSources = (state: DataSourcesState) => { const regex = new RegExp(state.searchQuery, 'i'); diff --git a/public/app/features/explore/state/reducers.test.ts b/public/app/features/explore/state/reducers.test.ts index db4161d9c49..5ece646ed83 100644 --- a/public/app/features/explore/state/reducers.test.ts +++ b/public/app/features/explore/state/reducers.test.ts @@ -2,11 +2,12 @@ import { DataQuery, DataSourceApi, dateTime, + ExploreMode, LoadingState, LogsDedupStrategy, - toDataFrame, - ExploreMode, RawTimeRange, + toDataFrame, + UrlQueryMap, } from '@grafana/data'; import { @@ -33,7 +34,6 @@ import { } from './actionTypes'; import { serializeStateToUrlParam } from 'app/core/utils/explore'; import { updateLocation } from '../../../core/actions'; -import { UrlQueryMap } from '@grafana/runtime'; describe('Explore item reducer', () => { describe('scanning', () => { diff --git a/public/app/features/manage-dashboards/state/actions.ts b/public/app/features/manage-dashboards/state/actions.ts index 3e1ad43383d..e25ce67b133 100644 --- a/public/app/features/manage-dashboards/state/actions.ts +++ b/public/app/features/manage-dashboards/state/actions.ts @@ -1,4 +1,4 @@ -import { AppEvents, DataSourceInstanceSettings, DataSourceSelectItem } from '@grafana/data'; +import { AppEvents, DataSourceInstanceSettings, DataSourceSelectItem, locationUtil } from '@grafana/data'; import { getBackendSrv } from '@grafana/runtime'; import config from 'app/core/config'; import { @@ -9,7 +9,6 @@ import { InputType, ImportDashboardDTO, } from './reducers'; -import locationUtil from 'app/core/utils/location_util'; import { updateLocation } from 'app/core/actions'; import { ThunkResult } from 'app/types'; import { appEvents } from '../../../core/core'; diff --git a/public/app/features/org/UserInviteForm.tsx b/public/app/features/org/UserInviteForm.tsx index 04b6e3c314f..31a38e04554 100644 --- a/public/app/features/org/UserInviteForm.tsx +++ b/public/app/features/org/UserInviteForm.tsx @@ -17,8 +17,7 @@ import { updateLocation } from 'app/core/actions'; import { connect } from 'react-redux'; import { hot } from 'react-hot-loader'; import { appEvents } from 'app/core/core'; -import { AppEvents } from '@grafana/data'; -import { assureBaseUrl } from 'app/core/utils/location_util'; +import { AppEvents, locationUtil } from '@grafana/data'; const roles = [ { label: 'Viewer', value: OrgRole.Viewer }, @@ -77,7 +76,7 @@ export const UserInviteForm: FC = ({ updateLocation }) => {
- + Back diff --git a/public/app/features/panel/panellinks/linkSuppliers.ts b/public/app/features/panel/panellinks/linkSuppliers.ts index 4b8b5ba7818..5fa8a254b8e 100644 --- a/public/app/features/panel/panellinks/linkSuppliers.ts +++ b/public/app/features/panel/panellinks/linkSuppliers.ts @@ -1,19 +1,20 @@ import { PanelModel } from 'app/features/dashboard/state/PanelModel'; import { + DataLink, + DisplayValue, + Field, FieldDisplay, - LinkModelSupplier, + formattedValueToString, + getFieldDisplayValuesProxy, getTimeField, Labels, - ScopedVars, - ScopedVar, - Field, LinkModel, - formattedValueToString, - DisplayValue, - DataLink, + LinkModelSupplier, + ScopedVar, + ScopedVars, } from '@grafana/data'; import { getLinkSrv } from './link_srv'; -import { getFieldDisplayValuesProxy } from './fieldDisplayValuesProxy'; +import { config } from 'app/core/config'; interface SeriesVars { name?: string; @@ -100,7 +101,9 @@ export const getFieldLinksSupplier = (value: FieldDisplay): LinkModelSupplier(link: DataLink, scopedVars: ScopedVars, origin: T): LinkModel => { const params: KeyValue = {}; - const timeRangeUrl = toUrlParams(this.timeSrv.timeRangeForUrl()); + const timeRangeUrl = urlUtil.toUrlParams(this.timeSrv.timeRangeForUrl()); let href = link.url; @@ -302,7 +302,7 @@ export class LinkSrv implements LinkService { this.templateSrv.fillVariableValuesForUrl(params, scopedVars); - const variablesQuery = toUrlParams(params); + const variablesQuery = urlUtil.toUrlParams(params); info.href = this.templateSrv.replace(info.href, { ...scopedVars, @@ -316,7 +316,7 @@ export class LinkSrv implements LinkService { }, }); - info.href = getConfig().disableSanitizeHtml ? info.href : sanitizeUrl(info.href); + info.href = getConfig().disableSanitizeHtml ? info.href : textUtil.sanitizeUrl(info.href); return info; }; diff --git a/public/app/features/panel/panellinks/specs/link_srv.test.ts b/public/app/features/panel/panellinks/specs/link_srv.test.ts index c8dcd3ebece..bfa35128bc6 100644 --- a/public/app/features/panel/panellinks/specs/link_srv.test.ts +++ b/public/app/features/panel/panellinks/specs/link_srv.test.ts @@ -1,9 +1,11 @@ import { LinkSrv } from '../link_srv'; -import { DataLinkBuiltInVars } from '@grafana/ui'; +import { DataLinkBuiltInVars, locationUtil, VariableModel } from '@grafana/data'; import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { TemplateSrv } from 'app/features/templating/template_srv'; import { advanceTo } from 'jest-date-mock'; import { updateConfig } from '../../../../core/config'; +import { variableAdapters } from '../../../variables/adapters'; +import { createQueryVariableAdapter } from '../../../variables/query/adapter'; jest.mock('app/core/core', () => ({ appEvents: { @@ -46,29 +48,48 @@ describe('linkSrv', () => { timeSrv.setTime({ from: 'now-1h', to: 'now' }); _dashboard.refresh = false; - const _templateSrv = new TemplateSrv(); - _templateSrv.init([ + const variablesMock = [ { type: 'query', name: 'test1', + label: 'Test1', + hide: false, current: { value: 'val1' }, + skipUrlSync: false, getValueForUrl: function() { - return this.current.value; + return 'val1'; }, - }, + } as VariableModel, { type: 'query', name: 'test2', + label: 'Test2', + hide: false, current: { value: 'val2' }, + skipUrlSync: false, getValueForUrl: function() { - return this.current.value; + return 'val2'; }, + } as VariableModel, + ]; + const _templateSrv = new TemplateSrv({ + // @ts-ignore + getVariables: () => { + return variablesMock; }, - ]); + // @ts-ignore + getVariableWithName: (name: string) => { + return variablesMock.filter(v => v.name === name)[0]; + }, + }); linkSrv = new LinkSrv(_templateSrv, timeSrv); } + beforeAll(() => { + variableAdapters.register(createQueryVariableAdapter()); + }); + beforeEach(() => { initLinkSrv(); advanceTo(1000000000); @@ -217,8 +238,14 @@ describe('linkSrv', () => { `( "when link '$url' and config.appSubUrl set to '$appSubUrl' then result should be '$expected'", ({ url, appSubUrl, expected }) => { - updateConfig({ - appSubUrl, + locationUtil.initialize({ + getConfig: () => { + return { appSubUrl } as any; + }, + // @ts-ignore + buildParamsFromVariables: () => {}, + // @ts-ignore + getTimeRangeForUrl: () => {}, }); const link = linkSrv.getDataLinkUIModel( diff --git a/public/app/features/playlist/playlist_srv.ts b/public/app/features/playlist/playlist_srv.ts index f15adecb09e..f294db740fa 100644 --- a/public/app/features/playlist/playlist_srv.ts +++ b/public/app/features/playlist/playlist_srv.ts @@ -2,14 +2,14 @@ import _ from 'lodash'; // Utils -import { toUrlParams } from 'app/core/utils/url'; import coreModule from '../../core/core_module'; import appEvents from 'app/core/app_events'; -import locationUtil from 'app/core/utils/location_util'; + import kbn from 'app/core/utils/kbn'; import { store } from 'app/store/store'; import { CoreEvents } from 'app/types'; import { getBackendSrv } from '@grafana/runtime'; +import { locationUtil, urlUtil } from '@grafana/data'; export const queryParamsToPreserve: { [key: string]: boolean } = { kiosk: true, @@ -55,7 +55,7 @@ export class PlaylistSrv { // this is done inside timeout to make sure digest happens after // as this can be called from react this.$timeout(() => { - this.$location.url(nextDashboardUrl + '?' + toUrlParams(filteredParams)); + this.$location.url(nextDashboardUrl + '?' + urlUtil.toUrlParams(filteredParams)); }); this.index++; diff --git a/public/app/features/plugins/AppRootPage.tsx b/public/app/features/plugins/AppRootPage.tsx index 1b3c9121741..9858827313e 100644 --- a/public/app/features/plugins/AppRootPage.tsx +++ b/public/app/features/plugins/AppRootPage.tsx @@ -2,15 +2,13 @@ import React, { Component } from 'react'; import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; - // Types import { StoreState } from 'app/types'; -import { UrlQueryMap } from '@grafana/runtime'; +import { AppEvents, AppPlugin, AppPluginMeta, NavModel, PluginType, UrlQueryMap } from '@grafana/data'; import Page from 'app/core/components/Page/Page'; import { getPluginSettings } from './PluginSettingsCache'; import { importAppPlugin } from './plugin_loader'; -import { AppPlugin, AppPluginMeta, PluginType, NavModel, AppEvents } from '@grafana/data'; import { getLoadingNav } from './PluginPage'; import { getNotFoundNav, getWarningNav } from 'app/core/nav_model_srv'; import { appEvents } from 'app/core/core'; diff --git a/public/app/features/plugins/PluginPage.tsx b/public/app/features/plugins/PluginPage.tsx index a05219660c2..018b826052c 100644 --- a/public/app/features/plugins/PluginPage.tsx +++ b/public/app/features/plugins/PluginPage.tsx @@ -4,21 +4,21 @@ import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; import find from 'lodash/find'; // Types -import { UrlQueryMap } from '@grafana/runtime'; -import { StoreState, AppNotificationSeverity, CoreEvents } from 'app/types'; -import { Alert, Tooltip } from '@grafana/ui'; import { AppPlugin, GrafanaPlugin, + NavModel, + NavModelItem, PluginDependencies, PluginInclude, PluginIncludeType, PluginMeta, PluginMetaInfo, PluginType, - NavModel, - NavModelItem, + UrlQueryMap, } from '@grafana/data'; +import { AppNotificationSeverity, CoreEvents, StoreState } from 'app/types'; +import { Alert, Tooltip } from '@grafana/ui'; import Page from 'app/core/components/Page/Page'; import { getPluginSettings } from './PluginSettingsCache'; diff --git a/public/app/features/search/components/DashboardListPage.tsx b/public/app/features/search/components/DashboardListPage.tsx index cd6d148b3ee..516877d0580 100644 --- a/public/app/features/search/components/DashboardListPage.tsx +++ b/public/app/features/search/components/DashboardListPage.tsx @@ -1,13 +1,12 @@ import React, { FC, memo } from 'react'; import { useAsync } from 'react-use'; import { connect, MapStateToProps } from 'react-redux'; -import { NavModel } from '@grafana/data'; +import { NavModel, locationUtil } from '@grafana/data'; import { getLocationSrv } from '@grafana/runtime'; import { StoreState } from 'app/types'; import { getNavModel } from 'app/core/selectors/navModel'; import { getRouteParams } from 'app/core/selectors/location'; import Page from 'app/core/components/Page/Page'; -import locationUtil from 'app/core/utils/location_util'; import { backendSrv } from 'app/core/services/backend_srv'; import { ManageDashboards } from './ManageDashboards'; diff --git a/public/app/features/templating/specs/template_srv.test.ts b/public/app/features/templating/specs/template_srv.test.ts index 635c97b5443..209e1f7c82e 100644 --- a/public/app/features/templating/specs/template_srv.test.ts +++ b/public/app/features/templating/specs/template_srv.test.ts @@ -1,6 +1,8 @@ import { TemplateSrv } from '../template_srv'; import { convertToStoreState } from 'test/helpers/convertToStoreState'; import { getTemplateSrvDependencies } from '../../../../test/helpers/getTemplateSrvDependencies'; +import { variableAdapters } from '../../variables/adapters'; +import { createQueryVariableAdapter } from '../../variables/query/adapter'; describe('templateSrv', () => { let _templateSrv: any; @@ -448,6 +450,9 @@ describe('templateSrv', () => { }); describe('fillVariableValuesForUrl with multi value', () => { + beforeAll(() => { + variableAdapters.register(createQueryVariableAdapter()); + }); beforeEach(() => { initTemplateSrv([ { diff --git a/public/app/features/templating/template_srv.ts b/public/app/features/templating/template_srv.ts index 478a0679d2b..52f66c8363c 100644 --- a/public/app/features/templating/template_srv.ts +++ b/public/app/features/templating/template_srv.ts @@ -1,13 +1,13 @@ import kbn from 'app/core/utils/kbn'; import _ from 'lodash'; -import { escapeHtml } from 'app/core/utils/text'; -import { deprecationWarning, ScopedVars, TimeRange } from '@grafana/data'; +import { deprecationWarning, ScopedVars, textUtil, TimeRange } from '@grafana/data'; import { getFilteredVariables, getVariables, getVariableWithName } from '../variables/state/selectors'; import { getConfig } from 'app/core/config'; import { variableRegex } from './utils'; import { isAdHoc } from '../variables/guard'; import { VariableModel } from './types'; import { setTemplateSrv, TemplateSrv as BaseTemplateSrv } from '@grafana/runtime'; +import { variableAdapters } from '../variables/adapters'; function luceneEscape(value: string) { return value.replace(/([\!\*\+\-\=<>\s\&\|\(\)\[\]\{\}\^\~\?\:\\/"])/g, '\\$1'); @@ -194,9 +194,9 @@ export class TemplateSrv implements BaseTemplateSrv { } case 'html': { if (_.isArray(value)) { - return escapeHtml(value.join(', ')); + return textUtil.escapeHtml(value.join(', ')); } - return escapeHtml(value); + return textUtil.escapeHtml(value); } case 'json': { return JSON.stringify(value); @@ -399,8 +399,8 @@ export class TemplateSrv implements BaseTemplateSrv { }); } - fillVariableValuesForUrl(params: any, scopedVars?: ScopedVars) { - _.each(this._variables, variable => { + fillVariableValuesForUrl = (params: any, scopedVars?: ScopedVars) => { + _.each(this.getVariables(), variable => { if (scopedVars && scopedVars[variable.name] !== void 0) { if (scopedVars[variable.name].skipUrlSync) { return; @@ -410,10 +410,10 @@ export class TemplateSrv implements BaseTemplateSrv { if (variable.skipUrlSync) { return; } - params['var-' + variable.name] = variable.getValueForUrl(); + params['var-' + variable.name] = variableAdapters.get(variable.type).getValueForUrl(variable); } }); - } + }; distributeVariable(value: any, variable: any) { value = _.map(value, (val: any, index: number) => { diff --git a/public/app/features/templating/variable_srv.ts b/public/app/features/templating/variable_srv.ts index 5798d5c0d34..33b399e634c 100644 --- a/public/app/features/templating/variable_srv.ts +++ b/public/app/features/templating/variable_srv.ts @@ -9,9 +9,8 @@ import { TemplateSrv } from 'app/features/templating/template_srv'; import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; // Types -import { AppEvents, TimeRange } from '@grafana/data'; +import { AppEvents, TimeRange, UrlQueryMap } from '@grafana/data'; import { CoreEvents } from 'app/types'; -import { UrlQueryMap } from '@grafana/runtime'; import { appEvents, contextSrv } from 'app/core/core'; export class VariableSrv { diff --git a/public/app/features/users/SignupInvited.tsx b/public/app/features/users/SignupInvited.tsx index 32ca6b6718d..aecfba98780 100644 --- a/public/app/features/users/SignupInvited.tsx +++ b/public/app/features/users/SignupInvited.tsx @@ -1,14 +1,15 @@ import React, { FC, useState } from 'react'; import { hot } from 'react-hot-loader'; -import { connect, MapStateToProps, MapDispatchToProps } from 'react-redux'; +import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux'; import { StoreState } from 'app/types'; import { updateLocation } from 'app/core/actions'; -import { UrlQueryValue, getBackendSrv } from '@grafana/runtime'; -import { Button, Input, Form, Field } from '@grafana/ui'; +import { getBackendSrv } from '@grafana/runtime'; +import { Button, Field, Form, Input } from '@grafana/ui'; import { useAsync } from 'react-use'; import Page from 'app/core/components/Page/Page'; import { contextSrv } from 'app/core/core'; import { getConfig } from 'app/core/config'; +import { UrlQueryValue } from '@grafana/data'; interface ConnectedProps { code?: UrlQueryValue; diff --git a/public/app/features/variables/adapters.ts b/public/app/features/variables/adapters.ts index 703eed3c394..bb3736882d3 100644 --- a/public/app/features/variables/adapters.ts +++ b/public/app/features/variables/adapters.ts @@ -1,6 +1,6 @@ import { ComponentType } from 'react'; import { Reducer } from 'redux'; -import { UrlQueryValue } from '@grafana/runtime'; +import { Registry, UrlQueryValue, VariableType } from '@grafana/data'; import { AdHocVariableModel, @@ -16,7 +16,6 @@ import { import { VariableEditorProps } from './editor/types'; import { VariablesState } from './state/variablesReducer'; import { VariablePickerProps } from './pickers/types'; -import { Registry, VariableType } from '@grafana/data'; import { createQueryVariableAdapter } from './query/adapter'; import { createCustomVariableAdapter } from './custom/adapter'; import { createTextBoxVariableAdapter } from './textbox/adapter'; diff --git a/public/app/features/variables/adhoc/urlParser.test.ts b/public/app/features/variables/adhoc/urlParser.test.ts index 97010e44039..1e1fd21ffae 100644 --- a/public/app/features/variables/adhoc/urlParser.test.ts +++ b/public/app/features/variables/adhoc/urlParser.test.ts @@ -1,6 +1,6 @@ import { toFilters, toUrl } from './urlParser'; import { AdHocVariableFilter } from 'app/features/templating/types'; -import { UrlQueryValue } from '@grafana/runtime'; +import { UrlQueryValue } from '@grafana/data'; describe('urlParser', () => { describe('parsing toUrl with no filters', () => { diff --git a/public/app/features/variables/adhoc/urlParser.ts b/public/app/features/variables/adhoc/urlParser.ts index 8430a4b71bf..76cd372b4eb 100644 --- a/public/app/features/variables/adhoc/urlParser.ts +++ b/public/app/features/variables/adhoc/urlParser.ts @@ -1,5 +1,5 @@ import { AdHocVariableFilter } from 'app/features/templating/types'; -import { UrlQueryValue } from '@grafana/runtime'; +import { UrlQueryValue } from '@grafana/data'; import { isArray, isString } from 'lodash'; export const toUrl = (filters: AdHocVariableFilter[]): string[] => { diff --git a/public/app/features/variables/state/actions.test.ts b/public/app/features/variables/state/actions.test.ts index 27e22d4db5e..3236b3de6c0 100644 --- a/public/app/features/variables/state/actions.test.ts +++ b/public/app/features/variables/state/actions.test.ts @@ -1,5 +1,5 @@ import { AnyAction } from 'redux'; -import { UrlQueryMap } from '@grafana/runtime'; +import { UrlQueryMap } from '@grafana/data'; import { getTemplatingAndLocationRootReducer, getTemplatingRootReducer } from './helpers'; import { variableAdapters } from '../adapters'; @@ -10,20 +10,20 @@ import { createConstantVariableAdapter } from '../constant/adapter'; import { reduxTester } from '../../../../test/core/redux/reduxTester'; import { TemplatingState } from 'app/features/variables/state/reducers'; import { + changeVariableMultiValue, initDashboardTemplating, processVariables, setOptionFromUrl, validateVariableSelectionState, - changeVariableMultiValue, } from './actions'; import { addInitLock, addVariable, + changeVariableProp, removeInitLock, removeVariable, resolveInitLock, setCurrentVariableValue, - changeVariableProp, } from './sharedReducer'; import { NEW_VARIABLE_ID, toVariableIdentifier, toVariablePayload } from './types'; import { diff --git a/public/app/features/variables/state/actions.ts b/public/app/features/variables/state/actions.ts index 594d6addf75..3055dc9981f 100644 --- a/public/app/features/variables/state/actions.ts +++ b/public/app/features/variables/state/actions.ts @@ -1,6 +1,5 @@ import castArray from 'lodash/castArray'; -import { UrlQueryMap, UrlQueryValue } from '@grafana/runtime'; -import { AppEvents, TimeRange } from '@grafana/data'; +import { AppEvents, TimeRange, UrlQueryMap, UrlQueryValue } from '@grafana/data'; import angular from 'angular'; import { @@ -8,8 +7,8 @@ import { VariableModel, VariableOption, VariableRefresh, - VariableWithOptions, VariableWithMultiSupport, + VariableWithOptions, } from '../../templating/types'; import { StoreState, ThunkResult } from '../../../types'; import { getVariable, getVariables } from './selectors'; @@ -19,10 +18,10 @@ import { updateLocation } from 'app/core/actions'; import { addInitLock, addVariable, + changeVariableProp, removeInitLock, resolveInitLock, setCurrentVariableValue, - changeVariableProp, } from './sharedReducer'; import { toVariableIdentifier, toVariablePayload, VariableIdentifier } from './types'; import { appEvents } from 'app/core/core'; diff --git a/public/app/features/variables/state/processVariable.test.ts b/public/app/features/variables/state/processVariable.test.ts index 88ab43b2189..2614fc58884 100644 --- a/public/app/features/variables/state/processVariable.test.ts +++ b/public/app/features/variables/state/processVariable.test.ts @@ -1,4 +1,4 @@ -import { UrlQueryMap } from '@grafana/runtime'; +import { UrlQueryMap } from '@grafana/data'; import { getTemplatingRootReducer } from './helpers'; import { variableAdapters } from '../adapters'; diff --git a/public/app/plugins/datasource/elasticsearch/configuration/DataLinks.tsx b/public/app/plugins/datasource/elasticsearch/configuration/DataLinks.tsx index 06d09451577..dd7fb13a96b 100644 --- a/public/app/plugins/datasource/elasticsearch/configuration/DataLinks.tsx +++ b/public/app/plugins/datasource/elasticsearch/configuration/DataLinks.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { css } from 'emotion'; -import { Button, DataLinkBuiltInVars, stylesFactory, useTheme } from '@grafana/ui'; -import { GrafanaTheme, VariableOrigin } from '@grafana/data'; +import { Button, stylesFactory, useTheme } from '@grafana/ui'; +import { GrafanaTheme, VariableOrigin, DataLinkBuiltInVars } from '@grafana/data'; import { DataLinkConfig } from '../types'; import { DataLink } from './DataLink'; diff --git a/public/app/plugins/datasource/loki/configuration/DerivedFields.tsx b/public/app/plugins/datasource/loki/configuration/DerivedFields.tsx index 1505d13a9af..7f9217efcc4 100644 --- a/public/app/plugins/datasource/loki/configuration/DerivedFields.tsx +++ b/public/app/plugins/datasource/loki/configuration/DerivedFields.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { css } from 'emotion'; -import { Button, DataLinkBuiltInVars, stylesFactory, useTheme } from '@grafana/ui'; -import { GrafanaTheme, VariableOrigin } from '@grafana/data'; +import { Button, stylesFactory, useTheme } from '@grafana/ui'; +import { GrafanaTheme, VariableOrigin, DataLinkBuiltInVars } from '@grafana/data'; import { DerivedFieldConfig } from '../types'; import { DerivedField } from './DerivedField'; import { DebugSection } from './DebugSection'; diff --git a/public/app/plugins/panel/bargauge/BarGaugePanel.tsx b/public/app/plugins/panel/bargauge/BarGaugePanel.tsx index bcc90b5f771..b0d4b7dca25 100644 --- a/public/app/plugins/panel/bargauge/BarGaugePanel.tsx +++ b/public/app/plugins/panel/bargauge/BarGaugePanel.tsx @@ -13,7 +13,6 @@ import { getDisplayValueAlignmentFactors, DisplayValueAlignmentFactors, } from '@grafana/data'; -import { getFieldLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers'; export class BarGaugePanel extends PureComponent> { renderValue = (valueProps: VizRepeaterRenderValueProps): JSX.Element => { @@ -22,7 +21,7 @@ export class BarGaugePanel extends PureComponent> { const { field, display, view, colIndex } = value; return ( - + {({ openMenu, targetClassName }) => { return ( > { renderValue = (valueProps: VizRepeaterRenderValueProps): JSX.Element => { @@ -19,7 +18,7 @@ export class GaugePanel extends PureComponent> { const { field, display } = value; return ( - + {({ openMenu, targetClassName }) => { return (
'; @@ -283,7 +283,7 @@ export default function GraphTooltip(this: any, elem: any, dashboard: any, scope self.renderAndShow(absoluteTime, seriesHtml, pos, xMode); } else if (item) { // single series tooltip - const color = sanitize(item.series.color); + const color = textUtil.sanitize(item.series.color); series = seriesList[item.seriesIndex]; group = '
'; group += ' ' + series.aliasEscaped + ':
'; @@ -294,7 +294,7 @@ export default function GraphTooltip(this: any, elem: any, dashboard: any, scope value = item.datapoint[1]; } - value = sanitize(series.formatValue(value)); + value = textUtil.sanitize(series.formatValue(value)); absoluteTime = dashboard.formatDate(item.datapoint[0], tooltipFormat); group += '
' + value + '
'; diff --git a/public/app/plugins/panel/news/NewsPanel.tsx b/public/app/plugins/panel/news/NewsPanel.tsx index 8286ee6cc48..54ee950b2b6 100755 --- a/public/app/plugins/panel/news/NewsPanel.tsx +++ b/public/app/plugins/panel/news/NewsPanel.tsx @@ -1,19 +1,18 @@ // Libraries import React, { PureComponent } from 'react'; -import { css } from 'emotion'; // Utils & Services -import { GrafanaTheme } from '@grafana/data'; -import { stylesFactory, CustomScrollbar } from '@grafana/ui'; +import { CustomScrollbar, stylesFactory } from '@grafana/ui'; + import config from 'app/core/config'; import { feedToDataFrame } from './utils'; -import { sanitize } from 'app/core/utils/text'; import { loadRSSFeed } from './rss'; // Types -import { PanelProps, DataFrameView, dateTime } from '@grafana/data'; +import { PanelProps, DataFrameView, dateTime, GrafanaTheme, textUtil } from '@grafana/data'; import { NewsOptions, NewsItem } from './types'; import { DEFAULT_FEED_URL, PROXY_PREFIX } from './constants'; +import { css } from 'emotion'; interface Props extends PanelProps {} @@ -82,7 +81,7 @@ export class NewsPanel extends PureComponent {
{item.title}
{dateTime(item.date).format('MMM DD')}
-
+
); })} diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index 850c9ac2cfd..b6d51ad5fd4 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -4,7 +4,6 @@ import $ from 'jquery'; import 'vendor/flot/jquery.flot'; import 'vendor/flot/jquery.flot.gauge'; import 'app/features/panel/panellinks/link_srv'; -import locationUtil from 'app/core/utils/location_util'; import { DataFrame, @@ -23,6 +22,7 @@ import { getColorFromHexRgbOrName, PanelEvents, formattedValueToString, + locationUtil, } from '@grafana/data'; import { convertOldAngularValueMapping } from '@grafana/ui'; diff --git a/public/app/plugins/panel/stat/StatPanel.tsx b/public/app/plugins/panel/stat/StatPanel.tsx index ecb6ccf2118..5e0fa1fcfde 100644 --- a/public/app/plugins/panel/stat/StatPanel.tsx +++ b/public/app/plugins/panel/stat/StatPanel.tsx @@ -24,8 +24,6 @@ import { DisplayValueAlignmentFactors, } from '@grafana/data'; -import { getFieldLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers'; - export class StatPanel extends PureComponent> { renderValue = (valueProps: VizRepeaterRenderValueProps): JSX.Element => { const { timeRange, options } = this.props; @@ -48,7 +46,7 @@ export class StatPanel extends PureComponent> { } return ( - + {({ openMenu, targetClassName }) => { return ( '); + const safeContent = textUtil.escapeHtml(content).replace(/\n/g, '
'); this.updateContent(safeContent); } @@ -95,7 +94,7 @@ export class TextPanelCtrl extends PanelCtrl { console.log('Text panel error: ', e); } - this.content = this.$sce.trustAsHtml(config.disableSanitizeHtml ? html : sanitize(html)); + this.content = this.$sce.trustAsHtml(config.disableSanitizeHtml ? html : textUtil.sanitize(html)); } } diff --git a/public/app/plugins/panel/text2/TextPanel.tsx b/public/app/plugins/panel/text2/TextPanel.tsx index cd6d202ebfd..4cbbb322f48 100644 --- a/public/app/plugins/panel/text2/TextPanel.tsx +++ b/public/app/plugins/panel/text2/TextPanel.tsx @@ -1,15 +1,11 @@ // Libraries import React, { PureComponent } from 'react'; import { debounce } from 'lodash'; -import { renderMarkdown } from '@grafana/data'; - +import { PanelProps, renderMarkdown, textUtil } from '@grafana/data'; // Utils -import { sanitize } from 'app/core/utils/text'; import config from 'app/core/config'; - // Types import { TextOptions } from './types'; -import { PanelProps } from '@grafana/data'; interface Props extends PanelProps {} interface State { @@ -43,7 +39,7 @@ export class TextPanel extends PureComponent { html = replaceVariables(html, {}, 'html'); - return config.disableSanitizeHtml ? html : sanitize(html); + return config.disableSanitizeHtml ? html : textUtil.sanitize(html); } prepareText(content: string): string { diff --git a/public/app/routes/GrafanaCtrl.ts b/public/app/routes/GrafanaCtrl.ts index 79beed497dd..eb0f18740b2 100644 --- a/public/app/routes/GrafanaCtrl.ts +++ b/public/app/routes/GrafanaCtrl.ts @@ -6,12 +6,12 @@ import Drop from 'tether-drop'; // Utils and servies import { colors } from '@grafana/ui'; -import { setBackendSrv, setDataSourceSrv } from '@grafana/runtime'; +import { getTemplateSrv, setBackendSrv, setDataSourceSrv } from '@grafana/runtime'; import config from 'app/core/config'; import coreModule from 'app/core/core_module'; import { profiler } from 'app/core/profiler'; import appEvents from 'app/core/app_events'; -import { TimeSrv, setTimeSrv } from 'app/features/dashboard/services/TimeSrv'; +import { TimeSrv, setTimeSrv, getTimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; import { KeybindingSrv, setKeybindingSrv } from 'app/core/services/keybindingSrv'; import { AngularLoader, setAngularLoader } from 'app/core/services/AngularLoader'; @@ -29,7 +29,7 @@ import { BridgeSrv } from 'app/core/services/bridge_srv'; import { PlaylistSrv } from 'app/features/playlist/playlist_srv'; import { DashboardSrv, setDashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; import { ILocationService, ITimeoutService, IRootScopeService, IAngularEvent } from 'angular'; -import { AppEvent, AppEvents } from '@grafana/data'; +import { AppEvent, AppEvents, locationUtil } from '@grafana/data'; import { backendSrv } from 'app/core/services/backend_srv'; export type GrafanaRootScope = IRootScopeService & AppEventEmitter & AppEventConsumer & { colors: string[] }; @@ -58,6 +58,13 @@ export class GrafanaCtrl { setKeybindingSrv(keybindingSrv); setDashboardSrv(dashboardSrv); + locationUtil.initialize({ + getConfig: () => config, + getTimeRangeForUrl: getTimeSrv().timeRangeForUrl, + // @ts-ignore + buildParamsFromVariables: getTemplateSrv().fillVariableValuesForUrl, + }); + const store = configureStore(); setLocationSrv({ update: (opt: LocationUpdate) => { diff --git a/public/app/routes/dashboard_loaders.ts b/public/app/routes/dashboard_loaders.ts index bbb17b721b7..81b62142454 100644 --- a/public/app/routes/dashboard_loaders.ts +++ b/public/app/routes/dashboard_loaders.ts @@ -1,9 +1,8 @@ import coreModule from 'app/core/core_module'; -import locationUtil from 'app/core/utils/location_util'; -import { UrlQueryMap } from '@grafana/runtime'; +import { locationUtil, UrlQueryMap } from '@grafana/data'; import { DashboardLoaderSrv } from 'app/features/dashboard/services/DashboardLoaderSrv'; import { ILocationService } from 'angular'; -import { Scope, CoreEvents, AppEventEmitter } from 'app/types'; +import { AppEventEmitter, CoreEvents, Scope } from 'app/types'; import { backendSrv } from 'app/core/services/backend_srv'; export class LoadDashboardCtrl { diff --git a/public/app/types/location.ts b/public/app/types/location.ts index 13a4e88ab79..c55cae9a4de 100644 --- a/public/app/types/location.ts +++ b/public/app/types/location.ts @@ -1,4 +1,4 @@ -import { UrlQueryMap } from '@grafana/runtime'; +import { UrlQueryMap } from '@grafana/data'; export interface LocationState { url: string;