mirror of https://github.com/grafana/grafana
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
pull/23693/head
parent
e6c9b1305e
commit
d2a13c4715
@ -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, |
||||
}; |
||||
|
||||
@ -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; |
||||
} |
||||
@ -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', |
||||
}; |
||||
@ -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/'); |
||||
}); |
||||
}); |
||||
}); |
||||
@ -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); |
||||
}, |
||||
}; |
||||
@ -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> |
||||
); |
||||
}; |
||||
|
||||
@ -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/'); |
||||
}); |
||||
}); |
||||
}); |
||||
@ -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 }; |
||||
Loading…
Reference in new issue