FieldOverride: Support data links via field overrides (#23590)

* Move xss and sanitize packages to grafana-data

* Move text, url and location utils to grafana-data

* Move grafana config types to grafana-data

* Move field display value proxy to grafana-data

* Fix

* Move data links built in vars to grafana-data

* Attach links supplier to when applying field overrides

* Prep tests

* Use links suppliers attached via field overrides

* locationUtil dependencies type

* Move sanitize-url declaration to grafana-data

* Revert "Move sanitize-url declaration to grafana-data"

This reverts commit 11db9f5e55.

* Fix typo

* fix ts vol1

* Remove import from runtime in data.... Make TS happy at the same time ;)

* Lovely TS, please shut up

* Lovely TS, please shut up vol2

* fix tests

* Fixes

* minor refactor

* Attach get links to FieldDisplayValue for seamless usage

* Update packages/grafana-data/src/field/fieldOverrides.ts

* Make storybook build
This commit is contained in:
Dominik Prokop 2020-04-20 07:37:38 +02:00 committed by GitHub
parent e6c9b1305e
commit d2a13c4715
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
87 changed files with 659 additions and 337 deletions

View File

@ -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"

View File

@ -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",

View File

@ -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,
})
: () => [],
});
}
}

View File

@ -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<LinkModel<Field>> => {
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<Field> = {
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;
});
};

View File

@ -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();
});

View File

@ -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<string, DisplayValue> {
/**
*
* @param frame
* @param rowIndex
* @param options
* @internal
*/
export function getFieldDisplayValuesProxy(
frame: DataFrame,
rowIndex: number,
options: {
theme: GrafanaTheme;
}
): Record<string, DisplayValue> {
return new Proxy({} as Record<string, DisplayValue>, {
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);

View File

@ -6,3 +6,4 @@ export * from './overrides/processors';
export { FieldConfigOptionsRegistry } from './FieldConfigOptionsRegistry';
export { applyFieldOverrides, validateFieldConfig } from './fieldOverrides';
export { getFieldDisplayValuesProxy } from './getFieldDisplayValuesProxy';

View File

@ -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,
};

View File

@ -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;
}

View File

@ -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<TOptions extends object = any> {
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<T = any, V = Vector<T>> {
/**
* Name of the field (column)
@ -87,6 +98,11 @@ export interface Field<T = any, V = Vector<T>> {
* Convert a value for display
*/
display?: DisplayProcessor;
/**
* Get value data links with variables interpolated
*/
getLinks?: (config: ValueLinkConfig) => Array<LinkModel<Field>>;
}
export interface DataFrame extends QueryResultBase {

View File

@ -37,7 +37,7 @@ export type LinkTarget = '_blank' | '_self';
/**
* Processed Link Model. The values are ready to use
*/
export interface LinkModel<T> {
export interface LinkModel<T = any> {
href: string;
title: string;
target: LinkTarget;

View File

@ -31,3 +31,4 @@ export { AppEvent, AppEvents };
import * as PanelEvents from './panelEvents';
export { PanelEvents };
export { GrafanaConfig, BuildInfo, FeatureToggles, LicenseInfo } from './config';

View File

@ -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',
};

View File

@ -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';

View File

@ -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/');
});
});
});

View File

@ -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<string, any>, 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);
},
};

View File

@ -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=:@');

View File

@ -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<string, UrlQueryValue>;
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,
};

View File

@ -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"]
}

View File

@ -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 = '';

View File

@ -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<string, UrlQueryValue>;
/**
* 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.

View File

@ -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"]
}

View File

@ -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"]
}

View File

@ -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"]
}

View File

@ -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);

View File

@ -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<HTMLElement>; targetClassName?: string }) => JSX.Element;
links?: LinkModelSupplier<any>;
links?: () => LinkModel[];
}
export const DataLinksContextMenu: React.FC<DataLinksContextMenuProps> = ({ children, links }) => {

View File

@ -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<FieldConfigEditorProps<DataLink[], DataLinksFieldConfigSettings>> = ({
@ -12,7 +17,7 @@ export const DataLinksValueEditor: React.FC<FieldConfigEditorProps<DataLink[], D
links={value}
onChange={onChange}
data={context.data}
suggestions={context.getSuggestions ? context.getSuggestions() : []}
suggestions={context.getSuggestions ? context.getSuggestions(VariableSuggestionsScope.Values) : []}
/>
);
};

View File

@ -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<TableCellProps> = props => {
const { field, cell, tableStyles } = props;
const { field, cell, tableStyles, row } = props;
let link: LinkModel<any> | undefined;
if (!field.display) {
return null;
}
const displayValue = field.display(cell.value);
return <div className={tableStyles.tableCell}>{formattedValueToString(displayValue)}</div>;
if (field.getLinks) {
link = field.getLinks({
valueRowIndex: row.index,
})[0];
}
const value = formattedValueToString(displayValue);
return (
<div className={tableStyles.tableCell}>
{link ? (
<a href={link.href} target={link.target} title={link.title}>
{value}
</a>
) : (
value
)}
</div>
);
};

View File

@ -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<any>) => 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

View File

@ -11,5 +11,5 @@
},
"exclude": ["dist", "node_modules"],
"extends": "@grafana/tsconfig",
"include": ["src/**/*.ts*"]
"include": ["src/**/*.ts*", "../../public/app/types/sanitize-url.d.ts"]
}

View File

@ -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;

View File

@ -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,

View File

@ -1,4 +1,4 @@
import { UrlQueryMap } from '@grafana/runtime';
import { UrlQueryMap } from '@grafana/data';
import { findTemplateVarChanges } from './bridge_srv';
describe('when checking template variables', () => {

View File

@ -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';

View File

@ -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;

View File

@ -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/');
});
});
});

View File

@ -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<strin
}
const exploreState = JSON.stringify({ ...state, originPanelId: panel.getSavedId() });
url = renderUrl('/explore', { left: exploreState });
url = urlUtil.renderUrl('/explore', { left: exploreState });
}
return url;
}

View File

@ -1,19 +0,0 @@
import { getConfig } from 'app/core/config';
export const stripBaseFromUrl = (url: string): string => {
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 };

View File

@ -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;
};

View File

@ -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<Props> {
<DashNavButton
tooltip="Open original dashboard"
classSuffix="snapshot-origin"
href={textUtil.sanitizeUrl(snapshotUrl)}
icon="link"
href={sanitizeUrl(snapshotUrl)}
/>
)}

View File

@ -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;

View File

@ -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';

View File

@ -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(

View File

@ -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';

View File

@ -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 {

View File

@ -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!)

View File

@ -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', () => ({}));

View File

@ -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 {

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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');

View File

@ -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', () => {

View File

@ -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';

View File

@ -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<Props> = ({ updateLocation }) => {
</Field>
<HorizontalGroup>
<Button type="submit">Submit</Button>
<LinkButton href={assureBaseUrl(getConfig().appSubUrl + '/org/users')} variant="secondary">
<LinkButton href={locationUtil.assureBaseUrl(getConfig().appSubUrl + '/org/users')} variant="secondary">
Back
</LinkButton>
</HorizontalGroup>

View File

@ -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<Fi
value: {
name: dataFrame.name,
refId: dataFrame.refId,
fields: getFieldDisplayValuesProxy(dataFrame, value.rowIndex!),
fields: getFieldDisplayValuesProxy(dataFrame, value.rowIndex!, {
theme: config.theme,
}),
},
text: 'Data',
};

View File

@ -2,23 +2,23 @@ import _ from 'lodash';
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
import templateSrv, { TemplateSrv } from 'app/features/templating/template_srv';
import coreModule from 'app/core/core_module';
import { appendQueryToUrl, toUrlParams } from 'app/core/utils/url';
import { sanitizeUrl } from 'app/core/utils/text';
import { getConfig } from 'app/core/config';
import locationUtil from 'app/core/utils/location_util';
import { DataLinkBuiltInVars } from '@grafana/ui';
import {
DataFrame,
DataLink,
DataLinkBuiltInVars,
deprecationWarning,
Field,
FieldType,
KeyValue,
LinkModel,
locationUtil,
ScopedVars,
VariableOrigin,
VariableSuggestion,
VariableSuggestionsScope,
urlUtil,
textUtil,
} from '@grafana/data';
const timeRangeVars = [
@ -253,8 +253,8 @@ export class LinkSrv implements LinkService {
this.templateSrv.fillVariableValuesForUrl(params);
}
url = appendQueryToUrl(url, toUrlParams(params));
return getConfig().disableSanitizeHtml ? url : sanitizeUrl(url);
url = urlUtil.appendQueryToUrl(url, urlUtil.toUrlParams(params));
return getConfig().disableSanitizeHtml ? url : textUtil.sanitizeUrl(url);
}
getAnchorInfo(link: any) {
@ -269,7 +269,7 @@ export class LinkSrv implements LinkService {
*/
getDataLinkUIModel = <T>(link: DataLink, scopedVars: ScopedVars, origin: T): LinkModel<T> => {
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;
};

View File

@ -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(

View File

@ -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++;

View File

@ -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';

View File

@ -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';

View File

@ -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';

View File

@ -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([
{

View File

@ -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) => {

View File

@ -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 {

View File

@ -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;

View File

@ -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';

View File

@ -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', () => {

View File

@ -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[] => {

View File

@ -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 {

View File

@ -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';

View File

@ -1,4 +1,4 @@
import { UrlQueryMap } from '@grafana/runtime';
import { UrlQueryMap } from '@grafana/data';
import { getTemplatingRootReducer } from './helpers';
import { variableAdapters } from '../adapters';

View File

@ -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';

View File

@ -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';

View File

@ -13,7 +13,6 @@ import {
getDisplayValueAlignmentFactors,
DisplayValueAlignmentFactors,
} from '@grafana/data';
import { getFieldLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers';
export class BarGaugePanel extends PureComponent<PanelProps<BarGaugeOptions>> {
renderValue = (valueProps: VizRepeaterRenderValueProps<FieldDisplay, DisplayValueAlignmentFactors>): JSX.Element => {
@ -22,7 +21,7 @@ export class BarGaugePanel extends PureComponent<PanelProps<BarGaugeOptions>> {
const { field, display, view, colIndex } = value;
return (
<DataLinksContextMenu links={getFieldLinksSupplier(value)}>
<DataLinksContextMenu links={value.getLinks}>
{({ openMenu, targetClassName }) => {
return (
<BarGauge

View File

@ -10,7 +10,6 @@ import { Gauge, DataLinksContextMenu, VizRepeater, VizRepeaterRenderValueProps }
// Types
import { GaugeOptions } from './types';
import { FieldDisplay, getFieldDisplayValues, VizOrientation, PanelProps } from '@grafana/data';
import { getFieldLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers';
export class GaugePanel extends PureComponent<PanelProps<GaugeOptions>> {
renderValue = (valueProps: VizRepeaterRenderValueProps<FieldDisplay>): JSX.Element => {
@ -19,7 +18,7 @@ export class GaugePanel extends PureComponent<PanelProps<GaugeOptions>> {
const { field, display } = value;
return (
<DataLinksContextMenu links={getFieldLinksSupplier(value)}>
<DataLinksContextMenu links={value.getLinks}>
{({ openMenu, targetClassName }) => {
return (
<Gauge

View File

@ -1,7 +1,7 @@
import $ from 'jquery';
import { appEvents } from 'app/core/core';
import { CoreEvents } from 'app/types';
import { sanitize } from 'app/core/utils/text';
import { textUtil } from '@grafana/data';
export default function GraphTooltip(this: any, elem: any, dashboard: any, scope: any, getSeriesFn: any) {
const self = this;
@ -268,10 +268,10 @@ export default function GraphTooltip(this: any, elem: any, dashboard: any, scope
}
series = seriesList[hoverInfo.index];
value = sanitize(series.formatValue(hoverInfo.value));
value = textUtil.sanitize(series.formatValue(hoverInfo.value));
const color = sanitize(hoverInfo.color);
const label = sanitize(hoverInfo.label);
const color = textUtil.sanitize(hoverInfo.color);
const label = textUtil.sanitize(hoverInfo.label);
seriesHtml +=
'<div class="graph-tooltip-list-item ' + highlightClass + '"><div class="graph-tooltip-series-name">';
@ -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 = '<div class="graph-tooltip-list-item"><div class="graph-tooltip-series-name">';
group += '<i class="fa fa-minus" style="color:' + color + ';"></i> ' + series.aliasEscaped + ':</div>';
@ -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 += '<div class="graph-tooltip-value">' + value + '</div>';

View File

@ -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<NewsOptions> {}
@ -82,7 +81,7 @@ export class NewsPanel extends PureComponent<Props, State> {
<div className={styles.title}>{item.title}</div>
<div className={styles.date}>{dateTime(item.date).format('MMM DD')} </div>
</a>
<div className={styles.content} dangerouslySetInnerHTML={{ __html: sanitize(item.content) }} />
<div className={styles.content} dangerouslySetInnerHTML={{ __html: textUtil.sanitize(item.content) }} />
</div>
);
})}

View File

@ -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';

View File

@ -24,8 +24,6 @@ import {
DisplayValueAlignmentFactors,
} from '@grafana/data';
import { getFieldLinksSupplier } from 'app/features/panel/panellinks/linkSuppliers';
export class StatPanel extends PureComponent<PanelProps<StatPanelOptions>> {
renderValue = (valueProps: VizRepeaterRenderValueProps<FieldDisplay, DisplayValueAlignmentFactors>): JSX.Element => {
const { timeRange, options } = this.props;
@ -48,7 +46,7 @@ export class StatPanel extends PureComponent<PanelProps<StatPanelOptions>> {
}
return (
<DataLinksContextMenu links={getFieldLinksSupplier(value)}>
<DataLinksContextMenu links={value.getLinks}>
{({ openMenu, targetClassName }) => {
return (
<BigValue

View File

@ -9,12 +9,12 @@ import {
ScopedVars,
stringStartsAsRegEx,
stringToJsRegex,
textUtil,
unEscapeStringFromRegex,
} from '@grafana/data';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { ColumnRender, TableRenderModel, ColumnStyle } from './types';
import { ColumnOptionsCtrl } from './column_options';
import { sanitizeUrl } from 'app/core/utils/text';
export class TableRenderer {
formatters: any[];
@ -298,7 +298,7 @@ export class TableRenderer {
scopedVars['__cell'] = { value: value, text: value ? value.toString() : '' };
const cellLink = this.templateSrv.replace(column.style.linkUrl, scopedVars, encodeURIComponent);
const sanitizedCellLink = sanitizeUrl(cellLink);
const sanitizedCellLink = textUtil.sanitizeUrl(cellLink);
const cellLinkTooltip = this.templateSrv.replace(column.style.linkTooltip, scopedVars);
const cellTarget = column.style.linkTargetBlank ? '_blank' : '';

View File

@ -1,11 +1,10 @@
import _ from 'lodash';
import { PanelCtrl } from 'app/plugins/sdk';
import { sanitize, escapeHtml } from 'app/core/utils/text';
import config from 'app/core/config';
import { auto, ISCEService } from 'angular';
import { TemplateSrv } from 'app/features/templating/template_srv';
import { PanelEvents } from '@grafana/data';
import { PanelEvents, textUtil } from '@grafana/data';
import { renderMarkdown } from '@grafana/data';
const defaultContent = `
@ -78,7 +77,7 @@ export class TextPanelCtrl extends PanelCtrl {
}
renderText(content: string) {
const safeContent = escapeHtml(content).replace(/\n/g, '<br/>');
const safeContent = textUtil.escapeHtml(content).replace(/\n/g, '<br/>');
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));
}
}

View File

@ -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<TextOptions> {}
interface State {
@ -43,7 +39,7 @@ export class TextPanel extends PureComponent<Props, State> {
html = replaceVariables(html, {}, 'html');
return config.disableSanitizeHtml ? html : sanitize(html);
return config.disableSanitizeHtml ? html : textUtil.sanitize(html);
}
prepareText(content: string): string {

View File

@ -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) => {

View File

@ -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 {

View File

@ -1,4 +1,4 @@
import { UrlQueryMap } from '@grafana/runtime';
import { UrlQueryMap } from '@grafana/data';
export interface LocationState {
url: string;