From c9ad411d8e181f57b1265c4234e7418c4f520eb0 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Wed, 19 Jun 2019 19:59:03 +0200 Subject: [PATCH 01/20] noImplicitAny: Fix basic errors (#17668) * Fix basic noImplicitAny errors * noImplicitAny HeatmapCtrl * Update error limit --- package.json | 1 + .../plugins/panel/heatmap/display_editor.ts | 2 +- .../app/plugins/panel/heatmap/heatmap_ctrl.ts | 26 +++++++------- .../plugins/panel/heatmap/heatmap_tooltip.ts | 34 +++++++++---------- .../panel/heatmap/specs/heatmap_ctrl.test.ts | 4 ++- .../panel/piechart/PieChartOptionsBox.tsx | 4 +-- public/app/plugins/panel/pluginlist/module.ts | 8 +++-- .../app/plugins/panel/table/column_options.ts | 2 +- public/test/jest-setup.ts | 4 +-- public/test/jest-shim.ts | 8 ++--- public/test/mocks/common.ts | 6 ++-- public/test/mocks/mockExploreState.ts | 2 +- public/test/specs/helpers.ts | 10 +++--- scripts/ci-frontend-metrics.sh | 2 +- yarn.lock | 15 ++++++++ 15 files changed, 75 insertions(+), 53 deletions(-) diff --git a/package.json b/package.json index 146a86db68b0f..8faeb8e43ab13 100644 --- a/package.json +++ b/package.json @@ -190,6 +190,7 @@ "@babel/polyfill": "7.2.5", "@torkelo/react-select": "2.4.1", "@types/angular-route": "1.7.0", + "@types/enzyme-adapter-react-16": "1.0.5", "@types/react-redux": "^7.0.8", "@types/reselect": "2.2.0", "angular": "1.6.6", diff --git a/public/app/plugins/panel/heatmap/display_editor.ts b/public/app/plugins/panel/heatmap/display_editor.ts index b30c77a1733ab..3dcfddfaeae6a 100644 --- a/public/app/plugins/panel/heatmap/display_editor.ts +++ b/public/app/plugins/panel/heatmap/display_editor.ts @@ -3,7 +3,7 @@ export class HeatmapDisplayEditorCtrl { panelCtrl: any; /** @ngInject */ - constructor($scope) { + constructor($scope: any) { $scope.editor = this; this.panelCtrl = $scope.ctrl; this.panel = this.panelCtrl.panel; diff --git a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts index ac576298321e4..8380d6383ac84 100644 --- a/public/app/plugins/panel/heatmap/heatmap_ctrl.ts +++ b/public/app/plugins/panel/heatmap/heatmap_ctrl.ts @@ -12,11 +12,13 @@ import { calculateBucketSize, sortSeriesByLabel, } from './heatmap_data_converter'; +import { auto } from 'angular'; +import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; const X_BUCKET_NUMBER_DEFAULT = 30; const Y_BUCKET_NUMBER_DEFAULT = 10; -const panelDefaults = { +const panelDefaults: any = { heatmap: {}, cards: { cardPadding: null, @@ -117,7 +119,7 @@ export class HeatmapCtrl extends MetricsPanelCtrl { scaledDecimals: number; /** @ngInject */ - constructor($scope, $injector, timeSrv) { + constructor($scope: any, $injector: auto.IInjectorService, timeSrv: TimeSrv) { super($scope, $injector); this.timeSrv = timeSrv; this.selectionActivated = false; @@ -143,7 +145,7 @@ export class HeatmapCtrl extends MetricsPanelCtrl { this.unitFormats = kbn.getUnitFormats(); } - zoomOut(evt) { + zoomOut(evt: any) { this.publishAppEvent('zoom-out', 2); } @@ -275,7 +277,7 @@ export class HeatmapCtrl extends MetricsPanelCtrl { } } - onDataReceived(dataList) { + onDataReceived(dataList: any) { this.series = dataList.map(this.seriesHandler.bind(this)); this.dataWarning = null; @@ -312,12 +314,12 @@ export class HeatmapCtrl extends MetricsPanelCtrl { this.render(); } - onCardColorChange(newColor) { + onCardColorChange(newColor: any) { this.panel.color.cardColor = newColor; this.render(); } - seriesHandler(seriesData) { + seriesHandler(seriesData: any) { if (seriesData.datapoints === undefined) { throw new Error('Heatmap error: data should be a time series'); } @@ -341,19 +343,19 @@ export class HeatmapCtrl extends MetricsPanelCtrl { return series; } - parseSeries(series) { + parseSeries(series: any[]) { const min = _.min(_.map(series, s => s.stats.min)); const minLog = _.min(_.map(series, s => s.stats.logmin)); const max = _.max(_.map(series, s => s.stats.max)); return { - max: max, - min: min, - minLog: minLog, + max, + min, + minLog, }; } - parseHistogramSeries(series) { + parseHistogramSeries(series: any[]) { const bounds = _.map(series, s => Number(s.alias)); const min = _.min(bounds); const minLog = _.min(bounds); @@ -366,7 +368,7 @@ export class HeatmapCtrl extends MetricsPanelCtrl { }; } - link(scope, elem, attrs, ctrl) { + link(scope: any, elem: any, attrs: any, ctrl: any) { rendering(scope, elem, attrs, ctrl); } } diff --git a/public/app/plugins/panel/heatmap/heatmap_tooltip.ts b/public/app/plugins/panel/heatmap/heatmap_tooltip.ts index 62f1514aac4e3..714db655084aa 100644 --- a/public/app/plugins/panel/heatmap/heatmap_tooltip.ts +++ b/public/app/plugins/panel/heatmap/heatmap_tooltip.ts @@ -19,7 +19,7 @@ export class HeatmapTooltip { mouseOverBucket: boolean; originalFillColor: any; - constructor(elem, scope) { + constructor(elem: JQuery, scope: any) { this.scope = scope; this.dashboard = scope.ctrl.dashboard; this.panelCtrl = scope.ctrl; @@ -35,7 +35,7 @@ export class HeatmapTooltip { this.destroy(); } - onMouseMove(e) { + onMouseMove(e: any) { if (!this.panel.tooltip.show) { return; } @@ -58,7 +58,7 @@ export class HeatmapTooltip { this.tooltip = null; } - show(pos, data) { + show(pos: { panelRelY: any }, data: any) { if (!this.panel.tooltip.show || !data) { return; } @@ -109,7 +109,7 @@ export class HeatmapTooltip { if (yData.bounds) { if (data.tsBuckets) { // Use Y-axis labels - const tickFormatter = valIndex => { + const tickFormatter = (valIndex: string | number) => { return data.tsBucketsFormatted ? data.tsBucketsFormatted[valIndex] : data.tsBuckets[valIndex]; }; @@ -152,13 +152,13 @@ export class HeatmapTooltip { this.move(pos); } - getBucketIndexes(pos, data) { + getBucketIndexes(pos: { panelRelY?: any; x?: any; y?: any }, data: any) { const xBucketIndex = this.getXBucketIndex(pos.x, data); const yBucketIndex = this.getYBucketIndex(pos.y, data); return { xBucketIndex, yBucketIndex }; } - getXBucketIndex(x, data) { + getXBucketIndex(x: number, data: { buckets: any; xBucketSize: number }) { // First try to find X bucket by checking x pos is in the // [bucket.x, bucket.x + xBucketSize] interval const xBucket: any = _.find(data.buckets, bucket => { @@ -167,7 +167,7 @@ export class HeatmapTooltip { return xBucket ? xBucket.x : getValueBucketBound(x, data.xBucketSize, 1); } - getYBucketIndex(y, data) { + getYBucketIndex(y: number, data: { tsBuckets: any; yBucketSize: number }) { if (data.tsBuckets) { return Math.floor(y); } @@ -175,17 +175,17 @@ export class HeatmapTooltip { return yBucketIndex; } - getSharedTooltipPos(pos) { + getSharedTooltipPos(pos: { pageX: any; x: any; pageY: any; panelRelY: number }) { // get pageX from position on x axis and pageY from relative position in original panel pos.pageX = this.heatmapPanel.offset().left + this.scope.xScale(pos.x); pos.pageY = this.heatmapPanel.offset().top + this.scope.chartHeight * pos.panelRelY; return pos; } - addHistogram(data) { + addHistogram(data: { x: string | number }) { const xBucket = this.scope.ctrl.data.buckets[data.x]; const yBucketSize = this.scope.ctrl.data.yBucketSize; - let min, max, ticks; + let min: number, max: number, ticks: number; if (this.scope.ctrl.data.tsBuckets) { min = 0; max = this.scope.ctrl.data.tsBuckets.length - 1; @@ -206,7 +206,7 @@ export class HeatmapTooltip { const scale = this.scope.yScale.copy(); const histXScale = scale.domain([min, max]).range([0, HISTOGRAM_WIDTH]); - let barWidth; + let barWidth: number; if (this.panel.yAxis.logBase === 1) { barWidth = Math.floor((HISTOGRAM_WIDTH / (max - min)) * yBucketSize * 0.9); } else { @@ -233,19 +233,19 @@ export class HeatmapTooltip { .data(histogramData) .enter() .append('rect') - .attr('x', d => { + .attr('x', (d: any[]) => { return histXScale(d[0]); }) .attr('width', barWidth) - .attr('y', d => { + .attr('y', (d: any[]) => { return HISTOGRAM_HEIGHT - histYScale(d[1]); }) - .attr('height', d => { + .attr('height', (d: any[]) => { return histYScale(d[1]); }); } - move(pos) { + move(pos: { panelRelY?: any; pageX?: any; pageY?: any }) { if (!this.tooltip) { return; } @@ -268,9 +268,9 @@ export class HeatmapTooltip { return this.tooltip.style('left', left + 'px').style('top', top + 'px'); } - countValueFormatter(decimals, scaledDecimals = null) { + countValueFormatter(decimals: number, scaledDecimals: any = null) { const format = 'short'; - return value => { + return (value: number) => { return getValueFormat(format)(value, decimals, scaledDecimals); }; } diff --git a/public/app/plugins/panel/heatmap/specs/heatmap_ctrl.test.ts b/public/app/plugins/panel/heatmap/specs/heatmap_ctrl.test.ts index 3eb4eb3812108..a9f33b7e392a9 100644 --- a/public/app/plugins/panel/heatmap/specs/heatmap_ctrl.test.ts +++ b/public/app/plugins/panel/heatmap/specs/heatmap_ctrl.test.ts @@ -1,5 +1,6 @@ import { HeatmapCtrl } from '../heatmap_ctrl'; import { dateTime } from '@grafana/ui/src/utils/moment_wrapper'; +import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; describe('HeatmapCtrl', () => { const ctx = {} as any; @@ -20,7 +21,8 @@ describe('HeatmapCtrl', () => { }; beforeEach(() => { - ctx.ctrl = new HeatmapCtrl($scope, $injector, {}); + //@ts-ignore + ctx.ctrl = new HeatmapCtrl($scope, $injector, {} as TimeSrv); }); describe('when time series are outside range', () => { diff --git a/public/app/plugins/panel/piechart/PieChartOptionsBox.tsx b/public/app/plugins/panel/piechart/PieChartOptionsBox.tsx index 7b2b42564f7bb..78d37b04b6512 100644 --- a/public/app/plugins/panel/piechart/PieChartOptionsBox.tsx +++ b/public/app/plugins/panel/piechart/PieChartOptionsBox.tsx @@ -14,8 +14,8 @@ const labelWidth = 8; const pieChartOptions = [{ value: PieChartType.PIE, label: 'Pie' }, { value: PieChartType.DONUT, label: 'Donut' }]; export class PieChartOptionsBox extends PureComponent> { - onPieTypeChange = pieType => this.props.onOptionsChange({ ...this.props.options, pieType: pieType.value }); - onStrokeWidthChange = ({ target }) => + onPieTypeChange = (pieType: any) => this.props.onOptionsChange({ ...this.props.options, pieType: pieType.value }); + onStrokeWidthChange = ({ target }: any) => this.props.onOptionsChange({ ...this.props.options, strokeWidth: target.value }); render() { diff --git a/public/app/plugins/panel/pluginlist/module.ts b/public/app/plugins/panel/pluginlist/module.ts index 55ca160652d2a..3aecced2ef13d 100644 --- a/public/app/plugins/panel/pluginlist/module.ts +++ b/public/app/plugins/panel/pluginlist/module.ts @@ -1,5 +1,7 @@ import _ from 'lodash'; import { PanelCtrl } from '../../../features/panel/panel_ctrl'; +import { auto } from 'angular'; +import { BackendSrv } from '@grafana/runtime'; class PluginListCtrl extends PanelCtrl { static templateUrl = 'module.html'; @@ -12,7 +14,7 @@ class PluginListCtrl extends PanelCtrl { panelDefaults = {}; /** @ngInject */ - constructor($scope, $injector, private backendSrv) { + constructor($scope: any, $injector: auto.IInjectorService, private backendSrv: BackendSrv) { super($scope, $injector); _.defaults(this.panel, this.panelDefaults); @@ -32,14 +34,14 @@ class PluginListCtrl extends PanelCtrl { this.addEditorTab('Options', 'public/app/plugins/panel/pluginlist/editor.html'); } - gotoPlugin(plugin, evt) { + gotoPlugin(plugin: { id: any }, evt: any) { if (evt) { evt.stopPropagation(); } this.$location.url(`plugins/${plugin.id}/edit`); } - updateAvailable(plugin, $event) { + updateAvailable(plugin: any, $event: any) { $event.stopPropagation(); $event.preventDefault(); diff --git a/public/app/plugins/panel/table/column_options.ts b/public/app/plugins/panel/table/column_options.ts index 3aaf943efab93..7fe67a621a00f 100644 --- a/public/app/plugins/panel/table/column_options.ts +++ b/public/app/plugins/panel/table/column_options.ts @@ -67,7 +67,7 @@ export class ColumnOptionsCtrl { } addColumnStyle() { - const newStyleRule = { + const newStyleRule: object = { unit: 'short', type: 'number', alias: '', diff --git a/public/test/jest-setup.ts b/public/test/jest-setup.ts index 86af992158de6..bfcceca7a590d 100644 --- a/public/test/jest-setup.ts +++ b/public/test/jest-setup.ts @@ -22,12 +22,12 @@ const global = window as any; global.$ = global.jQuery = $; const localStorageMock = (() => { - let store = {}; + let store: any = {}; return { getItem: (key: string) => { return store[key]; }, - setItem: (key: string, value) => { + setItem: (key: string, value: any) => { store[key] = value.toString(); }, clear: () => { diff --git a/public/test/jest-shim.ts b/public/test/jest-shim.ts index 98c12642a4016..6f9aa76fba47f 100644 --- a/public/test/jest-shim.ts +++ b/public/test/jest-shim.ts @@ -1,15 +1,15 @@ declare var global: NodeJS.Global; -(global as any).requestAnimationFrame = callback => { +(global as any).requestAnimationFrame = (callback: any) => { setTimeout(callback, 0); }; -(Promise.prototype as any).finally = function(onFinally) { +(Promise.prototype as any).finally = function(onFinally: any) { return this.then( /* onFulfilled */ - res => Promise.resolve(onFinally()).then(() => res), + (res: any) => Promise.resolve(onFinally()).then(() => res), /* onRejected */ - err => + (err: any) => Promise.resolve(onFinally()).then(() => { throw err; }) diff --git a/public/test/mocks/common.ts b/public/test/mocks/common.ts index 931e56d577b15..5f990fba5580d 100644 --- a/public/test/mocks/common.ts +++ b/public/test/mocks/common.ts @@ -8,11 +8,11 @@ export const backendSrv = { post: jest.fn(), }; -export function createNavTree(...args) { - const root = []; +export function createNavTree(...args: any[]) { + const root: any[] = []; let node = root; for (const arg of args) { - const child = { id: arg, url: `/url/${arg}`, text: `${arg}-Text`, children: [] }; + const child: any = { id: arg, url: `/url/${arg}`, text: `${arg}-Text`, children: [] }; node.push(child); node = child.children; } diff --git a/public/test/mocks/mockExploreState.ts b/public/test/mocks/mockExploreState.ts index 981f1fb2dbe4d..d6d2859f94c70 100644 --- a/public/test/mocks/mockExploreState.ts +++ b/public/test/mocks/mockExploreState.ts @@ -6,7 +6,7 @@ import { StoreState } from 'app/types'; export const mockExploreState = (options: any = {}) => { const isLive = options.isLive || false; - const history = []; + const history: any[] = []; const eventBridge = { emit: jest.fn(), }; diff --git a/public/test/specs/helpers.ts b/public/test/specs/helpers.ts index c7505a9aa8d69..648b73de40044 100644 --- a/public/test/specs/helpers.ts +++ b/public/test/specs/helpers.ts @@ -14,12 +14,12 @@ export function ControllerTestContext(this: any) { this.annotationsSrv = {}; this.contextSrv = {}; this.timeSrv = new TimeSrvStub(); - this.templateSrv = new TemplateSrvStub(); + this.templateSrv = TemplateSrvStub(); this.datasourceSrv = { getMetricSources: () => {}, get: () => { return { - then: callback => { + then: (callback: (a: any) => void) => { callback(self.datasource); }, }; @@ -84,7 +84,7 @@ export function ControllerTestContext(this: any) { self.scope.panel = {}; self.scope.dashboard = { meta: {} }; self.scope.dashboardMeta = {}; - self.scope.dashboardViewState = new DashboardViewStateStub(); + self.scope.dashboardViewState = DashboardViewStateStub(); self.scope.appEvent = sinon.spy(); self.scope.onAppEvent = sinon.spy(); @@ -102,14 +102,14 @@ export function ControllerTestContext(this: any) { }); }; - this.setIsUtc = (isUtc = false) => { + this.setIsUtc = (isUtc: any = false) => { self.isUtc = isUtc; }; } export function ServiceTestContext(this: any) { const self = this; - self.templateSrv = new TemplateSrvStub(); + self.templateSrv = TemplateSrvStub(); self.timeSrv = new TimeSrvStub(); self.datasourceSrv = {}; self.backendSrv = {}; diff --git a/scripts/ci-frontend-metrics.sh b/scripts/ci-frontend-metrics.sh index 3d41d9897bae6..5a8fc835127d0 100755 --- a/scripts/ci-frontend-metrics.sh +++ b/scripts/ci-frontend-metrics.sh @@ -3,7 +3,7 @@ echo -e "Collecting code stats (typescript errors & more)" -ERROR_COUNT_LIMIT=4599 +ERROR_COUNT_LIMIT=4400 DIRECTIVES_LIMIT=172 CONTROLLERS_LIMIT=139 diff --git a/yarn.lock b/yarn.lock index 8675a26864918..69ac2d4681361 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1965,6 +1965,21 @@ "@types/d3-voronoi" "*" "@types/d3-zoom" "*" +"@types/enzyme-adapter-react-16@^1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.0.5.tgz#1bf30a166f49be69eeda4b81e3f24113c8b4e9d5" + integrity sha512-K7HLFTkBDN5RyRmU90JuYt8OWEY2iKUn43SDWEoBOXd/PowUWjLZ3Q6qMBiQuZeFYK/TOstaZxsnI0fXoAfLpg== + dependencies: + "@types/enzyme" "*" + +"@types/enzyme@*": + version "3.9.3" + resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.9.3.tgz#d1029c0edd353d7b00f3924803eb88216460beed" + integrity sha512-jDKoZiiMA3lGO3skSO7dfqEHNvmiTLLV+PHD9EBQVlJANJvpY6qq1zzjRI24ZOtG7F+CS7BVWDXKewRmN8PjHQ== + dependencies: + "@types/cheerio" "*" + "@types/react" "*" + "@types/enzyme@3.9.0": version "3.9.0" resolved "https://registry.yarnpkg.com/@types/enzyme/-/enzyme-3.9.0.tgz#a81c91e2dfd2d70e67f013f2c0e5efed6df05489" From 428134482de3372ceec3498b3aa85a258c273b09 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Wed, 19 Jun 2019 11:31:47 -0700 Subject: [PATCH 02/20] @grafana/runtime: expose config and loadPluginCss (#17655) * move config to grafana runtime * set defaults * defaults in constructor * use @types/systemjs * rename Settings to GrafanaBootConfig --- packages/grafana-runtime/package.json | 1 + packages/grafana-runtime/src/config.ts | 78 +++++++++++++++++++ packages/grafana-runtime/src/index.ts | 2 + packages/grafana-runtime/src/utils/plugin.ts | 17 +++++ public/app/core/config.ts | 80 +------------------- public/app/core/utils/ConfigProvider.tsx | 4 +- public/app/features/plugins/plugin_loader.ts | 9 +-- public/app/plugins/sdk.ts | 2 +- 8 files changed, 105 insertions(+), 88 deletions(-) create mode 100644 packages/grafana-runtime/src/config.ts create mode 100644 packages/grafana-runtime/src/utils/plugin.ts diff --git a/packages/grafana-runtime/package.json b/packages/grafana-runtime/package.json index dcfcfcb21b81b..9f329846da1c4 100644 --- a/packages/grafana-runtime/package.json +++ b/packages/grafana-runtime/package.json @@ -18,6 +18,7 @@ "license": "Apache-2.0", "dependencies": {}, "devDependencies": { + "@types/systemjs": "^0.20.6", "awesome-typescript-loader": "^5.2.1", "lodash": "^4.17.10", "pretty-format": "^24.5.0", diff --git a/packages/grafana-runtime/src/config.ts b/packages/grafana-runtime/src/config.ts new file mode 100644 index 0000000000000..51d313597e3f7 --- /dev/null +++ b/packages/grafana-runtime/src/config.ts @@ -0,0 +1,78 @@ +import extend from 'lodash/extend'; +import { GrafanaTheme, getTheme, GrafanaThemeType, PanelPluginMeta, DataSourceInstanceSettings } from '@grafana/ui'; + +export interface BuildInfo { + version: string; + commit: string; + isEnterprise: boolean; + env: string; + latestVersion: string; + hasUpdate: boolean; +} + +export class GrafanaBootConfig { + datasources: { [str: string]: DataSourceInstanceSettings } = {}; + panels: { [key: string]: PanelPluginMeta } = {}; + appSubUrl = ''; + windowTitlePrefix = ''; + buildInfo: BuildInfo = {} as BuildInfo; + newPanelTitle = ''; + bootData: any; + externalUserMngLinkUrl = ''; + externalUserMngLinkName = ''; + externalUserMngInfo = ''; + allowOrgCreate = false; + disableLoginForm = false; + defaultDatasource = ''; + alertingEnabled = false; + alertingErrorOrTimeout = ''; + alertingNoDataOrNullValues = ''; + authProxyEnabled = false; + exploreEnabled = false; + ldapEnabled = false; + oauth: any; + disableUserSignUp = false; + loginHint: any; + passwordHint: any; + loginError: any; + viewersCanEdit = false; + editorsCanAdmin = false; + disableSanitizeHtml = false; + theme: GrafanaTheme; + pluginsToPreload: string[] = []; + + constructor(options: GrafanaBootConfig) { + this.theme = options.bootData.user.lightTheme ? getTheme(GrafanaThemeType.Light) : getTheme(GrafanaThemeType.Dark); + + const defaults = { + datasources: {}, + windowTitlePrefix: 'Grafana - ', + panels: {}, + newPanelTitle: 'Panel Title', + playlist_timespan: '1m', + unsaved_changes_warning: true, + appSubUrl: '', + buildInfo: { + version: 'v1.0', + commit: '1', + env: 'production', + isEnterprise: false, + }, + viewersCanEdit: false, + editorsCanAdmin: false, + disableSanitizeHtml: false, + }; + + extend(this, defaults, options); + } +} + +const bootData = (window as any).grafanaBootData || { + settings: {}, + user: {}, +}; + +const options = bootData.settings; +options.bootData = bootData; + +export const config = new GrafanaBootConfig(options); diff --git a/packages/grafana-runtime/src/index.ts b/packages/grafana-runtime/src/index.ts index e371345e62d82..9ae0a899cec16 100644 --- a/packages/grafana-runtime/src/index.ts +++ b/packages/grafana-runtime/src/index.ts @@ -1 +1,3 @@ export * from './services'; +export * from './config'; +export { loadPluginCss } from './utils/plugin'; diff --git a/packages/grafana-runtime/src/utils/plugin.ts b/packages/grafana-runtime/src/utils/plugin.ts new file mode 100644 index 0000000000000..8e1075dabb6d3 --- /dev/null +++ b/packages/grafana-runtime/src/utils/plugin.ts @@ -0,0 +1,17 @@ +import { config } from '../config'; + +/* tslint:disable:import-blacklist */ +import System from 'systemjs'; + +export interface PluginCssOptions { + light: string; + dark: string; +} + +export function loadPluginCss(options: PluginCssOptions) { + if (config.bootData.user.lightTheme) { + System.import(options.light + '!css'); + } else { + System.import(options.dark + '!css'); + } +} diff --git a/public/app/core/config.ts b/public/app/core/config.ts index 8d762514e003c..ff5b31a005f3c 100644 --- a/public/app/core/config.ts +++ b/public/app/core/config.ts @@ -1,79 +1,5 @@ -import _ from 'lodash'; -import { GrafanaTheme, getTheme, GrafanaThemeType, PanelPluginMeta, DataSourceInstanceSettings } from '@grafana/ui'; +import { config, GrafanaBootConfig } from '@grafana/runtime'; -export interface BuildInfo { - version: string; - commit: string; - isEnterprise: boolean; - env: string; - latestVersion: string; - hasUpdate: boolean; -} - -export class Settings { - datasources: { [str: string]: DataSourceInstanceSettings }; - panels: { [key: string]: PanelPluginMeta }; - 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; - authProxyEnabled: boolean; - exploreEnabled: boolean; - ldapEnabled: boolean; - oauth: any; - disableUserSignUp: boolean; - loginHint: any; - passwordHint: any; - loginError: any; - viewersCanEdit: boolean; - editorsCanAdmin: boolean; - disableSanitizeHtml: boolean; - theme: GrafanaTheme; - pluginsToPreload: string[]; - - constructor(options: Settings) { - this.theme = options.bootData.user.lightTheme ? getTheme(GrafanaThemeType.Light) : getTheme(GrafanaThemeType.Dark); - - const defaults = { - datasources: {}, - windowTitlePrefix: 'Grafana - ', - panels: {}, - newPanelTitle: 'Panel Title', - playlist_timespan: '1m', - unsaved_changes_warning: true, - appSubUrl: '', - buildInfo: { - version: 'v1.0', - commit: '1', - env: 'production', - isEnterprise: false, - }, - viewersCanEdit: false, - editorsCanAdmin: false, - disableSanitizeHtml: false, - }; - - _.extend(this, defaults, options); - } -} - -const bootData = (window as any).grafanaBootData || { - settings: {}, - user: {}, -}; - -const options = bootData.settings; -options.bootData = bootData; - -export const config = new Settings(options); +// Legacy binding paths +export { config, GrafanaBootConfig as Settings }; export default config; diff --git a/public/app/core/utils/ConfigProvider.tsx b/public/app/core/utils/ConfigProvider.tsx index 2162abae29b6c..a7a5f630d100f 100644 --- a/public/app/core/utils/ConfigProvider.tsx +++ b/public/app/core/utils/ConfigProvider.tsx @@ -1,8 +1,8 @@ import React from 'react'; -import config, { Settings } from 'app/core/config'; +import { config, GrafanaBootConfig } from '@grafana/runtime'; import { GrafanaThemeType, ThemeContext, getTheme } from '@grafana/ui'; -export const ConfigContext = React.createContext(config); +export const ConfigContext = React.createContext(config); export const ConfigConsumer = ConfigContext.Consumer; export const provideConfig = (component: React.ComponentType) => { diff --git a/public/app/features/plugins/plugin_loader.ts b/public/app/features/plugins/plugin_loader.ts index 38bcde862cbda..c86c92077a148 100644 --- a/public/app/features/plugins/plugin_loader.ts +++ b/public/app/features/plugins/plugin_loader.ts @@ -31,6 +31,7 @@ import * as d3 from 'd3'; import * as grafanaData from '@grafana/data'; import * as grafanaUI from '@grafana/ui'; import * as grafanaRuntime from '@grafana/runtime'; +export { loadPluginCss } from '@grafana/runtime'; // rxjs import { Observable, Subject } from 'rxjs'; @@ -230,11 +231,3 @@ export function importPanelPlugin(id: string): Promise { return getPanelPluginNotFound(id); }); } - -export function loadPluginCss(options) { - if (config.bootData.user.lightTheme) { - System.import(options.light + '!css'); - } else { - System.import(options.dark + '!css'); - } -} diff --git a/public/app/plugins/sdk.ts b/public/app/plugins/sdk.ts index 0f18327149567..04f73247e6830 100644 --- a/public/app/plugins/sdk.ts +++ b/public/app/plugins/sdk.ts @@ -2,6 +2,6 @@ import { PanelCtrl } from 'app/features/panel/panel_ctrl'; import { MetricsPanelCtrl } from 'app/features/panel/metrics_panel_ctrl'; import { QueryCtrl } from 'app/features/panel/query_ctrl'; import { alertTab } from 'app/features/alerting/AlertTabCtrl'; -import { loadPluginCss } from 'app/features/plugins/plugin_loader'; +import { loadPluginCss } from '@grafana/runtime'; export { PanelCtrl, MetricsPanelCtrl, QueryCtrl, alertTab, loadPluginCss }; From 6fd4aa4b46526bb70ce74d1aeab7b7d96ece10a3 Mon Sep 17 00:00:00 2001 From: Jared Burns Date: Wed, 19 Jun 2019 22:06:09 -0700 Subject: [PATCH 03/20] Docs: Flag serve_from_sub_path as available in 6.3 (#17674) Follow-up to PR #17048. Flag the new serve_from_sub_path option as available in 6.3. --- docs/sources/installation/configuration.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 486d63a1e7631..755b4615a5064 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -155,6 +155,7 @@ callback URL to be correct). > case add the subpath to the end of this URL setting. ### serve_from_sub_path +> Available in 6.3 and above Serve Grafana from subpath specified in `root_url` setting. By default it is set to `false` for compatibility reasons. From 57dadebbd8228ac451ea5c0bfb948ce891c9c131 Mon Sep 17 00:00:00 2001 From: kay delaney <45561153+kaydelaney@users.noreply.github.com> Date: Thu, 20 Jun 2019 12:56:47 +0100 Subject: [PATCH 04/20] Explore: Improves performance of Logs element by limiting re-rendering (#17685) * Explore: Improves performance of Logs element by limiting re-rendering Re-renders only when query has finished executing or when deduplication strategy changes. Closes #17663 * Explore: Adds logsHighlighterExpressions as prop to consider when re-rendering Logs --- public/app/features/explore/LogsContainer.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/public/app/features/explore/LogsContainer.tsx b/public/app/features/explore/LogsContainer.tsx index 79846e1d4bc9e..c6ba84aceb0d3 100644 --- a/public/app/features/explore/LogsContainer.tsx +++ b/public/app/features/explore/LogsContainer.tsx @@ -91,6 +91,16 @@ export class LogsContainer extends PureComponent { return []; }; + // Limit re-rendering to when a query is finished executing or when the deduplication strategy changes + // for performance reasons. + shouldComponentUpdate(nextProps: LogsContainerProps): boolean { + return ( + nextProps.loading !== this.props.loading || + nextProps.dedupStrategy !== this.props.dedupStrategy || + nextProps.logsHighlighterExpressions !== this.props.logsHighlighterExpressions + ); + } + render() { const { exploreId, From 49f0f0e89e3fe6359de1909ab668e24a90684b5e Mon Sep 17 00:00:00 2001 From: Kyle Brandt Date: Thu, 20 Jun 2019 10:15:21 -0400 Subject: [PATCH 05/20] config: fix connstr for remote_cache (#17675) fixes #17643 and adds test to check for commented out lines (but will only catch `;`, not `#`). --- conf/defaults.ini | 2 +- pkg/setting/setting_test.go | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index af362157aea2c..6f1b57d0b4810 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -118,7 +118,7 @@ type = database # database: will use Grafana primary database. # redis: config like redis server e.g. `addr=127.0.0.1:6379,pool_size=100,db=0`. Only addr is required. # memcache: 127.0.0.1:11211 -;connstr = +connstr = #################################### Data proxy ########################### [dataproxy] diff --git a/pkg/setting/setting_test.go b/pkg/setting/setting_test.go index f59e4d6e8aa0c..73425a82263e1 100644 --- a/pkg/setting/setting_test.go +++ b/pkg/setting/setting_test.go @@ -1,10 +1,12 @@ package setting import ( + "bufio" "os" "path" "path/filepath" "runtime" + "strings" "testing" "gopkg.in/ini.v1" @@ -30,6 +32,22 @@ func TestLoadingSettings(t *testing.T) { So(cfg.RendererCallbackUrl, ShouldEqual, "http://localhost:3000/") }) + Convey("default.ini should have no semi-colon commented entries", func() { + file, err := os.Open("../../conf/defaults.ini") + if err != nil { + t.Errorf("failed to load defaults.ini file: %v", err) + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + // This only catches values commented out with ";" and will not catch those that are commented out with "#". + if strings.HasPrefix(scanner.Text(), ";") { + t.Errorf("entries in defaults.ini must not be commented or environment variables will not work: %v", scanner.Text()) + } + } + }) + Convey("Should be able to override via environment variables", func() { os.Setenv("GF_SECURITY_ADMIN_USER", "superduper") From 219d711597fb34e53aba065190311af02c887200 Mon Sep 17 00:00:00 2001 From: Tobias Skarhed Date: Thu, 20 Jun 2019 20:41:01 +0200 Subject: [PATCH 06/20] noImplicitAny: slate (#17681) * Update slate types * Fix noImplicitAny --- package.json | 3 ++ public/app/features/explore/QueryField.tsx | 2 +- public/app/features/explore/Value.ts | 6 +-- .../editor/query_field.tsx | 2 +- public/app/plugins/panel/graph/axes_editor.ts | 4 +- .../panel/graph/time_region_manager.ts | 10 ++-- public/app/plugins/panel/graph2/types.ts | 1 + .../app/plugins/panel/heatmap/color_legend.ts | 50 +++++++++++++------ .../app/plugins/panel/heatmap/color_scale.ts | 7 ++- public/app/store/configureStore.ts | 2 +- public/app/types/explore.ts | 6 +-- yarn.lock | 43 +++++++++++++--- 12 files changed, 99 insertions(+), 37 deletions(-) diff --git a/package.json b/package.json index 8faeb8e43ab13..9d80b5d39a22c 100644 --- a/package.json +++ b/package.json @@ -190,9 +190,12 @@ "@babel/polyfill": "7.2.5", "@torkelo/react-select": "2.4.1", "@types/angular-route": "1.7.0", + "@types/d3-scale-chromatic": "1.3.1", "@types/enzyme-adapter-react-16": "1.0.5", "@types/react-redux": "^7.0.8", + "@types/redux-logger": "3.0.7", "@types/reselect": "2.2.0", + "@types/slate": "0.44.11", "angular": "1.6.6", "angular-bindonce": "0.3.1", "angular-native-dragdrop": "1.2.2", diff --git a/public/app/features/explore/QueryField.tsx b/public/app/features/explore/QueryField.tsx index d2a5d04e0d44c..52af733441f01 100644 --- a/public/app/features/explore/QueryField.tsx +++ b/public/app/features/explore/QueryField.tsx @@ -52,7 +52,7 @@ export interface QueryFieldState { typeaheadIndex: number; typeaheadPrefix: string; typeaheadText: string; - value: Value; + value: any; lastExecutedValue: Value; } diff --git a/public/app/features/explore/Value.ts b/public/app/features/explore/Value.ts index 48ee0060a2d42..78b54f1a43b05 100644 --- a/public/app/features/explore/Value.ts +++ b/public/app/features/explore/Value.ts @@ -15,7 +15,7 @@ export const makeFragment = (text: string, syntax?: string) => { Block.create({ type: 'code_line', nodes: [Text.create(line)], - }) + } as any) ); const block = Block.create({ @@ -24,7 +24,7 @@ export const makeFragment = (text: string, syntax?: string) => { }, type: 'code_block', nodes: lines, - }); + } as any); return Document.create({ nodes: [block], @@ -37,5 +37,5 @@ export const makeValue = (text: string, syntax?: string) => { return Value.create({ document: fragment, SCHEMA, - }); + } as any); }; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx index 3427bb5662d23..671fd2c8c068d 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx @@ -23,7 +23,7 @@ export const makeFragment = text => { Block.create({ type: 'paragraph', nodes: [Text.create(line)], - }) + } as any) ); const fragment = Document.create({ diff --git a/public/app/plugins/panel/graph/axes_editor.ts b/public/app/plugins/panel/graph/axes_editor.ts index 2cb7aa4eed0a4..41e7fe2576c01 100644 --- a/public/app/plugins/panel/graph/axes_editor.ts +++ b/public/app/plugins/panel/graph/axes_editor.ts @@ -10,7 +10,7 @@ export class AxesEditorCtrl { xNameSegment: any; /** @ngInject */ - constructor(private $scope) { + constructor(private $scope: any) { this.panelCtrl = $scope.ctrl; this.panel = this.panelCtrl.panel; this.$scope.ctrl = this; @@ -48,7 +48,7 @@ export class AxesEditorCtrl { } } - setUnitFormat(axis, subItem) { + setUnitFormat(axis: { format: any }, subItem: { value: any }) { axis.format = subItem.value; this.panelCtrl.render(); } diff --git a/public/app/plugins/panel/graph/time_region_manager.ts b/public/app/plugins/panel/graph/time_region_manager.ts index 1aaeb09b44a6f..2b65c39630047 100644 --- a/public/app/plugins/panel/graph/time_region_manager.ts +++ b/public/app/plugins/panel/graph/time_region_manager.ts @@ -37,13 +37,13 @@ export const colorModes = { export function getColorModes() { return _.map(Object.keys(colorModes), key => { return { - key: key, + key, value: colorModes[key].title, }; }); } -function getColor(timeRegion, theme: GrafanaThemeType): TimeRegionColorDefinition { +function getColor(timeRegion: any, theme: GrafanaThemeType): TimeRegionColorDefinition { if (Object.keys(colorModes).indexOf(timeRegion.colorMode) === -1) { timeRegion.colorMode = 'red'; } @@ -71,14 +71,14 @@ export class TimeRegionManager { plot: any; timeRegions: any; - constructor(private panelCtrl, private theme: GrafanaThemeType = GrafanaThemeType.Dark) {} + constructor(private panelCtrl: any, private theme: GrafanaThemeType = GrafanaThemeType.Dark) {} - draw(plot) { + draw(plot: any) { this.timeRegions = this.panelCtrl.panel.timeRegions; this.plot = plot; } - addFlotOptions(options, panel) { + addFlotOptions(options: any, panel: any) { if (!panel.timeRegions || panel.timeRegions.length === 0) { return; } diff --git a/public/app/plugins/panel/graph2/types.ts b/public/app/plugins/panel/graph2/types.ts index 18885c5025406..7643f8e4ac27b 100644 --- a/public/app/plugins/panel/graph2/types.ts +++ b/public/app/plugins/panel/graph2/types.ts @@ -4,6 +4,7 @@ import { GraphLegendEditorLegendOptions } from './GraphLegendEditor'; export interface SeriesOptions { color?: string; yAxis?: number; + [key: string]: any; } export interface GraphOptions { showBars: boolean; diff --git a/public/app/plugins/panel/heatmap/color_legend.ts b/public/app/plugins/panel/heatmap/color_legend.ts index 3629bce1b5efe..be1823807d512 100644 --- a/public/app/plugins/panel/heatmap/color_legend.ts +++ b/public/app/plugins/panel/heatmap/color_legend.ts @@ -90,7 +90,14 @@ coreModule.directive('heatmapLegend', () => { }; }); -function drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minValue) { +function drawColorLegend( + elem: JQuery, + colorScheme: any, + rangeFrom: number, + rangeTo: number, + maxValue: number, + minValue: number +) { const legendElem = $(elem).find('svg'); const legend = d3.select(legendElem.get(0)); clearLegend(elem); @@ -121,7 +128,14 @@ function drawColorLegend(elem, colorScheme, rangeFrom, rangeTo, maxValue, minVal drawLegendValues(elem, rangeFrom, rangeTo, maxValue, minValue, legendWidth, valuesRange); } -function drawOpacityLegend(elem, options, rangeFrom, rangeTo, maxValue, minValue) { +function drawOpacityLegend( + elem: JQuery, + options: { cardColor: null }, + rangeFrom: number, + rangeTo: number, + maxValue: any, + minValue: number +) { const legendElem = $(elem).find('svg'); const legend = d3.select(legendElem.get(0)); clearLegend(elem); @@ -153,7 +167,15 @@ function drawOpacityLegend(elem, options, rangeFrom, rangeTo, maxValue, minValue drawLegendValues(elem, rangeFrom, rangeTo, maxValue, minValue, legendWidth, valuesRange); } -function drawLegendValues(elem, rangeFrom, rangeTo, maxValue, minValue, legendWidth, valuesRange) { +function drawLegendValues( + elem: JQuery, + rangeFrom: number, + rangeTo: number, + maxValue: any, + minValue: any, + legendWidth: number, + valuesRange: number[] +) { const legendElem = $(elem).find('svg'); const legend = d3.select(legendElem.get(0)); @@ -188,7 +210,7 @@ function drawLegendValues(elem, rangeFrom, rangeTo, maxValue, minValue, legendWi .remove(); } -function drawSimpleColorLegend(elem, colorScale) { +function drawSimpleColorLegend(elem: JQuery, colorScale: any) { const legendElem = $(elem).find('svg'); clearLegend(elem); @@ -215,7 +237,7 @@ function drawSimpleColorLegend(elem, colorScale) { } } -function drawSimpleOpacityLegend(elem, options) { +function drawSimpleOpacityLegend(elem: JQuery, options: { colorScale: string; exponent: number; cardColor: string }) { const legendElem = $(elem).find('svg'); clearLegend(elem); @@ -224,7 +246,7 @@ function drawSimpleOpacityLegend(elem, options) { const legendHeight = legendElem.attr('height'); if (legendWidth) { - let legendOpacityScale; + let legendOpacityScale: any; if (options.colorScale === 'linear') { legendOpacityScale = d3 .scaleLinear() @@ -261,13 +283,13 @@ function drawSimpleOpacityLegend(elem, options) { } } -function clearLegend(elem) { +function clearLegend(elem: JQuery) { const legendElem = $(elem).find('svg'); legendElem.empty(); } -function getSvgElemX(elem) { - const svgElem = elem.get(0); +function getSvgElemX(elem: JQuery) { + const svgElem: any = elem.get(0) as any; if (svgElem && svgElem.x && svgElem.x.baseVal) { return svgElem.x.baseVal.value; } else { @@ -275,8 +297,8 @@ function getSvgElemX(elem) { } } -function getSvgElemHeight(elem) { - const svgElem = elem.get(0); +function getSvgElemHeight(elem: JQuery) { + const svgElem: any = elem.get(0); if (svgElem && svgElem.height && svgElem.height.baseVal) { return svgElem.height.baseVal.value; } else { @@ -284,7 +306,7 @@ function getSvgElemHeight(elem) { } } -function buildLegendTicks(rangeFrom, rangeTo, maxValue, minValue) { +function buildLegendTicks(rangeFrom: number, rangeTo: number, maxValue: number, minValue: number) { const range = rangeTo - rangeFrom; const tickStepSize = tickStep(rangeFrom, rangeTo, 3); const ticksNum = Math.ceil(range / tickStepSize); @@ -316,12 +338,12 @@ function buildLegendTicks(rangeFrom, rangeTo, maxValue, minValue) { return ticks; } -function isValueCloseTo(val, valueTo, step) { +function isValueCloseTo(val: number, valueTo: number, step: number) { const diff = Math.abs(val - valueTo); return diff < step * 0.3; } -function getFirstCloseTick(minValue, step) { +function getFirstCloseTick(minValue: number, step: number) { if (minValue < 0) { return Math.floor(minValue / step) * step; } diff --git a/public/app/plugins/panel/heatmap/color_scale.ts b/public/app/plugins/panel/heatmap/color_scale.ts index 2234deb840566..368e724883c5b 100644 --- a/public/app/plugins/panel/heatmap/color_scale.ts +++ b/public/app/plugins/panel/heatmap/color_scale.ts @@ -2,6 +2,7 @@ import * as d3 from 'd3'; import * as d3ScaleChromatic from 'd3-scale-chromatic'; export function getColorScale(colorScheme: any, lightTheme: boolean, maxValue: number, minValue = 0): (d: any) => any { + //@ts-ignore const colorInterpolator = d3ScaleChromatic[colorScheme.value]; const colorScaleInverted = colorScheme.invert === 'always' || colorScheme.invert === (lightTheme ? 'light' : 'dark'); @@ -11,7 +12,11 @@ export function getColorScale(colorScheme: any, lightTheme: boolean, maxValue: n return d3.scaleSequential(colorInterpolator).domain([start, end]); } -export function getOpacityScale(options, maxValue, minValue = 0) { +export function getOpacityScale( + options: { cardColor?: null; colorScale?: any; exponent?: any }, + maxValue: number, + minValue = 0 +) { let legendOpacityScale; if (options.colorScale === 'linear') { legendOpacityScale = d3 diff --git a/public/app/store/configureStore.ts b/public/app/store/configureStore.ts index 31c1d3964d7e2..1d238cf7dc3ac 100644 --- a/public/app/store/configureStore.ts +++ b/public/app/store/configureStore.ts @@ -49,7 +49,7 @@ const rootReducers = { ...organizationReducers, }; -export function addRootReducer(reducers) { +export function addRootReducer(reducers: any) { Object.assign(rootReducers, ...reducers); } diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index 98d137f1e7a5c..3774c74916a93 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -1,5 +1,4 @@ import { ComponentClass } from 'react'; -import { Value } from 'slate'; import { RawTimeRange, DataQuery, @@ -283,7 +282,7 @@ export interface HistoryItem { export abstract class LanguageProvider { datasource: any; - request: (url) => Promise; + request: (url: any) => Promise; /** * Returns startTask that resolves with a task list when main syntax is loaded. * Task list consists of secondary promises that load more detailed language features. @@ -297,7 +296,8 @@ export interface TypeaheadInput { prefix: string; wrapperClasses: string[]; labelKey?: string; - value?: Value; + //Should be Value from slate + value?: any; } export interface TypeaheadOutput { diff --git a/yarn.lock b/yarn.lock index 69ac2d4681361..7218d322f35f6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1839,9 +1839,10 @@ dependencies: "@types/d3-dsv" "*" -"@types/d3-scale-chromatic@*": +"@types/d3-scale-chromatic@*", "@types/d3-scale-chromatic@1.3.1": version "1.3.1" resolved "https://registry.yarnpkg.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-1.3.1.tgz#a294ae688634027870f0307bf8802f863aa2ddb3" + integrity sha512-Ny3rLbV5tnmqgW7w/poCcef4kXP8mHPo/p8EjTS5d9OUk8MlqAeRaM8eF7Vyv7QMLiIXNE94Pa1cMLSPkXQBoQ== "@types/d3-scale@*": version "2.1.1" @@ -1965,7 +1966,7 @@ "@types/d3-voronoi" "*" "@types/d3-zoom" "*" -"@types/enzyme-adapter-react-16@^1.0.5": +"@types/enzyme-adapter-react-16@1.0.5": version "1.0.5" resolved "https://registry.yarnpkg.com/@types/enzyme-adapter-react-16/-/enzyme-adapter-react-16-1.0.5.tgz#1bf30a166f49be69eeda4b81e3f24113c8b4e9d5" integrity sha512-K7HLFTkBDN5RyRmU90JuYt8OWEY2iKUn43SDWEoBOXd/PowUWjLZ3Q6qMBiQuZeFYK/TOstaZxsnI0fXoAfLpg== @@ -2213,6 +2214,13 @@ "@types/prop-types" "*" csstype "^2.2.0" +"@types/redux-logger@3.0.7": + version "3.0.7" + resolved "https://registry.yarnpkg.com/@types/redux-logger/-/redux-logger-3.0.7.tgz#163f6f6865c69c21d56f9356dc8d741718ec0db0" + integrity sha512-oV9qiCuowhVR/ehqUobWWkXJjohontbDGLV88Be/7T4bqMQ3kjXwkFNL7doIIqlbg3X2PC5WPziZ8/j/QHNQ4A== + dependencies: + redux "^3.6.0" + "@types/remarkable@1.7.4": version "1.7.4" resolved "https://registry.yarnpkg.com/@types/remarkable/-/remarkable-1.7.4.tgz#0faee73dc42cf21d718e20065a0961e53fa8e570" @@ -2316,6 +2324,14 @@ version "7.0.11" resolved "https://registry.yarnpkg.com/@types/sinon/-/sinon-7.0.11.tgz#6f28f005a36e779b7db0f1359b9fb9eef72aae88" +"@types/slate@0.44.11": + version "0.44.11" + resolved "https://registry.yarnpkg.com/@types/slate/-/slate-0.44.11.tgz#152568096d1a089fa4c5bb03de1cf044a377206c" + integrity sha512-UnOGipgkE1+rq3L4JjsTO0b02FbT6b59+0/hkW/QFBDvCcxCSAdwdr9HYjXkMSCSVlcsEfdC/cz+XOaB+tGvlg== + dependencies: + "@types/react" "*" + immutable "^3.8.2" + "@types/stack-utils@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-1.0.1.tgz#0a851d3bd96498fa25c33ab7278ed3bd65f06c3e" @@ -2345,6 +2361,11 @@ "@types/react" "*" "@types/webpack-env" "*" +"@types/systemjs@^0.20.6": + version "0.20.6" + resolved "https://registry.yarnpkg.com/@types/systemjs/-/systemjs-0.20.6.tgz#79838d2b4bce9c014330efa0b4c7b9345e830a72" + integrity sha512-p3yv9sBBJXi3noUG216BpUI7VtVBUAvBIfZNTiDROUY31YBfsFHM4DreS7XMekN8IjtX0ysvCnm6r3WnirnNeA== + "@types/tether-drop@1.4.8": version "1.4.8" resolved "https://registry.yarnpkg.com/@types/tether-drop/-/tether-drop-1.4.8.tgz#8d64288e673259d1bc28518250b80b5ef43af0bc" @@ -7834,7 +7855,7 @@ immer@^1.12.0: version "1.12.1" resolved "https://registry.yarnpkg.com/immer/-/immer-1.12.1.tgz#40c6e5b292c00560836c2993bda3a24379d466f5" -immutable@3.8.2: +immutable@3.8.2, immutable@^3.8.2: version "3.8.2" resolved "https://registry.yarnpkg.com/immutable/-/immutable-3.8.2.tgz#c2439951455bb39913daf281376f1530e104adf3" @@ -9436,7 +9457,7 @@ lockfile@^1.0.4: dependencies: signal-exit "^3.0.2" -lodash-es@^4.17.11: +lodash-es@^4.17.11, lodash-es@^4.2.1: version "4.17.11" resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.11.tgz#145ab4a7ac5c5e52a3531fb4f310255a152b4be0" @@ -9559,7 +9580,7 @@ lodash.without@~4.4.0: version "4.4.0" resolved "https://registry.yarnpkg.com/lodash.without/-/lodash.without-4.4.0.tgz#3cd4574a00b67bae373a94b748772640507b7aac" -lodash@4.17.11, lodash@>4.17.4, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.1.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.3.0, lodash@^4.7.0, lodash@^4.8.0, lodash@~4.17.10, lodash@~4.17.5: +lodash@4.17.11, lodash@>4.17.4, lodash@^4.0.0, lodash@^4.0.1, lodash@^4.1.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1, lodash@^4.3.0, lodash@^4.7.0, lodash@^4.8.0, lodash@~4.17.10, lodash@~4.17.5: version "4.17.11" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" @@ -12904,6 +12925,16 @@ redux@4.0.1, redux@^4.0.0: loose-envify "^1.4.0" symbol-observable "^1.2.0" +redux@^3.6.0: + version "3.7.2" + resolved "https://registry.yarnpkg.com/redux/-/redux-3.7.2.tgz#06b73123215901d25d065be342eb026bc1c8537b" + integrity sha512-pNqnf9q1hI5HHZRBkj3bAngGZW/JMCmexDlOxw4XagXY2o1327nHH54LoTjiPJ0gizoqPDRqWyX/00g0hD6w+A== + dependencies: + lodash "^4.2.1" + lodash-es "^4.2.1" + loose-envify "^1.1.0" + symbol-observable "^1.0.3" + reflect.ownkeys@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz#749aceec7f3fdf8b63f927a04809e90c5c0b3460" @@ -14505,7 +14536,7 @@ swap-case@^1.1.0: lower-case "^1.1.1" upper-case "^1.1.1" -symbol-observable@^1.0.4, symbol-observable@^1.1.0, symbol-observable@^1.2.0: +symbol-observable@^1.0.3, symbol-observable@^1.0.4, symbol-observable@^1.1.0, symbol-observable@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" From ec3b91e6b257c1f8201936d0bed99113e2576fa7 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 21 Jun 2019 02:20:45 +0200 Subject: [PATCH 07/20] Document issue triage process (#17669) Adds a first version of project documentation/guideline for triaging of issues. This adds some additional content and context so that the issue triage process can be understood by both maintainers and existing/new contributors and other interested parties. Co-Authored-By: Andrej Ocenas Closes #17648 --- ISSUE_TRIAGE.md | 324 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 324 insertions(+) create mode 100644 ISSUE_TRIAGE.md diff --git a/ISSUE_TRIAGE.md b/ISSUE_TRIAGE.md new file mode 100644 index 0000000000000..cb902931bcb8b --- /dev/null +++ b/ISSUE_TRIAGE.md @@ -0,0 +1,324 @@ +Triaging of issues +------------------ + +Grafana being a popular open source project there are a lot of incoming issues. The main goal of issue triage is to categorize all incoming issues and make sure it has all basic information needed for anyone else to understand and/or being able to start working with it. + +The core maintainers of the Grafana project is responsible for categorizing all incoming issues and delegate any critical and/or important issue to other maintainers. Currently there's one maintainer each week responsible. Besides that part, triage provides an important way to contribute to an open source project. Triage helps ensure issues resolve quickly by: + +* Describing the issue's intent and purpose is conveyed precisely. This is necessary because it can be difficult for an issue to explain how an end user experiences a problem and what actions they took. +* Giving a contributor the information they need before they commit to resolving an issue. +* Lowering the issue count by preventing duplicate issues. +* Streamlining the development process by preventing duplicate discussions. +* If you don't have the knowledge or time to code, consider helping with triage. The community will thank you for saving them time by spending some of yours. + +**Simplified flowchart diagram of the issue triage process:** + +``` + +--------------------------+ + +----------------+ New issue opened/ | + | | more information added | + | +-------------+------------+ + | Ask for more | + | information +-------------+------------+ + | | All information needed | + | +--------+ to categorize the issue? +--------+ + | | | | | + | | NO +--------------------------+ YES | + | | | ++------+-------+-------------+ +------------+---------+ +----------------------------+ +| | | | | | +| label: needs more details | | Needs investigation? +--YES---+ label: needs investigation | +| | | | | | ++----------------------------+ +----------------+-----+ +--------------+-------------+ + NO | | + | Investigate | + +-----------+----------+ | + | label: type/* | | + | label: area/* +------------------+ + | label: datasource/* | + +-----|----------+-----+ + | | + | | + | +--+--------------------+ +--------------------+ + | | | | label: priority/* | + | | Needs priority? +--YES---+| milestone? | + | | | | | + | +--------------------+--+ +----+---------------+ + | NO | | + | | | + +----+-------------+ +---+----------+ | + | | | | | + | Close issue +----------+ Done +------+ + | | | | + +------------------+ +--------------+ +``` + +## 1. Find uncategorized issues + +To get started with issue triage and finding issues that haven't been triaged you have two alternatives. + +### Browse unlabeled issues + +The easiest and straigt forward way of getting started and finding issues that haven't been triaged is to browse [unlabeled issues](https://github.com/grafana/grafana/issues?q=is%3Aopen+is%3Aissue+no%3Alabel) and starting from the bottom and working yourself to the top. + +### Subscribe to all notifications + +The more advanced, but recommended way is to subscribe to all notifications from this repository which means that all new issues, pull requests, comments and important status changes are sent to your configure email address. Read this [guide](https://help.github.com/en/articles/watching-and-unwatching-repositories#watching-a-single-repository) for help with setting this up. + +It's highly recommened that you setup filters to automatically remove emails from the inbox and label/categorize them accordingly to make it easy for you to understand when you need to act upon a notification or where to look for finding issues that haven't been triaged etc. + +Instructions for setting up filters in Gmail can be found [here](#setting-up-gmail-filters). Another alternative is to use [Trailer](https://github.com/ptsochantaris/trailer) or similar software. + +## 2. Ensure the issue contains basic information + +Before triaging an issue very far, make sure that the issue's author provided the standard issue information. This will help you make an educated recommendation on how to categorize the issue. The Grafana project utilizes [GitHub issue templates](https://help.github.com/en/articles/creating-issue-templates-for-your-repository) to guide contributors to provide standard information that must be included for each type of template or type of issue. + +### Standard issue information that must be included + +Given a certain [issue template]([template](https://github.com/grafana/grafana/issues/new/choose)) have been used by the issue author or depending how the issue is perceived by the issue triage responsible, the following should help you understand what standard issue information that must be included. + +#### Bug report? + +Should explain what happened, what was expected and how to reproduce it together with any additional information that may help giving a complete picture of what happened such as screenshots, [query inspector](https://community.grafana.com/t/using-grafanas-query-inspector-to-troubleshoot-issues/2630) output and any environment related information that's applicable and/or maybe related to the reported problem: +- Grafana version +- Data source type & version +- Platform & OS Grafana is installed on +- User OS & Browser + versions +- Using docker + what environment +- Which plugins +- Configuration database in use (sqlite, mysql, postgres) +- Reverse proxy in front of Grafana, what version and configuration +- Non-default configuration settings +- Development environment like Go and Node versions, if applicable + +#### Enhancement request? + +Should explain what enhancement or feature that the author wants to be added and why that is needed. + +#### Accessibility issue? + +This is a mix between a bug report and enhancement request but focused on accessibility issues to help make Grafana improve keyboard navigation, screen-reader support and being accessible to everyone. The report should include relevant WCAG criteria, if applicable. + +#### Support request? + +In general, if the issue description and title is perceived as a question no more information is needed. + +### Good practices + +To make it easier for everyone to understand and find issues they're searching for it's suggested as a general rule of thumbs to: + +* Make sure that issue titles are named to explain the subject of the issue, has a correct spelling and and doesn't include irrelevant information and/or sensitive information. +* Make sure that issue descriptions doesn't include irrelevant information, information from template that haven't been filled out and/or sensitive information. +* Do your best effort to change title and description or request suggested changes by adding a comment. + +Note: Above rules is applicable to both new and existing issues of the Grafana project. + +### Do you have all the information needed to categorize an issue? + +Depending on the issue, you might not feel all this information is needed. Use your best judgement. If you cannot triage an issue using what its author provided, explain kindly to the author that they must provide the above information to clarify the problem. Label issue with `needs more detail` and add any related `area/*` or `datasource/*` labels. + +If the author provides the standard information but you are still unable to triage the issue, request additional information. Do this kindly and politely because you are asking for more of the author's time. + +If the author does not respond requested information within the timespan of a week, close the issue with a kind note stating that the author can request for the issue to be reopened when the necessary information is provided. + +When you feel you have all the information needed you're ready to [categorizing the issue](#3-categorizing-an-issue). + +## 3. Categorizing an issue + +An issue can have multiple of the following labels. Typically, a properly categorized issue should at least have: + +- One label identifying its type (`type/*`). +- One or multiple labels identifying the functional areas of interest or component (`area/*`) and/or datasource (`datasource/*`), if applicable. + +Label | Description +------- | -------- +`type/bug` | A feature isn't working as expected given design or documentation. +`type/feature-request` | Request for a new feature or enhancement. +`type/docs` | Documentation problem or enhancement. +`type/accessibility` | Accessibility problem or enhancement. +`type/question` | Issue is or perceived as a question. +`type/duplicate` | An existing issue of the same subject/request have already been reported. +`type/works-as-intended` | A reported bug works as intended/by design. +`type/build-packaging` | Build or packaging problem or enhancement. +`area/*` | Subject is related to a functional area of interest or component. +`datasource/*` | Subject is related to a core datasource plugin. + +### Duplicate issue? + +Make sure that it's not a duplicate by searching existing issues using related terms from the issue title and description. If you think you know there are an existing issue, but can't find it please reach out to one of the maintainers and ask for help. If you identify that the issue is a duplicate of an existing issue: + +1. Add a comment `Duplicate of #`. GitHub will recognize this and add some additional context to the issue activity. +2. Close the issue and label it with `type/duplicate`. +3. Optionally add any related `area/*` or `datasource/*` labels. +4. If applicable, add a comment with additional information. + +### Bug report? + +If it's not perfectly clear that it's an actual bug, quickly try to reproduce it. + +**It's a bug/it can be reproduced:** + +1. Add a comment describing detailed steps for how to reproduce it, if applicable. +2. Label the issue `type/bug` and at least one `area/*` or `datasource/*` label. +3. If you know that maintainers wont be able to put any resources into it for some time then label the issue with `help wanted` and optionally `beginner friendly` together with pointers on which code to update to fix the bug. This should signal to the community that we would appreciate any help we can get to resolve this. +4. Move on to [prioritizing the issue](#4-prioritization-of-issues). + +**It can't be reproduced:** +1. Either [ask for more information](#2-ensure-the-issue-contains-basic-information) needed to investigate it more thoroughly. +2. Either [delegate further investigations](#investigation-of-issues) to someone else. + +**It works as intended/by design:** +1. Kindly and politely add a comment explaining briefly why we think it works as intended and close the issue. +2. Label the issue `type/works-as-intended`. + +### Enhancement/feature? + +1. Label the issue `type/feature-request` and at least one `area/*` or `datasource/*` label. +2. Move on to [prioritizing the issue](#4-prioritization-of-issues). + +### Documentation issue? + +First, evaluate if the documentation makes sense to be included in the Grafana project: + +- Is this something we want/can maintain as a project? +- Is this referring to usage of some specific integration/tool and in that case are those a popular use case in combination with Grafana? +- If unsure, kindly and politely add a comment explaining that we would need [upvotes](https://help.github.com/en/articles/about-conversations-on-github#reacting-to-ideas-in-comments) to identify that lots of other users wants/needs this. + +Second, label the issue `type/docs` and at least one `area/*` or `datasource/*` label. + +**Minor typo/error/lack of information:** + +There's a minor typo/error/lack of information that adds a lot of confusion for users and given the amount of work is a big win to make sure fixing it: +1. Either update the documentation yourself and open a pull request. +2. Either delegate the work to someone else by assigning that person to the issue and add the issue to next major/minor milestone. + +**Major error/lack of information:** + +1. Label the issue with `help wanted` and `beginner friendly`, if applicable, to signal that we find this important to fix and we would appreciate any help we can get from the community. +2. Move on to [prioritizing the issue](#4-prioritization-of-issues). + +### Accessibility issue? + +1. Label the issue `type/accessibility` and at least one `area/*` or `datasource/*` label. + +### Support request? + +1. Kindly and politely direct the issue author to the [community site](https://community.grafana.com/) and explain that GitHub is mainly used for tracking bugs and feature requests. If possible, it's usually a good idea to add some pointers to the issue author's question. +2. Close the issue and label it with `type/question`. + +## 4. Prioritization of issues + +In general bugs and enhancement issues should be labeled with a priority. + +This is the most difficult thing with triaging issues since it requires a lot of knowledge, context and experience before being able to think of and start feel comfortable adding a certain priority label. + +The key here is asking for help and discuss issues to understand how more experienced project members thinks and reason. By doing that you learn more and eventually be more and more comfortable with prioritizing issues. + +In any case there are uncertainty around the priorization of an issue, please ask the maintainers for help. + +Label | Description +------- | -------- +`priority/critical` | Highest priority. Must be actively worked on as someone's top priority right now. +`priority/support-subscription` | This is important for one or several customers having a paid Grafana support subscription. +`priority/important-soon` | Must be staffed and worked on either currently, or very soon, ideally in time for the next release. +`priority/important-longterm` | Important over the long term, but may not be staffed and/or may need multiple releases to complete. +`priority/nice-to-have` | It's a good idea, but not scheduled for any release. +`priority/awaiting-more-evidence` | Lowest priority. Possibly useful, but not yet enough interest in it. +`priority/unscheduled` | Something to look into before and to be discussed during the planning of the next (upcoming) major/minor stable release. + +**Critical bug?** + +1. If a bug have been categorized and any of the following problems applies the bug should be labeled as critical and must be actively worked on as someone's top priority right now. + + - Results in any data loss + - Critical security or performance issues + - Problem that makes a feature unusable + - Multiple users experience a severe problem affecting their business, users etc. + +2. Label the issue `priority/critical`. +3. If applicable, label the issue `priority/support-subscription`. +4. Add the issue to the next upcoming patch release milestone. Create a new milestone if there are none. +5. Escalate the problem to the maintainers. +6. Assign or ask a maintainer for help assigning someone to make this issue their top priority right now. + +**Important short-term?** + +1. Label the issue `priority/important-soon`. +2. If applicable, label the issue `priority/support-subscription`. +3. Add the issue to the next upcoming patch or major/minor stable release milestone. Ask maintainers for help if unsure if it's a patch or not. Create a new milestone if there are none. +4. Make sure to add the issue to a suitable backlog of a GitHub project and prioritize it or assign someone to work on it now or very soon. +5. Consider requesting [help from the community](#5-requesting-help-from-the-community), even though it may be problematic given a short amount of time until it should be released. + +**Important long-term?** + +1. Label the issue `priority/important-longterm`. +2. Consider requesting [help from the community](#5-requesting-help-from-the-community). + +**Nice to have?** + +1. Label the issue `priority/nice-to-have`. +2. Consider requesting [help from the community](#5-requesting-help-from-the-community). + +**Not critical, but unsure?** + +1. Label the issue `priority/unscheduled`. +2. Consider requesting [help from the community](#5-requesting-help-from-the-community). + +## 5. Requesting help from the community + +Depending on the issue and/or priority, it's always a good idea to consider signalling to the community that help from community is appreciated and needed in case an issue is not prioritized to be worked on by maintainers. Use your best judgement. In general, when requesting help from the community it means a contribution has a good chance of getting accepted and merged. + +In many cases the issue author or community as a whole is more suitable to contribute changes since they're experts in their domain. It's also quite common that someone has tried to get something to work using the documentation without success and made an effort to get it to work and/or reached out to the [community site](https://community.grafana.com/) to get the missing information. In especially these areas it's more likely that there exists experts in their own domain and usually a good idea to request help from contributors: + +- Database setups +- Authentication like OAuth providers and LDAP setups +- Platform specific things +- Reverse proxy setups +- Alert notifiers + +1. Kindly and politely add a comment to signal to users subscribed to updates of the issue. + - Explain that the issue would be nice to get resolved, but it isn't prioritized to work on by maintainers for an unforseen future. + - If possible or applicable, try to help contributors getting starting by adding pointers and references to what code/files need to be changed and/or ideas of a good way to solve/implement the issue. +2. Label the issue with `help wanted`. +3. If applicable, label the issue with `beginner friendly` to denote that the issue is suitable for a beginner to work on. +4. If possible, try to estimate the amount of work by adding `effort/small`, `effort/medium` or `effort/large`. + +## Investigation of issues + +When an issue has all basic information provided, but the triage responsible haven't been able to reproduce the reported problem at a first glance, the issue is labeled [Needs investigation](https://github.com/grafana/grafana/labels/needs%20investigation). Depending of the perceived severity and/or number of [upvotes](https://help.github.com/en/articles/about-conversations-on-github#reacting-to-ideas-in-comments), the investigation will either be delegated to another maintainer for further investigation or either put on hold until someone else (maintainer or contributor) picks it up and eventually start investigating it. + +Investigating issues can be a very time consuming task, especially for the maintainers given the huge number of combinations of plugins, datasources, platforms, databases, browsers, tools, hardware, integrations, versions and cloud services etc that are being used with Grafana. There are a certain amount of combinations that are more common than others and these are in general easier for maintainers to investigate. + +For some other combinations there may not be possible at all for a maintainer to setup a proper test environment for being able to investigate. In these cases we really appreciate any help we can get from the community. Otherwise the issue is highly likely to be closed. + +Even if you don't have the time or knowledge to investigate an issue we highly recommend that you [upvote](https://help.github.com/en/articles/about-conversations-on-github#reacting-to-ideas-in-comments) the issue if you happen to have the same problem. If you have further details that may help investigating the issue please provide as much information as possible. + +## Appendix + +### Setting up Gmail filters + +If you're using Gmail it's highly recommened that you setup filters to automatically remove email from the inbox and label them accordingly to make it easy for you to understand when you need to act upon a notification or process all incoming issues that haven't been triaged. + +This may be setup by personal preference, but here's a working configuration for reference. +1. Follow instructions in [gist](https://gist.github.com/marefr/9167c2e31466f6316c1cba118874e74f) +2. In Gmail, go to Settings -> Filters and Blocked Addresses +3. Import filters -> select xml file -> Open file +4. Review filters +5. Optional, Check Apply new filters to existing email +6. Create filters + +This will give you a structure of labels in the sidebar similar to the following: +``` + - Inbox + ... + - Github (mine) + - activity + - assigned + - mentions + - Github (other) + - Grafana +``` + +* All notifications you’ll need to read/take action on shows up as unread in Github (mine) and its sub-labels. +* All other notifications you don’t need to take action on shows up as unread in Github (other) and its sub-labels + * This is convenient for issue triage and to follow the activity in the Grafana project. From 0a74a3329c256c94a8ea87d2c596d41dab6b107b Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Sat, 22 Jun 2019 13:51:32 +0200 Subject: [PATCH 08/20] Project: Adds support resource docs (#17699) Adds a support resource document. For reference, https://help.github.com/en/articles/adding-support-resources-to-your-project. Closes #17650 --- SUPPORT.md | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 SUPPORT.md diff --git a/SUPPORT.md b/SUPPORT.md new file mode 100644 index 0000000000000..edafd7cde6676 --- /dev/null +++ b/SUPPORT.md @@ -0,0 +1,9 @@ +Need help or support? +------------------ + + **Please note:** +- Only submit issues for bug reports, feature requests or enhancements. +- Grafana project uses GitHub mainly for tracking bugs and feature requests. +- Asking a question by opening an issue will directly result in issue being closed. + +If you require help or support then ask a question and/or find existing questions/answers in the [Grafana community site](https://community.grafana.com/). \ No newline at end of file From 70b4fc8483921a69006e243d75ab48618e21b567 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Sun, 23 Jun 2019 00:25:29 +0200 Subject: [PATCH 09/20] Project: Adds a security policy (#17698) Adds a security policy document. For reference, https://help.github.com/en/articles/adding-a-security-policy-to-your-repository. Closes #17649 --- SECURITY.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 SECURITY.md diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000000000..789a6c238c3c3 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,17 @@ +Reporting Security Issues +------------------ + +If you think you have found a security vulnerability, please send a report to [security@grafana.com](mailto:security@grafana.com). This address can be used for all of Grafana Labs's open source and commercial products (including but not limited to Grafana, Grafana Cloud, Grafana Enterprise, and grafana.com). We can accept only vulnerability reports at this address. We would prefer that you encrypt your message to us; please use our PGP key. The key fingerprint is: + +F988 7BEA 027A 049F AE8E 5CAA D125 8932 BE24 C5CA + +The key is available from [pgp.mit.edu](https://pgp.mit.edu/pks/lookup?op=get&search=0xF9887BEA027A049FAE8E5CAAD1258932BE24C5CA) by searching for [grafana](https://pgp.mit.edu/pks/lookup?search=grafana&op=index). + +Grafana Labs will send you a response indicating the next steps in handling your report. After the initial reply to your report, the security team will keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance. + +**Important:** We ask you to not disclose the vulnerability before it have been fixed and announced, unless you have got a response from the Grafana Labs security team that you can do that. + +### Security Announcements + +We maintain a category on the community site called [Security Announcements](https://community.grafana.com/c/security-announcements), +where we will post a summary, remediation, and mitigation details for any patch containing security fixes. You can also subscribe to email updates to this category if you have a grafana.com account and sign on to the community site or track updates via an [RSS feed](https://community.grafana.com/c/security-announcements.rss). From dda8b731e88c65dcc80d189d70c8d3c8e30f6bfb Mon Sep 17 00:00:00 2001 From: Sergey Goppikov Date: Sun, 23 Jun 2019 18:38:30 +0300 Subject: [PATCH 10/20] Settings: Fix typo in defaults.ini (#17707) `backround` -> `backGround` --- conf/defaults.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conf/defaults.ini b/conf/defaults.ini index 6f1b57d0b4810..5eff89bcd6f29 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -394,7 +394,7 @@ enabled = false config_file = /etc/grafana/ldap.toml allow_sign_up = true -# LDAP backround sync (Enterprise only) +# LDAP background sync (Enterprise only) sync_cron = @hourly active_sync_enabled = false From 4ddeb94f5272ca0eaed355877fb4705aae1db2ec Mon Sep 17 00:00:00 2001 From: David Date: Mon, 24 Jun 2019 08:42:08 +0200 Subject: [PATCH 11/20] Dashboard: Use Explore's Prometheus editor in dashboard panel edit (#15364) * WIP prometheus editor same in panel * Dont use panel in plugin editors * prettiered modified files * Fix step in external link * Prevent exiting edit mode when slate suggestions are shown * Blur behavior and $__interval variable * Remove unused query controller * Basic render test * Chore: Fixes blacklisted import * Refactor: Adds correct start and end time --- public/app/core/services/keybindingSrv.ts | 2 +- .../datasource/prometheus/completer.ts | 424 -------------- .../prometheus/components/PromLink.tsx | 68 +++ .../components/PromQueryEditor.test.tsx | 53 ++ .../prometheus/components/PromQueryEditor.tsx | 177 ++++++ .../prometheus/components/PromQueryField.tsx | 19 +- .../PromQueryEditor.test.tsx.snap | 213 +++++++ .../datasource/prometheus/mode-prometheus.js | 532 ------------------ .../plugins/datasource/prometheus/module.ts | 4 +- .../prometheus/partials/query.editor.html | 57 -- .../plugins/datasource/prometheus/promql.ts | 1 + .../datasource/prometheus/query_ctrl.ts | 95 ---- .../prometheus/snippets/prometheus.js | 21 - .../prometheus/specs/completer.test.ts | 193 ------- .../specs/language_provider.test.ts | 13 +- 15 files changed, 534 insertions(+), 1338 deletions(-) delete mode 100644 public/app/plugins/datasource/prometheus/completer.ts create mode 100644 public/app/plugins/datasource/prometheus/components/PromLink.tsx create mode 100644 public/app/plugins/datasource/prometheus/components/PromQueryEditor.test.tsx create mode 100644 public/app/plugins/datasource/prometheus/components/PromQueryEditor.tsx create mode 100644 public/app/plugins/datasource/prometheus/components/__snapshots__/PromQueryEditor.test.tsx.snap delete mode 100644 public/app/plugins/datasource/prometheus/mode-prometheus.js delete mode 100644 public/app/plugins/datasource/prometheus/partials/query.editor.html delete mode 100644 public/app/plugins/datasource/prometheus/query_ctrl.ts delete mode 100644 public/app/plugins/datasource/prometheus/snippets/prometheus.js delete mode 100644 public/app/plugins/datasource/prometheus/specs/completer.test.ts diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index 0a4bd1c028ca2..66841f8372a19 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -69,7 +69,7 @@ export class KeybindingSrv { } exit() { - const popups = $('.popover.in'); + const popups = $('.popover.in, .slate-typeahead'); if (popups.length > 0) { return; } diff --git a/public/app/plugins/datasource/prometheus/completer.ts b/public/app/plugins/datasource/prometheus/completer.ts deleted file mode 100644 index ae52d94ca7b82..0000000000000 --- a/public/app/plugins/datasource/prometheus/completer.ts +++ /dev/null @@ -1,424 +0,0 @@ -import { PrometheusDatasource } from './datasource'; -import _ from 'lodash'; -import { TemplateSrv } from 'app/features/templating/template_srv'; - -export interface CompleterPosition { - row: number; - column: number; -} - -export interface CompleterToken { - type: string; - value: string; - row: number; - column: number; - index: number; -} - -export interface CompleterSession { - getTokenAt: (row: number, column: number) => CompleterToken; - getTokens: (row: number) => CompleterToken[]; -} - -export class PromCompleter { - labelQueryCache: any; - labelNameCache: any; - labelValueCache: any; - templateVariableCompletions: any; - - identifierRegexps = [/\[/, /[a-zA-Z0-9_:]/]; - - constructor(private datasource: PrometheusDatasource, private templateSrv: TemplateSrv) { - this.labelQueryCache = {}; - this.labelNameCache = {}; - this.labelValueCache = {}; - this.templateVariableCompletions = this.templateSrv.variables.map((variable: any) => { - return { - caption: '$' + variable.name, - value: '$' + variable.name, - meta: 'variable', - score: Number.MAX_VALUE, - }; - }); - } - - getCompletions(editor: any, session: CompleterSession, pos: CompleterPosition, prefix: string, callback: Function) { - const wrappedCallback = (err: any, completions: any[]) => { - completions = completions.concat(this.templateVariableCompletions); - return callback(err, completions); - }; - - const token = session.getTokenAt(pos.row, pos.column); - - switch (token.type) { - case 'entity.name.tag.label-matcher': - this.getCompletionsForLabelMatcherName(session, pos).then(completions => { - wrappedCallback(null, completions); - }); - return; - case 'string.quoted.label-matcher': - this.getCompletionsForLabelMatcherValue(session, pos).then(completions => { - wrappedCallback(null, completions); - }); - return; - case 'entity.name.tag.label-list-matcher': - this.getCompletionsForBinaryOperator(session, pos).then(completions => { - wrappedCallback(null, completions); - }); - return; - } - - if (token.type === 'paren.lparen' && token.value === '[') { - const vectors = []; - for (const unit of ['s', 'm', 'h']) { - for (const value of [1, 5, 10, 30]) { - vectors.push({ - caption: value + unit, - value: '[' + value + unit, - meta: 'range vector', - }); - } - } - - vectors.unshift({ - caption: '$__interval_ms', - value: '[$__interval_ms', - meta: 'range vector', - }); - - vectors.unshift({ - caption: '$__interval', - value: '[$__interval', - meta: 'range vector', - }); - - wrappedCallback(null, vectors); - return; - } - - const query = prefix; - - return this.datasource.performSuggestQuery(query, true).then((metricNames: string[]) => { - wrappedCallback( - null, - metricNames.map(name => { - let value = name; - if (prefix === '(') { - value = '(' + name; - } - - return { - caption: name, - value: value, - meta: 'metric', - }; - }) - ); - }); - } - - getCompletionsForLabelMatcherName(session: CompleterSession, pos: CompleterPosition) { - const metricName = this.findMetricName(session, pos.row, pos.column); - if (!metricName) { - return Promise.resolve(this.transformToCompletions(['__name__', 'instance', 'job'], 'label name')); - } - - if (this.labelNameCache[metricName]) { - return Promise.resolve(this.labelNameCache[metricName]); - } - - return this.getLabelNameAndValueForExpression(metricName, 'metricName').then(result => { - const labelNames = this.transformToCompletions( - _.uniq( - _.flatten( - result.map((r: any) => { - return Object.keys(r); - }) - ) - ), - 'label name' - ); - this.labelNameCache[metricName] = labelNames; - return Promise.resolve(labelNames); - }); - } - - getCompletionsForLabelMatcherValue(session: CompleterSession, pos: CompleterPosition) { - const metricName = this.findMetricName(session, pos.row, pos.column); - if (!metricName) { - return Promise.resolve([]); - } - - const labelNameToken = this.findToken( - session, - pos.row, - pos.column, - 'entity.name.tag.label-matcher', - null, - 'paren.lparen.label-matcher' - ); - if (!labelNameToken) { - return Promise.resolve([]); - } - const labelName = labelNameToken.value; - - if (this.labelValueCache[metricName] && this.labelValueCache[metricName][labelName]) { - return Promise.resolve(this.labelValueCache[metricName][labelName]); - } - - return this.getLabelNameAndValueForExpression(metricName, 'metricName').then(result => { - const labelValues = this.transformToCompletions( - _.uniq( - result.map((r: any) => { - return r[labelName]; - }) - ), - 'label value' - ); - this.labelValueCache[metricName] = this.labelValueCache[metricName] || {}; - this.labelValueCache[metricName][labelName] = labelValues; - return Promise.resolve(labelValues); - }); - } - - getCompletionsForBinaryOperator(session: CompleterSession, pos: CompleterPosition) { - const keywordOperatorToken = this.findToken(session, pos.row, pos.column, 'keyword.control', null, 'identifier'); - if (!keywordOperatorToken) { - return Promise.resolve([]); - } - let rparenToken: CompleterToken, expr: string; - switch (keywordOperatorToken.value) { - case 'by': - case 'without': - rparenToken = this.findToken( - session, - keywordOperatorToken.row, - keywordOperatorToken.column, - 'paren.rparen', - null, - 'identifier' - ); - if (!rparenToken) { - return Promise.resolve([]); - } - expr = this.findExpressionMatchedParen(session, rparenToken.row, rparenToken.column); - if (expr === '') { - return Promise.resolve([]); - } - return this.getLabelNameAndValueForExpression(expr, 'expression').then(result => { - const labelNames = this.transformToCompletions( - _.uniq( - _.flatten( - result.map((r: any) => { - return Object.keys(r); - }) - ) - ), - 'label name' - ); - this.labelNameCache[expr] = labelNames; - return labelNames; - }); - case 'on': - case 'ignoring': - case 'group_left': - case 'group_right': - const binaryOperatorToken = this.findToken( - session, - keywordOperatorToken.row, - keywordOperatorToken.column, - 'keyword.operator.binary', - null, - 'identifier' - ); - if (!binaryOperatorToken) { - return Promise.resolve([]); - } - rparenToken = this.findToken( - session, - binaryOperatorToken.row, - binaryOperatorToken.column, - 'paren.rparen', - null, - 'identifier' - ); - if (rparenToken) { - expr = this.findExpressionMatchedParen(session, rparenToken.row, rparenToken.column); - if (expr === '') { - return Promise.resolve([]); - } - return this.getLabelNameAndValueForExpression(expr, 'expression').then(result => { - const labelNames = this.transformToCompletions( - _.uniq( - _.flatten( - result.map((r: any) => { - return Object.keys(r); - }) - ) - ), - 'label name' - ); - this.labelNameCache[expr] = labelNames; - return labelNames; - }); - } else { - const metricName = this.findMetricName(session, binaryOperatorToken.row, binaryOperatorToken.column); - return this.getLabelNameAndValueForExpression(metricName, 'metricName').then(result => { - const labelNames = this.transformToCompletions( - _.uniq( - _.flatten( - result.map((r: any) => { - return Object.keys(r); - }) - ) - ), - 'label name' - ); - this.labelNameCache[metricName] = labelNames; - return Promise.resolve(labelNames); - }); - } - } - - return Promise.resolve([]); - } - - getLabelNameAndValueForExpression(expr: string, type: string): Promise { - if (this.labelQueryCache[expr]) { - return Promise.resolve(this.labelQueryCache[expr]); - } - let query = expr; - if (type === 'metricName') { - let op = '=~'; - if (/[a-zA-Z_:][a-zA-Z0-9_:]*/.test(expr)) { - op = '='; - } - query = '{__name__' + op + '"' + expr + '"}'; - } - const { start, end } = this.datasource.getTimeRange(); - const url = '/api/v1/series?match[]=' + encodeURIComponent(query) + '&start=' + start + '&end=' + end; - return this.datasource.metadataRequest(url).then((response: any) => { - this.labelQueryCache[expr] = response.data.data; - return response.data.data; - }); - } - - transformToCompletions(words: string[], meta: any) { - return words.map(name => { - return { - caption: name, - value: name, - meta, - score: Number.MAX_VALUE, - }; - }); - } - - findMetricName(session: CompleterSession, row: number, column: number) { - let metricName = ''; - - let tokens: CompleterToken[]; - const nameLabelNameToken = this.findToken( - session, - row, - column, - 'entity.name.tag.label-matcher', - '__name__', - 'paren.lparen.label-matcher' - ); - if (nameLabelNameToken) { - tokens = session.getTokens(nameLabelNameToken.row); - const nameLabelValueToken = tokens[nameLabelNameToken.index + 2]; - if (nameLabelValueToken && nameLabelValueToken.type === 'string.quoted.label-matcher') { - metricName = nameLabelValueToken.value.slice(1, -1); // cut begin/end quotation - } - } else { - const metricNameToken = this.findToken(session, row, column, 'identifier', null, null); - if (metricNameToken) { - tokens = session.getTokens(metricNameToken.row); - metricName = metricNameToken.value; - } - } - - return metricName; - } - - findToken(session: CompleterSession, row: number, column: number, target: string, value: string, guard: string) { - let tokens: CompleterToken[], idx: number; - // find index and get column of previous token - for (let r = row; r >= 0; r--) { - let c: number; - tokens = session.getTokens(r); - if (r === row) { - // current row - c = 0; - for (idx = 0; idx < tokens.length; idx++) { - const nc = c + tokens[idx].value.length; - if (nc >= column) { - break; - } - c = nc; - } - } else { - idx = tokens.length - 1; - c = - _.sum( - tokens.map(t => { - return t.value.length; - }) - ) - tokens[tokens.length - 1].value.length; - } - - for (; idx >= 0; idx--) { - if (tokens[idx].type === guard) { - return null; - } - - if (tokens[idx].type === target && (!value || tokens[idx].value === value)) { - tokens[idx].row = r; - tokens[idx].column = c; - tokens[idx].index = idx; - return tokens[idx]; - } - c -= tokens[idx].value.length; - } - } - - return null; - } - - findExpressionMatchedParen(session: CompleterSession, row: number, column: number) { - let tokens: CompleterToken[], idx: number; - let deep = 1; - let expression = ')'; - for (let r = row; r >= 0; r--) { - tokens = session.getTokens(r); - if (r === row) { - // current row - let c = 0; - for (idx = 0; idx < tokens.length; idx++) { - c += tokens[idx].value.length; - if (c >= column) { - break; - } - } - } else { - idx = tokens.length - 1; - } - - for (; idx >= 0; idx--) { - expression = tokens[idx].value + expression; - if (tokens[idx].type === 'paren.rparen') { - deep++; - } else if (tokens[idx].type === 'paren.lparen') { - deep--; - if (deep === 0) { - return expression; - } - } - } - } - - return expression; - } -} diff --git a/public/app/plugins/datasource/prometheus/components/PromLink.tsx b/public/app/plugins/datasource/prometheus/components/PromLink.tsx new file mode 100644 index 0000000000000..9ad46f171cde2 --- /dev/null +++ b/public/app/plugins/datasource/prometheus/components/PromLink.tsx @@ -0,0 +1,68 @@ +import _ from 'lodash'; +import React, { Component } from 'react'; + +import { PrometheusDatasource } from '../datasource'; +import { PromQuery } from '../types'; +import { DataQueryRequest, PanelData } from '@grafana/ui'; +import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; + +interface Props { + datasource: PrometheusDatasource; + query: PromQuery; + panelData: PanelData; +} + +interface State { + href: string; +} + +export default class PromLink extends Component { + state: State = { href: null }; + async componentDidUpdate(prevProps: Props) { + if (prevProps.panelData !== this.props.panelData && this.props.panelData.request) { + const href = await this.getExternalLink(); + this.setState({ href }); + } + } + + async getExternalLink(): Promise { + const { query, panelData } = this.props; + const target = panelData.request.targets.length > 0 ? panelData.request.targets[0] : { datasource: null }; + const datasourceName = target.datasource; + const datasource: PrometheusDatasource = datasourceName + ? (((await getDatasourceSrv().get(datasourceName)) as any) as PrometheusDatasource) + : (this.props.datasource as PrometheusDatasource); + + const range = panelData.request.range; + const start = datasource.getPrometheusTime(range.from, false); + const end = datasource.getPrometheusTime(range.to, true); + const rangeDiff = Math.ceil(end - start); + const endTime = range.to.utc().format('YYYY-MM-DD HH:mm'); + + const options = { + interval: panelData.request.interval, + } as DataQueryRequest; + const queryOptions = datasource.createQuery(query, options, start, end); + const expr = { + 'g0.expr': queryOptions.expr, + 'g0.range_input': rangeDiff + 's', + 'g0.end_input': endTime, + 'g0.step_input': queryOptions.step, + 'g0.tab': 0, + }; + + const args = _.map(expr, (v: string, k: string) => { + return k + '=' + encodeURIComponent(v); + }).join('&'); + return `${datasource.directUrl}/graph?${args}`; + } + + render() { + const { href } = this.state; + return ( + + Prometheus + + ); + } +} diff --git a/public/app/plugins/datasource/prometheus/components/PromQueryEditor.test.tsx b/public/app/plugins/datasource/prometheus/components/PromQueryEditor.test.tsx new file mode 100644 index 0000000000000..d3f0ff2aa2b59 --- /dev/null +++ b/public/app/plugins/datasource/prometheus/components/PromQueryEditor.test.tsx @@ -0,0 +1,53 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { dateTime } from '@grafana/ui'; + +import { PromQueryEditor } from './PromQueryEditor'; +import { PrometheusDatasource } from '../datasource'; +import { PromQuery } from '../types'; + +jest.mock('app/features/dashboard/services/TimeSrv', () => { + return { + getTimeSrv: () => ({ + timeRange: () => ({ + from: dateTime(), + to: dateTime(), + }), + }), + }; +}); + +const setup = (propOverrides?: object) => { + const datasourceMock: unknown = { + createQuery: jest.fn(q => q), + getPrometheusTime: jest.fn((date, roundup) => 123), + }; + const datasource: PrometheusDatasource = datasourceMock as PrometheusDatasource; + const onRunQuery = jest.fn(); + const onChange = jest.fn(); + const query: PromQuery = { expr: '', refId: 'A' }; + + const props: any = { + datasource, + onChange, + onRunQuery, + query, + }; + + Object.assign(props, propOverrides); + + const wrapper = shallow(); + const instance = wrapper.instance() as PromQueryEditor; + + return { + instance, + wrapper, + }; +}; + +describe('Render PromQueryEditor with basic options', () => { + it('should render', () => { + const { wrapper } = setup(); + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/app/plugins/datasource/prometheus/components/PromQueryEditor.tsx b/public/app/plugins/datasource/prometheus/components/PromQueryEditor.tsx new file mode 100644 index 0000000000000..fc5d71ff85853 --- /dev/null +++ b/public/app/plugins/datasource/prometheus/components/PromQueryEditor.tsx @@ -0,0 +1,177 @@ +import _ from 'lodash'; +import React, { PureComponent } from 'react'; + +// Types +import { FormLabel, Select, SelectOptionItem, Switch } from '@grafana/ui'; +import { QueryEditorProps, DataSourceStatus } from '@grafana/ui/src/types'; + +import { PrometheusDatasource } from '../datasource'; +import { PromQuery, PromOptions } from '../types'; + +import PromQueryField from './PromQueryField'; +import PromLink from './PromLink'; + +export type Props = QueryEditorProps; + +const FORMAT_OPTIONS: Array> = [ + { label: 'Time series', value: 'time_series' }, + { label: 'Table', value: 'table' }, + { label: 'Heatmap', value: 'heatmap' }, +]; + +const INTERVAL_FACTOR_OPTIONS: Array> = _.map([1, 2, 3, 4, 5, 10], (value: number) => ({ + value, + label: '1/' + value, +})); + +interface State { + legendFormat: string; + formatOption: SelectOptionItem; + interval: string; + intervalFactorOption: SelectOptionItem; + instant: boolean; +} + +export class PromQueryEditor extends PureComponent { + // Query target to be modified and used for queries + query: PromQuery; + + constructor(props: Props) { + super(props); + const { query } = props; + this.query = query; + // Query target properties that are fullu controlled inputs + this.state = { + // Fully controlled text inputs + interval: query.interval, + legendFormat: query.legendFormat, + // Select options + formatOption: FORMAT_OPTIONS.find(option => option.value === query.format) || FORMAT_OPTIONS[0], + intervalFactorOption: + INTERVAL_FACTOR_OPTIONS.find(option => option.value === query.intervalFactor) || INTERVAL_FACTOR_OPTIONS[0], + // Switch options + instant: Boolean(query.instant), + }; + } + + onFieldChange = (query: PromQuery, override?) => { + this.query.expr = query.expr; + }; + + onFormatChange = (option: SelectOptionItem) => { + this.query.format = option.value; + this.setState({ formatOption: option }, this.onRunQuery); + }; + + onInstantChange = (e: React.ChangeEvent) => { + const instant = e.target.checked; + this.query.instant = instant; + this.setState({ instant }, this.onRunQuery); + }; + + onIntervalChange = (e: React.SyntheticEvent) => { + const interval = e.currentTarget.value; + this.query.interval = interval; + this.setState({ interval }); + }; + + onIntervalFactorChange = (option: SelectOptionItem) => { + this.query.intervalFactor = option.value; + this.setState({ intervalFactorOption: option }, this.onRunQuery); + }; + + onLegendChange = (e: React.SyntheticEvent) => { + const legendFormat = e.currentTarget.value; + this.query.legendFormat = legendFormat; + this.setState({ legendFormat }); + }; + + onRunQuery = () => { + const { query } = this; + this.props.onChange(query); + this.props.onRunQuery(); + }; + + render() { + const { datasource, query, panelData, queryResponse } = this.props; + const { formatOption, instant, interval, intervalFactorOption, legendFormat } = this.state; + + return ( +
+
+ +
+ +
+
+ + Legend + + +
+ +
+ + Min step + + +
+ +
+
Resolution
+ + + + + + +
+
+
+ ); + } +} diff --git a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx index 7efb9185b0485..7d7878827b64d 100644 --- a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx +++ b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx @@ -13,9 +13,10 @@ import { TypeaheadOutput, HistoryItem } from 'app/types/explore'; import { getNextCharacter, getPreviousCousin } from 'app/features/explore/utils/dom'; import BracesPlugin from 'app/features/explore/slate-plugins/braces'; import QueryField, { TypeaheadInput, QueryFieldState } from 'app/features/explore/QueryField'; -import { PromQuery, PromContext } from '../types'; +import { PromQuery, PromContext, PromOptions } from '../types'; import { CancelablePromise, makePromiseCancelable } from 'app/core/utils/CancelablePromise'; -import { DataSourceApi, ExploreQueryFieldProps, DataSourceStatus, QueryHint } from '@grafana/ui'; +import { ExploreQueryFieldProps, DataSourceStatus, QueryHint, isSeriesData, toLegacyResponseData } from '@grafana/ui'; +import { PrometheusDatasource } from '../datasource'; const HISTOGRAM_GROUP = '__histograms__'; const METRIC_MARK = 'metric'; @@ -101,7 +102,7 @@ interface CascaderOption { disabled?: boolean; } -interface PromQueryFieldProps extends ExploreQueryFieldProps, PromQuery> { +interface PromQueryFieldProps extends ExploreQueryFieldProps { history: HistoryItem[]; } @@ -152,8 +153,9 @@ class PromQueryField extends React.PureComponent 0; - if (currentHasSeries && prevProps.queryResponse.series !== this.props.queryResponse.series) { + const { queryResponse } = this.props; + const currentHasSeries = queryResponse && queryResponse.series && queryResponse.series.length > 0 ? true : false; + if (currentHasSeries && prevProps.queryResponse && prevProps.queryResponse.series !== queryResponse.series) { this.refreshHint(); } @@ -175,11 +177,14 @@ class PromQueryField extends React.PureComponent { const { datasource, query, queryResponse } = this.props; - if (queryResponse.series && queryResponse.series.length === 0) { + if (!queryResponse || !queryResponse.series || queryResponse.series.length === 0) { return; } - const hints = datasource.getQueryHints(query, queryResponse.series); + const result = isSeriesData(queryResponse.series[0]) + ? queryResponse.series.map(toLegacyResponseData) + : queryResponse.series; + const hints = datasource.getQueryHints(query, result); const hint = hints && hints.length > 0 ? hints[0] : null; this.setState({ hint }); }; diff --git a/public/app/plugins/datasource/prometheus/components/__snapshots__/PromQueryEditor.test.tsx.snap b/public/app/plugins/datasource/prometheus/components/__snapshots__/PromQueryEditor.test.tsx.snap new file mode 100644 index 0000000000000..e9f83ccafd36f --- /dev/null +++ b/public/app/plugins/datasource/prometheus/components/__snapshots__/PromQueryEditor.test.tsx.snap @@ -0,0 +1,213 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render PromQueryEditor with basic options should render 1`] = ` +
+
+ +
+
+
+ + Legend + + +
+
+ + Min step + + +
+
+
+ Resolution +
+ + + + + +
+
+
+`; diff --git a/public/app/plugins/datasource/prometheus/mode-prometheus.js b/public/app/plugins/datasource/prometheus/mode-prometheus.js deleted file mode 100644 index 3989b59063815..0000000000000 --- a/public/app/plugins/datasource/prometheus/mode-prometheus.js +++ /dev/null @@ -1,532 +0,0 @@ -// jshint ignore: start -// jscs: disable -ace.define("ace/mode/prometheus_highlight_rules",["require","exports","module","ace/lib/oop","ace/mode/text_highlight_rules"], function(require, exports, module) { -"use strict"; - -var oop = require("../lib/oop"); -var TextHighlightRules = require("./text_highlight_rules").TextHighlightRules; - -var PrometheusHighlightRules = function() { - var keywords = ( - "count|count_values|min|max|avg|sum|stddev|stdvar|bottomk|topk|quantile" - ); - - var builtinConstants = ( - "true|false|null|__name__|job" - ); - - var builtinFunctions = ( - "abs|absent|ceil|changes|clamp_max|clamp_min|count_scalar|day_of_month|day_of_week|days_in_month|delta|deriv|" + "drop_common_labels|exp|floor|histogram_quantile|holt_winters|hour|idelta|increase|irate|label_replace|ln|log2|" + - "log10|minute|month|predict_linear|rate|resets|round|scalar|sort|sort_desc|sqrt|time|vector|year|avg_over_time|" + - "min_over_time|max_over_time|sum_over_time|count_over_time|quantile_over_time|stddev_over_time|stdvar_over_time" - ); - - var keywordMapper = this.createKeywordMapper({ - "support.function": builtinFunctions, - "keyword": keywords, - "constant.language": builtinConstants - }, "identifier", true); - - this.$rules = { - "start" : [ { - token : "string", // single line - regex : /"(?:[^"\\]|\\.)*?"/ - }, { - token : "string", // string - regex : "'.*?'" - }, { - token : "constant.numeric", // float - regex : "[-]?\\d+(?:(?:\\.\\d*)?(?:[eE][+-]?\\d+)?)?\\b" - }, { - token : "constant.language", // time - regex : "\\d+[smhdwy]" - }, { - token : "keyword.operator.binary", - regex : "\\+|\\-|\\*|\\/|%|\\^|==|!=|<=|>=|<|>|and|or|unless" - }, { - token : "keyword.other", - regex : "keep_common|offset|bool" - }, { - token : "keyword.control", - regex : "by|without|on|ignoring|group_left|group_right", - next : "start-label-list-matcher" - }, { - token : "variable", - regex : "\\$[A-Za-z0-9_]+" - }, { - token : keywordMapper, - regex : "[a-zA-Z_:][a-zA-Z0-9_:]*" - }, { - token : "paren.lparen", - regex : "[[(]" - }, { - token : "paren.lparen.label-matcher", - regex : "{", - next : "start-label-matcher" - }, { - token : "paren.rparen", - regex : "[\\])]" - }, { - token : "paren.rparen.label-matcher", - regex : "}" - }, { - token : "text", - regex : "\\s+" - } ], - "start-label-matcher" : [ { - token : "entity.name.tag.label-matcher", - regex : '[a-zA-Z_][a-zA-Z0-9_]*' - }, { - token : "keyword.operator.label-matcher", - regex : '=~|=|!~|!=' - }, { - token : "string.quoted.label-matcher", - regex : '"[^"]*"|\'[^\']*\'' - }, { - token : "punctuation.operator.label-matcher", - regex : "," - }, { - token : "paren.rparen.label-matcher", - regex : "}", - next : "start" - } ], - "start-label-list-matcher" : [ { - token : "paren.lparen.label-list-matcher", - regex : "[(]" - }, { - token : "entity.name.tag.label-list-matcher", - regex : '[a-zA-Z_][a-zA-Z0-9_]*' - }, { - token : "punctuation.operator.label-list-matcher", - regex : "," - }, { - token : "paren.rparen.label-list-matcher", - regex : "[)]", - next : "start" - } ] - }; - - this.normalizeRules(); -}; - -oop.inherits(PrometheusHighlightRules, TextHighlightRules); - -exports.PrometheusHighlightRules = PrometheusHighlightRules; -}); - -ace.define("ace/mode/prometheus_completions",["require","exports","module","ace/token_iterator", "ace/lib/lang"], function(require, exports, module) { -"use strict"; - -var lang = require("../lib/lang"); - -var prometheusKeyWords = [ - "by", "without", "keep_common", "offset", "bool", "and", "or", "unless", "ignoring", "on", "group_left", - "group_right", "count", "count_values", "min", "max", "avg", "sum", "stddev", "stdvar", "bottomk", "topk", "quantile" -]; - -var keyWordsCompletions = prometheusKeyWords.map(function(word) { - return { - caption: word, - value: word, - meta: "keyword", - score: Number.MAX_VALUE - } -}); - -var prometheusFunctions = [ - { - name: 'abs()', value: 'abs', - def: 'abs(v instant-vector)', - docText: 'Returns the input vector with all sample values converted to their absolute value.' - }, - { - name: 'absent()', value: 'absent', - def: 'absent(v instant-vector)', - docText: 'Returns an empty vector if the vector passed to it has any elements and a 1-element vector with the value 1 if the vector passed to it has no elements. This is useful for alerting on when no time series exist for a given metric name and label combination.' - }, - { - name: 'ceil()', value: 'ceil', - def: 'ceil(v instant-vector)', - docText: 'Rounds the sample values of all elements in `v` up to the nearest integer.' - }, - { - name: 'changes()', value: 'changes', - def: 'changes(v range-vector)', - docText: 'For each input time series, `changes(v range-vector)` returns the number of times its value has changed within the provided time range as an instant vector.' - }, - { - name: 'clamp_max()', value: 'clamp_max', - def: 'clamp_max(v instant-vector, max scalar)', - docText: 'Clamps the sample values of all elements in `v` to have an upper limit of `max`.' - }, - { - name: 'clamp_min()', value: 'clamp_min', - def: 'clamp_min(v instant-vector, min scalar)', - docText: 'Clamps the sample values of all elements in `v` to have a lower limit of `min`.' - }, - { - name: 'count_scalar()', value: 'count_scalar', - def: 'count_scalar(v instant-vector)', - docText: 'Returns the number of elements in a time series vector as a scalar. This is in contrast to the `count()` aggregation operator, which always returns a vector (an empty one if the input vector is empty) and allows grouping by labels via a `by` clause.' - }, - { - name: 'day_of_month()', value: 'day_of_month', - def: 'day_of_month(v=vector(time()) instant-vector)', - docText: 'Returns the day of the month for each of the given times in UTC. Returned values are from 1 to 31.' - }, - { - name: 'day_of_week()', value: 'day_of_week', - def: 'day_of_week(v=vector(time()) instant-vector)', - docText: 'Returns the day of the week for each of the given times in UTC. Returned values are from 0 to 6, where 0 means Sunday etc.' - }, - { - name: 'days_in_month()', value: 'days_in_month', - def: 'days_in_month(v=vector(time()) instant-vector)', - docText: 'Returns number of days in the month for each of the given times in UTC. Returned values are from 28 to 31.' - }, - { - name: 'delta()', value: 'delta', - def: 'delta(v range-vector)', - docText: 'Calculates the difference between the first and last value of each time series element in a range vector `v`, returning an instant vector with the given deltas and equivalent labels. The delta is extrapolated to cover the full time range as specified in the range vector selector, so that it is possible to get a non-integer result even if the sample values are all integers.' - }, - { - name: 'deriv()', value: 'deriv', - def: 'deriv(v range-vector)', - docText: 'Calculates the per-second derivative of the time series in a range vector `v`, using simple linear regression.' - }, - { - name: 'drop_common_labels()', value: 'drop_common_labels', - def: 'drop_common_labels(instant-vector)', - docText: 'Drops all labels that have the same name and value across all series in the input vector.' - }, - { - name: 'exp()', value: 'exp', - def: 'exp(v instant-vector)', - docText: 'Calculates the exponential function for all elements in `v`.\nSpecial cases are:\n* `Exp(+Inf) = +Inf` \n* `Exp(NaN) = NaN`' - }, - { - name: 'floor()', value: 'floor', - def: 'floor(v instant-vector)', - docText: 'Rounds the sample values of all elements in `v` down to the nearest integer.' - }, - { - name: 'histogram_quantile()', value: 'histogram_quantile', - def: 'histogram_quantile(φ float, b instant-vector)', - docText: 'Calculates the φ-quantile (0 ≤ φ ≤ 1) from the buckets `b` of a histogram. The samples in `b` are the counts of observations in each bucket. Each sample must have a label `le` where the label value denotes the inclusive upper bound of the bucket. (Samples without such a label are silently ignored.) The histogram metric type automatically provides time series with the `_bucket` suffix and the appropriate labels.' - }, - { - name: 'holt_winters()', value: 'holt_winters', - def: 'holt_winters(v range-vector, sf scalar, tf scalar)', - docText: 'Produces a smoothed value for time series based on the range in `v`. The lower the smoothing factor `sf`, the more importance is given to old data. The higher the trend factor `tf`, the more trends in the data is considered. Both `sf` and `tf` must be between 0 and 1.' - }, - { - name: 'hour()', value: 'hour', - def: 'hour(v=vector(time()) instant-vector)', - docText: 'Returns the hour of the day for each of the given times in UTC. Returned values are from 0 to 23.' - }, - { - name: 'idelta()', value: 'idelta', - def: 'idelta(v range-vector)', - docText: 'Calculates the difference between the last two samples in the range vector `v`, returning an instant vector with the given deltas and equivalent labels.' - }, - { - name: 'increase()', value: 'increase', - def: 'increase(v range-vector)', - docText: 'Calculates the increase in the time series in the range vector. Breaks in monotonicity (such as counter resets due to target restarts) are automatically adjusted for. The increase is extrapolated to cover the full time range as specified in the range vector selector, so that it is possible to get a non-integer result even if a counter increases only by integer increments.' - }, - { - name: 'irate()', value: 'irate', - def: 'irate(v range-vector)', - docText: 'Calculates the per-second instant rate of increase of the time series in the range vector. This is based on the last two data points. Breaks in monotonicity (such as counter resets due to target restarts) are automatically adjusted for.' - }, - { - name: 'label_replace()', value: 'label_replace', - def: 'label_replace(v instant-vector, dst_label string, replacement string, src_label string, regex string)', - docText: 'For each timeseries in `v`, `label_replace(v instant-vector, dst_label string, replacement string, src_label string, regex string)` matches the regular expression `regex` against the label `src_label`. If it matches, then the timeseries is returned with the label `dst_label` replaced by the expansion of `replacement`. `$1` is replaced with the first matching subgroup, `$2` with the second etc. If the regular expression doesn\'t match then the timeseries is returned unchanged.' - }, - { - name: 'ln()', value: 'ln', - def: 'ln(v instant-vector)', - docText: 'calculates the natural logarithm for all elements in `v`.\nSpecial cases are:\n * `ln(+Inf) = +Inf`\n * `ln(0) = -Inf`\n * `ln(x < 0) = NaN`\n * `ln(NaN) = NaN`' - }, - { - name: 'log2()', value: 'log2', - def: 'log2(v instant-vector)', - docText: 'Calculates the binary logarithm for all elements in `v`. The special cases are equivalent to those in `ln`.' - }, - { - name: 'log10()', value: 'log10', - def: 'log10(v instant-vector)', - docText: 'Calculates the decimal logarithm for all elements in `v`. The special cases are equivalent to those in `ln`.' - }, - { - name: 'minute()', value: 'minute', - def: 'minute(v=vector(time()) instant-vector)', - docText: 'Returns the minute of the hour for each of the given times in UTC. Returned values are from 0 to 59.' - }, - { - name: 'month()', value: 'month', - def: 'month(v=vector(time()) instant-vector)', - docText: 'Returns the month of the year for each of the given times in UTC. Returned values are from 1 to 12, where 1 means January etc.' - }, - { - name: 'predict_linear()', value: 'predict_linear', - def: 'predict_linear(v range-vector, t scalar)', - docText: 'Predicts the value of time series `t` seconds from now, based on the range vector `v`, using simple linear regression.' - }, - { - name: 'rate()', value: 'rate', - def: 'rate(v range-vector)', - docText: "Calculates the per-second average rate of increase of the time series in the range vector. Breaks in monotonicity (such as counter resets due to target restarts) are automatically adjusted for. Also, the calculation extrapolates to the ends of the time range, allowing for missed scrapes or imperfect alignment of scrape cycles with the range's time period." - }, - { - name: 'resets()', value: 'resets', - def: 'resets(v range-vector)', - docText: 'For each input time series, `resets(v range-vector)` returns the number of counter resets within the provided time range as an instant vector. Any decrease in the value between two consecutive samples is interpreted as a counter reset.' - }, - { - name: 'round()', value: 'round', - def: 'round(v instant-vector, to_nearest=1 scalar)', - docText: 'Rounds the sample values of all elements in `v` to the nearest integer. Ties are resolved by rounding up. The optional `to_nearest` argument allows specifying the nearest multiple to which the sample values should be rounded. This multiple may also be a fraction.' - }, - { - name: 'scalar()', value: 'scalar', - def: 'scalar(v instant-vector)', - docText: 'Given a single-element input vector, `scalar(v instant-vector)` returns the sample value of that single element as a scalar. If the input vector does not have exactly one element, `scalar` will return `NaN`.' - }, - { - name: 'sort()', value: 'sort', - def: 'sort(v instant-vector)', - docText: 'Returns vector elements sorted by their sample values, in ascending order.' - }, - { - name: 'sort_desc()', value: 'sort_desc', - def: 'sort_desc(v instant-vector)', - docText: 'Returns vector elements sorted by their sample values, in descending order.' - }, - { - name: 'sqrt()', value: 'sqrt', - def: 'sqrt(v instant-vector)', - docText: 'Calculates the square root of all elements in `v`.' - }, - { - name: 'time()', value: 'time', - def: 'time()', - docText: 'Returns the number of seconds since January 1, 1970 UTC. Note that this does not actually return the current time, but the time at which the expression is to be evaluated.' - }, - { - name: 'vector()', value: 'vector', - def: 'vector(s scalar)', - docText: 'Returns the scalar `s` as a vector with no labels.' - }, - { - name: 'year()', value: 'year', - def: 'year(v=vector(time()) instant-vector)', - docText: 'Returns the year for each of the given times in UTC.' - }, - { - name: 'avg_over_time()', value: 'avg_over_time', - def: 'avg_over_time(range-vector)', - docText: 'The average value of all points in the specified interval.' - }, - { - name: 'min_over_time()', value: 'min_over_time', - def: 'min_over_time(range-vector)', - docText: 'The minimum value of all points in the specified interval.' - }, - { - name: 'max_over_time()', value: 'max_over_time', - def: 'max_over_time(range-vector)', - docText: 'The maximum value of all points in the specified interval.' - }, - { - name: 'sum_over_time()', value: 'sum_over_time', - def: 'sum_over_time(range-vector)', - docText: 'The sum of all values in the specified interval.' - }, - { - name: 'count_over_time()', value: 'count_over_time', - def: 'count_over_time(range-vector)', - docText: 'The count of all values in the specified interval.' - }, - { - name: 'quantile_over_time()', value: 'quantile_over_time', - def: 'quantile_over_time(scalar, range-vector)', - docText: 'The φ-quantile (0 ≤ φ ≤ 1) of the values in the specified interval.' - }, - { - name: 'stddev_over_time()', value: 'stddev_over_time', - def: 'stddev_over_time(range-vector)', - docText: 'The population standard deviation of the values in the specified interval.' - }, - { - name: 'stdvar_over_time()', value: 'stdvar_over_time', - def: 'stdvar_over_time(range-vector)', - docText: 'The population standard variance of the values in the specified interval.' - }, -]; - -function wrapText(str, len) { - len = len || 60; - var lines = []; - var space_index = 0; - var line_start = 0; - var next_line_end = len; - var line = ""; - for (var i = 0; i < str.length; i++) { - if (str[i] === ' ') { - space_index = i; - } else if (i >= next_line_end && space_index != 0) { - line = str.slice(line_start, space_index); - lines.push(line); - line_start = space_index + 1; - next_line_end = i + len; - space_index = 0; - } - } - line = str.slice(line_start); - lines.push(line); - return lines.join(" 
"); -} - -function convertMarkDownTags(text) { - text = text.replace(/```(.+)```/, "
$1
"); - text = text.replace(/`([^`]+)`/, "$1"); - return text; -} - -function convertToHTML(item) { - var docText = lang.escapeHTML(item.docText); - docText = convertMarkDownTags(wrapText(docText, 40)); - return [ - "", lang.escapeHTML(item.def), "", "
", docText, "
 " - ].join(""); -} - -var functionsCompletions = prometheusFunctions.map(function(item) { - return { - caption: item.name, - value: item.value, - docHTML: convertToHTML(item), - meta: "function", - score: Number.MAX_VALUE - }; -}); - -var PrometheusCompletions = function() {}; - -(function() { - this.getCompletions = function(state, session, pos, prefix, callback) { - var token = session.getTokenAt(pos.row, pos.column); - if (token.type === 'entity.name.tag.label-matcher' - || token.type === 'string.quoted.label-matcher' - || token.type === 'entity.name.tag.label-list-matcher') { - return callback(null, []); - } - - var completions = keyWordsCompletions.concat(functionsCompletions); - callback(null, completions); - }; - -}).call(PrometheusCompletions.prototype); - -exports.PrometheusCompletions = PrometheusCompletions; -}); - -ace.define("ace/mode/behaviour/prometheus",["require","exports","module","ace/lib/oop","ace/mode/behaviour","ace/mode/behaviour/cstyle","ace/token_iterator"], function(require, exports, module) { -"use strict"; - -var oop = require("../../lib/oop"); -var Behaviour = require("../behaviour").Behaviour; -var CstyleBehaviour = require("./cstyle").CstyleBehaviour; -var TokenIterator = require("../../token_iterator").TokenIterator; - -function getWrapped(selection, selected, opening, closing) { - var rowDiff = selection.end.row - selection.start.row; - return { - text: opening + selected + closing, - selection: [ - 0, - selection.start.column + 1, - rowDiff, - selection.end.column + (rowDiff ? 0 : 1) - ] - }; -}; - -var PrometheusBehaviour = function () { - this.inherit(CstyleBehaviour); - - // Rewrite default CstyleBehaviour for {} braces - this.add("braces", "insertion", function(state, action, editor, session, text) { - if (text == '{') { - var selection = editor.getSelectionRange(); - var selected = session.doc.getTextRange(selection); - if (selected !== "" && editor.getWrapBehavioursEnabled()) { - return getWrapped(selection, selected, '{', '}'); - } else if (CstyleBehaviour.isSaneInsertion(editor, session)) { - return { - text: '{}', - selection: [1, 1] - }; - } - } else if (text == '}') { - var cursor = editor.getCursorPosition(); - var line = session.doc.getLine(cursor.row); - var rightChar = line.substring(cursor.column, cursor.column + 1); - if (rightChar == '}') { - var matching = session.$findOpeningBracket('}', {column: cursor.column + 1, row: cursor.row}); - if (matching !== null && CstyleBehaviour.isAutoInsertedClosing(cursor, line, text)) { - return { - text: '', - selection: [1, 1] - }; - } - } - } - }); - - this.add("braces", "deletion", function(state, action, editor, session, range) { - var selected = session.doc.getTextRange(range); - if (!range.isMultiLine() && selected == '{') { - var line = session.doc.getLine(range.start.row); - var rightChar = line.substring(range.start.column + 1, range.start.column + 2); - if (rightChar == '}') { - range.end.column++; - return range; - } - } - }); - -} -oop.inherits(PrometheusBehaviour, CstyleBehaviour); - -exports.PrometheusBehaviour = PrometheusBehaviour; -}); - -ace.define("ace/mode/prometheus",["require","exports","module","ace/lib/oop","ace/mode/text","ace/mode/prometheus_highlight_rules"], function(require, exports, module) { -"use strict"; - -var oop = require("../lib/oop"); -var TextMode = require("./text").Mode; -var PrometheusHighlightRules = require("./prometheus_highlight_rules").PrometheusHighlightRules; -var PrometheusCompletions = require("./prometheus_completions").PrometheusCompletions; -var PrometheusBehaviour = require("./behaviour/prometheus").PrometheusBehaviour; - -var Mode = function() { - this.HighlightRules = PrometheusHighlightRules; - this.$behaviour = new PrometheusBehaviour(); - this.$completer = new PrometheusCompletions(); - // replace keyWordCompleter - this.completer = this.$completer; -}; -oop.inherits(Mode, TextMode); - -(function() { - - this.$id = "ace/mode/prometheus"; -}).call(Mode.prototype); - -exports.Mode = Mode; - -}); diff --git a/public/app/plugins/datasource/prometheus/module.ts b/public/app/plugins/datasource/prometheus/module.ts index 814f6fbe60a94..0922ce0d3f68e 100644 --- a/public/app/plugins/datasource/prometheus/module.ts +++ b/public/app/plugins/datasource/prometheus/module.ts @@ -1,5 +1,5 @@ import { PrometheusDatasource } from './datasource'; -import { PrometheusQueryCtrl } from './query_ctrl'; +import { PromQueryEditor } from './components/PromQueryEditor'; import { PrometheusConfigCtrl } from './config_ctrl'; import PrometheusStartPage from './components/PromStart'; @@ -11,7 +11,7 @@ class PrometheusAnnotationsQueryCtrl { export { PrometheusDatasource as Datasource, - PrometheusQueryCtrl as QueryCtrl, + PromQueryEditor as QueryEditor, PrometheusConfigCtrl as ConfigCtrl, PrometheusAnnotationsQueryCtrl as AnnotationsQueryCtrl, PromQueryField as ExploreQueryField, diff --git a/public/app/plugins/datasource/prometheus/partials/query.editor.html b/public/app/plugins/datasource/prometheus/partials/query.editor.html deleted file mode 100644 index 94819582f6dd6..0000000000000 --- a/public/app/plugins/datasource/prometheus/partials/query.editor.html +++ /dev/null @@ -1,57 +0,0 @@ - -
-
- - -
-
- -
-
- - - - - Controls the name of the time series, using name or pattern. For example - {{hostname}} will be replaced with label value for the label hostname. - -
- -
- - - - Leave blank for auto handling based on time range and panel width. Note that the actual dates used in the query will be adjusted - to a multiple of the interval step. - -
- -
- -
- -
-
- -
- -
- -
- - - -
-
-
diff --git a/public/app/plugins/datasource/prometheus/promql.ts b/public/app/plugins/datasource/prometheus/promql.ts index ae0c883525a19..d827677ee2d8e 100644 --- a/public/app/plugins/datasource/prometheus/promql.ts +++ b/public/app/plugins/datasource/prometheus/promql.ts @@ -3,6 +3,7 @@ import { CompletionItem } from 'app/types/explore'; export const RATE_RANGES: CompletionItem[] = [ + { label: '$__interval', sortText: '$__interval' }, { label: '1m', sortText: '00:01:00' }, { label: '5m', sortText: '00:05:00' }, { label: '10m', sortText: '00:10:00' }, diff --git a/public/app/plugins/datasource/prometheus/query_ctrl.ts b/public/app/plugins/datasource/prometheus/query_ctrl.ts deleted file mode 100644 index aa15da12e64a4..0000000000000 --- a/public/app/plugins/datasource/prometheus/query_ctrl.ts +++ /dev/null @@ -1,95 +0,0 @@ -import angular from 'angular'; -import _ from 'lodash'; -import { QueryCtrl } from 'app/plugins/sdk'; -import { PromCompleter } from './completer'; -import './mode-prometheus'; -import './snippets/prometheus'; -import { TemplateSrv } from 'app/features/templating/template_srv'; - -class PrometheusQueryCtrl extends QueryCtrl { - static templateUrl = 'partials/query.editor.html'; - - metric: any; - resolutions: any; - formats: any; - instant: any; - oldTarget: any; - suggestMetrics: any; - getMetricsAutocomplete: any; - linkToPrometheus: any; - - /** @ngInject */ - constructor($scope: any, $injector: angular.auto.IInjectorService, private templateSrv: TemplateSrv) { - super($scope, $injector); - - const target = this.target; - target.expr = target.expr || ''; - target.intervalFactor = target.intervalFactor || 1; - target.format = target.format || this.getDefaultFormat(); - - this.metric = ''; - this.resolutions = _.map([1, 2, 3, 4, 5, 10], f => { - return { factor: f, label: '1/' + f }; - }); - - this.formats = [ - { text: 'Time series', value: 'time_series' }, - { text: 'Table', value: 'table' }, - { text: 'Heatmap', value: 'heatmap' }, - ]; - - this.instant = false; - - this.updateLink(); - } - - getCompleter(query: string) { - return new PromCompleter(this.datasource, this.templateSrv); - } - - getDefaultFormat() { - if (this.panelCtrl.panel.type === 'table') { - return 'table'; - } else if (this.panelCtrl.panel.type === 'heatmap') { - return 'heatmap'; - } - - return 'time_series'; - } - - refreshMetricData() { - if (!_.isEqual(this.oldTarget, this.target)) { - this.oldTarget = angular.copy(this.target); - this.panelCtrl.refresh(); - this.updateLink(); - } - } - - updateLink() { - const range = this.panelCtrl.range; - if (!range) { - return; - } - - const rangeDiff = Math.ceil((range.to.valueOf() - range.from.valueOf()) / 1000); - const endTime = range.to.utc().format('YYYY-MM-DD HH:mm'); - const expr = { - 'g0.expr': this.templateSrv.replace( - this.target.expr, - this.panelCtrl.panel.scopedVars, - this.datasource.interpolateQueryExpr - ), - 'g0.range_input': rangeDiff + 's', - 'g0.end_input': endTime, - 'g0.step_input': this.target.step, - 'g0.stacked': this.panelCtrl.panel.stack ? 1 : 0, - 'g0.tab': 0, - }; - const args = _.map(expr, (v, k) => { - return k + '=' + encodeURIComponent(v); - }).join('&'); - this.linkToPrometheus = this.datasource.directUrl + '/graph?' + args; - } -} - -export { PrometheusQueryCtrl }; diff --git a/public/app/plugins/datasource/prometheus/snippets/prometheus.js b/public/app/plugins/datasource/prometheus/snippets/prometheus.js deleted file mode 100644 index eaaef48293270..0000000000000 --- a/public/app/plugins/datasource/prometheus/snippets/prometheus.js +++ /dev/null @@ -1,21 +0,0 @@ -// jshint ignore: start -// jscs: disable -ace.define("ace/snippets/prometheus",["require","exports","module"], function(require, exports, module) { -"use strict"; - -// exports.snippetText = "# rate\n\ -// snippet r\n\ -// rate(${1:metric}[${2:range}])\n\ -// "; - -exports.snippets = [ - { - "content": "rate(${1:metric}[${2:range}])", - "name": "rate()", - "scope": "prometheus", - "tabTrigger": "r" - } -]; - -exports.scope = "prometheus"; -}); diff --git a/public/app/plugins/datasource/prometheus/specs/completer.test.ts b/public/app/plugins/datasource/prometheus/specs/completer.test.ts deleted file mode 100644 index 9d1d66cbbf02e..0000000000000 --- a/public/app/plugins/datasource/prometheus/specs/completer.test.ts +++ /dev/null @@ -1,193 +0,0 @@ -import { PromCompleter } from '../completer'; -import { PrometheusDatasource } from '../datasource'; -import { BackendSrv } from 'app/core/services/backend_srv'; -import { DataSourceInstanceSettings } from '@grafana/ui'; -import { PromOptions } from '../types'; -import { TemplateSrv } from 'app/features/templating/template_srv'; -import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; -import { IQService } from 'angular'; -jest.mock('../datasource'); -jest.mock('@grafana/ui'); - -describe('Prometheus editor completer', () => { - function getSessionStub(data: any) { - return { - getTokenAt: jest.fn(() => data.currentToken), - getTokens: jest.fn(() => data.tokens), - getLine: jest.fn(() => data.line), - }; - } - - const editor = {}; - - const backendSrv = {} as BackendSrv; - const datasourceStub = new PrometheusDatasource( - {} as DataSourceInstanceSettings, - {} as IQService, - backendSrv, - {} as TemplateSrv, - {} as TimeSrv - ); - - datasourceStub.metadataRequest = jest.fn(() => - Promise.resolve({ data: { data: [{ metric: { job: 'node', instance: 'localhost:9100' } }] } }) - ); - datasourceStub.getTimeRange = jest.fn(() => { - return { start: 1514732400, end: 1514818800 }; - }); - datasourceStub.performSuggestQuery = jest.fn(() => Promise.resolve(['node_cpu'])); - - const templateSrv: TemplateSrv = ({ - variables: [ - { - name: 'var_name', - options: [{ text: 'foo', value: 'foo', selected: false }, { text: 'bar', value: 'bar', selected: true }], - }, - ], - } as any) as TemplateSrv; - const completer = new PromCompleter(datasourceStub, templateSrv); - - describe('When inside brackets', () => { - it('Should return range vectors', () => { - const session = getSessionStub({ - currentToken: { type: 'paren.lparen', value: '[', index: 2, start: 9 }, - tokens: [{ type: 'identifier', value: 'node_cpu' }, { type: 'paren.lparen', value: '[' }], - line: 'node_cpu[', - }); - - return completer.getCompletions(editor, session, { row: 0, column: 10 }, '[', (s: any, res: any) => { - expect(res[0].caption).toEqual('$__interval'); - expect(res[0].value).toEqual('[$__interval'); - expect(res[0].meta).toEqual('range vector'); - }); - }); - }); - - describe('When inside label matcher, and located at label name', () => { - it('Should return label name list', () => { - const session = getSessionStub({ - currentToken: { - type: 'entity.name.tag.label-matcher', - value: 'j', - index: 2, - start: 9, - }, - tokens: [ - { type: 'identifier', value: 'node_cpu' }, - { type: 'paren.lparen.label-matcher', value: '{' }, - { - type: 'entity.name.tag.label-matcher', - value: 'j', - index: 2, - start: 9, - }, - { type: 'paren.rparen.label-matcher', value: '}' }, - ], - line: 'node_cpu{j}', - }); - - return completer.getCompletions(editor, session, { row: 0, column: 10 }, 'j', (s: any, res: any) => { - expect(res[0].meta).toEqual('label name'); - }); - }); - }); - - describe('When inside label matcher, and located at label name with __name__ match', () => { - it('Should return label name list', () => { - const session = getSessionStub({ - currentToken: { - type: 'entity.name.tag.label-matcher', - value: 'j', - index: 5, - start: 22, - }, - tokens: [ - { type: 'paren.lparen.label-matcher', value: '{' }, - { type: 'entity.name.tag.label-matcher', value: '__name__' }, - { type: 'keyword.operator.label-matcher', value: '=~' }, - { type: 'string.quoted.label-matcher', value: '"node_cpu"' }, - { type: 'punctuation.operator.label-matcher', value: ',' }, - { - type: 'entity.name.tag.label-matcher', - value: 'j', - index: 5, - start: 22, - }, - { type: 'paren.rparen.label-matcher', value: '}' }, - ], - line: '{__name__=~"node_cpu",j}', - }); - - return completer.getCompletions(editor, session, { row: 0, column: 23 }, 'j', (s: any, res: any) => { - expect(res[0].meta).toEqual('label name'); - }); - }); - }); - - describe('When inside label matcher, and located at label value', () => { - it('Should return label value list', () => { - const session = getSessionStub({ - currentToken: { - type: 'string.quoted.label-matcher', - value: '"n"', - index: 4, - start: 13, - }, - tokens: [ - { type: 'identifier', value: 'node_cpu' }, - { type: 'paren.lparen.label-matcher', value: '{' }, - { type: 'entity.name.tag.label-matcher', value: 'job' }, - { type: 'keyword.operator.label-matcher', value: '=' }, - { - type: 'string.quoted.label-matcher', - value: '"n"', - index: 4, - start: 13, - }, - { type: 'paren.rparen.label-matcher', value: '}' }, - ], - line: 'node_cpu{job="n"}', - }); - - return completer.getCompletions(editor, session, { row: 0, column: 15 }, 'n', (s: any, res: any) => { - expect(res[0].meta).toEqual('label value'); - }); - }); - }); - - describe('When inside by', () => { - it('Should return label name list', () => { - const session = getSessionStub({ - currentToken: { - type: 'entity.name.tag.label-list-matcher', - value: 'm', - index: 9, - start: 22, - }, - tokens: [ - { type: 'paren.lparen', value: '(' }, - { type: 'keyword', value: 'count' }, - { type: 'paren.lparen', value: '(' }, - { type: 'identifier', value: 'node_cpu' }, - { type: 'paren.rparen', value: '))' }, - { type: 'text', value: ' ' }, - { type: 'keyword.control', value: 'by' }, - { type: 'text', value: ' ' }, - { type: 'paren.lparen.label-list-matcher', value: '(' }, - { - type: 'entity.name.tag.label-list-matcher', - value: 'm', - index: 9, - start: 22, - }, - { type: 'paren.rparen.label-list-matcher', value: ')' }, - ], - line: '(count(node_cpu)) by (m)', - }); - - return completer.getCompletions(editor, session, { row: 0, column: 23 }, 'm', (s: any, res: any) => { - expect(res[0].meta).toEqual('label name'); - }); - }); - }); -}); diff --git a/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts b/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts index 59f6a7f6a2794..47102d11265eb 100644 --- a/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts @@ -80,12 +80,13 @@ describe('Language completion provider', () => { expect(result.suggestions).toMatchObject([ { items: [ - { label: '1m' }, - { label: '5m' }, - { label: '10m' }, - { label: '30m' }, - { label: '1h' }, - { label: '1d' }, + { label: '$__interval', sortText: '$__interval' }, // TODO: figure out why this row and sortText is needed + { label: '1m', sortText: '00:01:00' }, + { label: '5m', sortText: '00:05:00' }, + { label: '10m', sortText: '00:10:00' }, + { label: '30m', sortText: '00:30:00' }, + { label: '1h', sortText: '01:00:00' }, + { label: '1d', sortText: '24:00:00' }, ], label: 'Range vector', }, From d0852f061807f1e6f379091483cf99c775f43745 Mon Sep 17 00:00:00 2001 From: Sofia Papagiannaki Date: Mon, 24 Jun 2019 10:34:51 +0300 Subject: [PATCH 12/20] Fix link in pkg/README (#17714) Remove leading backslash from url --- pkg/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/README.md b/pkg/README.md index 1bc27df59338b..c6fa805482e77 100644 --- a/pkg/README.md +++ b/pkg/README.md @@ -21,7 +21,7 @@ The plan is to move all settings to from package level vars in settings package [Injection example](https://github.com/grafana/grafana/blob/df917663e6f358a076ed3daa9b199412e95c11f4/pkg/services/cleanup/cleanup.go#L20) ## Reduce the use of Goconvey -We want to migrated away from using Goconvey and use stdlib testing as its the most common approuch in the GO community and we think it will make it easier for new contributors. Read more about how we want to write tests in the [ARCHITECTURE.MD](/ARCHITECTURE.md#Testing) docs. +We want to migrated away from using Goconvey and use stdlib testing as its the most common approuch in the GO community and we think it will make it easier for new contributors. Read more about how we want to write tests in the [ARCHITECTURE.MD](ARCHITECTURE.md#Testing) docs. ## Sqlstore refactoring The sqlstore handlers all use a global xorm engine variable. This should be refactored to use the Sqlstore instance. From 0adbb001dba56bb7f61ac57f0d7020b8a6881e44 Mon Sep 17 00:00:00 2001 From: Kyle Brandt Date: Mon, 24 Jun 2019 05:51:39 -0400 Subject: [PATCH 13/20] RemoteCache: redis connection string parsing test (#17702) --- pkg/infra/remotecache/redis_storage_test.go | 65 +++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 pkg/infra/remotecache/redis_storage_test.go diff --git a/pkg/infra/remotecache/redis_storage_test.go b/pkg/infra/remotecache/redis_storage_test.go new file mode 100644 index 0000000000000..592d863fe108f --- /dev/null +++ b/pkg/infra/remotecache/redis_storage_test.go @@ -0,0 +1,65 @@ +package remotecache + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + redis "gopkg.in/redis.v2" +) + +func Test_parseRedisConnStr(t *testing.T) { + cases := map[string]struct { + InputConnStr string + OutputOptions *redis.Options + ShouldErr bool + }{ + "all redis options should parse": { + "addr=127.0.0.1:6379,pool_size=100,db=1,password=grafanaRocks", + &redis.Options{ + Addr: "127.0.0.1:6379", + PoolSize: 100, + DB: 1, + Password: "grafanaRocks", + Network: "tcp", + }, + false, + }, + "subset of redis options should parse": { + "addr=127.0.0.1:6379,pool_size=100", + &redis.Options{ + Addr: "127.0.0.1:6379", + PoolSize: 100, + Network: "tcp", + }, + false, + }, + "trailing comma should err": { + "addr=127.0.0.1:6379,pool_size=100,", + nil, + true, + }, + "invalid key should err": { + "addr=127.0.0.1:6379,puddle_size=100", + nil, + true, + }, + "empty connection string should err": { + "", + nil, + true, + }, + } + + for reason, testCase := range cases { + options, err := parseRedisConnStr(testCase.InputConnStr) + if testCase.ShouldErr { + assert.Error(t, err, fmt.Sprintf("error cases should return non-nil error for test case %v", reason)) + assert.Nil(t, options, fmt.Sprintf("error cases should return nil for redis options for test case %v", reason)) + continue + } + assert.NoError(t, err, reason) + assert.EqualValues(t, testCase.OutputOptions, options, reason) + + } +} From 0412a28d2ed3f7e2e726b6c6620b289ef26a3c49 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Mon, 24 Jun 2019 14:39:59 +0200 Subject: [PATCH 14/20] TimePicker: New time picker dropdown & custom range UI (#16811) * feat: Add new picker to DashNavTimeControls * chore: noImplicitAny limit reached * chore: noImplicityAny fix * chore: Add momentUtc helper to avoid the isUtc conditionals * chore: Move getRaw from Explore's time picker to grafana/ui utils and rename to getRawRange * feat: Use helper functions to convert utc to browser time * fix: Dont Select current value when pressing tab when using Time Picker * fix: Add tabIndex to time range inputs so tab works smoothly and prevent mouseDown event to propagate to react-select * fix: Add spacing to custom range labels * fix: Updated snapshot * fix: Re-adding getRaw() temporary to fix the build * fix: Disable scroll event in Popper when we're using the TimePicker so the popup wont "follow" the menu * fix: Move all "Last xxxx" quick ranges to the menu and show a "UTC" text when applicable * fix: Add zoom functionality * feat: Add logic to mark selected option as active * fix: Add tooltip to zoom button * fix: lint fix after rebase * chore: Remove old time picker from DashNav * TimePicker: minor design update * chore: Move all time picker quick ranges to the menu * fix: Remove the popover border-right, since the quick ranges are gone * chore: Remove function not in use * Fix: Close time picker on resize event * Fix: Remove border bottom * Fix: Use fa icons on prev/next arrows * Fix: Pass ref from TimePicker to TimePickerOptionGroup so the popover will align as it should * Fix: time picker ui adjustments to get better touch area on buttons * Fix: Dont increase line height on large screens * TimePicker: style updates * Fix: Add more prominent colors for selected dates and fade out dates in previous/next month * TimePicker: style updates2 * TimePicker: Big refactorings and style changes * Removed use of Popper not sure we need that here? * Made active selected item in the list have the "selected" checkmark * Changed design of popover * Changed design of and implementation of the Custom selection in the dropdown it did not feel like a item you could select like the rest now the list is just a normal list * TimePicker: Refactoring & style changes * TimePicker: use same date format everywhere * TimePicker: Calendar style updates * TimePicker: fixed unit test * fixed unit test * TimeZone: refactoring time zone type * TimePicker: refactoring * TimePicker: finally to UTC to work * TimePicker: better way to handle calendar utc dates * TimePicker: Fixed tooltip issues * Updated snapshot * TimePicker: moved tooltip from DashNavControls into TimePicker --- .../src/components/Select/ButtonSelect.tsx | 20 +- .../src/components/Select/Select.tsx | 240 ++--- .../src/components/Select/_Select.scss | 2 +- .../TimePicker/TimePicker.story.tsx | 182 +--- .../src/components/TimePicker/TimePicker.tsx | 364 +++----- .../TimePicker/TimePickerCalendar.story.tsx | 2 +- .../TimePicker/TimePickerCalendar.tsx | 34 +- .../components/TimePicker/TimePickerInput.tsx | 13 +- .../TimePickerOptionGroup.story.tsx | 51 -- .../TimePicker/TimePickerOptionGroup.tsx | 66 -- .../TimePicker/TimePickerPopover.story.tsx | 4 +- .../TimePicker/TimePickerPopover.tsx | 173 ++-- .../components/TimePicker/_TimePicker.scss | 209 +++-- .../src/components/TimePicker/time.ts | 35 +- .../src/components/Tooltip/Popper.tsx | 10 +- packages/grafana-ui/src/components/index.ts | 1 + .../src/themes/_variables.dark.scss.tmpl.ts | 6 + .../src/themes/_variables.light.scss.tmpl.ts | 7 + packages/grafana-ui/src/types/time.ts | 17 +- packages/grafana-ui/src/utils/datemath.ts | 5 +- packages/grafana-ui/src/utils/index.ts | 1 + packages/grafana-ui/src/utils/rangeutil.ts | 3 +- public/app/core/specs/rangeutil.test.ts | 6 +- public/app/core/utils/explore.ts | 8 +- .../dashboard/components/DashNav/DashNav.tsx | 28 +- .../DashNav/DashNavTimeControls.tsx | 96 +- .../features/dashboard/services/TimeSrv.ts | 4 +- .../dashboard/state/DashboardModel.ts | 6 +- .../app/features/explore/ExploreToolbar.tsx | 2 +- public/app/features/explore/Graph.tsx | 4 +- .../app/features/explore/GraphContainer.tsx | 4 +- public/app/features/explore/LogsContainer.tsx | 4 +- .../app/features/explore/TimePicker.test.tsx | 24 +- public/app/features/explore/TimePicker.tsx | 44 +- .../app/features/profile/state/selectors.ts | 3 +- .../__snapshots__/TeamMemberRow.test.tsx.snap | 2 + .../grafana/specs/datasource.test.ts | 4 +- .../PromQueryEditor.test.tsx.snap | 2 + public/app/plugins/panel/graph/graph.ts | 4 +- public/app/types/user.ts | 4 +- public/sass/_variables.dark.generated.scss | 6 + public/sass/_variables.light.generated.scss | 7 + public/sass/components/_navbar.scss | 2 +- public/sass/components/_panel_graph.scss | 2 +- public/sass/components/_panel_logs.scss | 2 +- public/test/specs/helpers.ts | 2 +- public/vendor/flot/jquery.flot.pie.js | 818 ------------------ 47 files changed, 724 insertions(+), 1809 deletions(-) delete mode 100644 packages/grafana-ui/src/components/TimePicker/TimePickerOptionGroup.story.tsx delete mode 100644 packages/grafana-ui/src/components/TimePicker/TimePickerOptionGroup.tsx diff --git a/packages/grafana-ui/src/components/Select/ButtonSelect.tsx b/packages/grafana-ui/src/components/Select/ButtonSelect.tsx index 5ed410067078b..8bc80ca49dfa2 100644 --- a/packages/grafana-ui/src/components/Select/ButtonSelect.tsx +++ b/packages/grafana-ui/src/components/Select/ButtonSelect.tsx @@ -1,9 +1,9 @@ -import React, { PureComponent } from 'react'; +import React, { PureComponent, ReactElement } from 'react'; import Select, { SelectOptionItem } from './Select'; import { PopperContent } from '../Tooltip/PopperController'; interface ButtonComponentProps { - label: string | undefined; + label: ReactElement | string | undefined; className: string | undefined; iconClass?: string; } @@ -21,7 +21,8 @@ const ButtonComponent = (buttonProps: ButtonComponentProps) => (props: any) => {
{iconClass && } {label ? label : ''} - + {!props.menuIsOpen && } + {props.menuIsOpen && }
); @@ -30,8 +31,8 @@ const ButtonComponent = (buttonProps: ButtonComponentProps) => (props: any) => { export interface Props { className: string | undefined; options: Array>; - value: SelectOptionItem; - label?: string; + value?: SelectOptionItem; + label?: ReactElement | string; iconClass?: string; components?: any; maxMenuHeight?: number; @@ -40,6 +41,7 @@ export interface Props { isMenuOpen?: boolean; onOpenMenu?: () => void; onCloseMenu?: () => void; + tabSelectsValue?: boolean; } export class ButtonSelect extends PureComponent> { @@ -61,6 +63,7 @@ export class ButtonSelect extends PureComponent> { isMenuOpen, onOpenMenu, onCloseMenu, + tabSelectsValue, } = this.props; const combinedComponents = { ...components, @@ -75,13 +78,14 @@ export class ButtonSelect extends PureComponent> { options={options} onChange={this.onChange} value={value} + isOpen={isMenuOpen} + onOpenMenu={onOpenMenu} + onCloseMenu={onCloseMenu} maxMenuHeight={maxMenuHeight} components={combinedComponents} className="gf-form-select-box-button-select" tooltipContent={tooltipContent} - isOpen={isMenuOpen} - onOpenMenu={onOpenMenu} - onCloseMenu={onCloseMenu} + tabSelectsValue={tabSelectsValue} /> ); } diff --git a/packages/grafana-ui/src/components/Select/Select.tsx b/packages/grafana-ui/src/components/Select/Select.tsx index d2c784ca88394..7463ed6d8c2c6 100644 --- a/packages/grafana-ui/src/components/Select/Select.tsx +++ b/packages/grafana-ui/src/components/Select/Select.tsx @@ -53,6 +53,7 @@ export interface CommonProps { tooltipContent?: PopperContent; onOpenMenu?: () => void; onCloseMenu?: () => void; + tabSelectsValue?: boolean; } export interface SelectProps extends CommonProps { @@ -65,26 +66,6 @@ interface AsyncProps extends CommonProps { loadingMessage?: () => string; } -const wrapInTooltip = ( - component: React.ReactElement, - tooltipContent: PopperContent | undefined, - isMenuOpen: boolean | undefined -) => { - const showTooltip = isMenuOpen ? false : undefined; - if (tooltipContent) { - return ( - -
- {/* div needed for tooltip */} - {component} -
-
- ); - } else { - return
{component}
; - } -}; - export const MenuList = (props: any) => { return ( @@ -107,6 +88,7 @@ export class Select extends PureComponent> { isLoading: false, backspaceRemovesValue: true, maxMenuHeight: 300, + tabSelectsValue: true, components: { Option: SelectOption, SingleValue, @@ -116,20 +98,6 @@ export class Select extends PureComponent> { }, }; - onOpenMenu = () => { - const { onOpenMenu } = this.props; - if (onOpenMenu) { - onOpenMenu(); - } - }; - - onCloseMenu = () => { - const { onCloseMenu } = this.props; - if (onCloseMenu) { - onCloseMenu(); - } - }; - render() { const { defaultValue, @@ -155,6 +123,9 @@ export class Select extends PureComponent> { isOpen, components, tooltipContent, + tabSelectsValue, + onCloseMenu, + onOpenMenu, } = this.props; let widthClass = ''; @@ -164,37 +135,43 @@ export class Select extends PureComponent> { const selectClassNames = classNames('gf-form-input', 'gf-form-input--form-dropdown', widthClass, className); const selectComponents = { ...Select.defaultProps.components, ...components }; - return wrapInTooltip( - , - tooltipContent, - isOpen + + return ( + + {(onOpenMenuInternal, onCloseMenuInternal) => { + return ( + + ); + }} + ); } } @@ -239,6 +216,9 @@ export class AsyncSelect extends PureComponent> { maxMenuHeight, isMulti, tooltipContent, + onCloseMenu, + onOpenMenu, + isOpen, } = this.props; let widthClass = ''; @@ -248,43 +228,105 @@ export class AsyncSelect extends PureComponent> { const selectClassNames = classNames('gf-form-input', 'gf-form-input--form-dropdown', widthClass, className); - return wrapInTooltip( - + {(onOpenMenuInternal, onCloseMenuInternal) => { + return ( + + ); }} - defaultValue={defaultValue} - value={value} - getOptionLabel={getOptionLabel} - getOptionValue={getOptionValue} - menuShouldScrollIntoView={false} - onChange={onChange} - loadOptions={loadOptions} - isLoading={isLoading} - defaultOptions={defaultOptions} - placeholder={placeholder || 'Choose'} - styles={resetSelectStyles()} - loadingMessage={loadingMessage} - noOptionsMessage={noOptionsMessage} - isDisabled={isDisabled} - isSearchable={isSearchable} - isClearable={isClearable} - autoFocus={autoFocus} - onBlur={onBlur} - openMenuOnFocus={openMenuOnFocus} - maxMenuHeight={maxMenuHeight} - isMulti={isMulti} - backspaceRemovesValue={backspaceRemovesValue} - />, - tooltipContent, - false + ); } } +export interface TooltipWrapperProps { + children: (onOpenMenu: () => void, onCloseMenu: () => void) => React.ReactNode; + onOpenMenu?: () => void; + onCloseMenu?: () => void; + isOpen?: boolean; + tooltipContent?: PopperContent; +} + +export interface TooltipWrapperState { + isOpenInternal: boolean; +} + +export class WrapInTooltip extends PureComponent { + state: TooltipWrapperState = { + isOpenInternal: false, + }; + + onOpenMenu = () => { + const { onOpenMenu } = this.props; + if (onOpenMenu) { + onOpenMenu(); + } + this.setState({ isOpenInternal: true }); + }; + + onCloseMenu = () => { + const { onCloseMenu } = this.props; + if (onCloseMenu) { + onCloseMenu(); + } + this.setState({ isOpenInternal: false }); + }; + + render() { + const { children, isOpen, tooltipContent } = this.props; + const { isOpenInternal } = this.state; + + let showTooltip: boolean | undefined = undefined; + + if (isOpenInternal || isOpen) { + showTooltip = false; + } + + if (tooltipContent) { + return ( + +
+ {/* div needed for tooltip */} + {children(this.onOpenMenu, this.onCloseMenu)} +
+
+ ); + } else { + return
{children(this.onOpenMenu, this.onCloseMenu)}
; + } + } +} + export default Select; diff --git a/packages/grafana-ui/src/components/Select/_Select.scss b/packages/grafana-ui/src/components/Select/_Select.scss index 608c69bea1344..e4b493055a165 100644 --- a/packages/grafana-ui/src/components/Select/_Select.scss +++ b/packages/grafana-ui/src/components/Select/_Select.scss @@ -53,7 +53,7 @@ $select-input-bg-disabled: $input-bg-disabled; } .gf-form-select-box__menu { - background: $input-bg; + background: $menu-dropdown-bg; box-shadow: $menu-dropdown-shadow; position: absolute; z-index: $zindex-dropdown; diff --git a/packages/grafana-ui/src/components/TimePicker/TimePicker.story.tsx b/packages/grafana-ui/src/components/TimePicker/TimePicker.story.tsx index 36c6eaf3f1116..b37e9386e69d1 100644 --- a/packages/grafana-ui/src/components/TimePicker/TimePicker.story.tsx +++ b/packages/grafana-ui/src/components/TimePicker/TimePicker.story.tsx @@ -9,168 +9,6 @@ import { TimeFragment } from '../../types/time'; import { dateTime } from '../../utils/moment_wrapper'; const TimePickerStories = storiesOf('UI/TimePicker', module); -export const popoverOptions = { - '0': [ - { - from: 'now-2d', - to: 'now', - display: 'Last 2 days', - section: 0, - active: false, - }, - { - from: 'now-7d', - to: 'now', - display: 'Last 7 days', - section: 0, - active: false, - }, - { - from: 'now-30d', - to: 'now', - display: 'Last 30 days', - section: 0, - active: false, - }, - { - from: 'now-90d', - to: 'now', - display: 'Last 90 days', - section: 0, - active: false, - }, - { - from: 'now-6M', - to: 'now', - display: 'Last 6 months', - section: 0, - active: false, - }, - { - from: 'now-1y', - to: 'now', - display: 'Last 1 year', - section: 0, - active: false, - }, - { - from: 'now-2y', - to: 'now', - display: 'Last 2 years', - section: 0, - active: false, - }, - { - from: 'now-5y', - to: 'now', - display: 'Last 5 years', - section: 0, - active: false, - }, - ], - '1': [ - { - from: 'now-1d/d', - to: 'now-1d/d', - display: 'Yesterday', - section: 1, - active: false, - }, - { - from: 'now-2d/d', - to: 'now-2d/d', - display: 'Day before yesterday', - section: 1, - active: false, - }, - { - from: 'now-7d/d', - to: 'now-7d/d', - display: 'This day last week', - section: 1, - active: false, - }, - { - from: 'now-1w/w', - to: 'now-1w/w', - display: 'Previous week', - section: 1, - active: false, - }, - { - from: 'now-1M/M', - to: 'now-1M/M', - display: 'Previous month', - section: 1, - active: false, - }, - { - from: 'now-1y/y', - to: 'now-1y/y', - display: 'Previous year', - section: 1, - active: false, - }, - ], - '2': [ - { - from: 'now/d', - to: 'now/d', - display: 'Today', - section: 2, - active: true, - }, - { - from: 'now/d', - to: 'now', - display: 'Today so far', - section: 2, - active: false, - }, - { - from: 'now/w', - to: 'now/w', - display: 'This week', - section: 2, - active: false, - }, - { - from: 'now/w', - to: 'now', - display: 'This week so far', - section: 2, - active: false, - }, - { - from: 'now/M', - to: 'now/M', - display: 'This month', - section: 2, - active: false, - }, - { - from: 'now/M', - to: 'now', - display: 'This month so far', - section: 2, - active: false, - }, - { - from: 'now/y', - to: 'now/y', - display: 'This year', - section: 2, - active: false, - }, - { - from: 'now/y', - to: 'now', - display: 'This year so far', - section: 2, - active: false, - }, - ], -}; TimePickerStories.addDecorator(withRighAlignedStory); @@ -186,20 +24,18 @@ TimePickerStories.add('default', () => { {(value, updateValue) => { return ( { action('onChange fired')(timeRange); updateValue(timeRange); diff --git a/packages/grafana-ui/src/components/TimePicker/TimePicker.tsx b/packages/grafana-ui/src/components/TimePicker/TimePicker.tsx index 5fb625df25716..454abfe4e0dd7 100644 --- a/packages/grafana-ui/src/components/TimePicker/TimePicker.tsx +++ b/packages/grafana-ui/src/components/TimePicker/TimePicker.tsx @@ -1,275 +1,142 @@ -import React, { PureComponent } from 'react'; +// Libraries +import React, { PureComponent, createRef } from 'react'; + +// Components import { ButtonSelect } from '../Select/ButtonSelect'; -import { mapTimeOptionToTimeRange, mapTimeRangeToRangeString } from './time'; -import { Props as TimePickerPopoverProps } from './TimePickerPopover'; -import { TimePickerOptionGroup } from './TimePickerOptionGroup'; -import { PopperContent } from '../Tooltip/PopperController'; -import { Timezone } from '../../utils/datemath'; -import { TimeRange, TimeOption, TimeOptions } from '../../types/time'; -import { SelectOptionItem } from '../Select/Select'; +import { Tooltip } from '../Tooltip/Tooltip'; +import { TimePickerPopover } from './TimePickerPopover'; +import { ClickOutsideWrapper } from '../ClickOutsideWrapper/ClickOutsideWrapper'; + +// Utils & Services import { isDateTime } from '../../utils/moment_wrapper'; +import * as rangeUtil from '../../utils/rangeutil'; +import { rawToTimeRange } from './time'; + +// Types +import { TimeRange, TimeOption, TimeZone, TIME_FORMAT } from '../../types/time'; +import { SelectOptionItem } from '../Select/Select'; export interface Props { value: TimeRange; - isTimezoneUtc: boolean; - popoverOptions: TimeOptions; selectOptions: TimeOption[]; - timezone?: Timezone; + timeZone?: TimeZone; onChange: (timeRange: TimeRange) => void; onMoveBackward: () => void; onMoveForward: () => void; onZoom: () => void; - tooltipContent?: PopperContent; } -const defaultSelectOptions = [ - { from: 'now-5m', to: 'now', display: 'Last 5 minutes', section: 3, active: false }, - { from: 'now-15m', to: 'now', display: 'Last 15 minutes', section: 3, active: false }, - { from: 'now-30m', to: 'now', display: 'Last 30 minutes', section: 3, active: false }, - { from: 'now-1h', to: 'now', display: 'Last 1 hour', section: 3, active: false }, - { from: 'now-3h', to: 'now', display: 'Last 3 hours', section: 3, active: false }, - { from: 'now-6h', to: 'now', display: 'Last 6 hours', section: 3, active: false }, - { from: 'now-12h', to: 'now', display: 'Last 12 hours', section: 3, active: false }, - { from: 'now-24h', to: 'now', display: 'Last 24 hours', section: 3, active: false }, +export const defaultSelectOptions: TimeOption[] = [ + { from: 'now-5m', to: 'now', display: 'Last 5 minutes', section: 3 }, + { from: 'now-15m', to: 'now', display: 'Last 15 minutes', section: 3 }, + { from: 'now-30m', to: 'now', display: 'Last 30 minutes', section: 3 }, + { from: 'now-1h', to: 'now', display: 'Last 1 hour', section: 3 }, + { from: 'now-3h', to: 'now', display: 'Last 3 hours', section: 3 }, + { from: 'now-6h', to: 'now', display: 'Last 6 hours', section: 3 }, + { from: 'now-12h', to: 'now', display: 'Last 12 hours', section: 3 }, + { from: 'now-24h', to: 'now', display: 'Last 24 hours', section: 3 }, + { from: 'now-2d', to: 'now', display: 'Last 2 days', section: 3 }, + { from: 'now-7d', to: 'now', display: 'Last 7 days', section: 3 }, + { from: 'now-30d', to: 'now', display: 'Last 30 days', section: 3 }, + { from: 'now-90d', to: 'now', display: 'Last 90 days', section: 3 }, + { from: 'now-6M', to: 'now', display: 'Last 6 months', section: 3 }, + { from: 'now-1y', to: 'now', display: 'Last 1 year', section: 3 }, + { from: 'now-2y', to: 'now', display: 'Last 2 years', section: 3 }, + { from: 'now-5y', to: 'now', display: 'Last 5 years', section: 3 }, + { from: 'now-1d/d', to: 'now-1d/d', display: 'Yesterday', section: 3 }, + { from: 'now-2d/d', to: 'now-2d/d', display: 'Day before yesterday', section: 3 }, + { from: 'now-7d/d', to: 'now-7d/d', display: 'This day last week', section: 3 }, + { from: 'now-1w/w', to: 'now-1w/w', display: 'Previous week', section: 3 }, + { from: 'now-1M/M', to: 'now-1M/M', display: 'Previous month', section: 3 }, + { from: 'now-1y/y', to: 'now-1y/y', display: 'Previous year', section: 3 }, + { from: 'now/d', to: 'now/d', display: 'Today', section: 3 }, + { from: 'now/d', to: 'now', display: 'Today so far', section: 3 }, + { from: 'now/w', to: 'now/w', display: 'This week', section: 3 }, + { from: 'now/w', to: 'now', display: 'This week so far', section: 3 }, + { from: 'now/M', to: 'now/M', display: 'This month', section: 3 }, + { from: 'now/M', to: 'now', display: 'This month so far', section: 3 }, + { from: 'now/y', to: 'now/y', display: 'This year', section: 3 }, + { from: 'now/y', to: 'now', display: 'This year so far', section: 3 }, ]; -const defaultPopoverOptions = { - '0': [ - { - from: 'now-2d', - to: 'now', - display: 'Last 2 days', - section: 0, - active: false, - }, - { - from: 'now-7d', - to: 'now', - display: 'Last 7 days', - section: 0, - active: false, - }, - { - from: 'now-30d', - to: 'now', - display: 'Last 30 days', - section: 0, - active: false, - }, - { - from: 'now-90d', - to: 'now', - display: 'Last 90 days', - section: 0, - active: false, - }, - { - from: 'now-6M', - to: 'now', - display: 'Last 6 months', - section: 0, - active: false, - }, - { - from: 'now-1y', - to: 'now', - display: 'Last 1 year', - section: 0, - active: false, - }, - { - from: 'now-2y', - to: 'now', - display: 'Last 2 years', - section: 0, - active: false, - }, - { - from: 'now-5y', - to: 'now', - display: 'Last 5 years', - section: 0, - active: false, - }, - ], - '1': [ - { - from: 'now-1d/d', - to: 'now-1d/d', - display: 'Yesterday', - section: 1, - active: false, - }, - { - from: 'now-2d/d', - to: 'now-2d/d', - display: 'Day before yesterday', - section: 1, - active: false, - }, - { - from: 'now-7d/d', - to: 'now-7d/d', - display: 'This day last week', - section: 1, - active: false, - }, - { - from: 'now-1w/w', - to: 'now-1w/w', - display: 'Previous week', - section: 1, - active: false, - }, - { - from: 'now-1M/M', - to: 'now-1M/M', - display: 'Previous month', - section: 1, - active: false, - }, - { - from: 'now-1y/y', - to: 'now-1y/y', - display: 'Previous year', - section: 1, - active: false, - }, - ], - '2': [ - { - from: 'now/d', - to: 'now/d', - display: 'Today', - section: 2, - active: true, - }, - { - from: 'now/d', - to: 'now', - display: 'Today so far', - section: 2, - active: false, - }, - { - from: 'now/w', - to: 'now/w', - display: 'This week', - section: 2, - active: false, - }, - { - from: 'now/w', - to: 'now', - display: 'This week so far', - section: 2, - active: false, - }, - { - from: 'now/M', - to: 'now/M', - display: 'This month', - section: 2, - active: false, - }, - { - from: 'now/M', - to: 'now', - display: 'This month so far', - section: 2, - active: false, - }, - { - from: 'now/y', - to: 'now/y', - display: 'This year', - section: 2, - active: false, - }, - { - from: 'now/y', - to: 'now', - display: 'This year so far', - section: 2, - active: false, - }, - ], +const defaultZoomOutTooltip = () => { + return ( + <> + Time range zoom out
CTRL+Z + + ); }; export interface State { - isMenuOpen: boolean; + isCustomOpen: boolean; } - export class TimePicker extends PureComponent { - static defaultSelectOptions = defaultSelectOptions; - static defaultPopoverOptions = defaultPopoverOptions; + pickerTriggerRef = createRef(); + state: State = { - isMenuOpen: false, + isCustomOpen: false, }; mapTimeOptionsToSelectOptionItems = (selectOptions: TimeOption[]) => { - const { value, popoverOptions, isTimezoneUtc, timezone } = this.props; const options = selectOptions.map(timeOption => { - return { label: timeOption.display, value: timeOption }; + return { + label: timeOption.display, + value: timeOption, + }; }); - const popoverProps: TimePickerPopoverProps = { - value, - options: popoverOptions, - isTimezoneUtc, - timezone, - }; + options.unshift({ + label: 'Custom time range', + value: { from: 'custom', to: 'custom', display: 'Custom', section: 1 }, + }); - return [ - { - label: 'Custom', - expanded: true, - options, - onPopoverOpen: () => undefined, - onPopoverClose: (timeRange: TimeRange) => this.onPopoverClose(timeRange), - popoverProps, - }, - ]; + return options; }; onSelectChanged = (item: SelectOptionItem) => { - const { isTimezoneUtc, onChange, timezone } = this.props; + const { onChange, timeZone } = this.props; - // @ts-ignore - onChange(mapTimeOptionToTimeRange(item.value, isTimezoneUtc, timezone)); - }; + if (item.value && item.value.from === 'custom') { + // this is to prevent the ClickOutsideWrapper from directly closing the popover + setTimeout(() => { + this.setState({ isCustomOpen: true }); + }, 1); + return; + } - onChangeMenuOpenState = (isOpen: boolean) => { - this.setState({ - isMenuOpen: isOpen, - }); + if (item.value) { + onChange(rawToTimeRange({ from: item.value.from, to: item.value.to }, timeZone)); + } }; - onOpenMenu = () => this.onChangeMenuOpenState(true); - onCloseMenu = () => this.onChangeMenuOpenState(false); - onPopoverClose = (timeRange: TimeRange) => { + onCustomChange = (timeRange: TimeRange) => { const { onChange } = this.props; onChange(timeRange); - // Here we should also close the Select but no sure how to solve this without introducing state in this component - // Edit: State introduced - this.onCloseMenu(); + this.setState({ isCustomOpen: false }); + }; + + onCloseCustom = () => { + this.setState({ isCustomOpen: false }); }; render() { - const { - selectOptions: selectTimeOptions, - value, - onMoveBackward, - onMoveForward, - onZoom, - tooltipContent, - } = this.props; + const { selectOptions: selectTimeOptions, value, onMoveBackward, onMoveForward, onZoom, timeZone } = this.props; + const { isCustomOpen } = this.state; const options = this.mapTimeOptionsToSelectOptionItems(selectTimeOptions); - const rangeString = mapTimeRangeToRangeString(value); + const currentOption = options.find(item => isTimeOptionEqualToTimeRange(item.value, value)); + const rangeString = rangeUtil.describeTimeRange(value.raw); + + const label = ( + <> + {isCustomOpen && Custom time range} + {!isCustomOpen && {rangeString}} + {timeZone === 'utc' && UTC} + + ); const isAbsolute = isDateTime(value.raw.to); return ( -
+
{isAbsolute && ( )} - + + + + + + {isCustomOpen && ( + + + + )}
); } } + +const TimePickerTooltipContent = ({ timeRange }: { timeRange: TimeRange }) => ( + <> + {timeRange.from.format(TIME_FORMAT)} +
+ to +
+ {timeRange.to.format(TIME_FORMAT)} + +); + +function isTimeOptionEqualToTimeRange(option: TimeOption, range: TimeRange): boolean { + return range.raw.from === option.from && range.raw.to === option.to; +} diff --git a/packages/grafana-ui/src/components/TimePicker/TimePickerCalendar.story.tsx b/packages/grafana-ui/src/components/TimePicker/TimePickerCalendar.story.tsx index 5a6687d17fc50..a6ccb3baf3fff 100644 --- a/packages/grafana-ui/src/components/TimePicker/TimePickerCalendar.story.tsx +++ b/packages/grafana-ui/src/components/TimePicker/TimePickerCalendar.story.tsx @@ -16,7 +16,7 @@ TimePickerCalendarStories.add('default', () => ( {(value, updateValue) => { return ( { action('onChange fired')(timeRange); diff --git a/packages/grafana-ui/src/components/TimePicker/TimePickerCalendar.tsx b/packages/grafana-ui/src/components/TimePicker/TimePickerCalendar.tsx index 84dcbc90b660c..f0cd06e3595cb 100644 --- a/packages/grafana-ui/src/components/TimePicker/TimePickerCalendar.tsx +++ b/packages/grafana-ui/src/components/TimePicker/TimePickerCalendar.tsx @@ -1,45 +1,51 @@ import React, { PureComponent } from 'react'; import Calendar from 'react-calendar/dist/entry.nostyle'; -import { TimeFragment } from '../../types/time'; -import { Timezone } from '../../utils/datemath'; -import { DateTime, dateTime, isDateTime } from '../../utils/moment_wrapper'; - +import { TimeFragment, TimeZone, TIME_FORMAT } from '../../types/time'; +import { DateTime, dateTime, toUtc } from '../../utils/moment_wrapper'; import { stringToDateTimeType } from './time'; export interface Props { value: TimeFragment; - isTimezoneUtc: boolean; roundup?: boolean; - timezone?: Timezone; + timeZone?: TimeZone; onChange: (value: DateTime) => void; } export class TimePickerCalendar extends PureComponent { onCalendarChange = (date: Date | Date[]) => { - const { onChange } = this.props; + const { onChange, timeZone } = this.props; if (Array.isArray(date)) { return; } - onChange(dateTime(date)); + let newDate = dateTime(date); + + if (timeZone === 'utc') { + newDate = toUtc(newDate.format(TIME_FORMAT)); + } + + onChange(newDate); }; render() { - const { value, isTimezoneUtc, roundup, timezone } = this.props; - const dateValue = isDateTime(value) - ? value.toDate() - : stringToDateTimeType(value, isTimezoneUtc, roundup, timezone).toDate(); - const calendarValue = dateValue instanceof Date && !isNaN(dateValue.getTime()) ? dateValue : dateTime().toDate(); + const { value, roundup, timeZone } = this.props; + let date = stringToDateTimeType(value, roundup, timeZone); + + if (!date.isValid()) { + date = dateTime(); + } return ( } + prevLabel={} /> ); } diff --git a/packages/grafana-ui/src/components/TimePicker/TimePickerInput.tsx b/packages/grafana-ui/src/components/TimePicker/TimePickerInput.tsx index 26eaf2a18f759..a291aa5fa7379 100644 --- a/packages/grafana-ui/src/components/TimePicker/TimePickerInput.tsx +++ b/packages/grafana-ui/src/components/TimePicker/TimePickerInput.tsx @@ -1,27 +1,27 @@ import React, { PureComponent, ChangeEvent } from 'react'; -import { TimeFragment, TIME_FORMAT } from '../../types/time'; +import { TimeFragment, TIME_FORMAT, TimeZone } from '../../types/time'; import { Input } from '../Input/Input'; import { stringToDateTimeType, isValidTimeString } from './time'; import { isDateTime } from '../../utils/moment_wrapper'; export interface Props { value: TimeFragment; - isTimezoneUtc: boolean; roundup?: boolean; - timezone?: string; + timeZone?: TimeZone; onChange: (value: string, isValid: boolean) => void; + tabIndex?: number; } export class TimePickerInput extends PureComponent { isValid = (value: string) => { - const { isTimezoneUtc } = this.props; + const { timeZone, roundup } = this.props; if (value.indexOf('now') !== -1) { const isValid = isValidTimeString(value); return isValid; } - const parsed = stringToDateTimeType(value, isTimezoneUtc); + const parsed = stringToDateTimeType(value, roundup, timeZone); const isValid = parsed.isValid(); return isValid; }; @@ -42,7 +42,7 @@ export class TimePickerInput extends PureComponent { }; render() { - const { value } = this.props; + const { value, tabIndex } = this.props; const valueString = this.valueToString(value); const error = !this.isValid(valueString); @@ -54,6 +54,7 @@ export class TimePickerInput extends PureComponent { hideErrorMessage={true} value={valueString} className={`time-picker-input${error ? '-error' : ''}`} + tabIndex={tabIndex} /> ); } diff --git a/packages/grafana-ui/src/components/TimePicker/TimePickerOptionGroup.story.tsx b/packages/grafana-ui/src/components/TimePicker/TimePickerOptionGroup.story.tsx deleted file mode 100644 index 795c536ece25b..0000000000000 --- a/packages/grafana-ui/src/components/TimePicker/TimePickerOptionGroup.story.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React, { ComponentType } from 'react'; -import { storiesOf } from '@storybook/react'; -import { action } from '@storybook/addon-actions'; - -import { TimePickerOptionGroup } from './TimePickerOptionGroup'; -import { TimeRange } from '../../types/time'; -import { withRighAlignedStory } from '../../utils/storybook/withRightAlignedStory'; -import { popoverOptions } from './TimePicker.story'; -import { dateTime } from '../../utils/moment_wrapper'; - -const TimePickerOptionGroupStories = storiesOf('UI/TimePicker/TimePickerOptionGroup', module); - -TimePickerOptionGroupStories.addDecorator(withRighAlignedStory); - -const data = { - isPopoverOpen: false, - onPopoverOpen: () => { - action('onPopoverOpen fired')(); - }, - onPopoverClose: (timeRange: TimeRange) => { - action('onPopoverClose fired')(timeRange); - }, - popoverProps: { - value: { from: dateTime(), to: dateTime(), raw: { from: 'now/d', to: 'now/d' } }, - options: popoverOptions, - isTimezoneUtc: false, - onChange: (timeRange: TimeRange) => { - action('onChange fired')(timeRange); - }, - }, -}; - -TimePickerOptionGroupStories.add('default', () => ( - {}} - className={''} - cx={() => {}} - getStyles={(name, props) => ({})} - getValue={() => {}} - hasValue - isMulti={false} - options={[]} - selectOption={() => {}} - selectProps={''} - setValue={(value, action) => {}} - label={'Custom'} - children={null} - Heading={(null as any) as ComponentType} - data={data} - /> -)); diff --git a/packages/grafana-ui/src/components/TimePicker/TimePickerOptionGroup.tsx b/packages/grafana-ui/src/components/TimePicker/TimePickerOptionGroup.tsx deleted file mode 100644 index 017668b40569e..0000000000000 --- a/packages/grafana-ui/src/components/TimePicker/TimePickerOptionGroup.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import React, { PureComponent, createRef } from 'react'; -import { GroupProps } from 'react-select/lib/components/Group'; -import { Props as TimePickerProps, TimePickerPopover } from './TimePickerPopover'; -import { TimeRange } from '../../types/time'; -import { Popper } from '../Tooltip/Popper'; - -export interface DataProps { - onPopoverOpen: () => void; - onPopoverClose: (timeRange: TimeRange) => void; - popoverProps: TimePickerProps; -} - -interface Props extends GroupProps { - data: DataProps; -} - -interface State { - isPopoverOpen: boolean; -} - -export class TimePickerOptionGroup extends PureComponent { - pickerTriggerRef = createRef(); - state: State = { isPopoverOpen: false }; - - onClick = () => { - this.setState({ isPopoverOpen: true }); - this.props.data.onPopoverOpen(); - }; - - render() { - const { children, label } = this.props; - const { isPopoverOpen } = this.state; - const { onPopoverClose } = this.props.data; - const popover = TimePickerPopover; - const popoverElement = React.createElement(popover, { - ...this.props.data.popoverProps, - onChange: (timeRange: TimeRange) => { - onPopoverClose(timeRange); - this.setState({ isPopoverOpen: false }); - }, - }); - - return ( - <> -
-
- {label} - -
- {children} -
-
- {this.pickerTriggerRef.current && ( - - )} -
- - ); - } -} diff --git a/packages/grafana-ui/src/components/TimePicker/TimePickerPopover.story.tsx b/packages/grafana-ui/src/components/TimePicker/TimePickerPopover.story.tsx index 03dfaa1fb63f1..e7a397cee8405 100644 --- a/packages/grafana-ui/src/components/TimePicker/TimePickerPopover.story.tsx +++ b/packages/grafana-ui/src/components/TimePicker/TimePickerPopover.story.tsx @@ -5,7 +5,6 @@ import { storiesOf } from '@storybook/react'; import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; import { TimePickerPopover } from './TimePickerPopover'; import { UseState } from '../../utils/storybook/UseState'; -import { popoverOptions } from './TimePicker.story'; import { dateTime, DateTime } from '../../utils/moment_wrapper'; const TimePickerPopoverStories = storiesOf('UI/TimePicker/TimePickerPopover', module); @@ -24,12 +23,11 @@ TimePickerPopoverStories.add('default', () => ( return ( { action('onChange fired')(timeRange); updateValue(timeRange); }} - options={popoverOptions} /> ); }} diff --git a/packages/grafana-ui/src/components/TimePicker/TimePickerPopover.tsx b/packages/grafana-ui/src/components/TimePicker/TimePickerPopover.tsx index 37c05863a03a2..0f255df51b0e1 100644 --- a/packages/grafana-ui/src/components/TimePicker/TimePickerPopover.tsx +++ b/packages/grafana-ui/src/components/TimePicker/TimePickerPopover.tsx @@ -1,167 +1,118 @@ -import React, { Component, SyntheticEvent } from 'react'; -import { TimeRange, TimeOptions, TimeOption } from '../../types/time'; +// Libraries +import React, { Component } from 'react'; +// Components import { TimePickerCalendar } from './TimePickerCalendar'; import { TimePickerInput } from './TimePickerInput'; -import { mapTimeOptionToTimeRange } from './time'; -import { Timezone } from '../../utils/datemath'; +import { rawToTimeRange } from './time'; + +// Types import { DateTime } from '../../utils/moment_wrapper'; +import { TimeRange, TimeZone } from '../../types/time'; export interface Props { value: TimeRange; - options: TimeOptions; - isTimezoneUtc: boolean; - timezone?: Timezone; - onChange?: (timeRange: TimeRange) => void; + timeZone?: TimeZone; + onChange: (timeRange: TimeRange) => void; } export interface State { - value: TimeRange; + from: DateTime | string; + to: DateTime | string; isFromInputValid: boolean; isToInputValid: boolean; } export class TimePickerPopover extends Component { static popoverClassName = 'time-picker-popover'; + constructor(props: Props) { super(props); - this.state = { value: props.value, isFromInputValid: true, isToInputValid: true }; + + this.state = { + from: props.value.raw.from, + to: props.value.raw.to, + isFromInputValid: true, + isToInputValid: true, + }; } onFromInputChanged = (value: string, valid: boolean) => { - this.setState({ - value: { ...this.state.value, raw: { ...this.state.value.raw, from: value } }, - isFromInputValid: valid, - }); + this.setState({ from: value, isFromInputValid: valid }); }; onToInputChanged = (value: string, valid: boolean) => { - this.setState({ - value: { ...this.state.value, raw: { ...this.state.value.raw, to: value } }, - isToInputValid: valid, - }); + this.setState({ to: value, isToInputValid: valid }); }; onFromCalendarChanged = (value: DateTime) => { - this.setState({ - value: { ...this.state.value, raw: { ...this.state.value.raw, from: value } }, - }); + this.setState({ from: value }); }; onToCalendarChanged = (value: DateTime) => { - this.setState({ - value: { ...this.state.value, raw: { ...this.state.value.raw, to: value } }, - }); - }; - - onTimeOptionClick = (timeOption: TimeOption) => { - const { isTimezoneUtc, timezone, onChange } = this.props; - - if (onChange) { - onChange(mapTimeOptionToTimeRange(timeOption, isTimezoneUtc, timezone)); - } + this.setState({ to: value }); }; onApplyClick = () => { - const { onChange } = this.props; - if (onChange) { - onChange(this.state.value); - } + const { onChange, timeZone } = this.props; + const { from, to } = this.state; + + onChange(rawToTimeRange({ from, to }, timeZone)); }; render() { - const { options, isTimezoneUtc, timezone } = this.props; - const { isFromInputValid, isToInputValid, value } = this.state; + const { timeZone } = this.props; + const { isFromInputValid, isToInputValid, from, to } = this.state; + const isValid = isFromInputValid && isToInputValid; return (
-
-
- Quick ranges -
-
- {Object.keys(options).map(key => { - return ( - - ); - })} -
-
-
-
- Custom range -
-
-
-
- From: +
+
+
+
+
-
- -
-
-
- To: +
+ +
+
+
+
+
+ -
-
-
+
+ +
-
- -
+
+
+
); diff --git a/packages/grafana-ui/src/components/TimePicker/_TimePicker.scss b/packages/grafana-ui/src/components/TimePicker/_TimePicker.scss index ffbbd009d7c5c..398cac9925c0f 100644 --- a/packages/grafana-ui/src/components/TimePicker/_TimePicker.scss +++ b/packages/grafana-ui/src/components/TimePicker/_TimePicker.scss @@ -6,119 +6,158 @@ display: flex; } } + .time-picker-popover-popper { z-index: $zindex-timepicker-popover; } +.time-picker-utc { + color: $orange; + font-size: 75%; + padding: 3px; + font-weight: 500; + margin-left: 4px; + position: relative; +} + .time-picker-popover { display: flex; flex-flow: row nowrap; justify-content: space-around; border: 1px solid $popover-border-color; border-radius: $border-radius; - background-color: $popover-border-color; + background: $popover-bg; color: $popover-color; - - .time-picker-popover-box { - max-width: 500px; - padding: 20px; - - ul { - padding-right: $spacer; - padding-top: $spacer; - list-style-type: none; - - li { - line-height: 22px; - display: list-item; - text-align: left; - } - - li.active { - border-bottom: 1px solid $blue; - font-weight: $font-weight-semi-bold; - } - } - - .time-picker-popover-box-body { - display: flex; - flex-flow: row nowrap; - justify-content: space-around; - } + box-shadow: $popover-shadow; + position: absolute; + flex-direction: column; + max-width: 600px; + top: 48px; + right: 20px; + + .time-picker-popover-body { + display: flex; + flex-flow: row nowrap; + justify-content: space-around; + padding: $space-md; + padding-bottom: 0; } - .time-picker-popover-box-title { - font-size: $font-size-lg; + .time-picker-popover-title { + font-size: $font-size-md; font-weight: $font-weight-semi-bold; } - .time-picker-popover-box:first-child { - border-right: 1px ridge; - } - - .time-picker-popover-box-body-custom-ranges:first-child { - margin-right: $spacer; + .time-picker-popover-body-custom-ranges:first-child { + margin-right: $space-md; } - .time-picker-popover-box-body-custom-ranges-input { + .time-picker-popover-body-custom-ranges-input { display: flex; flex-flow: row nowrap; align-items: center; - margin: $spacer 0; + margin-bottom: $space-sm; - .our-custom-wrapper-class { - margin-left: $spacer; - width: 100%; - - .time-picker-input-error { - box-shadow: inset 0 0px 5px $red; - } + .time-picker-input-error { + box-shadow: inset 0 0px 5px $red; } } - .time-picker-popover-box-footer { + .time-picker-popover-footer { display: flex; flex-flow: row nowrap; - justify-content: flex-end; - margin-top: $spacer; + justify-content: center; + padding: $space-md; } } +.time-picker-popover-header { + background: $popover-header-bg; + padding: $space-sm; +} + +.time-picker-input { + max-width: 170px; +} + +.react-calendar__navigation__label { + line-height: 31px; + padding-bottom: 0; +} + +.react-calendar__navigation__arrow { + font-size: $font-size-lg; +} + +$arrowPaddingToBorder: 7px; +$arrowPadding: $arrowPaddingToBorder * 3; + +.react-calendar__navigation__next-button { + padding-left: $arrowPadding; + padding-right: $arrowPaddingToBorder; +} + +.react-calendar__navigation__prev-button { + padding-left: $arrowPaddingToBorder; + padding-right: $arrowPadding; +} + +.react-calendar__month-view__days__day--neighboringMonth abbr { + opacity: 0.35; +} + +.react-calendar__month-view__days { + padding: 4px; +} + .time-picker-calendar { border: 1px solid $popover-border-color; - max-width: 220px; color: $black; .react-calendar__navigation__label, .react-calendar__navigation__arrow, .react-calendar__navigation { color: $input-color; - background-color: $input-bg; + background-color: $input-label-bg; border: 0; } .react-calendar__month-view__weekdays { - background-color: $popover-border-color; + background-color: $input-bg; text-align: center; abbr { border: 0; text-decoration: none; cursor: default; - color: $popover-color; + color: $orange; font-weight: $font-weight-semi-bold; + display: block; + padding: 4px 0 0 0; } } .time-picker-calendar-tile { - color: $input-color; - background-color: $input-bg; - border: 0; - line-height: 22px; + color: $text-color; + background-color: inherit; + line-height: 26px; + font-size: $font-size-md; + border: 1px solid transparent; + border-radius: $border-radius; + + &:hover { + box-shadow: $panel-editor-viz-item-shadow-hover; + background: $panel-editor-viz-item-bg-hover; + border: $panel-editor-viz-item-border-hover; + color: $text-color-strong; + } } - button.time-picker-calendar-tile:hover { - font-weight: $font-weight-semi-bold; + .react-calendar__month-view__days { + background-color: $calendar-bg-days; + } + .react-calendar__tile--now { + background-color: $calendar-bg-now; } .react-calendar__navigation__label, @@ -128,47 +167,46 @@ } .react-calendar__tile--now { - color: $orange; + border-radius: $border-radius; } - .react-calendar__tile--active { - color: $blue; + .react-calendar__tile--active, + .react-calendar__tile--active:hover { + color: $white; font-weight: $font-weight-semi-bold; + background: linear-gradient(0deg, $blue-base, $blue-shade); + box-shadow: none; + border: 1px solid transparent; } } -@media only screen and (max-width: 1116px) { +.time-picker-popover-custom-range-label { + padding-right: $space-xs; +} + +@include media-breakpoint-down(md) { .time-picker-popover { margin-left: $spacer; display: flex; flex-flow: column nowrap; + max-width: 400px; - .time-picker-popover-box { - padding: $spacer / 2 $spacer; - - .time-picker-popover-box-title { - font-size: $font-size-md; - font-weight: $font-weight-semi-bold; - } + .time-picker-popover-title { + font-size: $font-size-md; } - .time-picker-popover-box:first-child { - border-right: none; - border-bottom: 1px ridge; + .time-picker-popover-body { + padding: $space-sm; + display: flex; + flex-flow: column nowrap; } - .time-picker-popover-box:last-child { - .time-picker-popover-box-body { - display: flex; - flex-flow: column nowrap; - - .time-picker-popover-box-body-custom-ranges:first-child { - margin: 0; - } - } + .time-picker-popover-body-custom-ranges:first-child { + margin-right: 0; + margin-bottom: $space-sm; } - .time-picker-popover-box-footer { + .time-picker-popover-footer { display: flex; flex-flow: row nowrap; justify-content: flex-end; @@ -177,13 +215,6 @@ } .time-picker-calendar { - max-width: 500px; width: 100%; } } - -@media only screen and (max-width: 746px) { - .time-picker-popover { - margin-top: 48px; - } -} diff --git a/packages/grafana-ui/src/components/TimePicker/time.ts b/packages/grafana-ui/src/components/TimePicker/time.ts index f697456eeb378..c794bf406dca2 100644 --- a/packages/grafana-ui/src/components/TimePicker/time.ts +++ b/packages/grafana-ui/src/components/TimePicker/time.ts @@ -1,43 +1,38 @@ -import { TimeOption, TimeRange, TIME_FORMAT } from '../../types/time'; +import { TimeRange, TIME_FORMAT, RawTimeRange, TimeZone } from '../../types/time'; import { describeTimeRange } from '../../utils/rangeutil'; import * as dateMath from '../../utils/datemath'; -import { dateTime, DateTime, toUtc } from '../../utils/moment_wrapper'; +import { isDateTime, dateTime, DateTime, toUtc } from '../../utils/moment_wrapper'; -export const mapTimeOptionToTimeRange = ( - timeOption: TimeOption, - isTimezoneUtc: boolean, - timezone?: dateMath.Timezone -): TimeRange => { - const fromMoment = stringToDateTimeType(timeOption.from, isTimezoneUtc, false, timezone); - const toMoment = stringToDateTimeType(timeOption.to, isTimezoneUtc, true, timezone); +export const rawToTimeRange = (raw: RawTimeRange, timeZone?: TimeZone): TimeRange => { + const from = stringToDateTimeType(raw.from, false, timeZone); + const to = stringToDateTimeType(raw.to, true, timeZone); - return { from: fromMoment, to: toMoment, raw: { from: timeOption.from, to: timeOption.to } }; + return { from, to, raw }; }; -export const stringToDateTimeType = ( - value: string, - isTimezoneUtc: boolean, - roundUp?: boolean, - timezone?: dateMath.Timezone -): DateTime => { +export const stringToDateTimeType = (value: string | DateTime, roundUp?: boolean, timeZone?: TimeZone): DateTime => { + if (isDateTime(value)) { + return value; + } + if (value.indexOf('now') !== -1) { if (!dateMath.isValid(value)) { return dateTime(); } - const parsed = dateMath.parse(value, roundUp, timezone); + const parsed = dateMath.parse(value, roundUp, timeZone); return parsed || dateTime(); } - if (isTimezoneUtc) { + if (timeZone === 'utc') { return toUtc(value, TIME_FORMAT); } return dateTime(value, TIME_FORMAT); }; -export const mapTimeRangeToRangeString = (timeRange: TimeRange): string => { - return describeTimeRange(timeRange.raw); +export const mapTimeRangeToRangeString = (timeRange: RawTimeRange): string => { + return describeTimeRange(timeRange); }; export const isValidTimeString = (text: string) => dateMath.isValid(text); diff --git a/packages/grafana-ui/src/components/Tooltip/Popper.tsx b/packages/grafana-ui/src/components/Tooltip/Popper.tsx index e439ecf043711..84b92d8233a08 100644 --- a/packages/grafana-ui/src/components/Tooltip/Popper.tsx +++ b/packages/grafana-ui/src/components/Tooltip/Popper.tsx @@ -26,9 +26,14 @@ interface Props extends React.HTMLAttributes { referenceElement: PopperJS.ReferenceObject; wrapperClassName?: string; renderArrow?: RenderPopperArrowFn; + eventsEnabled?: boolean; } class Popper extends PureComponent { + static defaultProps: Partial = { + eventsEnabled: true, + }; + render() { const { content, @@ -39,6 +44,8 @@ class Popper extends PureComponent { className, wrapperClassName, renderArrow, + referenceElement, + eventsEnabled, } = this.props; return ( @@ -49,7 +56,8 @@ class Popper extends PureComponent { diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 860dd5a97ab0e..abc4d7c2c9f39 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -33,6 +33,7 @@ export { UnitPicker } from './UnitPicker/UnitPicker'; export { StatsPicker } from './StatsPicker/StatsPicker'; export { Input, InputStatus } from './Input/Input'; export { RefreshPicker } from './RefreshPicker/RefreshPicker'; +export { TimePicker } from './TimePicker/TimePicker'; export { List } from './List/List'; // Renderless diff --git a/packages/grafana-ui/src/themes/_variables.dark.scss.tmpl.ts b/packages/grafana-ui/src/themes/_variables.dark.scss.tmpl.ts index eef3e6eedf77b..c51de3278415b 100644 --- a/packages/grafana-ui/src/themes/_variables.dark.scss.tmpl.ts +++ b/packages/grafana-ui/src/themes/_variables.dark.scss.tmpl.ts @@ -39,6 +39,7 @@ $gray-2: ${theme.colors.gray2}; $gray-3: ${theme.colors.gray3}; $gray-4: ${theme.colors.gray4}; $gray-5: ${theme.colors.gray5}; +$gray-6: ${theme.colors.gray6}; $gray-blue: ${theme.colors.grayBlue}; $input-black: #09090b; @@ -282,6 +283,7 @@ $alert-info-bg: linear-gradient(100deg, $blue-base, $blue-shade); $popover-bg: $dark-2; $popover-color: $text-color; $popover-border-color: $dark-9; +$popover-header-bg: $dark-9; $popover-shadow: 0 0 20px black; $popover-help-bg: $btn-secondary-bg; @@ -395,4 +397,8 @@ $button-toggle-group-btn-seperator-border: 1px solid $dark-2; $vertical-resize-handle-bg: $dark-10; $vertical-resize-handle-dots: $gray-1; $vertical-resize-handle-dots-hover: $gray-2; + +// Calendar +$calendar-bg-days: $input-bg; +$calendar-bg-now: $dark-10; `; diff --git a/packages/grafana-ui/src/themes/_variables.light.scss.tmpl.ts b/packages/grafana-ui/src/themes/_variables.light.scss.tmpl.ts index 96b2936354c08..b37e91ba3f421 100644 --- a/packages/grafana-ui/src/themes/_variables.light.scss.tmpl.ts +++ b/packages/grafana-ui/src/themes/_variables.light.scss.tmpl.ts @@ -27,6 +27,8 @@ $black: ${theme.colors.black}; $dark-1: ${theme.colors.dark1}; $dark-2: ${theme.colors.dark2}; +$dark-4: ${theme.colors.dark4}; +$dark-10: ${theme.colors.dark10}; $gray-1: ${theme.colors.gray1}; $gray-2: ${theme.colors.gray2}; $gray-3: ${theme.colors.gray3}; @@ -269,6 +271,7 @@ $alert-info-bg: linear-gradient(100deg, $blue-base, $blue-shade); $popover-bg: $page-bg; $popover-color: $text-color; $popover-border-color: $gray-5; +$popover-header-bg: $gray-5; $popover-shadow: 0 0 20px $white; $popover-help-bg: $btn-secondary-bg; @@ -382,4 +385,8 @@ $button-toggle-group-btn-seperator-border: 1px solid $gray-6; $vertical-resize-handle-bg: $gray-4; $vertical-resize-handle-dots: $gray-3; $vertical-resize-handle-dots-hover: $gray-2; + +// Calendar +$calendar-bg-days: $white; +$calendar-bg-now: $gray-6; `; diff --git a/packages/grafana-ui/src/types/time.ts b/packages/grafana-ui/src/types/time.ts index c7120776facf6..ae8137277f386 100644 --- a/packages/grafana-ui/src/types/time.ts +++ b/packages/grafana-ui/src/types/time.ts @@ -21,26 +21,17 @@ export interface IntervalValues { intervalMs: number; } -export interface TimeZone { - raw: string; - isUtc: boolean; -} - -export const parseTimeZone = (raw: string): TimeZone => { - return { - raw, - isUtc: raw === 'utc', - }; -}; +export type TimeZoneUtc = 'utc'; +export type TimeZoneBrowser = 'browser'; +export type TimeZone = TimeZoneBrowser | TimeZoneUtc | string; -export const DefaultTimeZone = parseTimeZone('browser'); +export const DefaultTimeZone: TimeZone = 'browser'; export interface TimeOption { from: string; to: string; display: string; section: number; - active: boolean; } export interface TimeOptions { diff --git a/packages/grafana-ui/src/utils/datemath.ts b/packages/grafana-ui/src/utils/datemath.ts index 5fde248313f49..bd14403070bc4 100644 --- a/packages/grafana-ui/src/utils/datemath.ts +++ b/packages/grafana-ui/src/utils/datemath.ts @@ -1,11 +1,10 @@ import includes from 'lodash/includes'; import isDate from 'lodash/isDate'; import { DateTime, dateTime, toUtc, ISO_8601, isDateTime, DurationUnit } from '../utils/moment_wrapper'; +import { TimeZone } from '../types'; const units: DurationUnit[] = ['y', 'M', 'w', 'd', 'h', 'm', 's']; -export type Timezone = 'utc'; - /** * Parses different types input to a moment instance. There is a specific formatting language that can be used * if text arg is string. See unit tests for examples. @@ -13,7 +12,7 @@ export type Timezone = 'utc'; * @param roundUp See parseDateMath function. * @param timezone Only string 'utc' is acceptable here, for anything else, local timezone is used. */ -export function parse(text: string | DateTime | Date, roundUp?: boolean, timezone?: Timezone): DateTime | undefined { +export function parse(text: string | DateTime | Date, roundUp?: boolean, timezone?: TimeZone): DateTime | undefined { if (!text) { return undefined; } diff --git a/packages/grafana-ui/src/utils/index.ts b/packages/grafana-ui/src/utils/index.ts index 11eb7b72736ae..c757451dac20c 100644 --- a/packages/grafana-ui/src/utils/index.ts +++ b/packages/grafana-ui/src/utils/index.ts @@ -10,6 +10,7 @@ export * from './fieldDisplay'; export * from './deprecationWarning'; export * from './logs'; export * from './labels'; +export * from './labels'; export { getMappedValue } from './valueMappings'; export * from './validate'; export { getFlotPairs } from './flotPairs'; diff --git a/packages/grafana-ui/src/utils/rangeutil.ts b/packages/grafana-ui/src/utils/rangeutil.ts index bcc27ecfd8eae..01dc6a89cf830 100644 --- a/packages/grafana-ui/src/utils/rangeutil.ts +++ b/packages/grafana-ui/src/utils/rangeutil.ts @@ -51,7 +51,6 @@ const rangeOptions = [ { from: 'now-6h', to: 'now', display: 'Last 6 hours', section: 3 }, { from: 'now-12h', to: 'now', display: 'Last 12 hours', section: 3 }, { from: 'now-24h', to: 'now', display: 'Last 24 hours', section: 3 }, - { from: 'now-2d', to: 'now', display: 'Last 2 days', section: 0 }, { from: 'now-7d', to: 'now', display: 'Last 7 days', section: 0 }, { from: 'now-30d', to: 'now', display: 'Last 30 days', section: 0 }, @@ -62,7 +61,7 @@ const rangeOptions = [ { from: 'now-5y', to: 'now', display: 'Last 5 years', section: 0 }, ]; -const absoluteFormat = 'MMM D, YYYY HH:mm:ss'; +const absoluteFormat = 'YYYY-MM-DD HH:mm:ss'; const rangeIndex: any = {}; _.each(rangeOptions, (frame: any) => { diff --git a/public/app/core/specs/rangeutil.test.ts b/public/app/core/specs/rangeutil.test.ts index d23bbf9f0eb24..3ecdb8b6c71ef 100644 --- a/public/app/core/specs/rangeutil.test.ts +++ b/public/app/core/specs/rangeutil.test.ts @@ -72,7 +72,7 @@ describe('rangeUtil', () => { from: dateTime([2014, 10, 10, 2, 3, 4]), to: 'now', }); - expect(text).toBe('Nov 10, 2014 02:03:04 to a few seconds ago'); + expect(text).toBe('2014-11-10 02:03:04 to a few seconds ago'); }); it('Date range with absolute to relative', () => { @@ -80,7 +80,7 @@ describe('rangeUtil', () => { from: dateTime([2014, 10, 10, 2, 3, 4]), to: 'now-1d', }); - expect(text).toBe('Nov 10, 2014 02:03:04 to a day ago'); + expect(text).toBe('2014-11-10 02:03:04 to a day ago'); }); it('Date range with relative to absolute', () => { @@ -88,7 +88,7 @@ describe('rangeUtil', () => { from: 'now-7d', to: dateTime([2014, 10, 10, 2, 3, 4]), }); - expect(text).toBe('7 days ago to Nov 10, 2014 02:03:04'); + expect(text).toBe('7 days ago to 2014-11-10 02:03:04'); }); it('Date range with non matching default ranges', () => { diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index 811950a9251f1..135490e630528 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -366,8 +366,8 @@ export const getQueryKeys = (queries: DataQuery[], datasourceInstance: DataSourc export const getTimeRange = (timeZone: TimeZone, rawRange: RawTimeRange): TimeRange => { return { - from: dateMath.parse(rawRange.from, false, timeZone.raw as any), - to: dateMath.parse(rawRange.to, true, timeZone.raw as any), + from: dateMath.parse(rawRange.from, false, timeZone as any), + to: dateMath.parse(rawRange.to, true, timeZone as any), raw: rawRange, }; }; @@ -406,8 +406,8 @@ export const getTimeRangeFromUrl = (range: RawTimeRange, timeZone: TimeZone): Ti }; return { - from: dateMath.parse(raw.from, false, timeZone.raw as any), - to: dateMath.parse(raw.to, true, timeZone.raw as any), + from: dateMath.parse(raw.from, false, timeZone as any), + to: dateMath.parse(raw.to, true, timeZone as any), raw, }; }; diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx index 8db88e9ba55e6..89f28fe40413c 100644 --- a/public/app/features/dashboard/components/DashNav/DashNav.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx @@ -3,7 +3,6 @@ import React, { PureComponent } from 'react'; import { connect } from 'react-redux'; // Utils & Services -import { AngularComponent, getAngularLoader } from '@grafana/runtime'; import { appEvents } from 'app/core/app_events'; import { PlaylistSrv } from 'app/features/playlist/playlist_srv'; @@ -36,8 +35,6 @@ export interface StateProps { type Props = StateProps & OwnProps; export class DashNav extends PureComponent { - timePickerEl: HTMLElement; - timepickerCmp: AngularComponent; playlistSrv: PlaylistSrv; constructor(props: Props) { @@ -45,21 +42,6 @@ export class DashNav extends PureComponent { this.playlistSrv = this.props.$injector.get('playlistSrv'); } - componentDidMount() { - const loader = getAngularLoader(); - const template = - ''; - const scopeProps = { dashboard: this.props.dashboard }; - - this.timepickerCmp = loader.load(this.timePickerEl, scopeProps, template); - } - - componentWillUnmount() { - if (this.timepickerCmp) { - this.timepickerCmp.destroy(); - } - } - onDahboardNameClick = () => { appEvents.emit('show-dash-search'); }; @@ -187,7 +169,7 @@ export class DashNav extends PureComponent { } render() { - const { dashboard, onAddPanel, location } = this.props; + const { dashboard, onAddPanel, location, $injector } = this.props; const { canStar, canSave, canShare, showSettings, isStarred } = dashboard.meta; const { snapshot } = dashboard; const snapshotUrl = snapshot && snapshot.originalUrl; @@ -281,8 +263,12 @@ export class DashNav extends PureComponent { {!dashboard.timepicker.hidden && (
-
(this.timePickerEl = element)} /> - +
)}
diff --git a/public/app/features/dashboard/components/DashNav/DashNavTimeControls.tsx b/public/app/features/dashboard/components/DashNav/DashNavTimeControls.tsx index 42d3a64fc4b58..065596d73df0a 100644 --- a/public/app/features/dashboard/components/DashNav/DashNavTimeControls.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNavTimeControls.tsx @@ -1,20 +1,24 @@ // Libaries import React, { Component } from 'react'; +import { toUtc } from '@grafana/ui/src/utils/moment_wrapper'; // Types import { DashboardModel } from '../../state'; import { LocationState } from 'app/types'; +import { TimeRange, TimeOption } from '@grafana/ui'; // State import { updateLocation } from 'app/core/actions'; // Components -import { RefreshPicker } from '@grafana/ui'; +import { TimePicker, RefreshPicker, RawTimeRange } from '@grafana/ui'; // Utils & Services import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv'; +import { defaultSelectOptions } from '@grafana/ui/src/components/TimePicker/TimePicker'; export interface Props { + $injector: any; dashboard: DashboardModel; updateLocation: typeof updateLocation; location: LocationState; @@ -22,6 +26,7 @@ export interface Props { export class DashNavTimeControls extends Component { timeSrv: TimeSrv = getTimeSrv(); + $rootScope = this.props.$injector.get('$rootScope'); get refreshParamInUrl(): string { return this.props.location.query.refresh as string; @@ -37,17 +42,92 @@ export class DashNavTimeControls extends Component { return Promise.resolve(); }; + onMoveTimePicker = (direction: number) => { + const range = this.timeSrv.timeRange(); + const timespan = (range.to.valueOf() - range.from.valueOf()) / 2; + let to: number, from: number; + + if (direction === -1) { + to = range.to.valueOf() - timespan; + from = range.from.valueOf() - timespan; + } else if (direction === 1) { + to = range.to.valueOf() + timespan; + from = range.from.valueOf() + timespan; + if (to > Date.now() && range.to.valueOf() < Date.now()) { + to = Date.now(); + from = range.from.valueOf(); + } + } else { + to = range.to.valueOf(); + from = range.from.valueOf(); + } + + this.timeSrv.setTime({ + from: toUtc(from), + to: toUtc(to), + }); + }; + + onMoveForward = () => this.onMoveTimePicker(1); + onMoveBack = () => this.onMoveTimePicker(-1); + + onChangeTimePicker = (timeRange: TimeRange) => { + const { dashboard } = this.props; + const panel = dashboard.timepicker; + const hasDelay = panel.nowDelay && timeRange.raw.to === 'now'; + + const nextRange = { + from: timeRange.raw.from, + to: hasDelay ? 'now-' + panel.nowDelay : timeRange.raw.to, + }; + + this.timeSrv.setTime(nextRange); + }; + + onZoom = () => { + this.$rootScope.appEvent('zoom-out', 2); + }; + + setActiveTimeOption = (timeOptions: TimeOption[], rawTimeRange: RawTimeRange): TimeOption[] => { + return timeOptions.map(option => { + if (option.to === rawTimeRange.to && option.from === rawTimeRange.from) { + return { + ...option, + active: true, + }; + } + return { + ...option, + active: false, + }; + }); + }; + render() { const { dashboard } = this.props; const intervals = dashboard.timepicker.refresh_intervals; + const timePickerValue = this.timeSrv.timeRange(); + const timeZone = dashboard.getTimezone(); + return ( - + <> + + + ); } } diff --git a/public/app/features/dashboard/services/TimeSrv.ts b/public/app/features/dashboard/services/TimeSrv.ts index 9c415b9aea589..ca9b244b956a6 100644 --- a/public/app/features/dashboard/services/TimeSrv.ts +++ b/public/app/features/dashboard/services/TimeSrv.ts @@ -7,7 +7,7 @@ import coreModule from 'app/core/core_module'; import * as dateMath from '@grafana/ui/src/utils/datemath'; // Types -import { TimeRange, RawTimeRange } from '@grafana/ui'; +import { TimeRange, RawTimeRange, TimeZone } from '@grafana/ui'; import { ITimeoutService, ILocationService } from 'angular'; import { ContextSrv } from 'app/core/services/context_srv'; import { DashboardModel } from '../state/DashboardModel'; @@ -224,7 +224,7 @@ export class TimeSrv { to: isDateTime(this.time.to) ? dateTime(this.time.to) : this.time.to, }; - const timezone = this.dashboard && this.dashboard.getTimezone(); + const timezone: TimeZone = this.dashboard ? this.dashboard.getTimezone() : undefined; return { from: dateMath.parse(raw.from, false, timezone), diff --git a/public/app/features/dashboard/state/DashboardModel.ts b/public/app/features/dashboard/state/DashboardModel.ts index 88e92cd09df9f..ff8fe5ac8d6df 100644 --- a/public/app/features/dashboard/state/DashboardModel.ts +++ b/public/app/features/dashboard/state/DashboardModel.ts @@ -13,7 +13,7 @@ import sortByKeys from 'app/core/utils/sort_by_keys'; // Types import { PanelModel, GridPos } from './PanelModel'; import { DashboardMigrator } from './DashboardMigrator'; -import { TimeRange } from '@grafana/ui'; +import { TimeRange, TimeZone } from '@grafana/ui'; import { UrlQueryValue } from '@grafana/runtime'; import { KIOSK_MODE_TV, DashboardMeta } from 'app/types'; import { toUtc, DateTimeInput, dateTime, isDateTime } from '@grafana/ui/src/utils/moment_wrapper'; @@ -832,8 +832,8 @@ export class DashboardModel { return this.snapshot !== undefined; } - getTimezone() { - return this.timezone ? this.timezone : contextSrv.user.timezone; + getTimezone(): TimeZone { + return (this.timezone ? this.timezone : contextSrv.user.timezone) as TimeZone; } private updateSchema(old: any) { diff --git a/public/app/features/explore/ExploreToolbar.tsx b/public/app/features/explore/ExploreToolbar.tsx index 9d3cb98412088..75f8cc75b7c04 100644 --- a/public/app/features/explore/ExploreToolbar.tsx +++ b/public/app/features/explore/ExploreToolbar.tsx @@ -216,7 +216,7 @@ export class UnConnectedExploreToolbar extends PureComponent {
{!isLive && ( - + )} diff --git a/public/app/features/explore/Graph.tsx b/public/app/features/explore/Graph.tsx index b5cdca318afa6..1a65bd2397cf6 100644 --- a/public/app/features/explore/Graph.tsx +++ b/public/app/features/explore/Graph.tsx @@ -129,7 +129,7 @@ export class Graph extends PureComponent { this.$el.unbind('plotselected', this.onPlotSelected); } - onPlotSelected = (event, ranges) => { + onPlotSelected = (event: JQueryEventObject, ranges) => { const { onChangeTime } = this.props; if (onChangeTime) { this.props.onChangeTime({ @@ -151,7 +151,7 @@ export class Graph extends PureComponent { max: max, label: 'Datetime', ticks: ticks, - timezone: timeZone.raw, + timezone: timeZone, timeformat: time_format(ticks, min, max), }, }; diff --git a/public/app/features/explore/GraphContainer.tsx b/public/app/features/explore/GraphContainer.tsx index 6d1bb6c4e3878..632613f60c02a 100644 --- a/public/app/features/explore/GraphContainer.tsx +++ b/public/app/features/explore/GraphContainer.tsx @@ -34,8 +34,8 @@ export class GraphContainer extends PureComponent { onChangeTime = (absRange: AbsoluteTimeRange) => { const { exploreId, timeZone, changeTime } = this.props; const range = { - from: timeZone.isUtc ? toUtc(absRange.from) : dateTime(absRange.from), - to: timeZone.isUtc ? toUtc(absRange.to) : dateTime(absRange.to), + from: timeZone === 'utc' ? toUtc(absRange.from) : dateTime(absRange.from), + to: timeZone === 'utc' ? toUtc(absRange.to) : dateTime(absRange.to), }; changeTime(exploreId, range); diff --git a/public/app/features/explore/LogsContainer.tsx b/public/app/features/explore/LogsContainer.tsx index c6ba84aceb0d3..975923015c5e5 100644 --- a/public/app/features/explore/LogsContainer.tsx +++ b/public/app/features/explore/LogsContainer.tsx @@ -57,8 +57,8 @@ export class LogsContainer extends PureComponent { onChangeTime = (absRange: AbsoluteTimeRange) => { const { exploreId, timeZone, changeTime } = this.props; const range = { - from: timeZone.isUtc ? toUtc(absRange.from) : dateTime(absRange.from), - to: timeZone.isUtc ? toUtc(absRange.to) : dateTime(absRange.to), + from: timeZone === 'utc' ? toUtc(absRange.from) : dateTime(absRange.from), + to: timeZone === 'utc' ? toUtc(absRange.to) : dateTime(absRange.to), }; changeTime(exploreId, range); diff --git a/public/app/features/explore/TimePicker.test.tsx b/public/app/features/explore/TimePicker.test.tsx index 9cdf7a9b517fd..ea793096374b8 100644 --- a/public/app/features/explore/TimePicker.test.tsx +++ b/public/app/features/explore/TimePicker.test.tsx @@ -29,7 +29,7 @@ const fromRaw = (rawRange: RawTimeRange): TimeRange => { describe('', () => { it('render default values when closed and relative time range', () => { const range = fromRaw(DEFAULT_RANGE); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.state('fromRaw')).toBe(DEFAULT_RANGE.from); expect(wrapper.state('toRaw')).toBe(DEFAULT_RANGE.to); expect(wrapper.find('.timepicker-rangestring').text()).toBe('Last 6 hours'); @@ -39,7 +39,7 @@ describe('', () => { it('render default values when closed, utc and relative time range', () => { const range = fromRaw(DEFAULT_RANGE); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.state('fromRaw')).toBe(DEFAULT_RANGE.from); expect(wrapper.state('toRaw')).toBe(DEFAULT_RANGE.to); expect(wrapper.find('.timepicker-rangestring').text()).toBe('Last 6 hours'); @@ -49,7 +49,7 @@ describe('', () => { it('renders default values when open and relative range', () => { const range = fromRaw(DEFAULT_RANGE); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.state('fromRaw')).toBe(DEFAULT_RANGE.from); expect(wrapper.state('toRaw')).toBe(DEFAULT_RANGE.to); expect(wrapper.find('.timepicker-rangestring').text()).toBe('Last 6 hours'); @@ -61,7 +61,7 @@ describe('', () => { it('renders default values when open, utc and relative range', () => { const range = fromRaw(DEFAULT_RANGE); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.state('fromRaw')).toBe(DEFAULT_RANGE.from); expect(wrapper.state('toRaw')).toBe(DEFAULT_RANGE.to); expect(wrapper.find('.timepicker-rangestring').text()).toBe('Last 6 hours'); @@ -91,7 +91,7 @@ describe('', () => { const expectedRangeString = rangeUtil.describeTimeRange(localRange); const onChangeTime = sinon.spy(); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.state('fromRaw')).toBe(localRange.from.format(TIME_FORMAT)); expect(wrapper.state('toRaw')).toBe(localRange.to.format(TIME_FORMAT)); expect(wrapper.state('initialRange')).toBe(range.raw); @@ -118,11 +118,11 @@ describe('', () => { }, }; const onChangeTime = sinon.spy(); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.state('fromRaw')).toBe('1970-01-01 00:00:00'); expect(wrapper.state('toRaw')).toBe('1970-01-01 00:00:01'); expect(wrapper.state('initialRange')).toBe(range.raw); - expect(wrapper.find('.timepicker-rangestring').text()).toBe('Jan 1, 1970 00:00:00 to Jan 1, 1970 00:00:01'); + expect(wrapper.find('.timepicker-rangestring').text()).toBe('1970-01-01 00:00:00 to 1970-01-01 00:00:01'); expect(wrapper.find('.timepicker-from').props().value).toBe('1970-01-01 00:00:00'); expect(wrapper.find('.timepicker-to').props().value).toBe('1970-01-01 00:00:01'); @@ -132,7 +132,7 @@ describe('', () => { expect(onChangeTime.getCall(0).args[0].to.valueOf()).toBe(1000); expect(wrapper.state('isOpen')).toBeFalsy(); - expect(wrapper.state('rangeString')).toBe('Jan 1, 1970 00:00:00 to Jan 1, 1970 00:00:01'); + expect(wrapper.state('rangeString')).toBe('1970-01-01 00:00:00 to 1970-01-01 00:00:01'); }); it('moves ranges backward by half the range on left arrow click when utc', () => { @@ -147,7 +147,7 @@ describe('', () => { const range = fromRaw(rawRange); const onChangeTime = sinon.spy(); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.state('fromRaw')).toBe('1970-01-01 00:00:02'); expect(wrapper.state('toRaw')).toBe('1970-01-01 00:00:04'); @@ -176,7 +176,7 @@ describe('', () => { }; const onChangeTime = sinon.spy(); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.state('fromRaw')).toBe(localRange.from.format(TIME_FORMAT)); expect(wrapper.state('toRaw')).toBe(localRange.to.format(TIME_FORMAT)); @@ -197,7 +197,7 @@ describe('', () => { }; const onChangeTime = sinon.spy(); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.state('fromRaw')).toBe('1970-01-01 00:00:01'); expect(wrapper.state('toRaw')).toBe('1970-01-01 00:00:03'); @@ -226,7 +226,7 @@ describe('', () => { }; const onChangeTime = sinon.spy(); - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper.state('fromRaw')).toBe(localRange.from.format(TIME_FORMAT)); expect(wrapper.state('toRaw')).toBe(localRange.to.format(TIME_FORMAT)); diff --git a/public/app/features/explore/TimePicker.tsx b/public/app/features/explore/TimePicker.tsx index 9a10edc062550..510646dcfe4fb 100644 --- a/public/app/features/explore/TimePicker.tsx +++ b/public/app/features/explore/TimePicker.tsx @@ -1,12 +1,12 @@ -import React, { PureComponent } from 'react'; +import React, { PureComponent, ChangeEvent } from 'react'; import * as rangeUtil from '@grafana/ui/src/utils/rangeutil'; -import { Input, RawTimeRange, TimeRange, TIME_FORMAT } from '@grafana/ui'; +import { Input, RawTimeRange, TimeRange, TIME_FORMAT, TimeZone } from '@grafana/ui'; import { toUtc, isDateTime, dateTime } from '@grafana/ui/src/utils/moment_wrapper'; interface TimePickerProps { isOpen?: boolean; - isUtc?: boolean; range: TimeRange; + timeZone: TimeZone; onChangeTime?: (range: RawTimeRange, scanning?: boolean) => void; } @@ -22,21 +22,21 @@ interface TimePickerState { toRaw: string; } -const getRaw = (isUtc: boolean, range: any) => { +const getRaw = (range: any, timeZone: TimeZone) => { const rawRange = { from: range.raw.from, to: range.raw.to, }; if (isDateTime(rawRange.from)) { - if (!isUtc) { + if (timeZone === 'browser') { rawRange.from = rawRange.from.local(); } rawRange.from = rawRange.from.format(TIME_FORMAT); } if (isDateTime(rawRange.to)) { - if (!isUtc) { + if (timeZone === 'browser') { rawRange.to = rawRange.to.local(); } rawRange.to = rawRange.to.format(TIME_FORMAT); @@ -61,19 +61,19 @@ export default class TimePicker extends PureComponent { + handleChangeFrom = (event: ChangeEvent) => { this.setState({ - fromRaw: e.target.value, + fromRaw: event.target.value, }); }; - handleChangeTo = e => { + handleChangeTo = (event: ChangeEvent) => { this.setState({ - toRaw: e.target.value, + toRaw: event.target.value, }); }; handleClickApply = () => { - const { onChangeTime, isUtc } = this.props; + const { onChangeTime, timeZone } = this.props; let rawRange; + this.setState( state => { const { toRaw, fromRaw } = this.state; @@ -149,11 +153,11 @@ export default class TimePicker extends PureComponent parseTimeZone(state.timeZone); +export const getTimeZone = (state: UserState) => state.timeZone; diff --git a/public/app/features/teams/__snapshots__/TeamMemberRow.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamMemberRow.test.tsx.snap index 96ff1b4cbc993..501a082994225 100644 --- a/public/app/features/teams/__snapshots__/TeamMemberRow.test.tsx.snap +++ b/public/app/features/teams/__snapshots__/TeamMemberRow.test.tsx.snap @@ -115,6 +115,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned off should not ren }, ] } + tabSelectsValue={true} value={ Object { "description": "Is team member", @@ -199,6 +200,7 @@ exports[`Render when feature toggle editorsCanAdmin is turned on should render p }, ] } + tabSelectsValue={true} value={ Object { "description": "Is team member", diff --git a/public/app/plugins/datasource/grafana/specs/datasource.test.ts b/public/app/plugins/datasource/grafana/specs/datasource.test.ts index 4c189ac4caa98..47202c428ccbc 100644 --- a/public/app/plugins/datasource/grafana/specs/datasource.test.ts +++ b/public/app/plugins/datasource/grafana/specs/datasource.test.ts @@ -6,14 +6,14 @@ describe('grafana data source', () => { describe('when executing an annotations query', () => { let calledBackendSrvParams; const backendSrvStub = { - get: (url, options) => { + get: (url: string, options) => { calledBackendSrvParams = options; return q.resolve([]); }, }; const templateSrvStub = { - replace: val => { + replace: (val: string) => { return val.replace('$var2', 'replaced__delimiter__replaced2').replace('$var', 'replaced'); }, }; diff --git a/public/app/plugins/datasource/prometheus/components/__snapshots__/PromQueryEditor.test.tsx.snap b/public/app/plugins/datasource/prometheus/components/__snapshots__/PromQueryEditor.test.tsx.snap index e9f83ccafd36f..a705b312ea470 100644 --- a/public/app/plugins/datasource/prometheus/components/__snapshots__/PromQueryEditor.test.tsx.snap +++ b/public/app/plugins/datasource/prometheus/components/__snapshots__/PromQueryEditor.test.tsx.snap @@ -123,6 +123,7 @@ exports[`Render PromQueryEditor with basic options should render 1`] = ` }, ] } + tabSelectsValue={true} value={ Object { "label": "1/1", @@ -176,6 +177,7 @@ exports[`Render PromQueryEditor with basic options should render 1`] = ` }, ] } + tabSelectsValue={true} value={ Object { "label": "Time series", diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 5f7db2de60b6b..657e687a28129 100644 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -149,7 +149,7 @@ class GraphElement { } } - onPlotSelected(event, ranges) { + onPlotSelected(event: JQueryEventObject, ranges) { if (this.panel.xaxis.mode !== 'time') { // Skip if panel in histogram or series mode this.plot.clearSelection(); @@ -171,7 +171,7 @@ class GraphElement { } } - onPlotClick(event, pos, item) { + onPlotClick(event: JQueryEventObject, pos, item) { if (this.panel.xaxis.mode !== 'time') { // Skip if panel in histogram or series mode return; diff --git a/public/app/types/user.ts b/public/app/types/user.ts index 954bf278011f6..ec3761c713746 100644 --- a/public/app/types/user.ts +++ b/public/app/types/user.ts @@ -1,3 +1,5 @@ +import { TimeZone } from '@grafana/ui/src/types'; + export interface OrgUser { avatarUrl: string; email: string; @@ -46,7 +48,7 @@ export interface UsersState { export interface UserState { orgId: number; - timeZone: string; + timeZone: TimeZone; } export interface UserSession { diff --git a/public/sass/_variables.dark.generated.scss b/public/sass/_variables.dark.generated.scss index 9a2f449175014..fb157a92eef1f 100644 --- a/public/sass/_variables.dark.generated.scss +++ b/public/sass/_variables.dark.generated.scss @@ -42,6 +42,7 @@ $gray-2: #8e8e8e; $gray-3: #b3b3b3; $gray-4: #d8d9da; $gray-5: #ececec; +$gray-6: #f4f5f8; $gray-blue: #212327; $input-black: #09090b; @@ -285,6 +286,7 @@ $alert-info-bg: linear-gradient(100deg, $blue-base, $blue-shade); $popover-bg: $dark-2; $popover-color: $text-color; $popover-border-color: $dark-9; +$popover-header-bg: $dark-9; $popover-shadow: 0 0 20px black; $popover-help-bg: $btn-secondary-bg; @@ -398,3 +400,7 @@ $button-toggle-group-btn-seperator-border: 1px solid $dark-2; $vertical-resize-handle-bg: $dark-10; $vertical-resize-handle-dots: $gray-1; $vertical-resize-handle-dots-hover: $gray-2; + +// Calendar +$calendar-bg-days: $input-bg; +$calendar-bg-now: $dark-10; diff --git a/public/sass/_variables.light.generated.scss b/public/sass/_variables.light.generated.scss index b989af2eb2111..c61211fe30a04 100644 --- a/public/sass/_variables.light.generated.scss +++ b/public/sass/_variables.light.generated.scss @@ -30,6 +30,8 @@ $black: #000000; $dark-1: #1e2028; $dark-2: #41444b; +$dark-4: #35373f; +$dark-10: #424345; $gray-1: #52545c; $gray-2: #767980; $gray-3: #acb6bf; @@ -272,6 +274,7 @@ $alert-info-bg: linear-gradient(100deg, $blue-base, $blue-shade); $popover-bg: $page-bg; $popover-color: $text-color; $popover-border-color: $gray-5; +$popover-header-bg: $gray-5; $popover-shadow: 0 0 20px $white; $popover-help-bg: $btn-secondary-bg; @@ -385,3 +388,7 @@ $button-toggle-group-btn-seperator-border: 1px solid $gray-6; $vertical-resize-handle-bg: $gray-4; $vertical-resize-handle-dots: $gray-3; $vertical-resize-handle-dots-hover: $gray-2; + +// Calendar +$calendar-bg-days: $white; +$calendar-bg-now: $gray-6; diff --git a/public/sass/components/_navbar.scss b/public/sass/components/_navbar.scss index 67071bb61c86c..434ecbacb1e44 100644 --- a/public/sass/components/_navbar.scss +++ b/public/sass/components/_navbar.scss @@ -134,7 +134,7 @@ i.navbar-page-btn__search { align-items: center; font-weight: $btn-font-weight; padding: 6px $space-sm; - line-height: 16px; + line-height: 18px; color: $text-muted; border: 1px solid $navbar-button-border; margin-left: $space-xs; diff --git a/public/sass/components/_panel_graph.scss b/public/sass/components/_panel_graph.scss index bc4cc773c397e..48c6384a1480a 100644 --- a/public/sass/components/_panel_graph.scss +++ b/public/sass/components/_panel_graph.scss @@ -304,7 +304,7 @@ } .graph-annotation__header { - background-color: $popover-border-color; + background: $popover-header-bg; padding: 6px 10px; display: flex; } diff --git a/public/sass/components/_panel_logs.scss b/public/sass/components/_panel_logs.scss index 3c6ffd83a6fe3..b0156ce9ceeea 100644 --- a/public/sass/components/_panel_logs.scss +++ b/public/sass/components/_panel_logs.scss @@ -252,7 +252,7 @@ $column-horizontal-spacing: 10px; } .logs-stats__header { - background-color: $popover-border-color; + background: $popover-header-bg; padding: 6px 10px; display: flex; } diff --git a/public/test/specs/helpers.ts b/public/test/specs/helpers.ts index 648b73de40044..aeef0404582ca 100644 --- a/public/test/specs/helpers.ts +++ b/public/test/specs/helpers.ts @@ -19,7 +19,7 @@ export function ControllerTestContext(this: any) { getMetricSources: () => {}, get: () => { return { - then: (callback: (a: any) => void) => { + then: (callback: (ds: any) => void) => { callback(self.datasource); }, }; diff --git a/public/vendor/flot/jquery.flot.pie.js b/public/vendor/flot/jquery.flot.pie.js index dee47e6e50493..e69de29bb2d1d 100644 --- a/public/vendor/flot/jquery.flot.pie.js +++ b/public/vendor/flot/jquery.flot.pie.js @@ -1,818 +0,0 @@ -/* Flot plugin for rendering pie charts. - -Copyright (c) 2007-2013 IOLA and Ole Laursen. -Licensed under the MIT license. - -The plugin assumes that each series has a single data value, and that each -value is a positive integer or zero. Negative numbers don't make sense for a -pie chart, and have unpredictable results. The values do NOT need to be -passed in as percentages; the plugin will calculate the total and per-slice -percentages internally. - -* Created by Brian Medendorp - -* Updated with contributions from btburnett3, Anthony Aragues and Xavi Ivars - -The plugin supports these options: - - series: { - pie: { - show: true/false - radius: 0-1 for percentage of fullsize, or a specified pixel length, or 'auto' - innerRadius: 0-1 for percentage of fullsize or a specified pixel length, for creating a donut effect - startAngle: 0-2 factor of PI used for starting angle (in radians) i.e 3/2 starts at the top, 0 and 2 have the same result - tilt: 0-1 for percentage to tilt the pie, where 1 is no tilt, and 0 is completely flat (nothing will show) - offset: { - top: integer value to move the pie up or down - left: integer value to move the pie left or right, or 'auto' - }, - stroke: { - color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#FFF') - width: integer pixel width of the stroke - }, - label: { - show: true/false, or 'auto' - formatter: a user-defined function that modifies the text/style of the label text - radius: 0-1 for percentage of fullsize, or a specified pixel length - background: { - color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#000') - opacity: 0-1 - }, - threshold: 0-1 for the percentage value at which to hide labels (if they're too small) - }, - combine: { - threshold: 0-1 for the percentage value at which to combine slices (if they're too small) - color: any hexidecimal color value (other formats may or may not work, so best to stick with something like '#CCC'), if null, the plugin will automatically use the color of the first slice to be combined - label: any text value of what the combined slice should be labeled - } - highlight: { - opacity: 0-1 - } - } - } - -More detail and specific examples can be found in the included HTML file. - -*/ - -(function($) { - - // Maximum redraw attempts when fitting labels within the plot - - var REDRAW_ATTEMPTS = 10; - - // Factor by which to shrink the pie when fitting labels within the plot - - var REDRAW_SHRINK = 0.95; - - function init(plot) { - - var canvas = null, - target = null, - maxRadius = null, - centerLeft = null, - centerTop = null, - processed = false, - options = null, - ctx = null; - - // interactive variables - - var highlights = []; - - // add hook to determine if pie plugin in enabled, and then perform necessary operations - - plot.hooks.processOptions.push(function(plot, options) { - if (options.series.pie.show) { - - options.grid.show = false; - - // set labels.show - - if (options.series.pie.label.show == "auto") { - if (options.legend.show) { - options.series.pie.label.show = false; - } else { - options.series.pie.label.show = true; - } - } - - // set radius - - if (options.series.pie.radius == "auto") { - if (options.series.pie.label.show) { - options.series.pie.radius = 3/4; - } else { - options.series.pie.radius = 1; - } - } - - // ensure sane tilt - - if (options.series.pie.tilt > 1) { - options.series.pie.tilt = 1; - } else if (options.series.pie.tilt < 0) { - options.series.pie.tilt = 0; - } - } - }); - - plot.hooks.bindEvents.push(function(plot, eventHolder) { - var options = plot.getOptions(); - if (options.series.pie.show) { - if (options.grid.hoverable) { - eventHolder.unbind("mousemove").mousemove(onMouseMove); - } - if (options.grid.clickable) { - eventHolder.unbind("click").click(onClick); - } - } - }); - - plot.hooks.processDatapoints.push(function(plot, series, data, datapoints) { - var options = plot.getOptions(); - if (options.series.pie.show) { - processDatapoints(plot, series, data, datapoints); - } - }); - - plot.hooks.drawOverlay.push(function(plot, octx) { - var options = plot.getOptions(); - if (options.series.pie.show) { - drawOverlay(plot, octx); - } - }); - - plot.hooks.draw.push(function(plot, newCtx) { - var options = plot.getOptions(); - if (options.series.pie.show) { - draw(plot, newCtx); - } - }); - - function processDatapoints(plot, series, datapoints) { - if (!processed) { - processed = true; - canvas = plot.getCanvas(); - target = $(canvas).parent(); - options = plot.getOptions(); - plot.setData(combine(plot.getData())); - } - } - - function combine(data) { - - var total = 0, - combined = 0, - numCombined = 0, - color = options.series.pie.combine.color, - newdata = []; - - // Fix up the raw data from Flot, ensuring the data is numeric - - for (var i = 0; i < data.length; ++i) { - - var value = data[i].data; - - // If the data is an array, we'll assume that it's a standard - // Flot x-y pair, and are concerned only with the second value. - - // Note how we use the original array, rather than creating a - // new one; this is more efficient and preserves any extra data - // that the user may have stored in higher indexes. - - if ($.isArray(value) && value.length == 1) { - value = value[0]; - } - - if ($.isArray(value)) { - // Equivalent to $.isNumeric() but compatible with jQuery < 1.7 - if (!isNaN(parseFloat(value[1])) && isFinite(value[1])) { - value[1] = +value[1]; - } else { - value[1] = 0; - } - } else if (!isNaN(parseFloat(value)) && isFinite(value)) { - value = [1, +value]; - } else { - value = [1, 0]; - } - - data[i].data = [value]; - } - - // Sum up all the slices, so we can calculate percentages for each - - for (var i = 0; i < data.length; ++i) { - total += data[i].data[0][1]; - } - - // Count the number of slices with percentages below the combine - // threshold; if it turns out to be just one, we won't combine. - - for (var i = 0; i < data.length; ++i) { - var value = data[i].data[0][1]; - if (value / total <= options.series.pie.combine.threshold) { - combined += value; - numCombined++; - if (!color) { - color = data[i].color; - } - } - } - - for (var i = 0; i < data.length; ++i) { - var value = data[i].data[0][1]; - if (numCombined < 2 || value / total > options.series.pie.combine.threshold) { - newdata.push({ - data: [[1, value]], - color: data[i].color, - label: data[i].label, - angle: value * Math.PI * 2 / total, - percent: value / (total / 100) - }); - } - } - - if (numCombined > 1) { - newdata.push({ - data: [[1, combined]], - color: color, - label: options.series.pie.combine.label, - angle: combined * Math.PI * 2 / total, - percent: combined / (total / 100) - }); - } - - return newdata; - } - - function draw(plot, newCtx) { - - if (!target) { - return; // if no series were passed - } - - var canvasWidth = plot.getPlaceholder().width(), - canvasHeight = plot.getPlaceholder().height(), - legendWidth = target.children().filter(".legend").children().width() || 0; - - ctx = newCtx; - - // WARNING: HACK! REWRITE THIS CODE AS SOON AS POSSIBLE! - - // When combining smaller slices into an 'other' slice, we need to - // add a new series. Since Flot gives plugins no way to modify the - // list of series, the pie plugin uses a hack where the first call - // to processDatapoints results in a call to setData with the new - // list of series, then subsequent processDatapoints do nothing. - - // The plugin-global 'processed' flag is used to control this hack; - // it starts out false, and is set to true after the first call to - // processDatapoints. - - // Unfortunately this turns future setData calls into no-ops; they - // call processDatapoints, the flag is true, and nothing happens. - - // To fix this we'll set the flag back to false here in draw, when - // all series have been processed, so the next sequence of calls to - // processDatapoints once again starts out with a slice-combine. - // This is really a hack; in 0.9 we need to give plugins a proper - // way to modify series before any processing begins. - - processed = false; - - // calculate maximum radius and center point - - maxRadius = Math.min(canvasWidth, canvasHeight / options.series.pie.tilt) / 2; - centerTop = canvasHeight / 2 + options.series.pie.offset.top; - centerLeft = canvasWidth / 2; - - if (options.series.pie.offset.left == "auto") { - if (options.legend.position.match("w")) { - centerLeft += legendWidth / 2; - } else { - centerLeft -= legendWidth / 2; - } - } else { - centerLeft += options.series.pie.offset.left; - } - - if (centerLeft < maxRadius) { - centerLeft = maxRadius; - } else if (centerLeft > canvasWidth - maxRadius) { - centerLeft = canvasWidth - maxRadius; - } - - var slices = plot.getData(), - attempts = 0; - - // Keep shrinking the pie's radius until drawPie returns true, - // indicating that all the labels fit, or we try too many times. - - do { - if (attempts > 0) { - maxRadius *= REDRAW_SHRINK; - } - attempts += 1; - clear(); - if (options.series.pie.tilt <= 0.8) { - drawShadow(); - } - } while (!drawPie() && attempts < REDRAW_ATTEMPTS) - - if (attempts >= REDRAW_ATTEMPTS) { - clear(); - target.prepend("
Could not draw pie with labels contained inside canvas
"); - } - - if (plot.setSeries && plot.insertLegend) { - plot.setSeries(slices); - plot.insertLegend(); - } - - // we're actually done at this point, just defining internal functions at this point - - function clear() { - ctx.clearRect(0, 0, canvasWidth, canvasHeight); - target.children().filter(".pieLabel, .pieLabelBackground").remove(); - } - - function drawShadow() { - - var shadowLeft = options.series.pie.shadow.left; - var shadowTop = options.series.pie.shadow.top; - var edge = 10; - var alpha = options.series.pie.shadow.alpha; - var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; - - if (radius >= canvasWidth / 2 - shadowLeft || radius * options.series.pie.tilt >= canvasHeight / 2 - shadowTop || radius <= edge) { - return; // shadow would be outside canvas, so don't draw it - } - - ctx.save(); - ctx.translate(shadowLeft,shadowTop); - ctx.globalAlpha = alpha; - ctx.fillStyle = "#000"; - - // center and rotate to starting position - - ctx.translate(centerLeft,centerTop); - ctx.scale(1, options.series.pie.tilt); - - //radius -= edge; - - for (var i = 1; i <= edge; i++) { - ctx.beginPath(); - ctx.arc(0, 0, radius, 0, Math.PI * 2, false); - ctx.fill(); - radius -= i; - } - - ctx.restore(); - } - - function drawPie() { - - var startAngle = Math.PI * options.series.pie.startAngle; - var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; - - // center and rotate to starting position - - ctx.save(); - ctx.translate(centerLeft,centerTop); - ctx.scale(1, options.series.pie.tilt); - //ctx.rotate(startAngle); // start at top; -- This doesn't work properly in Opera - - // draw slices - - ctx.save(); - var currentAngle = startAngle; - for (var i = 0; i < slices.length; ++i) { - slices[i].startAngle = currentAngle; - drawSlice(slices[i].angle, slices[i].color, true); - } - ctx.restore(); - - // draw slice outlines - - if (options.series.pie.stroke.width > 0) { - ctx.save(); - ctx.lineWidth = options.series.pie.stroke.width; - currentAngle = startAngle; - for (var i = 0; i < slices.length; ++i) { - drawSlice(slices[i].angle, options.series.pie.stroke.color, false); - } - ctx.restore(); - } - - // draw donut hole - - drawDonutHole(ctx); - - ctx.restore(); - - // Draw the labels, returning true if they fit within the plot - - if (options.series.pie.label.show) { - return drawLabels(); - } else return true; - - function drawSlice(angle, color, fill) { - - if (angle <= 0 || isNaN(angle)) { - return; - } - - if (fill) { - ctx.fillStyle = color; - } else { - ctx.strokeStyle = color; - ctx.lineJoin = "round"; - } - - ctx.beginPath(); - if (Math.abs(angle - Math.PI * 2) > 0.000000001) { - ctx.moveTo(0, 0); // Center of the pie - } - - //ctx.arc(0, 0, radius, 0, angle, false); // This doesn't work properly in Opera - ctx.arc(0, 0, radius,currentAngle, currentAngle + angle / 2, false); - ctx.arc(0, 0, radius,currentAngle + angle / 2, currentAngle + angle, false); - ctx.closePath(); - //ctx.rotate(angle); // This doesn't work properly in Opera - currentAngle += angle; - - if (fill) { - ctx.fill(); - } else { - ctx.stroke(); - } - } - - function drawLabels() { - - var currentAngle = startAngle; - var radius = options.series.pie.label.radius > 1 ? options.series.pie.label.radius : maxRadius * options.series.pie.label.radius; - - for (var i = 0; i < slices.length; ++i) { - if (slices[i].percent >= options.series.pie.label.threshold * 100) { - if (!drawLabel(slices[i], currentAngle, i)) { - return false; - } - } - currentAngle += slices[i].angle; - } - - return true; - - function drawLabel(slice, startAngle, index) { - - if (slice.data[0][1] == 0) { - return true; - } - - // format label text - - var lf = options.legend.labelFormatter, text, plf = options.series.pie.label.formatter; - - if (lf) { - text = lf(slice.label, slice); - } else { - text = slice.label; - } - - if (plf) { - text = plf(text, slice); - } - - var halfAngle = ((startAngle + slice.angle) + startAngle) / 2; - var x = centerLeft + Math.round(Math.cos(halfAngle) * radius); - var y = centerTop + Math.round(Math.sin(halfAngle) * radius) * options.series.pie.tilt; - - var html = "" + text + ""; - target.append(html); - - var label = target.children("#pieLabel" + index); - var labelTop = (y - label.height() / 2); - var labelLeft = (x - label.width() / 2); - - label.css("top", labelTop); - label.css("left", labelLeft); - - // check to make sure that the label is not outside the canvas - - if (0 - labelTop > 0 || 0 - labelLeft > 0 || canvasHeight - (labelTop + label.height()) < 0 || canvasWidth - (labelLeft + label.width()) < 0) { - return false; - } - - if (options.series.pie.label.background.opacity != 0) { - - // put in the transparent background separately to avoid blended labels and label boxes - - var c = options.series.pie.label.background.color; - - if (c == null) { - c = slice.color; - } - - var pos = "top:" + labelTop + "px;left:" + labelLeft + "px;"; - $("
") - .css("opacity", options.series.pie.label.background.opacity) - .insertBefore(label); - } - - return true; - } // end individual label function - } // end drawLabels function - } // end drawPie function - } // end draw function - - // Placed here because it needs to be accessed from multiple locations - - function drawDonutHole(layer) { - if (options.series.pie.innerRadius > 0) { - - // subtract the center - - layer.save(); - var innerRadius = options.series.pie.innerRadius > 1 ? options.series.pie.innerRadius : maxRadius * options.series.pie.innerRadius; - layer.globalCompositeOperation = "destination-out"; // this does not work with excanvas, but it will fall back to using the stroke color - layer.beginPath(); - layer.fillStyle = options.series.pie.stroke.color; - layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false); - layer.fill(); - layer.closePath(); - layer.restore(); - - // add inner stroke - - layer.save(); - layer.beginPath(); - layer.strokeStyle = options.series.pie.stroke.color; - layer.arc(0, 0, innerRadius, 0, Math.PI * 2, false); - layer.stroke(); - layer.closePath(); - layer.restore(); - - // TODO: add extra shadow inside hole (with a mask) if the pie is tilted. - } - } - - //-- Additional Interactive related functions -- - - function isPointInPoly(poly, pt) { - for(var c = false, i = -1, l = poly.length, j = l - 1; ++i < l; j = i) - ((poly[i][1] <= pt[1] && pt[1] < poly[j][1]) || (poly[j][1] <= pt[1] && pt[1]< poly[i][1])) - && (pt[0] < (poly[j][0] - poly[i][0]) * (pt[1] - poly[i][1]) / (poly[j][1] - poly[i][1]) + poly[i][0]) - && (c = !c); - return c; - } - - function findNearbySlice(mouseX, mouseY) { - - var slices = plot.getData(), - options = plot.getOptions(), - radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius, - x, y; - - for (var i = 0; i < slices.length; ++i) { - - var s = slices[i]; - - if (s.pie.show) { - - ctx.save(); - ctx.beginPath(); - ctx.moveTo(0, 0); // Center of the pie - //ctx.scale(1, options.series.pie.tilt); // this actually seems to break everything when here. - ctx.arc(0, 0, radius, s.startAngle, s.startAngle + s.angle / 2, false); - ctx.arc(0, 0, radius, s.startAngle + s.angle / 2, s.startAngle + s.angle, false); - ctx.closePath(); - x = mouseX - centerLeft; - y = mouseY - centerTop; - - if (ctx.isPointInPath) { - if (ctx.isPointInPath(mouseX - centerLeft, mouseY - centerTop)) { - ctx.restore(); - return { - datapoint: [s.percent, s.data], - dataIndex: 0, - series: s, - seriesIndex: i - }; - } - } else { - - // excanvas for IE doesn;t support isPointInPath, this is a workaround. - - var p1X = radius * Math.cos(s.startAngle), - p1Y = radius * Math.sin(s.startAngle), - p2X = radius * Math.cos(s.startAngle + s.angle / 4), - p2Y = radius * Math.sin(s.startAngle + s.angle / 4), - p3X = radius * Math.cos(s.startAngle + s.angle / 2), - p3Y = radius * Math.sin(s.startAngle + s.angle / 2), - p4X = radius * Math.cos(s.startAngle + s.angle / 1.5), - p4Y = radius * Math.sin(s.startAngle + s.angle / 1.5), - p5X = radius * Math.cos(s.startAngle + s.angle), - p5Y = radius * Math.sin(s.startAngle + s.angle), - arrPoly = [[0, 0], [p1X, p1Y], [p2X, p2Y], [p3X, p3Y], [p4X, p4Y], [p5X, p5Y]], - arrPoint = [x, y]; - - // TODO: perhaps do some mathmatical trickery here with the Y-coordinate to compensate for pie tilt? - - if (isPointInPoly(arrPoly, arrPoint)) { - ctx.restore(); - return { - datapoint: [s.percent, s.data], - dataIndex: 0, - series: s, - seriesIndex: i - }; - } - } - - ctx.restore(); - } - } - - return null; - } - - function onMouseMove(e) { - triggerClickHoverEvent("plothover", e); - } - - function onClick(e) { - triggerClickHoverEvent("plotclick", e); - } - - // trigger click or hover event (they send the same parameters so we share their code) - - function triggerClickHoverEvent(eventname, e) { - - var offset = plot.offset(); - var canvasX = parseInt(e.pageX - offset.left); - var canvasY = parseInt(e.pageY - offset.top); - var item = findNearbySlice(canvasX, canvasY); - - if (options.grid.autoHighlight) { - - // clear auto-highlights - - for (var i = 0; i < highlights.length; ++i) { - var h = highlights[i]; - if (h.auto == eventname && !(item && h.series == item.series)) { - unhighlight(h.series); - } - } - } - - // highlight the slice - - if (item) { - highlight(item.series, eventname); - } - - // trigger any hover bind events - - var pos = { pageX: e.pageX, pageY: e.pageY }; - target.trigger(eventname, [pos, item]); - } - - function highlight(s, auto) { - //if (typeof s == "number") { - // s = series[s]; - //} - - var i = indexOfHighlight(s); - - if (i == -1) { - highlights.push({ series: s, auto: auto }); - plot.triggerRedrawOverlay(); - } else if (!auto) { - highlights[i].auto = false; - } - } - - function unhighlight(s) { - if (s == null) { - highlights = []; - plot.triggerRedrawOverlay(); - } - - //if (typeof s == "number") { - // s = series[s]; - //} - - var i = indexOfHighlight(s); - - if (i != -1) { - highlights.splice(i, 1); - plot.triggerRedrawOverlay(); - } - } - - function indexOfHighlight(s) { - for (var i = 0; i < highlights.length; ++i) { - var h = highlights[i]; - if (h.series == s) - return i; - } - return -1; - } - - function drawOverlay(plot, octx) { - - var options = plot.getOptions(); - - var radius = options.series.pie.radius > 1 ? options.series.pie.radius : maxRadius * options.series.pie.radius; - - octx.save(); - octx.translate(centerLeft, centerTop); - octx.scale(1, options.series.pie.tilt); - - for (var i = 0; i < highlights.length; ++i) { - drawHighlight(highlights[i].series); - } - - drawDonutHole(octx); - - octx.restore(); - - function drawHighlight(series) { - - if (series.angle <= 0 || isNaN(series.angle)) { - return; - } - - //octx.fillStyle = parseColor(options.series.pie.highlight.color).scale(null, null, null, options.series.pie.highlight.opacity).toString(); - octx.fillStyle = "rgba(255, 255, 255, " + options.series.pie.highlight.opacity + ")"; // this is temporary until we have access to parseColor - octx.beginPath(); - if (Math.abs(series.angle - Math.PI * 2) > 0.000000001) { - octx.moveTo(0, 0); // Center of the pie - } - octx.arc(0, 0, radius, series.startAngle, series.startAngle + series.angle / 2, false); - octx.arc(0, 0, radius, series.startAngle + series.angle / 2, series.startAngle + series.angle, false); - octx.closePath(); - octx.fill(); - } - } - } // end init (plugin body) - - // define pie specific options and their default values - - var options = { - series: { - pie: { - show: false, - radius: "auto", // actual radius of the visible pie (based on full calculated radius if <=1, or hard pixel value) - innerRadius: 0, /* for donut */ - startAngle: 3/2, - tilt: 1, - shadow: { - left: 5, // shadow left offset - top: 15, // shadow top offset - alpha: 0.02 // shadow alpha - }, - offset: { - top: 0, - left: "auto" - }, - stroke: { - color: "#fff", - width: 1 - }, - label: { - show: "auto", - formatter: function(label, slice) { - return "
" + label + "
" + Math.round(slice.percent) + "%
"; - }, // formatter function - radius: 1, // radius at which to place the labels (based on full calculated radius if <=1, or hard pixel value) - background: { - color: null, - opacity: 0 - }, - threshold: 0 // percentage at which to hide the label (i.e. the slice is too narrow) - }, - combine: { - threshold: -1, // percentage at which to combine little slices into one larger slice - color: null, // color to give the new slice (auto-generated if null) - label: "Other" // label to give the new slice - }, - highlight: { - //color: "#fff", // will add this functionality once parseColor is available - opacity: 0.5 - } - } - } - }; - - $.plot.plugins.push({ - init: init, - options: options, - name: "pie", - version: "1.1" - }); - -})(jQuery); From bc94f85deee75467a493438b483f8ca554a9bc16 Mon Sep 17 00:00:00 2001 From: gotjosh Date: Mon, 24 Jun 2019 16:00:01 +0100 Subject: [PATCH 15/20] Improvement: Grafana release process minor improvements (#17661) * Don't display changelog category title when no items The output of the changelog is meant to be copy/pasted with ease. When a changelog category does not contain items is better to not display title at all thus avoiding having the manually modify the output as we include it in the steps of the process. * Introduce a CLI task to close milestones whilst doing a Grafana release As part of a Grafana release, we need to eventually close the GitHub milestone to indicate is done and remove all the cherry-pick labels from issues/prs within the milestone to avoid our cherry-pick CLI command to pick them up on the next release. * Abstract the GitHub client into a module * Introduce `GitHubClient` to all CLI tasks --- jest.config.js | 3 +- scripts/cli/index.ts | 16 ++++++ scripts/cli/tasks/changelog.ts | 22 ++++---- scripts/cli/tasks/cherrypick.ts | 12 ++--- scripts/cli/tasks/closeMilestone.ts | 75 ++++++++++++++++++++++++++ scripts/cli/utils/githubClient.test.ts | 66 +++++++++++++++++++++++ scripts/cli/utils/githubClient.ts | 41 ++++++++++++++ 7 files changed, 216 insertions(+), 19 deletions(-) create mode 100644 scripts/cli/tasks/closeMilestone.ts create mode 100644 scripts/cli/utils/githubClient.test.ts create mode 100644 scripts/cli/utils/githubClient.ts diff --git a/jest.config.js b/jest.config.js index 09342e1472079..da5ff59a47dd8 100644 --- a/jest.config.js +++ b/jest.config.js @@ -8,7 +8,8 @@ module.exports = { "roots": [ "/public/app", "/public/test", - "/packages" + "/packages", + "/scripts", ], "testRegex": "(\\.|/)(test)\\.(jsx?|tsx?)$", "moduleFileExtensions": [ diff --git a/scripts/cli/index.ts b/scripts/cli/index.ts index 722fc2cac26ac..dec2a05c424f2 100644 --- a/scripts/cli/index.ts +++ b/scripts/cli/index.ts @@ -6,6 +6,7 @@ import { buildTask } from './tasks/grafanaui.build'; import { releaseTask } from './tasks/grafanaui.release'; import { changelogTask } from './tasks/changelog'; import { cherryPickTask } from './tasks/cherrypick'; +import { closeMilestoneTask } from './tasks/closeMilestone'; import { precommitTask } from './tasks/precommit'; import { searchTestDataSetupTask } from './tasks/searchTestDataSetup'; @@ -66,6 +67,21 @@ program await execTask(cherryPickTask)({}); }); +program + .command('close-milestone') + .option('-m, --milestone ', 'Specify milestone') + .description('Helps ends a milestone by removing the cherry-pick label and closing it') + .action(async cmd => { + if (!cmd.milestone) { + console.log('Please specify milestone, example: -m '); + return; + } + + await execTask(closeMilestoneTask)({ + milestone: cmd.milestone, + }); + }); + program .command('precommit') .description('Executes checks') diff --git a/scripts/cli/tasks/changelog.ts b/scripts/cli/tasks/changelog.ts index 2b419d32a6f1f..e2cb9da7e55d1 100644 --- a/scripts/cli/tasks/changelog.ts +++ b/scripts/cli/tasks/changelog.ts @@ -1,18 +1,14 @@ -import axios from 'axios'; import _ from 'lodash'; import { Task, TaskRunner } from './task'; - -const githubGrafanaUrl = 'https://github.com/grafana/grafana'; +import GithubClient from '../utils/githubClient'; interface ChangelogOptions { milestone: string; } const changelogTaskRunner: TaskRunner = async ({ milestone }) => { - const client = axios.create({ - baseURL: 'https://api.github.com/repos/grafana/grafana', - timeout: 10000, - }); + const githubClient = new GithubClient(); + const client = githubClient.client; if (!/^\d+$/.test(milestone)) { console.log('Use milestone number not title, find number in milestone url'); @@ -45,13 +41,20 @@ const changelogTaskRunner: TaskRunner = async ({ milestone }) const notBugs = _.sortBy(issues.filter(item => !bugs.find(bug => bug === item)), 'title'); - let markdown = '### Features / Enhancements\n'; + let markdown = ''; + + if (notBugs.length > 0) { + markdown = '### Features / Enhancements\n'; + } for (const item of notBugs) { markdown += getMarkdownLineForIssue(item); } - markdown += '\n### Bug Fixes\n'; + if (bugs.length > 0) { + markdown += '\n### Bug Fixes\n'; + } + for (const item of bugs) { markdown += getMarkdownLineForIssue(item); } @@ -60,6 +63,7 @@ const changelogTaskRunner: TaskRunner = async ({ milestone }) }; function getMarkdownLineForIssue(item: any) { + const githubGrafanaUrl = 'https://github.com/grafana/grafana'; let markdown = ''; const title = item.title.replace(/^([^:]*)/, (match, g1) => { return `**${g1}**`; diff --git a/scripts/cli/tasks/cherrypick.ts b/scripts/cli/tasks/cherrypick.ts index ac92f223a7eb1..3e5a7addf4259 100644 --- a/scripts/cli/tasks/cherrypick.ts +++ b/scripts/cli/tasks/cherrypick.ts @@ -1,17 +1,11 @@ import { Task, TaskRunner } from './task'; -import axios from 'axios'; +import GithubClient from '../utils/githubClient'; interface CherryPickOptions {} const cherryPickRunner: TaskRunner = async () => { - let client = axios.create({ - baseURL: 'https://api.github.com/repos/grafana/grafana', - timeout: 10000, - // auth: { - // username: '', - // password: '', - // }, - }); + const githubClient = new GithubClient(); + const client = githubClient.client; const res = await client.get('/issues', { params: { diff --git a/scripts/cli/tasks/closeMilestone.ts b/scripts/cli/tasks/closeMilestone.ts new file mode 100644 index 0000000000000..9873863dc253c --- /dev/null +++ b/scripts/cli/tasks/closeMilestone.ts @@ -0,0 +1,75 @@ +import { Task, TaskRunner } from './task'; +import GithubClient from '../utils/githubClient'; + +interface CloseMilestoneOptions { + milestone: string; +} + +const closeMilestoneTaskRunner: TaskRunner = async ({ milestone }) => { + const githubClient = new GithubClient(true); + + const cherryPickLabel = 'cherry-pick needed'; + const client = githubClient.client; + + if (!/^\d+$/.test(milestone)) { + console.log('Use milestone number not title, find number in milestone url'); + return; + } + + const milestoneRes = await client.get(`/milestones/${milestone}`, {}); + + const milestoneState = milestoneRes.data.state; + + if (milestoneState === 'closed') { + console.log('milestone already closed. ✅'); + return; + } + + console.log('fetching issues/PRs of the milestone ⏬'); + + // Get all the issues/PRs with the label cherry-pick + // Every pull request is actually an issue + const issuesRes = await client.get('/issues', { + params: { + state: 'closed', + labels: cherryPickLabel, + per_page: 100, + milestone: milestone, + }, + }); + + if (issuesRes.data.length < 1) { + console.log('no issues to remove label from'); + } else { + console.log(`found ${issuesRes.data.length} issues to remove the cherry-pick label from 🔎`); + } + + for (const issue of issuesRes.data) { + // the reason for using stdout.write is for achieving 'action -> result' on + // the same line + process.stdout.write(`🔧removing label from issue #${issue.number} 🗑...`); + const resDelete = await client.delete(`/issues/${issue.number}/labels/${cherryPickLabel}`, {}); + if (resDelete.status === 200) { + process.stdout.write('done ✅\n'); + } else { + console.log('failed ❌'); + } + } + + console.log(`cleaned up ${issuesRes.data.length} issues/prs ⚡️`); + + const resClose = await client.patch(`/milestones/${milestone}`, { + state: 'closed', + }); + + if (resClose.status === 200) { + console.log('milestone closed 🙌'); + } else { + console.log('failed to close the milestone, response:'); + console.log(resClose); + } +}; + +export const closeMilestoneTask = new Task(); +closeMilestoneTask.setName('Close Milestone generator task'); +closeMilestoneTask.setRunner(closeMilestoneTaskRunner); diff --git a/scripts/cli/utils/githubClient.test.ts b/scripts/cli/utils/githubClient.test.ts new file mode 100644 index 0000000000000..95c67cb3111c4 --- /dev/null +++ b/scripts/cli/utils/githubClient.test.ts @@ -0,0 +1,66 @@ +import GithubClient from './githubClient'; + +const fakeClient = jest.fn(); + +beforeEach(() => { + delete process.env.GITHUB_USERNAME; + delete process.env.GITHUB_ACCESS_TOKEN; +}); + +afterEach(() => { + delete process.env.GITHUB_USERNAME; + delete process.env.GITHUB_ACCESS_TOKEN; +}); + +describe('GithubClient', () => { + it('should initialise a GithubClient', () => { + const github = new GithubClient(); + expect(github).toBeInstanceOf(GithubClient); + }); + + describe('#client', () => { + it('it should contain a client', () => { + const spy = jest.spyOn(GithubClient.prototype, 'createClient').mockImplementation(() => fakeClient); + + const github = new GithubClient(); + const client = github.client; + + expect(spy).toHaveBeenCalledWith({ + baseURL: 'https://api.github.com/repos/grafana/grafana', + timeout: 10000, + }); + expect(client).toEqual(fakeClient); + }); + + describe('when the credentials are required', () => { + it('should create the client when the credentials are defined', () => { + const username = 'grafana'; + const token = 'averysecureaccesstoken'; + + process.env.GITHUB_USERNAME = username; + process.env.GITHUB_ACCESS_TOKEN = token; + + const spy = jest.spyOn(GithubClient.prototype, 'createClient').mockImplementation(() => fakeClient); + + const github = new GithubClient(true); + const client = github.client; + + expect(spy).toHaveBeenCalledWith({ + baseURL: 'https://api.github.com/repos/grafana/grafana', + timeout: 10000, + auth: { username, password: token }, + }); + + expect(client).toEqual(fakeClient); + }); + + describe('when the credentials are not defined', () => { + it('should throw an error', () => { + expect(() => { + new GithubClient(true); + }).toThrow(/operation needs a GITHUB_USERNAME and GITHUB_ACCESS_TOKEN environment variables/); + }); + }); + }); + }); +}); diff --git a/scripts/cli/utils/githubClient.ts b/scripts/cli/utils/githubClient.ts new file mode 100644 index 0000000000000..a3eff8c532bce --- /dev/null +++ b/scripts/cli/utils/githubClient.ts @@ -0,0 +1,41 @@ +import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'; + +const baseURL = 'https://api.github.com/repos/grafana/grafana'; + +// Encapsulates the creation of a client for the Github API +// +// Two key things: +// 1. You can specify whenever you want the credentials to be required or not when imported. +// 2. If the the credentials are available as part of the environment, even if +// they're not required - the library will use them. This allows us to overcome +// any API rate limiting imposed without authentication. + +class GithubClient { + client: AxiosInstance; + + constructor(required = false) { + const username = process.env.GITHUB_USERNAME; + const token = process.env.GITHUB_ACCESS_TOKEN; + + const clientConfig: AxiosRequestConfig = { + baseURL: baseURL, + timeout: 10000, + }; + + if (required && !username && !token) { + throw new Error('operation needs a GITHUB_USERNAME and GITHUB_ACCESS_TOKEN environment variables'); + } + + if (username && token) { + clientConfig.auth = { username: username, password: token }; + } + + this.client = this.createClient(clientConfig); + } + + private createClient(clientConfig: AxiosRequestConfig) { + return axios.create(clientConfig); + } +} + +export default GithubClient; From c41eba92feddf60dbfce05ea8fbf91d6f1e2865a Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 24 Jun 2019 18:00:21 +0200 Subject: [PATCH 16/20] Project: Issue triage doc improvement (#17709) Adds some fixes of types and a new section describing how you can help with the Grafana project related to issues. Ref #17648 --- ISSUE_TRIAGE.md | 66 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/ISSUE_TRIAGE.md b/ISSUE_TRIAGE.md index cb902931bcb8b..95e870b5ebc40 100644 --- a/ISSUE_TRIAGE.md +++ b/ISSUE_TRIAGE.md @@ -53,6 +53,66 @@ The core maintainers of the Grafana project is responsible for categorizing all +------------------+ +--------------+ ``` +## How you can help + +There are multiple ways that you can help with the Grafana project, especially without writing a single line of code. Everyone in the Grafana community will be greatly thankful you for helping out with any of the below tasks. + +### Answer/ask questions + +The [community site](https://community.grafana.com/) is the main channel to be used for asking and answering questions related to the Grafana project. This may be the first place a new or existing Grafana user look/ask for help after they found that the [documentation](https://grafana.com/docs) wasn't answering their questions. It's very important to help new and existing users so that these new users can find proper answers and eventually help out other users and by that keep growing the Grafana community. + +Please signup to the Grafana [community site](https://community.grafana.com/) and start help other Grafana users by answering their questions and/or ask for help. + +### Report documentation enhancements + +If you visit the [documentation site](https://grafana.com/docs) and find typos/error/lack of information please report these by clicking on the `Request doc changes` link found on every page and/or contribute the changes yourself by clicking on `Edit this page` and open a pull request. Everyone in the community will greatly thank you for. + +Please read about how documentation issues is triaged [below](#documentation-issue) to understand what kind of documentation may be suitable to request/add. + +### Report a security vulnerability + +Please review the [security policy](https://github.com/grafana/grafana/security/policy) for more details. + +### Report bugs + +Report a bug you found when using Grafana by [opening a new bug report](https://github.com/grafana/grafana/issues/new?labels=type%3A+bug&template=1-bug_report.md). + +### Request enhancements/new features + +Suggest an enhancement or new feature for the Grafana project by [opening a new enhancement issue](https://github.com/grafana/grafana/issues/new?labels=type%3A+feature+request&template=2-feature_request.md). + +Alternatively, help make Grafana be better at being accessible to all by [opening a new accessibility issue](https://github.com/grafana/grafana/issues/new?labels=type%3A+accessibility&template=3-accessibility.md). + +### Report inaccurate issue information + +If you find an issue that have a badly formatted title and/or description, bad language/grammar and/or wrong labels it's important to let the issue author or maintainers know so it can be fixed. See [good practices](#good-practices) regarding basic information for issues below. + +### Request closing of issues + +The Grafana project have a lot of open issues and the main goal is to only have issues open if their still relevant. If you find an issue that you think already have been resolved or no longer is relevant please report by adding a comment and explain why you think it should be closed including related issues (`#`), if applicable, and optionally mention one of the maintainers. + +### Investigate issues + +See [investigation of issues](#investigation-of-issues). + +### Vote on enhancements/bugs + +Helping the Grafana project to know which issues are most important by users and the community is crucial for the success of the project. Read more about [prioritizing issues](#4-prioritization-of-issues) for details about how issues are being prioritized. The Grafana project use GitGub issues and reactions for collecting votes on enhancement and bugs. + +**Please don't add `+1` issue comments or similar since that will notify everyone that have subscribed to an issue and it doesn't add any useful update, rather it creates a bad .** + +If you want to show your interest or importance of an issue, please use [GitHub's reactions](https://help.github.com/en/articles/about-conversations-on-github#reacting-to-ideas-in-comments). + +### Report duplicates + +If you find two issues describing the same bug/enhancement/feature please add a comment in one of the issue and explain which issues (`#`) you think is a duplicate of another issue (`#`). + +### Suggest ideas for resolving bugs/enhancements + +Related to how [issues are being prioritized](#4-prioritization-of-issues) it's important to help anyone that's interested in contributing code for resolving a bug or enhancement. This can be anything from getting started and setup the development environment to reference code and files where changes probably needs to be made and/or suggest ideas on how enhancements may function/be implemented. + +Please read about how [help from the community](#5-requesting-help-from-the-community) may be requested when issues being triaged. + ## 1. Find uncategorized issues To get started with issue triage and finding issues that haven't been triaged you have two alternatives. @@ -63,9 +123,9 @@ The easiest and straigt forward way of getting started and finding issues that h ### Subscribe to all notifications -The more advanced, but recommended way is to subscribe to all notifications from this repository which means that all new issues, pull requests, comments and important status changes are sent to your configure email address. Read this [guide](https://help.github.com/en/articles/watching-and-unwatching-repositories#watching-a-single-repository) for help with setting this up. +The more advanced, but recommended way is to subscribe to all notifications from this repository which means that all new issues, pull requests, comments and important status changes are sent to your configured email address. Read this [guide](https://help.github.com/en/articles/watching-and-unwatching-repositories#watching-a-single-repository) for help with setting this up. -It's highly recommened that you setup filters to automatically remove emails from the inbox and label/categorize them accordingly to make it easy for you to understand when you need to act upon a notification or where to look for finding issues that haven't been triaged etc. +It's highly recommended that you setup filters to automatically remove emails from the inbox and label/categorize them accordingly to make it easy for you to understand when you need to act upon a notification or where to look for finding issues that haven't been triaged etc. Instructions for setting up filters in Gmail can be found [here](#setting-up-gmail-filters). Another alternative is to use [Trailer](https://github.com/ptsochantaris/trailer) or similar software. @@ -107,7 +167,7 @@ In general, if the issue description and title is perceived as a question no mor To make it easier for everyone to understand and find issues they're searching for it's suggested as a general rule of thumbs to: -* Make sure that issue titles are named to explain the subject of the issue, has a correct spelling and and doesn't include irrelevant information and/or sensitive information. +* Make sure that issue titles are named to explain the subject of the issue, has a correct spelling and doesn't include irrelevant information and/or sensitive information. * Make sure that issue descriptions doesn't include irrelevant information, information from template that haven't been filled out and/or sensitive information. * Do your best effort to change title and description or request suggested changes by adding a comment. From 4c97d26102dafb7c6b470364e6a99f9b581a7575 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Mon, 24 Jun 2019 18:29:57 +0200 Subject: [PATCH 17/20] Docs: clarified usage of go get and go mod (#17637) --- pkg/README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pkg/README.md b/pkg/README.md index c6fa805482e77..0f86eba213fad 100644 --- a/pkg/README.md +++ b/pkg/README.md @@ -35,14 +35,17 @@ The Grafana project uses [Go modules](https://golang.org/cmd/go/#hdr-Modules__mo All dependencies are vendored in the `vendor/` directory. +_Note:_ Since most developers of Grafana still use the `GOPATH` we need to specify `GO111MODULE=on` to make `go mod` and `got get` work as intended. If you have setup Grafana outside of the `GOPATH` on your machine you can skip `GO111MODULE=on` when running the commands below. + To add or update a new dependency, use the `go get` command: ```bash +# The GO111MODULE variable can be omitted when the code isn't located in GOPATH. # Pick the latest tagged release. -go get example.com/some/module/pkg +GO111MODULE=on go get example.com/some/module/pkg # Pick a specific version. -go get example.com/some/module/pkg@vX.Y.Z +GO111MODULE=on go get example.com/some/module/pkg@vX.Y.Z ``` Tidy up the `go.mod` and `go.sum` files and copy the new/updated dependency to the `vendor/` directory: From e7e9d3619e90c681c710b171bfa7164decdbb279 Mon Sep 17 00:00:00 2001 From: Sofia Papagiannaki Date: Mon, 24 Jun 2019 20:39:28 +0300 Subject: [PATCH 18/20] Add guidelines for SQL date comparisons (#17732) --- pkg/README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkg/README.md b/pkg/README.md index 0f86eba213fad..bcf14b420b70b 100644 --- a/pkg/README.md +++ b/pkg/README.md @@ -29,6 +29,9 @@ The sqlstore handlers all use a global xorm engine variable. This should be refa ## Avoid global HTTP Handler functions HTTP handlers should be refactored to so the handler methods are on the HttpServer instance or a more detailed handler struct. E.g (AuthHandler). This way they get access to HttpServer service dependencies (& Cfg object) and can avoid global state +## Date comparison +Newly introduced date columns in the database should be stored as epochs if date comparison is required. This permits to have a unifed approach for comparing dates against all the supported databases instead of handling seperately each one of them. In addition to this, by comparing epochs error pruning transformations from/to other time zones are no more needed. + # Dependency management The Grafana project uses [Go modules](https://golang.org/cmd/go/#hdr-Modules__module_versions__and_more) to manage dependencies on external packages. This requires a working Go environment with version 1.11 or greater installed. From 2fb45eeec898e0c8fa4ab088a6dc843420af8e2d Mon Sep 17 00:00:00 2001 From: gotjosh Date: Mon, 24 Jun 2019 20:20:21 +0100 Subject: [PATCH 19/20] Grafana-CLI: Wrapper for `grafana-cli` within RPM/DEB packages and config/homepath are now global flags (#17695) * Feature: Introduce a grafana-cli wrapper When our users install the *nix packed version of grafana, tendency is to use the services and scripts installed as part of the package for grafana-server. These leverage the default configuration options by specifying the several default paths. This introduces a similar approach for the grafana-cli binary. We exposed it through a wrapper to ensure a proper configuration is in place. To enable that, we add the .real suffix to the original binary (grafana-cli.real) and then use a bash script named grafana-cli as the wrapper. * Make the config and homepath flags global * Introduce `configOverrides` as a global flag This flag allows us to pass configuration overrides as a string. The string follows the convention of configuration arguments separated by a space e.g. "cfg:default.paths.data=/dev/nullX cfg:default.paths.logs=/dev/nullX" Also, it is backwards compatible with similar the previous configuration method through tailing arguments. Tailing arguments take presedence over the configuration options string. * Only log configuration information in debug mode * Move the grafana-cli binary to $GRAFANA_HOME/bin As part of the package install process, we copy all the release files and directories into the grafana home directory. This includes the /bin folder from where we copied the binaries into their respective destinations. After that, the /bin folder gets deleted as we don't want to keep duplicates of the binaries around. As part of this commit, we moved the re-creation of /bin within grafana-home and the copy of the original binary (again) after the folder gets deleted. --- build.go | 53 +++++++++++++++-------- docs/sources/administration/cli.md | 2 +- packaging/wrappers/grafana-cli | 39 +++++++++++++++++ pkg/cmd/grafana-cli/commands/commands.go | 26 ++++------- pkg/cmd/grafana-cli/main.go | 12 +++++ pkg/cmd/grafana-cli/utils/command_line.go | 8 ++++ 6 files changed, 104 insertions(+), 36 deletions(-) create mode 100755 packaging/wrappers/grafana-cli diff --git a/build.go b/build.go index 41441e9d8920f..6cfa79ea75093 100644 --- a/build.go +++ b/build.go @@ -43,7 +43,9 @@ var ( workingDir string includeBuildId bool = true buildId string = "0" - binaries []string = []string{"grafana-server", "grafana-cli"} + serverBinary string = "grafana-server" + cliBinary string = "grafana-cli" + binaries []string = []string{serverBinary, cliBinary} isDev bool = false enterprise bool = false skipRpmGen bool = false @@ -230,6 +232,7 @@ type linuxPackageOptions struct { packageType string packageArch string homeDir string + homeBinDir string binPath string serverBinPath string cliBinPath string @@ -240,10 +243,11 @@ type linuxPackageOptions struct { initdScriptFilePath string systemdServiceFilePath string - postinstSrc string - initdScriptSrc string - defaultFileSrc string - systemdFileSrc string + postinstSrc string + initdScriptSrc string + defaultFileSrc string + systemdFileSrc string + cliBinaryWrapperSrc string depends []string } @@ -258,6 +262,7 @@ func createDebPackages() { packageType: "deb", packageArch: debPkgArch, homeDir: "/usr/share/grafana", + homeBinDir: "/usr/share/grafana/bin", binPath: "/usr/sbin", configDir: "/etc/grafana", etcDefaultPath: "/etc/default", @@ -265,10 +270,11 @@ func createDebPackages() { initdScriptFilePath: "/etc/init.d/grafana-server", systemdServiceFilePath: "/usr/lib/systemd/system/grafana-server.service", - postinstSrc: "packaging/deb/control/postinst", - initdScriptSrc: "packaging/deb/init.d/grafana-server", - defaultFileSrc: "packaging/deb/default/grafana-server", - systemdFileSrc: "packaging/deb/systemd/grafana-server.service", + postinstSrc: "packaging/deb/control/postinst", + initdScriptSrc: "packaging/deb/init.d/grafana-server", + defaultFileSrc: "packaging/deb/default/grafana-server", + systemdFileSrc: "packaging/deb/systemd/grafana-server.service", + cliBinaryWrapperSrc: "packaging/wrappers/grafana-cli", depends: []string{"adduser", "libfontconfig1"}, }) @@ -286,6 +292,7 @@ func createRpmPackages() { packageType: "rpm", packageArch: rpmPkgArch, homeDir: "/usr/share/grafana", + homeBinDir: "/usr/share/grafana/bin", binPath: "/usr/sbin", configDir: "/etc/grafana", etcDefaultPath: "/etc/sysconfig", @@ -293,10 +300,11 @@ func createRpmPackages() { initdScriptFilePath: "/etc/init.d/grafana-server", systemdServiceFilePath: "/usr/lib/systemd/system/grafana-server.service", - postinstSrc: "packaging/rpm/control/postinst", - initdScriptSrc: "packaging/rpm/init.d/grafana-server", - defaultFileSrc: "packaging/rpm/sysconfig/grafana-server", - systemdFileSrc: "packaging/rpm/systemd/grafana-server.service", + postinstSrc: "packaging/rpm/control/postinst", + initdScriptSrc: "packaging/rpm/init.d/grafana-server", + defaultFileSrc: "packaging/rpm/sysconfig/grafana-server", + systemdFileSrc: "packaging/rpm/systemd/grafana-server.service", + cliBinaryWrapperSrc: "packaging/wrappers/grafana-cli", depends: []string{"/sbin/service", "fontconfig", "freetype", "urw-fonts"}, }) @@ -323,10 +331,12 @@ func createPackage(options linuxPackageOptions) { runPrint("mkdir", "-p", filepath.Join(packageRoot, "/usr/lib/systemd/system")) runPrint("mkdir", "-p", filepath.Join(packageRoot, "/usr/sbin")) - // copy binary - for _, binary := range binaries { - runPrint("cp", "-p", filepath.Join(workingDir, "tmp/bin/"+binary), filepath.Join(packageRoot, "/usr/sbin/"+binary)) - } + // copy grafana-cli wrapper + runPrint("cp", "-p", options.cliBinaryWrapperSrc, filepath.Join(packageRoot, "/usr/sbin/"+cliBinary)) + + // copy grafana-server binary + runPrint("cp", "-p", filepath.Join(workingDir, "tmp/bin/"+serverBinary), filepath.Join(packageRoot, "/usr/sbin/"+serverBinary)) + // copy init.d script runPrint("cp", "-p", options.initdScriptSrc, filepath.Join(packageRoot, options.initdScriptFilePath)) // copy environment var file @@ -338,6 +348,13 @@ func createPackage(options linuxPackageOptions) { // remove bin path runPrint("rm", "-rf", filepath.Join(packageRoot, options.homeDir, "bin")) + // create /bin within home + runPrint("mkdir", "-p", filepath.Join(packageRoot, options.homeBinDir)) + // The grafana-cli binary is exposed through a wrapper to ensure a proper + // configuration is in place. To enable that, we need to store the original + // binary in a separate location to avoid conflicts. + runPrint("cp", "-p", filepath.Join(workingDir, "tmp/bin/"+cliBinary), filepath.Join(packageRoot, options.homeBinDir, cliBinary)) + args := []string{ "-s", "dir", "--description", "Grafana", @@ -391,7 +408,7 @@ func createPackage(options linuxPackageOptions) { args = append(args, "--iteration", linuxPackageIteration) } - // add dependenciesj + // add dependencies for _, dep := range options.depends { args = append(args, "--depends", dep) } diff --git a/docs/sources/administration/cli.md b/docs/sources/administration/cli.md index 998627ca25f7c..74943c9dc2219 100644 --- a/docs/sources/administration/cli.md +++ b/docs/sources/administration/cli.md @@ -37,7 +37,7 @@ If running the command returns this error: then there are two flags that can be used to set homepath and the config file path. -`grafana-cli admin reset-admin-password --homepath "/usr/share/grafana" newpass` +`grafana-cli --homepath "/usr/share/grafana" admin reset-admin-password newpass` If you have not lost the admin password then it is better to set in the Grafana UI. If you need to set the password in a script then the [Grafana API](http://docs.grafana.org/http_api/user/#change-password) can be used. Here is an example using curl with basic auth: diff --git a/packaging/wrappers/grafana-cli b/packaging/wrappers/grafana-cli new file mode 100755 index 0000000000000..9cad151c0d7ad --- /dev/null +++ b/packaging/wrappers/grafana-cli @@ -0,0 +1,39 @@ +#! /usr/bin/env bash + +# Wrapper for the grafana-cli binary +# This file serves as a wrapper for the grafana-cli binary. It ensures we set +# the system-wide Grafana configuration that was bundled with the package as we +# use the binary. + +DEFAULT=/etc/default/grafana + +GRAFANA_HOME=/usr/share/grafana +CONF_DIR=/etc/grafana +DATA_DIR=/var/lib/grafana +PLUGINS_DIR=/var/lib/grafana/plugins +LOG_DIR=/var/log/grafana + +CONF_FILE=$CONF_DIR/grafana.ini +PROVISIONING_CFG_DIR=$CONF_DIR/provisioning + +EXECUTABLE=$GRAFANA_HOME/bin/grafana-cli + +if [ ! -x $EXECUTABLE ]; then + echo "Program not installed or not executable" + exit 5 +fi + +# overwrite settings from default file +if [ -f "$DEFAULT" ]; then + . "$DEFAULT" +fi + +OPTS="--homepath=${GRAFANA_HOME} \ + --config=${CONF_FILE} \ + --pluginsDir=${PLUGINS_DIR} \ + --configOverrides='cfg:default.paths.provisioning=$PROVISIONING_CFG_DIR \ + cfg:default.paths.data=${DATA_DIR} \ + cfg:default.paths.logs=${LOG_DIR} \ + cfg:default.paths.plugins=${PLUGINS_DIR}'" + +eval $EXECUTABLE "$OPTS" "$@" diff --git a/pkg/cmd/grafana-cli/commands/commands.go b/pkg/cmd/grafana-cli/commands/commands.go index b5a47ecb765b0..4c20cd4f0c368 100644 --- a/pkg/cmd/grafana-cli/commands/commands.go +++ b/pkg/cmd/grafana-cli/commands/commands.go @@ -2,6 +2,7 @@ package commands import ( "os" + "strings" "github.com/codegangsta/cli" "github.com/fatih/color" @@ -16,16 +17,20 @@ import ( func runDbCommand(command func(commandLine utils.CommandLine, sqlStore *sqlstore.SqlStore) error) func(context *cli.Context) { return func(context *cli.Context) { cmd := &utils.ContextCommandLine{Context: context} + debug := cmd.GlobalBool("debug") cfg := setting.NewCfg() + configOptions := strings.Split(cmd.GlobalString("configOverrides"), " ") cfg.Load(&setting.CommandLineArgs{ - Config: cmd.String("config"), - HomePath: cmd.String("homepath"), - Args: context.Args(), + Config: cmd.ConfigFile(), + HomePath: cmd.HomePath(), + Args: append(configOptions, cmd.Args()...), // tailing arguments have precedence over the options string }) - cfg.LogConfigSources() + if debug { + cfg.LogConfigSources() + } engine := &sqlstore.SqlStore{} engine.Cfg = cfg @@ -95,23 +100,11 @@ var pluginCommands = []cli.Command{ }, } -var dbCommandFlags = []cli.Flag{ - cli.StringFlag{ - Name: "homepath", - Usage: "path to grafana install/home path, defaults to working directory", - }, - cli.StringFlag{ - Name: "config", - Usage: "path to config file", - }, -} - var adminCommands = []cli.Command{ { Name: "reset-admin-password", Usage: "reset-admin-password ", Action: runDbCommand(resetPasswordCommand), - Flags: dbCommandFlags, }, { Name: "data-migration", @@ -121,7 +114,6 @@ var adminCommands = []cli.Command{ Name: "encrypt-datasource-passwords", Usage: "Migrates passwords from unsecured fields to secure_json_data field. Return ok unless there is an error. Safe to execute multiple times.", Action: runDbCommand(datamigrations.EncryptDatasourcePaswords), - Flags: dbCommandFlags, }, }, }, diff --git a/pkg/cmd/grafana-cli/main.go b/pkg/cmd/grafana-cli/main.go index 016acde802a75..4c4039c071f03 100644 --- a/pkg/cmd/grafana-cli/main.go +++ b/pkg/cmd/grafana-cli/main.go @@ -51,6 +51,18 @@ func main() { Name: "debug, d", Usage: "enable debug logging", }, + cli.StringFlag{ + Name: "configOverrides", + Usage: "configuration options to override defaults as a string. e.g. cfg:default.paths.log=/dev/null", + }, + cli.StringFlag{ + Name: "homepath", + Usage: "path to grafana install/home path, defaults to working directory", + }, + cli.StringFlag{ + Name: "config", + Usage: "path to config file", + }, } app.Before = func(c *cli.Context) error { diff --git a/pkg/cmd/grafana-cli/utils/command_line.go b/pkg/cmd/grafana-cli/utils/command_line.go index d3142d0f195ec..15546f2f392ec 100644 --- a/pkg/cmd/grafana-cli/utils/command_line.go +++ b/pkg/cmd/grafana-cli/utils/command_line.go @@ -38,6 +38,10 @@ func (c *ContextCommandLine) Application() *cli.App { return c.App } +func (c *ContextCommandLine) HomePath() string { return c.GlobalString("homepath") } + +func (c *ContextCommandLine) ConfigFile() string { return c.GlobalString("config") } + func (c *ContextCommandLine) PluginDirectory() string { return c.GlobalString("pluginsDir") } @@ -49,3 +53,7 @@ func (c *ContextCommandLine) RepoDirectory() string { func (c *ContextCommandLine) PluginURL() string { return c.GlobalString("pluginUrl") } + +func (c *ContextCommandLine) OptionsString() string { + return c.GlobalString("configOverrides") +} From eecd8d1064c2e4594baf96266b0d604dec6b89b8 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 24 Jun 2019 22:15:03 +0200 Subject: [PATCH 20/20] Elasticsearch: Visualize logs in Explore (#17605) * explore: try to use existing mode when switching datasource * elasticsearch: initial explore logs support * Elasticsearch: Adds ElasticsearchOptions type Updates tests accordingly * Elasticsearch: Adds typing to query method * Elasticsearch: Makes maxConcurrentShardRequests optional * Explore: Allows empty query for elasticsearch datasource * Elasticsearch: Unifies ElasticsearchQuery interface definition Removes check for context === 'explore' * Elasticsearch: Removes context property from ElasticsearchQuery interface Adds field property Removes metricAggs property Adds typing to metrics property * Elasticsearch: Runs default 'empty' query when 'clear all' button is pressed * Elasticsearch: Removes index property from ElasticsearchOptions interface * Elasticsearch: Removes commented code from ElasticsearchQueryField.tsx * Elasticsearch: Adds comment warning usage of for...in to elastic_response.ts * Elasticsearch: adds tests related to log queries --- devenv/datasources.yaml | 3 + pkg/api/frontendsettings.go | 6 +- public/app/features/explore/state/reducers.ts | 2 +- .../components/ElasticsearchQueryField.tsx | 91 ++++++++++ .../datasource/elasticsearch/config_ctrl.ts | 2 + .../datasource/elasticsearch/datasource.ts | 87 ++++++--- .../elasticsearch/elastic_response.ts | 140 +++++++++++++++ .../datasource/elasticsearch/metric_agg.ts | 3 +- .../datasource/elasticsearch/module.ts | 13 +- .../elasticsearch/partials/config.html | 13 ++ .../datasource/elasticsearch/plugin.json | 1 + .../datasource/elasticsearch/query_builder.ts | 27 +++ .../datasource/elasticsearch/query_ctrl.ts | 3 +- .../elasticsearch/specs/datasource.test.ts | 168 +++++++++++++++--- .../specs/elastic_response.test.ts | 90 ++++++++++ .../elasticsearch/specs/query_builder.test.ts | 6 + .../plugins/datasource/elasticsearch/types.ts | 26 +++ 17 files changed, 617 insertions(+), 64 deletions(-) create mode 100644 public/app/plugins/datasource/elasticsearch/components/ElasticsearchQueryField.tsx create mode 100644 public/app/plugins/datasource/elasticsearch/types.ts diff --git a/devenv/datasources.yaml b/devenv/datasources.yaml index 509a603862e8d..a805d153336e8 100644 --- a/devenv/datasources.yaml +++ b/devenv/datasources.yaml @@ -153,6 +153,9 @@ datasources: interval: Daily timeField: "@timestamp" esVersion: 70 + timeInterval: "10s" + logMessageField: message + logLevelField: fields.level - name: gdev-elasticsearch-v7-metricbeat type: elasticsearch diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 643a53d18fbd6..b9b1db32bca1e 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -115,11 +115,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *m.ReqContext) (map[string]interf } } - if ds.Type == m.DS_ES { - dsMap["index"] = ds.Database - } - - if ds.Type == m.DS_INFLUXDB { + if (ds.Type == m.DS_INFLUXDB) || (ds.Type == m.DS_ES) { dsMap["database"] = ds.Database } diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index eed4be614ac54..5c159611cdf87 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -240,7 +240,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta const supportsGraph = datasourceInstance.meta.metrics; const supportsLogs = datasourceInstance.meta.logs; - let mode = ExploreMode.Metrics; + let mode = state.mode || ExploreMode.Metrics; const supportedModes: ExploreMode[] = []; if (supportsGraph) { diff --git a/public/app/plugins/datasource/elasticsearch/components/ElasticsearchQueryField.tsx b/public/app/plugins/datasource/elasticsearch/components/ElasticsearchQueryField.tsx new file mode 100644 index 0000000000000..b19d6db2ff278 --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/components/ElasticsearchQueryField.tsx @@ -0,0 +1,91 @@ +import _ from 'lodash'; +import React from 'react'; +// @ts-ignore +import PluginPrism from 'slate-prism'; +// @ts-ignore +import Prism from 'prismjs'; + +// dom also includes Element polyfills +import QueryField from 'app/features/explore/QueryField'; +import { ExploreQueryFieldProps } from '@grafana/ui'; +import { ElasticDatasource } from '../datasource'; +import { ElasticsearchOptions, ElasticsearchQuery } from '../types'; + +interface Props extends ExploreQueryFieldProps {} + +interface State { + syntaxLoaded: boolean; +} + +class ElasticsearchQueryField extends React.PureComponent { + plugins: any[]; + + constructor(props: Props, context: React.Context) { + super(props, context); + + this.plugins = [ + PluginPrism({ + onlyIn: (node: any) => node.type === 'code_block', + getSyntax: (node: any) => 'lucene', + }), + ]; + + this.state = { + syntaxLoaded: false, + }; + } + + componentDidMount() { + this.onChangeQuery('', true); + } + + componentWillUnmount() {} + + componentDidUpdate(prevProps: Props) { + // if query changed from the outside (i.e. cleared via explore toolbar) + if (!this.props.query.isLogsQuery) { + this.onChangeQuery('', true); + } + } + + onChangeQuery = (value: string, override?: boolean) => { + // Send text change to parent + const { query, onChange, onRunQuery } = this.props; + if (onChange) { + const nextQuery: ElasticsearchQuery = { ...query, query: value, isLogsQuery: true }; + onChange(nextQuery); + + if (override && onRunQuery) { + onRunQuery(); + } + } + }; + + render() { + const { queryResponse, query } = this.props; + const { syntaxLoaded } = this.state; + + return ( + <> +
+
+ +
+
+ {queryResponse && queryResponse.error ? ( +
{queryResponse.error.message}
+ ) : null} + + ); + } +} + +export default ElasticsearchQueryField; diff --git a/public/app/plugins/datasource/elasticsearch/config_ctrl.ts b/public/app/plugins/datasource/elasticsearch/config_ctrl.ts index a3b22ce712c93..adc266fa6dfe8 100644 --- a/public/app/plugins/datasource/elasticsearch/config_ctrl.ts +++ b/public/app/plugins/datasource/elasticsearch/config_ctrl.ts @@ -11,6 +11,8 @@ export class ElasticConfigCtrl { const defaultMaxConcurrentShardRequests = this.current.jsonData.esVersion >= 70 ? 5 : 256; this.current.jsonData.maxConcurrentShardRequests = this.current.jsonData.maxConcurrentShardRequests || defaultMaxConcurrentShardRequests; + this.current.jsonData.logMessageField = this.current.jsonData.logMessageField || ''; + this.current.jsonData.logLevelField = this.current.jsonData.logLevelField || ''; } indexPatternTypes = [ diff --git a/public/app/plugins/datasource/elasticsearch/datasource.ts b/public/app/plugins/datasource/elasticsearch/datasource.ts index 1f7ed97b7d67b..ea9389bf9dc1c 100644 --- a/public/app/plugins/datasource/elasticsearch/datasource.ts +++ b/public/app/plugins/datasource/elasticsearch/datasource.ts @@ -1,11 +1,17 @@ -import angular from 'angular'; +import angular, { IQService } from 'angular'; import _ from 'lodash'; +import { DataSourceApi, DataSourceInstanceSettings, DataQueryRequest, DataQueryResponse } from '@grafana/ui'; import { ElasticResponse } from './elastic_response'; import { IndexPattern } from './index_pattern'; import { ElasticQueryBuilder } from './query_builder'; import { toUtc } from '@grafana/ui/src/utils/moment_wrapper'; +import * as queryDef from './query_def'; +import { BackendSrv } from 'app/core/services/backend_srv'; +import { TemplateSrv } from 'app/features/templating/template_srv'; +import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; +import { ElasticsearchOptions, ElasticsearchQuery } from './types'; -export class ElasticDatasource { +export class ElasticDatasource extends DataSourceApi { basicAuth: string; withCredentials: boolean; url: string; @@ -17,23 +23,44 @@ export class ElasticDatasource { maxConcurrentShardRequests: number; queryBuilder: ElasticQueryBuilder; indexPattern: IndexPattern; + logMessageField?: string; + logLevelField?: string; /** @ngInject */ - constructor(instanceSettings, private $q, private backendSrv, private templateSrv, private timeSrv) { + constructor( + instanceSettings: DataSourceInstanceSettings, + private $q: IQService, + private backendSrv: BackendSrv, + private templateSrv: TemplateSrv, + private timeSrv: TimeSrv + ) { + super(instanceSettings); this.basicAuth = instanceSettings.basicAuth; this.withCredentials = instanceSettings.withCredentials; this.url = instanceSettings.url; this.name = instanceSettings.name; - this.index = instanceSettings.index; - this.timeField = instanceSettings.jsonData.timeField; - this.esVersion = instanceSettings.jsonData.esVersion; - this.indexPattern = new IndexPattern(instanceSettings.index, instanceSettings.jsonData.interval); - this.interval = instanceSettings.jsonData.timeInterval; - this.maxConcurrentShardRequests = instanceSettings.jsonData.maxConcurrentShardRequests; + this.index = instanceSettings.database; + const settingsData = instanceSettings.jsonData || ({} as ElasticsearchOptions); + + this.timeField = settingsData.timeField; + this.esVersion = settingsData.esVersion; + this.indexPattern = new IndexPattern(this.index, settingsData.interval); + this.interval = settingsData.timeInterval; + this.maxConcurrentShardRequests = settingsData.maxConcurrentShardRequests; this.queryBuilder = new ElasticQueryBuilder({ timeField: this.timeField, esVersion: this.esVersion, }); + this.logMessageField = settingsData.logMessageField || ''; + this.logLevelField = settingsData.logLevelField || ''; + + if (this.logMessageField === '') { + this.logMessageField = null; + } + + if (this.logLevelField === '') { + this.logLevelField = null; + } } private request(method, url, data?) { @@ -200,7 +227,6 @@ export class ElasticDatasource { } testDatasource() { - this.timeSrv.setTime({ from: 'now-1m', to: 'now' }, true); // validate that the index exist and has date field return this.getFields({ type: 'date' }).then( dateFields => { @@ -240,10 +266,10 @@ export class ElasticDatasource { return angular.toJson(queryHeader); } - query(options) { + query(options: DataQueryRequest): Promise { let payload = ''; const targets = _.cloneDeep(options.targets); - const sentTargets = []; + const sentTargets: ElasticsearchQuery[] = []; // add global adhoc filters to timeFilter const adhocFilters = this.templateSrv.getAdhocFilters(this.name); @@ -253,16 +279,25 @@ export class ElasticDatasource { continue; } - if (target.alias) { - target.alias = this.templateSrv.replace(target.alias, options.scopedVars, 'lucene'); - } - let queryString = this.templateSrv.replace(target.query, options.scopedVars, 'lucene'); // Elasticsearch queryString should always be '*' if empty string if (!queryString || queryString === '') { queryString = '*'; } - const queryObj = this.queryBuilder.build(target, adhocFilters, queryString); + + let queryObj; + if (target.isLogsQuery) { + target.bucketAggs = [queryDef.defaultBucketAgg()]; + target.metrics = [queryDef.defaultMetricAgg()]; + queryObj = this.queryBuilder.getLogsQuery(target, queryString); + } else { + if (target.alias) { + target.alias = this.templateSrv.replace(target.alias, options.scopedVars, 'lucene'); + } + + queryObj = this.queryBuilder.build(target, adhocFilters, queryString); + } + const esQuery = angular.toJson(queryObj); const searchType = queryObj.size === 0 && this.esVersion < 5 ? 'count' : 'query_then_fetch'; @@ -270,21 +305,27 @@ export class ElasticDatasource { payload += header + '\n'; payload += esQuery + '\n'; + sentTargets.push(target); } if (sentTargets.length === 0) { - return this.$q.when([]); + return Promise.resolve({ data: [] }); } - payload = payload.replace(/\$timeFrom/g, options.range.from.valueOf()); - payload = payload.replace(/\$timeTo/g, options.range.to.valueOf()); + payload = payload.replace(/\$timeFrom/g, options.range.from.valueOf().toString()); + payload = payload.replace(/\$timeTo/g, options.range.to.valueOf().toString()); payload = this.templateSrv.replace(payload, options.scopedVars); const url = this.getMultiSearchUrl(); return this.post(url, payload).then(res => { - return new ElasticResponse(sentTargets, res).getTimeSeries(); + const er = new ElasticResponse(sentTargets, res); + if (sentTargets.some(target => target.isLogsQuery)) { + return er.getLogs(this.logMessageField, this.logLevelField); + } + + return er.getTimeSeries(); }); } @@ -380,8 +421,8 @@ export class ElasticDatasource { const header = this.getQueryHeader(searchType, range.from, range.to); let esQuery = angular.toJson(this.queryBuilder.getTermsQuery(queryDef)); - esQuery = esQuery.replace(/\$timeFrom/g, range.from.valueOf()); - esQuery = esQuery.replace(/\$timeTo/g, range.to.valueOf()); + esQuery = esQuery.replace(/\$timeFrom/g, range.from.valueOf().toString()); + esQuery = esQuery.replace(/\$timeTo/g, range.to.valueOf().toString()); esQuery = header + '\n' + esQuery + '\n'; const url = this.getMultiSearchUrl(); diff --git a/public/app/plugins/datasource/elasticsearch/elastic_response.ts b/public/app/plugins/datasource/elasticsearch/elastic_response.ts index b6424c7667b20..5449901f268a0 100644 --- a/public/app/plugins/datasource/elasticsearch/elastic_response.ts +++ b/public/app/plugins/datasource/elasticsearch/elastic_response.ts @@ -1,6 +1,8 @@ import _ from 'lodash'; +import flatten from 'app/core/utils/flatten'; import * as queryDef from './query_def'; import TableModel from 'app/core/table_model'; +import { SeriesData, DataQueryResponse, toSeriesData, FieldType } from '@grafana/ui'; export class ElasticResponse { constructor(private targets, private response) { @@ -410,4 +412,142 @@ export class ElasticResponse { return { data: seriesList }; } + + getLogs(logMessageField?: string, logLevelField?: string): DataQueryResponse { + const seriesData: SeriesData[] = []; + const docs: any[] = []; + + for (let n = 0; n < this.response.responses.length; n++) { + const response = this.response.responses[n]; + if (response.error) { + throw this.getErrorFromElasticResponse(this.response, response.error); + } + + const hits = response.hits; + let propNames: string[] = []; + let propName, hit, doc, i; + + for (i = 0; i < hits.hits.length; i++) { + hit = hits.hits[i]; + const flattened = hit._source ? flatten(hit._source, null) : {}; + doc = {}; + doc[this.targets[0].timeField] = null; + doc = { + ...doc, + _id: hit._id, + _type: hit._type, + _index: hit._index, + ...flattened, + }; + + // Note: the order of for...in is arbitrary amd implementation dependant + // and should probably not be relied upon. + for (propName in hit.fields) { + if (propNames.indexOf(propName) === -1) { + propNames.push(propName); + } + doc[propName] = hit.fields[propName]; + } + + for (propName in doc) { + if (propNames.indexOf(propName) === -1) { + propNames.push(propName); + } + } + + doc._source = { ...flattened }; + + docs.push(doc); + } + + if (docs.length > 0) { + propNames = propNames.sort(); + const series: SeriesData = { + fields: [ + { + name: this.targets[0].timeField, + type: FieldType.time, + }, + ], + rows: [], + }; + + if (logMessageField) { + series.fields.push({ + name: logMessageField, + type: FieldType.string, + }); + } else { + series.fields.push({ + name: '_source', + type: FieldType.string, + }); + } + + if (logLevelField) { + series.fields.push({ + name: 'level', + type: FieldType.string, + }); + } + + for (const propName of propNames) { + if (propName === this.targets[0].timeField || propName === '_source') { + continue; + } + + series.fields.push({ + name: propName, + type: FieldType.string, + }); + } + + for (const doc of docs) { + const row: any[] = []; + row.push(doc[this.targets[0].timeField][0]); + + if (logMessageField) { + row.push(doc[logMessageField] || ''); + } else { + row.push(JSON.stringify(doc._source, null, 2)); + } + + if (logLevelField) { + row.push(doc[logLevelField] || ''); + } + + for (const propName of propNames) { + if (doc.hasOwnProperty(propName)) { + row.push(doc[propName]); + } else { + row.push(null); + } + } + + series.rows.push(row); + } + + seriesData.push(series); + } + + if (response.aggregations) { + const aggregations = response.aggregations; + const target = this.targets[n]; + const tmpSeriesList = []; + const table = new TableModel(); + + this.processBuckets(aggregations, target, tmpSeriesList, table, {}, 0); + this.trimDatapoints(tmpSeriesList, target); + this.nameSeries(tmpSeriesList, target); + + for (let y = 0; y < tmpSeriesList.length; y++) { + const series = toSeriesData(tmpSeriesList[y]); + series.labels = {}; + seriesData.push(series); + } + } + } + + return { data: seriesData }; + } } diff --git a/public/app/plugins/datasource/elasticsearch/metric_agg.ts b/public/app/plugins/datasource/elasticsearch/metric_agg.ts index 15a3808282948..216d88318cf9e 100644 --- a/public/app/plugins/datasource/elasticsearch/metric_agg.ts +++ b/public/app/plugins/datasource/elasticsearch/metric_agg.ts @@ -1,11 +1,12 @@ import coreModule from 'app/core/core_module'; import _ from 'lodash'; import * as queryDef from './query_def'; +import { ElasticsearchAggregation } from './types'; export class ElasticMetricAggCtrl { /** @ngInject */ constructor($scope, uiSegmentSrv, $q, $rootScope) { - const metricAggs = $scope.target.metrics; + const metricAggs: ElasticsearchAggregation[] = $scope.target.metrics; $scope.metricAggTypes = queryDef.getMetricAggTypes($scope.esVersion); $scope.extendedStats = queryDef.extendedStats; $scope.pipelineAggOptions = []; diff --git a/public/app/plugins/datasource/elasticsearch/module.ts b/public/app/plugins/datasource/elasticsearch/module.ts index 022347d43c637..ea384fec5b93a 100644 --- a/public/app/plugins/datasource/elasticsearch/module.ts +++ b/public/app/plugins/datasource/elasticsearch/module.ts @@ -1,14 +1,15 @@ +import { DataSourcePlugin } from '@grafana/ui'; import { ElasticDatasource } from './datasource'; import { ElasticQueryCtrl } from './query_ctrl'; import { ElasticConfigCtrl } from './config_ctrl'; +import ElasticsearchQueryField from './components/ElasticsearchQueryField'; class ElasticAnnotationsQueryCtrl { static templateUrl = 'partials/annotations.editor.html'; } -export { - ElasticDatasource as Datasource, - ElasticQueryCtrl as QueryCtrl, - ElasticConfigCtrl as ConfigCtrl, - ElasticAnnotationsQueryCtrl as AnnotationsQueryCtrl, -}; +export const plugin = new DataSourcePlugin(ElasticDatasource) + .setQueryCtrl(ElasticQueryCtrl) + .setConfigCtrl(ElasticConfigCtrl) + .setExploreLogsQueryField(ElasticsearchQueryField) + .setAnnotationQueryCtrl(ElasticAnnotationsQueryCtrl); diff --git a/public/app/plugins/datasource/elasticsearch/partials/config.html b/public/app/plugins/datasource/elasticsearch/partials/config.html index 6fd773fed497a..431933aa7bb91 100644 --- a/public/app/plugins/datasource/elasticsearch/partials/config.html +++ b/public/app/plugins/datasource/elasticsearch/partials/config.html @@ -51,3 +51,16 @@

Elasticsearch details

+ +Logs + +
+
+ Message field name + +
+
+ Level field name + +
+
diff --git a/public/app/plugins/datasource/elasticsearch/plugin.json b/public/app/plugins/datasource/elasticsearch/plugin.json index b34dcce9838e1..2390f8f6bc2a8 100644 --- a/public/app/plugins/datasource/elasticsearch/plugin.json +++ b/public/app/plugins/datasource/elasticsearch/plugin.json @@ -21,6 +21,7 @@ "alerting": true, "annotations": true, "metrics": true, + "logs": true, "queryOptions": { "minInterval": true diff --git a/public/app/plugins/datasource/elasticsearch/query_builder.ts b/public/app/plugins/datasource/elasticsearch/query_builder.ts index 7d4d23aa6f043..994a5f916635e 100644 --- a/public/app/plugins/datasource/elasticsearch/query_builder.ts +++ b/public/app/plugins/datasource/elasticsearch/query_builder.ts @@ -367,4 +367,31 @@ export class ElasticQueryBuilder { return query; } + + getLogsQuery(target, querystring) { + let query: any = { + size: 0, + query: { + bool: { + filter: [{ range: this.getRangeFilter() }], + }, + }, + }; + + if (target.query) { + query.query.bool.filter.push({ + query_string: { + analyze_wildcard: true, + query: target.query, + }, + }); + } + + query = this.documentQuery(query, 500); + + return { + ...query, + aggs: this.build(target, null, querystring).aggs, + }; + } } diff --git a/public/app/plugins/datasource/elasticsearch/query_ctrl.ts b/public/app/plugins/datasource/elasticsearch/query_ctrl.ts index 73a262427f449..47382c3e2e06c 100644 --- a/public/app/plugins/datasource/elasticsearch/query_ctrl.ts +++ b/public/app/plugins/datasource/elasticsearch/query_ctrl.ts @@ -6,6 +6,7 @@ import angular from 'angular'; import _ from 'lodash'; import * as queryDef from './query_def'; import { QueryCtrl } from 'app/plugins/sdk'; +import { ElasticsearchAggregation } from './types'; export class ElasticQueryCtrl extends QueryCtrl { static templateUrl = 'partials/query.editor.html'; @@ -53,7 +54,7 @@ export class ElasticQueryCtrl extends QueryCtrl { } getCollapsedText() { - const metricAggs = this.target.metrics; + const metricAggs: ElasticsearchAggregation[] = this.target.metrics; const bucketAggs = this.target.bucketAggs; const metricAggTypes = queryDef.getMetricAggTypes(this.esVersion); const bucketAggTypes = queryDef.bucketAggTypes; diff --git a/public/app/plugins/datasource/elasticsearch/specs/datasource.test.ts b/public/app/plugins/datasource/elasticsearch/specs/datasource.test.ts index 6578129f391de..ecfb99a160596 100644 --- a/public/app/plugins/datasource/elasticsearch/specs/datasource.test.ts +++ b/public/app/plugins/datasource/elasticsearch/specs/datasource.test.ts @@ -1,20 +1,25 @@ -import angular from 'angular'; +import angular, { IQService } from 'angular'; import * as dateMath from '@grafana/ui/src/utils/datemath'; import _ from 'lodash'; import { ElasticDatasource } from '../datasource'; import { toUtc, dateTime } from '@grafana/ui/src/utils/moment_wrapper'; +import { BackendSrv } from 'app/core/services/backend_srv'; +import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; +import { TemplateSrv } from 'app/features/templating/template_srv'; +import { DataSourceInstanceSettings } from '@grafana/ui'; +import { ElasticsearchOptions } from '../types'; describe('ElasticDatasource', function(this: any) { - const backendSrv = { + const backendSrv: any = { datasourceRequest: jest.fn(), }; - const $rootScope = { + const $rootScope: any = { $on: jest.fn(), appEvent: jest.fn(), }; - const templateSrv = { + const templateSrv: any = { replace: jest.fn(text => { if (text.startsWith('$')) { return `resolvedVariable`; @@ -25,12 +30,12 @@ describe('ElasticDatasource', function(this: any) { getAdhocFilters: jest.fn(() => []), }; - const timeSrv = { + const timeSrv: any = { time: { from: 'now-1h', to: 'now' }, timeRange: jest.fn(() => { return { - from: dateMath.parse(this.time.from, false), - to: dateMath.parse(this.time.to, true), + from: dateMath.parse(timeSrv.time.from, false), + to: dateMath.parse(timeSrv.time.to, true), }; }), setTime: jest.fn(time => { @@ -43,18 +48,24 @@ describe('ElasticDatasource', function(this: any) { backendSrv, } as any; - function createDatasource(instanceSettings) { - instanceSettings.jsonData = instanceSettings.jsonData || {}; - ctx.ds = new ElasticDatasource(instanceSettings, {}, backendSrv, templateSrv, timeSrv); + function createDatasource(instanceSettings: DataSourceInstanceSettings) { + instanceSettings.jsonData = instanceSettings.jsonData || ({} as ElasticsearchOptions); + ctx.ds = new ElasticDatasource( + instanceSettings, + {} as IQService, + backendSrv as BackendSrv, + templateSrv as TemplateSrv, + timeSrv as TimeSrv + ); } describe('When testing datasource with index pattern', () => { beforeEach(() => { createDatasource({ url: 'http://es.com', - index: '[asd-]YYYY.MM.DD', - jsonData: { interval: 'Daily', esVersion: '2' }, - }); + database: '[asd-]YYYY.MM.DD', + jsonData: { interval: 'Daily', esVersion: 2 } as ElasticsearchOptions, + } as DataSourceInstanceSettings); }); it('should translate index pattern to current day', () => { @@ -77,9 +88,9 @@ describe('ElasticDatasource', function(this: any) { beforeEach(async () => { createDatasource({ url: 'http://es.com', - index: '[asd-]YYYY.MM.DD', - jsonData: { interval: 'Daily', esVersion: '2' }, - }); + database: '[asd-]YYYY.MM.DD', + jsonData: { interval: 'Daily', esVersion: 2 } as ElasticsearchOptions, + } as DataSourceInstanceSettings); ctx.backendSrv.datasourceRequest = jest.fn(options => { requestOptions = options; @@ -142,15 +153,110 @@ describe('ElasticDatasource', function(this: any) { }); }); + describe('When issuing logs query with interval pattern', () => { + let query, queryBuilderSpy; + + beforeEach(async () => { + createDatasource({ + url: 'http://es.com', + database: 'mock-index', + jsonData: { interval: 'Daily', esVersion: 2, timeField: '@timestamp' } as ElasticsearchOptions, + } as DataSourceInstanceSettings); + + ctx.backendSrv.datasourceRequest = jest.fn(options => { + return Promise.resolve({ + data: { + responses: [ + { + aggregations: { + '2': { + buckets: [ + { + doc_count: 10, + key: 1000, + }, + { + doc_count: 15, + key: 2000, + }, + ], + }, + }, + hits: { + hits: [ + { + '@timestamp': ['2019-06-24T09:51:19.765Z'], + _id: 'fdsfs', + _type: '_doc', + _index: 'mock-index', + _source: { + '@timestamp': '2019-06-24T09:51:19.765Z', + host: 'djisaodjsoad', + message: 'hello, i am a message', + }, + fields: { + '@timestamp': ['2019-06-24T09:51:19.765Z'], + }, + }, + { + '@timestamp': ['2019-06-24T09:52:19.765Z'], + _id: 'kdospaidopa', + _type: '_doc', + _index: 'mock-index', + _source: { + '@timestamp': '2019-06-24T09:52:19.765Z', + host: 'dsalkdakdop', + message: 'hello, i am also message', + }, + fields: { + '@timestamp': ['2019-06-24T09:52:19.765Z'], + }, + }, + ], + }, + }, + ], + }, + }); + }); + + query = { + range: { + from: toUtc([2015, 4, 30, 10]), + to: toUtc([2019, 7, 1, 10]), + }, + targets: [ + { + alias: '$varAlias', + refId: 'A', + bucketAggs: [{ type: 'date_histogram', settings: { interval: 'auto' }, id: '2' }], + metrics: [{ type: 'count', id: '1' }], + query: 'escape\\:test', + interval: '10s', + isLogsQuery: true, + timeField: '@timestamp', + }, + ], + }; + + queryBuilderSpy = jest.spyOn(ctx.ds.queryBuilder, 'getLogsQuery'); + await ctx.ds.query(query); + }); + + it('should call getLogsQuery()', () => { + expect(queryBuilderSpy).toHaveBeenCalled(); + }); + }); + describe('When issuing document query', () => { let requestOptions, parts, header; beforeEach(() => { createDatasource({ url: 'http://es.com', - index: 'test', - jsonData: { esVersion: '2' }, - }); + database: 'test', + jsonData: { esVersion: 2 } as ElasticsearchOptions, + } as DataSourceInstanceSettings); ctx.backendSrv.datasourceRequest = jest.fn(options => { requestOptions = options; @@ -187,7 +293,11 @@ describe('ElasticDatasource', function(this: any) { describe('When getting fields', () => { beforeEach(() => { - createDatasource({ url: 'http://es.com', index: 'metricbeat', jsonData: { esVersion: 50 } }); + createDatasource({ + url: 'http://es.com', + database: 'metricbeat', + jsonData: { esVersion: 50 } as ElasticsearchOptions, + } as DataSourceInstanceSettings); ctx.backendSrv.datasourceRequest = jest.fn(options => { return Promise.resolve({ @@ -279,7 +389,11 @@ describe('ElasticDatasource', function(this: any) { describe('When getting fields from ES 7.0', () => { beforeEach(() => { - createDatasource({ url: 'http://es.com', index: 'genuine.es7._mapping.response', jsonData: { esVersion: 70 } }); + createDatasource({ + url: 'http://es.com', + database: 'genuine.es7._mapping.response', + jsonData: { esVersion: 70 } as ElasticsearchOptions, + } as DataSourceInstanceSettings); ctx.backendSrv.datasourceRequest = jest.fn(options => { return Promise.resolve({ @@ -430,9 +544,9 @@ describe('ElasticDatasource', function(this: any) { beforeEach(() => { createDatasource({ url: 'http://es.com', - index: 'test', - jsonData: { esVersion: '5' }, - }); + database: 'test', + jsonData: { esVersion: 5 } as ElasticsearchOptions, + } as DataSourceInstanceSettings); ctx.backendSrv.datasourceRequest = jest.fn(options => { requestOptions = options; @@ -473,9 +587,9 @@ describe('ElasticDatasource', function(this: any) { beforeEach(() => { createDatasource({ url: 'http://es.com', - index: 'test', - jsonData: { esVersion: '5' }, - }); + database: 'test', + jsonData: { esVersion: 5 } as ElasticsearchOptions, + } as DataSourceInstanceSettings); ctx.backendSrv.datasourceRequest = jest.fn(options => { requestOptions = options; diff --git a/public/app/plugins/datasource/elasticsearch/specs/elastic_response.test.ts b/public/app/plugins/datasource/elasticsearch/specs/elastic_response.test.ts index 1c6bcc863323f..b7144534761a2 100644 --- a/public/app/plugins/datasource/elasticsearch/specs/elastic_response.test.ts +++ b/public/app/plugins/datasource/elasticsearch/specs/elastic_response.test.ts @@ -784,4 +784,94 @@ describe('ElasticResponse', () => { expect(result.data[2].datapoints[1][0]).toBe(12); }); }); + + describe('simple logs query and count', () => { + beforeEach(() => { + targets = [ + { + refId: 'A', + metrics: [{ type: 'count', id: '1' }], + bucketAggs: [{ type: 'date_histogram', settings: { interval: 'auto' }, id: '2' }], + context: 'explore', + interval: '10s', + isLogsQuery: true, + key: 'Q-1561369883389-0.7611823271062786-0', + live: false, + maxDataPoints: 1620, + query: '', + timeField: '@timestamp', + }, + ]; + response = { + responses: [ + { + aggregations: { + '2': { + buckets: [ + { + doc_count: 10, + key: 1000, + }, + { + doc_count: 15, + key: 2000, + }, + ], + }, + }, + hits: { + hits: [ + { + _id: 'fdsfs', + _type: '_doc', + _index: 'mock-index', + _source: { + '@timestamp': '2019-06-24T09:51:19.765Z', + host: 'djisaodjsoad', + message: 'hello, i am a message', + }, + fields: { + '@timestamp': ['2019-06-24T09:51:19.765Z'], + }, + }, + { + _id: 'kdospaidopa', + _type: '_doc', + _index: 'mock-index', + _source: { + '@timestamp': '2019-06-24T09:52:19.765Z', + host: 'dsalkdakdop', + message: 'hello, i am also message', + }, + fields: { + '@timestamp': ['2019-06-24T09:52:19.765Z'], + }, + }, + ], + }, + }, + ], + }; + + result = new ElasticResponse(targets, response).getLogs(); + }); + + it('should return histogram aggregation and documents', () => { + expect(result.data.length).toBe(2); + expect(result.data[0].fields).toContainEqual({ name: '@timestamp', type: 'time' }); + expect(result.data[0].fields).toContainEqual({ name: 'host', type: 'string' }); + expect(result.data[0].fields).toContainEqual({ name: 'message', type: 'string' }); + result.data[0].rows.forEach((row, i) => { + expect(row).toContain(response.responses[0].hits.hits[i]._id); + expect(row).toContain(response.responses[0].hits.hits[i]._type); + expect(row).toContain(response.responses[0].hits.hits[i]._index); + expect(row).toContain(JSON.stringify(response.responses[0].hits.hits[i]._source, undefined, 2)); + }); + + expect(result.data[1]).toHaveProperty('name', 'Count'); + response.responses[0].aggregations['2'].buckets.forEach(bucket => { + expect(result.data[1].rows).toContainEqual([bucket.doc_count, bucket.key]); + }); + }); + }); }); diff --git a/public/app/plugins/datasource/elasticsearch/specs/query_builder.test.ts b/public/app/plugins/datasource/elasticsearch/specs/query_builder.test.ts index 993eaccfbe2fd..ef296543bd3f0 100644 --- a/public/app/plugins/datasource/elasticsearch/specs/query_builder.test.ts +++ b/public/app/plugins/datasource/elasticsearch/specs/query_builder.test.ts @@ -490,4 +490,10 @@ describe('ElasticQueryBuilder', () => { const query = builder6x.getTermsQuery({}); expect(query.aggs['1'].terms.order._key).toBe('asc'); }); + + it('getTermsQuery should request documents and date histogram', () => { + const query = builder.getLogsQuery({}); + expect(query).toHaveProperty('query.bool.filter'); + expect(query.aggs['2']).toHaveProperty('date_histogram'); + }); }); diff --git a/public/app/plugins/datasource/elasticsearch/types.ts b/public/app/plugins/datasource/elasticsearch/types.ts new file mode 100644 index 0000000000000..ef88a8512fd14 --- /dev/null +++ b/public/app/plugins/datasource/elasticsearch/types.ts @@ -0,0 +1,26 @@ +import { DataQuery, DataSourceJsonData } from '@grafana/ui'; + +export interface ElasticsearchOptions extends DataSourceJsonData { + timeField: string; + esVersion: number; + interval: string; + timeInterval: string; + maxConcurrentShardRequests?: number; + logMessageField?: string; + logLevelField?: string; +} + +export interface ElasticsearchAggregation { + id: string; + type: string; + settings?: any; + field?: string; +} + +export interface ElasticsearchQuery extends DataQuery { + isLogsQuery: boolean; + alias?: string; + query?: string; + bucketAggs?: ElasticsearchAggregation[]; + metrics?: ElasticsearchAggregation[]; +}