From a2967565deb39f8633da1fb4d653c909f505b409 Mon Sep 17 00:00:00 2001 From: Benjamin Schweizer Date: Mon, 30 Jul 2018 17:19:41 +0200 Subject: [PATCH 001/274] added urlescape formatting option --- docs/sources/reference/templating.md | 1 + public/app/features/templating/specs/template_srv.jest.ts | 5 +++++ public/app/features/templating/template_srv.ts | 7 +++++++ 3 files changed, 13 insertions(+) diff --git a/docs/sources/reference/templating.md b/docs/sources/reference/templating.md index ce1a1299d26..d59117fefea 100644 --- a/docs/sources/reference/templating.md +++ b/docs/sources/reference/templating.md @@ -52,6 +52,7 @@ Filter Option | Example | Raw | Interpolated | Description `csv`| ${servers:csv} | `'test1', 'test2'` | `test1,test2` | Formats multi-value variable as a comma-separated string `distributed`| ${servers:distributed} | `'test1', 'test2'` | `test1,servers=test2` | Formats multi-value variable in custom format for OpenTSDB. `lucene`| ${servers:lucene} | `'test', 'test2'` | `("test" OR "test2")` | Formats multi-value variable as a lucene expression. +`urlescape` | ${servers:urlescape} | `'foo()bar baz', 'test2'` | `{foo%28%29bar%20baz%2Ctest2}` | Formats multi-value variable into a glob, url escaped Test the formatting options on the [Grafana Play site](http://play.grafana.org/d/cJtIfcWiz/template-variable-formatting-options?orgId=1). diff --git a/public/app/features/templating/specs/template_srv.jest.ts b/public/app/features/templating/specs/template_srv.jest.ts index 86b6aa7ec99..040597888b6 100644 --- a/public/app/features/templating/specs/template_srv.jest.ts +++ b/public/app/features/templating/specs/template_srv.jest.ts @@ -275,6 +275,11 @@ describe('templateSrv', function() { expect(result).toBe('test,test2'); }); + it('multi value and urlescape format should render url-escaped string', function() { + var result = _templateSrv.formatValue(['foo()bar baz', 'test2'], 'urlescape'); + expect(result).toBe('foo%28%29bar%20baz%2Ctest2'); + }); + it('slash should be properly escaped in regex format', function() { var result = _templateSrv.formatValue('Gi3/14', 'regex'); expect(result).toBe('Gi3\\/14'); diff --git a/public/app/features/templating/template_srv.ts b/public/app/features/templating/template_srv.ts index fc79d12ff9e..7ce539b6506 100644 --- a/public/app/features/templating/template_srv.ts +++ b/public/app/features/templating/template_srv.ts @@ -124,6 +124,13 @@ export class TemplateSrv { } return value; } + case 'urlescape': { + // like glob, but url escaped + if (_.isArray(value)) { + return escape('{' + value.join(',') + '}'); + } + return escape(value); + } default: { if (_.isArray(value)) { return '{' + value.join(',') + '}'; From 7da9c33ae4dae4232d242e8ab6710be720bdfd35 Mon Sep 17 00:00:00 2001 From: Benjamin Schweizer Date: Mon, 30 Jul 2018 17:28:50 +0200 Subject: [PATCH 002/274] fixed test result --- public/app/features/templating/specs/template_srv.jest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/templating/specs/template_srv.jest.ts b/public/app/features/templating/specs/template_srv.jest.ts index 040597888b6..85a159fc098 100644 --- a/public/app/features/templating/specs/template_srv.jest.ts +++ b/public/app/features/templating/specs/template_srv.jest.ts @@ -277,7 +277,7 @@ describe('templateSrv', function() { it('multi value and urlescape format should render url-escaped string', function() { var result = _templateSrv.formatValue(['foo()bar baz', 'test2'], 'urlescape'); - expect(result).toBe('foo%28%29bar%20baz%2Ctest2'); + expect(result).toBe('%7Bfoo%28%29bar%20baz%2Ctest2%7D'); }); it('slash should be properly escaped in regex format', function() { From 9220f83b3dcd4376d1ec8a5ab5bdf0a0e031a65f Mon Sep 17 00:00:00 2001 From: Benjamin Schweizer Date: Mon, 6 Aug 2018 21:54:12 +0200 Subject: [PATCH 003/274] replaced escape() call, renamed formatter to be more expressive --- docs/sources/reference/templating.md | 2 +- .../features/templating/specs/template_srv.jest.ts | 6 +++--- public/app/features/templating/template_srv.ts | 13 ++++++++++--- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/docs/sources/reference/templating.md b/docs/sources/reference/templating.md index d59117fefea..1482eb34350 100644 --- a/docs/sources/reference/templating.md +++ b/docs/sources/reference/templating.md @@ -52,7 +52,7 @@ Filter Option | Example | Raw | Interpolated | Description `csv`| ${servers:csv} | `'test1', 'test2'` | `test1,test2` | Formats multi-value variable as a comma-separated string `distributed`| ${servers:distributed} | `'test1', 'test2'` | `test1,servers=test2` | Formats multi-value variable in custom format for OpenTSDB. `lucene`| ${servers:lucene} | `'test', 'test2'` | `("test" OR "test2")` | Formats multi-value variable as a lucene expression. -`urlescape` | ${servers:urlescape} | `'foo()bar baz', 'test2'` | `{foo%28%29bar%20baz%2Ctest2}` | Formats multi-value variable into a glob, url escaped +`percentencode` | ${servers:percentencode} | `'foo()bar BAZ', 'test2'` | `{foo%28%29bar%20BAZ%2Ctest2}` | Formats multi-value variable into a glob, percent-escaped Test the formatting options on the [Grafana Play site](http://play.grafana.org/d/cJtIfcWiz/template-variable-formatting-options?orgId=1). diff --git a/public/app/features/templating/specs/template_srv.jest.ts b/public/app/features/templating/specs/template_srv.jest.ts index 85a159fc098..b4501e81f59 100644 --- a/public/app/features/templating/specs/template_srv.jest.ts +++ b/public/app/features/templating/specs/template_srv.jest.ts @@ -275,9 +275,9 @@ describe('templateSrv', function() { expect(result).toBe('test,test2'); }); - it('multi value and urlescape format should render url-escaped string', function() { - var result = _templateSrv.formatValue(['foo()bar baz', 'test2'], 'urlescape'); - expect(result).toBe('%7Bfoo%28%29bar%20baz%2Ctest2%7D'); + it('multi value and percentencode format should render percent-encoded string', function() { + var result = _templateSrv.formatValue(['foo()bar BAZ', 'test2'], 'percentencode'); + expect(result).toBe('%7Bfoo%28%29bar%20BAZ%2Ctest2%7D'); }); it('slash should be properly escaped in regex format', function() { diff --git a/public/app/features/templating/template_srv.ts b/public/app/features/templating/template_srv.ts index 7ce539b6506..3d462f1bcde 100644 --- a/public/app/features/templating/template_srv.ts +++ b/public/app/features/templating/template_srv.ts @@ -83,6 +83,13 @@ export class TemplateSrv { return '(' + quotedValues.join(' OR ') + ')'; } + // like encodeURIComponent() but for all characters except alpha-numerics + encodeURIQueryValue(str) { + return str.replace(/[^a-z0-9]/gi, function(c) { + return '%' + c.charCodeAt(0).toString(16); + }); + } + formatValue(value, format, variable) { // for some scopedVars there is no variable variable = variable || {}; @@ -124,12 +131,12 @@ export class TemplateSrv { } return value; } - case 'urlescape': { + case 'percentencode': { // like glob, but url escaped if (_.isArray(value)) { - return escape('{' + value.join(',') + '}'); + return this.encodeURIQueryValue('{' + value.join(',') + '}'); } - return escape(value); + return this.encodeURIQueryValue(value); } default: { if (_.isArray(value)) { From a653b277f312766ebfe0bb00ea0f6268139591d1 Mon Sep 17 00:00:00 2001 From: Benjamin Schweizer Date: Mon, 6 Aug 2018 22:04:33 +0200 Subject: [PATCH 004/274] switched to lowercase --- public/app/features/templating/specs/template_srv.jest.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/templating/specs/template_srv.jest.ts b/public/app/features/templating/specs/template_srv.jest.ts index b4501e81f59..3d8b3af1ddd 100644 --- a/public/app/features/templating/specs/template_srv.jest.ts +++ b/public/app/features/templating/specs/template_srv.jest.ts @@ -277,7 +277,7 @@ describe('templateSrv', function() { it('multi value and percentencode format should render percent-encoded string', function() { var result = _templateSrv.formatValue(['foo()bar BAZ', 'test2'], 'percentencode'); - expect(result).toBe('%7Bfoo%28%29bar%20BAZ%2Ctest2%7D'); + expect(result).toBe('%7bfoo%28%29bar%20BAZ%2ctest2%7d'); }); it('slash should be properly escaped in regex format', function() { From f4b29b5782bea2efe8d428f9cc164678306cd7dd Mon Sep 17 00:00:00 2001 From: Benjamin Schweizer Date: Mon, 3 Sep 2018 16:08:52 +0200 Subject: [PATCH 005/274] fixed testcase --- public/app/features/templating/specs/template_srv.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/templating/specs/template_srv.test.ts b/public/app/features/templating/specs/template_srv.test.ts index 06c7ac552c9..3e5ddf8bf54 100644 --- a/public/app/features/templating/specs/template_srv.test.ts +++ b/public/app/features/templating/specs/template_srv.test.ts @@ -276,7 +276,7 @@ describe('templateSrv', function() { }); it('multi value and percentencode format should render percent-encoded string', function() { - var result = _templateSrv.formatValue(['foo()bar BAZ', 'test2'], 'percentencode'); + const result = _templateSrv.formatValue(['foo()bar BAZ', 'test2'], 'percentencode'); expect(result).toBe('%7bfoo%28%29bar%20BAZ%2ctest2%7d'); }); From 0644410b99be40be56c6acac2fcacffc0cc58169 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 14 Jan 2019 14:55:22 +0100 Subject: [PATCH 006/274] wip: react query editors --- public/app/features/dashboard/dashgrid/QueriesTab.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/public/app/features/dashboard/dashgrid/QueriesTab.tsx b/public/app/features/dashboard/dashgrid/QueriesTab.tsx index 47c4f358136..500dc5c4884 100644 --- a/public/app/features/dashboard/dashgrid/QueriesTab.tsx +++ b/public/app/features/dashboard/dashgrid/QueriesTab.tsx @@ -198,6 +198,11 @@ export class QueriesTab extends PureComponent { this.setState({ isAddingMixed: false }); }; + renderQueryRow(query: DataQuery) { + console.log('render query row', this.state.currentDS); + return
(this.element = element)} />; + } + render() { const { panel } = this.props; const { currentDS, isAddingMixed } = this.state; @@ -218,7 +223,7 @@ export class QueriesTab extends PureComponent { <>
-
(this.element = element)} /> + {panel.targets.map(query => this.renderQueryRow(query))}
From 0260c779e8f167bcd7a83af1b890ccd1b0b69b8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 14 Jan 2019 15:44:58 +0100 Subject: [PATCH 007/274] wip: another wip commit --- .../dashboard/panel_editor/QueriesTab.tsx | 7 +---- .../dashboard/panel_editor/QueryEditorRow.tsx | 30 +++++++++++++++++++ 2 files changed, 31 insertions(+), 6 deletions(-) create mode 100644 public/app/features/dashboard/panel_editor/QueryEditorRow.tsx diff --git a/public/app/features/dashboard/panel_editor/QueriesTab.tsx b/public/app/features/dashboard/panel_editor/QueriesTab.tsx index 500dc5c4884..47c4f358136 100644 --- a/public/app/features/dashboard/panel_editor/QueriesTab.tsx +++ b/public/app/features/dashboard/panel_editor/QueriesTab.tsx @@ -198,11 +198,6 @@ export class QueriesTab extends PureComponent { this.setState({ isAddingMixed: false }); }; - renderQueryRow(query: DataQuery) { - console.log('render query row', this.state.currentDS); - return
(this.element = element)} />; - } - render() { const { panel } = this.props; const { currentDS, isAddingMixed } = this.state; @@ -223,7 +218,7 @@ export class QueriesTab extends PureComponent { <>
- {panel.targets.map(query => this.renderQueryRow(query))} +
(this.element = element)} />
diff --git a/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx b/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx new file mode 100644 index 00000000000..b90c11da7c3 --- /dev/null +++ b/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx @@ -0,0 +1,30 @@ +// Libraries +import React, { PureComponent } from 'react'; + +// Utils & Services +import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader'; + +// Types +import { PanelModel } from '../panel_model'; +import { DashboardModel } from '../dashboard_model'; + +interface Props { + panel: PanelModel; + dashboard: DashboardModel; +} + +interface State { +} + +export class VisualizationTab extends PureComponent { + element: HTMLElement; + angularQueryEditor: AngularComponent; + + constructor(props) { + super(props); + } + + render() { + + } +} From ac62e4a99201de9070dd736759ee12f0fb8fc491 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 14 Jan 2019 22:09:06 +0000 Subject: [PATCH 008/274] FormGroup component and implements --- .../src/components/FormGroup/FormGroup.tsx | 27 +++++++ .../src}/components/Label/Label.tsx | 0 packages/grafana-ui/src/components/index.ts | 6 +- .../SharedPreferences/SharedPreferences.tsx | 2 +- .../datasources/settings/BasicSettings.tsx | 2 +- public/app/features/teams/TeamSettings.tsx | 2 +- .../panel/gauge/GaugeOptionsEditor.tsx | 20 ++--- public/app/plugins/panel/gauge/MappingRow.tsx | 80 ++++++++++--------- .../app/plugins/panel/gauge/ValueOptions.tsx | 49 +++++++----- 9 files changed, 114 insertions(+), 74 deletions(-) create mode 100644 packages/grafana-ui/src/components/FormGroup/FormGroup.tsx rename {public/app/core => packages/grafana-ui/src}/components/Label/Label.tsx (100%) diff --git a/packages/grafana-ui/src/components/FormGroup/FormGroup.tsx b/packages/grafana-ui/src/components/FormGroup/FormGroup.tsx new file mode 100644 index 00000000000..ac761fa5d2c --- /dev/null +++ b/packages/grafana-ui/src/components/FormGroup/FormGroup.tsx @@ -0,0 +1,27 @@ +import React, { SFC } from 'react'; +import { Label } from '..'; + +interface Props { + label: string; + inputProps: {}; + labelWidth?: number; + inputWidth?: number; +} + +const defaultProps = { + labelWidth: 6, + inputProps: {}, + inputWidth: 12, +}; + +const FormGroup: SFC = ({ label, labelWidth, inputProps, inputWidth }) => { + return ( +
+ + +
+ ); +}; + +FormGroup.defaultProps = defaultProps; +export { FormGroup }; diff --git a/public/app/core/components/Label/Label.tsx b/packages/grafana-ui/src/components/Label/Label.tsx similarity index 100% rename from public/app/core/components/Label/Label.tsx rename to packages/grafana-ui/src/components/Label/Label.tsx diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 5420fcf14b7..ab0edf45ed0 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -9,12 +9,16 @@ export { IndicatorsContainer } from './Select/IndicatorsContainer'; export { NoOptionsMessage } from './Select/NoOptionsMessage'; export { default as resetSelectStyles } from './Select/resetSelectStyles'; +// Forms +export { GfFormLabel } from './GfFormLabel/GfFormLabel'; +export { FormGroup } from './FormGroup/FormGroup'; +export { Label } from './Label/Label'; + export { LoadingPlaceholder } from './LoadingPlaceholder/LoadingPlaceholder'; export { ColorPicker } from './ColorPicker/ColorPicker'; export { SeriesColorPickerPopover } from './ColorPicker/SeriesColorPickerPopover'; export { SeriesColorPicker } from './ColorPicker/SeriesColorPicker'; export { ThresholdsEditor } from './ThresholdsEditor/ThresholdsEditor'; -export { GfFormLabel } from './GfFormLabel/GfFormLabel'; export { Graph } from './Graph/Graph'; export { PanelOptionsGroup } from './PanelOptionsGroup/PanelOptionsGroup'; export { PanelOptionsGrid } from './PanelOptionsGrid/PanelOptionsGrid'; diff --git a/public/app/core/components/SharedPreferences/SharedPreferences.tsx b/public/app/core/components/SharedPreferences/SharedPreferences.tsx index b13393ab2e1..ca933332db9 100644 --- a/public/app/core/components/SharedPreferences/SharedPreferences.tsx +++ b/public/app/core/components/SharedPreferences/SharedPreferences.tsx @@ -1,6 +1,6 @@ import React, { PureComponent } from 'react'; -import { Label } from 'app/core/components/Label/Label'; +import { Label } from '../../../../../packages/grafana-ui/src/components/Label/Label'; import { Select } from '@grafana/ui'; import { getBackendSrv, BackendSrv } from 'app/core/services/backend_srv'; diff --git a/public/app/features/datasources/settings/BasicSettings.tsx b/public/app/features/datasources/settings/BasicSettings.tsx index 120e002ac68..55dc9b54211 100644 --- a/public/app/features/datasources/settings/BasicSettings.tsx +++ b/public/app/features/datasources/settings/BasicSettings.tsx @@ -1,5 +1,5 @@ import React, { SFC } from 'react'; -import { Label } from 'app/core/components/Label/Label'; +import { Label } from '../../../../../packages/grafana-ui/src/components/Label/Label'; import { Switch } from '../../../core/components/Switch/Switch'; export interface Props { diff --git a/public/app/features/teams/TeamSettings.tsx b/public/app/features/teams/TeamSettings.tsx index 5e058289bf0..3424f39d22c 100644 --- a/public/app/features/teams/TeamSettings.tsx +++ b/public/app/features/teams/TeamSettings.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { connect } from 'react-redux'; -import { Label } from 'app/core/components/Label/Label'; +import { Label } from '../../../../packages/grafana-ui/src/components/Label/Label'; import { SharedPreferences } from 'app/core/components/SharedPreferences/SharedPreferences'; import { updateTeam } from './state/actions'; import { getRouteParamsId } from 'app/core/selectors/location'; diff --git a/public/app/plugins/panel/gauge/GaugeOptionsEditor.tsx b/public/app/plugins/panel/gauge/GaugeOptionsEditor.tsx index f1f78ab1172..c7758642080 100644 --- a/public/app/plugins/panel/gauge/GaugeOptionsEditor.tsx +++ b/public/app/plugins/panel/gauge/GaugeOptionsEditor.tsx @@ -2,7 +2,7 @@ import React, { PureComponent } from 'react'; import { GaugeOptions, PanelOptionsProps, PanelOptionsGroup } from '@grafana/ui'; import { Switch } from 'app/core/components/Switch/Switch'; -import { Label } from '../../../core/components/Label/Label'; +import { FormGroup } from '@grafana/ui/src'; export default class GaugeOptionsEditor extends PureComponent> { onToggleThresholdLabels = () => @@ -21,14 +21,16 @@ export default class GaugeOptionsEditor extends PureComponent -
- - -
-
- - -
+ this.onMinValueChange(event), value: minValue }} + /> + this.onMaxValueChange(event), value: maxValue }} + /> { if (type === MappingType.RangeToText) { return ( <> -
- - -
-
- - -
-
- - -
+ this.onMappingFromChange(event), + onBlur: () => this.updateMapping(), + value: from, + }} + inputWidth={8} + /> + this.updateMapping, + onChange: event => this.onMappingToChange(event), + value: to, + }} + inputWidth={8} + /> + this.updateMapping, + onChange: event => this.onMappingTextChange(event), + value: text, + }} + inputWidth={10} + /> ); } return ( <> -
- - -
+ this.updateMapping, + onChange: event => this.onMappingValueChange(event), + value: value, + }} + inputWidth={8} + />
Unit
-
- - -
-
- - -
-
- - -
+ this.onDecimalChange(event), + value: decimals || '', + type: 'number', + }} + /> + this.onPrefixChange(event), + value: prefix || '', + }} + /> + this.onSuffixChange(event), + value: suffix || '', + }} + /> ); } From e172bade40b71d932eed46364fd2e3e597091639 Mon Sep 17 00:00:00 2001 From: sharkpc0813 Date: Tue, 15 Jan 2019 17:30:51 +0900 Subject: [PATCH 009/274] fix that alert context and result handle context do not use the same derived context. --- pkg/services/alerting/engine.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pkg/services/alerting/engine.go b/pkg/services/alerting/engine.go index 0f8e24bcef5..f784a322513 100644 --- a/pkg/services/alerting/engine.go +++ b/pkg/services/alerting/engine.go @@ -105,8 +105,9 @@ func (e *AlertingService) runJobDispatcher(grafanaCtx context.Context) error { var ( unfinishedWorkTimeout = time.Second * 5 // TODO: Make alertTimeout and alertMaxAttempts configurable in the config file. - alertTimeout = time.Second * 30 - alertMaxAttempts = 3 + alertTimeout = time.Second * 30 + resultHandleTimeout = time.Second * 30 + alertMaxAttempts = 3 ) func (e *AlertingService) processJobWithRetry(grafanaCtx context.Context, job *Job) error { @@ -116,7 +117,7 @@ func (e *AlertingService) processJobWithRetry(grafanaCtx context.Context, job *J } }() - cancelChan := make(chan context.CancelFunc, alertMaxAttempts) + cancelChan := make(chan context.CancelFunc, alertMaxAttempts*2) attemptChan := make(chan int, 1) // Initialize with first attemptID=1 @@ -204,6 +205,9 @@ func (e *AlertingService) processJob(attemptID int, attemptChan chan int, cancel } } + resultHandleCtx, resultHandleCancelFn := context.WithTimeout(context.Background(), resultHandleTimeout) + cancelChan <- resultHandleCancelFn + evalContext.Ctx = resultHandleCtx evalContext.Rule.State = evalContext.GetNewState() e.resultHandler.Handle(evalContext) span.Finish() From 33feb26fb5239d3d05b2653d3224632428311174 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 15 Jan 2019 11:40:12 +0100 Subject: [PATCH 010/274] WIP: good progress on react query editor support --- .../dashboard/panel_editor/QueriesTab.tsx | 73 ++++-------- .../dashboard/panel_editor/QueryEditorRow.tsx | 106 ++++++++++++++++-- public/app/features/panel/metrics_tab.ts | 31 ----- .../features/panel/partials/metrics_tab.html | 24 ---- .../app/features/plugins/plugin_component.ts | 28 ++--- public/app/types/plugins.ts | 1 + public/app/types/series.ts | 6 +- 7 files changed, 135 insertions(+), 134 deletions(-) delete mode 100644 public/app/features/panel/metrics_tab.ts delete mode 100644 public/app/features/panel/partials/metrics_tab.html diff --git a/public/app/features/dashboard/panel_editor/QueriesTab.tsx b/public/app/features/dashboard/panel_editor/QueriesTab.tsx index 47c4f358136..1c842e6572c 100644 --- a/public/app/features/dashboard/panel_editor/QueriesTab.tsx +++ b/public/app/features/dashboard/panel_editor/QueriesTab.tsx @@ -3,18 +3,16 @@ import React, { PureComponent } from 'react'; import _ from 'lodash'; // Components -import 'app/features/panel/metrics_tab'; import { EditorTabBody, EditorToolbarView } from './EditorTabBody'; import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker'; import { QueryInspector } from './QueryInspector'; import { QueryOptions } from './QueryOptions'; -import { AngularQueryComponentScope } from 'app/features/panel/metrics_tab'; import { PanelOptionsGroup } from '@grafana/ui'; +import { QueryEditorRow } from './QueryEditorRow'; // Services import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { BackendSrv, getBackendSrv } from 'app/core/services/backend_srv'; -import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader'; import config from 'app/core/config'; // Types @@ -37,63 +35,22 @@ interface State { } export class QueriesTab extends PureComponent { - element: HTMLElement; - component: AngularComponent; datasources: DataSourceSelectItem[] = getDatasourceSrv().getMetricSources(); backendSrv: BackendSrv = getBackendSrv(); - constructor(props) { - super(props); - - this.state = { - isLoadingHelp: false, - currentDS: this.findCurrentDataSource(), - helpContent: null, - isPickerOpen: false, - isAddingMixed: false, - }; - } + state: State = { + isLoadingHelp: false, + currentDS: this.findCurrentDataSource(), + helpContent: null, + isPickerOpen: false, + isAddingMixed: false, + }; findCurrentDataSource(): DataSourceSelectItem { const { panel } = this.props; return this.datasources.find(datasource => datasource.value === panel.datasource) || this.datasources[0]; } - getAngularQueryComponentScope(): AngularQueryComponentScope { - const { panel, dashboard } = this.props; - - return { - panel: panel, - dashboard: dashboard, - refresh: () => panel.refresh(), - render: () => panel.render, - addQuery: this.onAddQuery, - moveQuery: this.onMoveQuery, - removeQuery: this.onRemoveQuery, - events: panel.events, - }; - } - - componentDidMount() { - if (!this.element) { - return; - } - - const loader = getAngularLoader(); - const template = ''; - const scopeProps = { - ctrl: this.getAngularQueryComponentScope(), - }; - - this.component = loader.load(this.element, scopeProps, template); - } - - componentWillUnmount() { - if (this.component) { - this.component.destroy(); - } - } - onChangeDataSource = datasource => { const { panel } = this.props; const { currentDS } = this.state; @@ -147,7 +104,6 @@ export class QueriesTab extends PureComponent { } this.props.panel.addQuery(); - this.component.digest(); this.forceUpdate(); }; @@ -190,7 +146,6 @@ export class QueriesTab extends PureComponent { onAddMixedQuery = datasource => { this.onAddQuery({ datasource: datasource.name }); - this.component.digest(); this.setState({ isAddingMixed: false }); }; @@ -218,7 +173,17 @@ export class QueriesTab extends PureComponent { <>
-
(this.element = element)} /> + {panel.targets.map((query, index) => ( + + ))}
diff --git a/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx b/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx index b90c11da7c3..1028815cf08 100644 --- a/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx +++ b/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx @@ -2,29 +2,121 @@ import React, { PureComponent } from 'react'; // Utils & Services +import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader'; +import { Emitter } from 'app/core/utils/emitter'; // Types import { PanelModel } from '../panel_model'; -import { DashboardModel } from '../dashboard_model'; +import { DataQuery, DataSourceApi } from 'app/types/series'; interface Props { panel: PanelModel; - dashboard: DashboardModel; + query: DataQuery; + onAddQuery: (query?: DataQuery) => void; + onRemoveQuery: (query: DataQuery) => void; + onMoveQuery: (query: DataQuery, direction: number) => void; + datasourceName: string | null; } interface State { + datasource: DataSourceApi | null; } -export class VisualizationTab extends PureComponent { - element: HTMLElement; - angularQueryEditor: AngularComponent; +export class QueryEditorRow extends PureComponent { + element: HTMLElement | null = null; + angularQueryEditor: AngularComponent | null = null; - constructor(props) { - super(props); + state: State = { + datasource: null, + }; + + componentDidMount() { + this.loadDatasource(); + } + + getAngularQueryComponentScope(): AngularQueryComponentScope { + const { panel, onAddQuery, onMoveQuery, onRemoveQuery, query } = this.props; + const { datasource } = this.state; + + return { + datasource: datasource, + target: query, + panel: panel, + refresh: () => panel.refresh(), + render: () => panel.render, + addQuery: onAddQuery, + moveQuery: onMoveQuery, + removeQuery: onRemoveQuery, + events: panel.events, + }; + } + + async loadDatasource() { + const { query, panel } = this.props; + const dataSourceSrv = getDatasourceSrv(); + const datasource = await dataSourceSrv.get(query.datasource || panel.datasource); + + this.setState({ datasource }); + } + + componentDidUpdate() { + const { datasource } = this.state; + + // check if we need to load another datasource + if (datasource && datasource.name !== this.props.datasourceName) { + if (this.angularQueryEditor) { + this.angularQueryEditor.destroy(); + this.angularQueryEditor = null; + } + this.loadDatasource(); + return; + } + + if (!this.element || this.angularQueryEditor) { + return; + } + + const loader = getAngularLoader(); + const template = ''; + const scopeProps = { ctrl: this.getAngularQueryComponentScope() }; + + this.angularQueryEditor = loader.load(this.element, scopeProps, template); + } + + componentWillUnmount() { + if (this.angularQueryEditor) { + this.angularQueryEditor.destroy(); + } } render() { + const { datasource } = this.state; + if (!datasource) { + return null; + } + + if (datasource.pluginExports.QueryCtrl) { + return
(this.element = element)} />; + } else if (datasource.pluginExports.QueryEditor) { + const QueryEditor = datasource.pluginExports.QueryEditor; + return ; + } + + return
Data source plugin does not export any Query Editor component
; } } + +export interface AngularQueryComponentScope { + target: DataQuery; + panel: PanelModel; + events: Emitter; + refresh: () => void; + render: () => void; + removeQuery: (query: DataQuery) => void; + addQuery: (query?: DataQuery) => void; + moveQuery: (query: DataQuery, direction: number) => void; + datasource: DataSourceApi; +} + diff --git a/public/app/features/panel/metrics_tab.ts b/public/app/features/panel/metrics_tab.ts deleted file mode 100644 index 74418484e3a..00000000000 --- a/public/app/features/panel/metrics_tab.ts +++ /dev/null @@ -1,31 +0,0 @@ -// Services & utils -import coreModule from 'app/core/core_module'; -import { Emitter } from 'app/core/utils/emitter'; - -// Types -import { DashboardModel } from '../dashboard/dashboard_model'; -import { PanelModel } from '../dashboard/panel_model'; -import { DataQuery } from 'app/types'; - -export interface AngularQueryComponentScope { - panel: PanelModel; - dashboard: DashboardModel; - events: Emitter; - refresh: () => void; - render: () => void; - removeQuery: (query: DataQuery) => void; - addQuery: (query?: DataQuery) => void; - moveQuery: (query: DataQuery, direction: number) => void; -} - -/** @ngInject */ -export function metricsTabDirective() { - 'use strict'; - return { - restrict: 'E', - scope: true, - templateUrl: 'public/app/features/panel/partials/metrics_tab.html', - }; -} - -coreModule.directive('metricsTab', metricsTabDirective); diff --git a/public/app/features/panel/partials/metrics_tab.html b/public/app/features/panel/partials/metrics_tab.html deleted file mode 100644 index 5e9f23ba2ef..00000000000 --- a/public/app/features/panel/partials/metrics_tab.html +++ /dev/null @@ -1,24 +0,0 @@ -
- - - - -
- - - - - - - - - - - - - - - - - - diff --git a/public/app/features/plugins/plugin_component.ts b/public/app/features/plugins/plugin_component.ts index 7092608085d..0b305e05f5b 100644 --- a/public/app/features/plugins/plugin_component.ts +++ b/public/app/features/plugins/plugin_component.ts @@ -105,23 +105,17 @@ function pluginDirectiveLoader($compile, datasourceSrv, $rootScope, $q, $http, $ switch (attrs.type) { // QueryCtrl case 'query-ctrl': { - const datasource = scope.target.datasource || scope.ctrl.panel.datasource; - return datasourceSrv.get(datasource).then(ds => { - scope.datasource = ds; - - return importPluginModule(ds.meta.module).then(dsModule => { - return { - baseUrl: ds.meta.baseUrl, - name: 'query-ctrl-' + ds.meta.id, - bindings: { target: '=', panelCtrl: '=', datasource: '=' }, - attrs: { - target: 'target', - 'panel-ctrl': 'ctrl', - datasource: 'datasource', - }, - Component: dsModule.QueryCtrl, - }; - }); + const ds = scope.ctrl.datasource; + return $q.when({ + baseUrl: ds.meta.baseUrl, + name: 'query-ctrl-' + ds.meta.id, + bindings: { target: '=', panelCtrl: '=', datasource: '=' }, + attrs: { + target: 'ctrl.target', + 'panel-ctrl': 'ctrl', + datasource: 'ctrl.datasource', + }, + Component: ds.pluginExports.QueryCtrl, }); } // Annotations diff --git a/public/app/types/plugins.ts b/public/app/types/plugins.ts index a1403c7a71c..4dacb3f8ccb 100644 --- a/public/app/types/plugins.ts +++ b/public/app/types/plugins.ts @@ -4,6 +4,7 @@ import { PanelProps, PanelOptionsProps } from '@grafana/ui'; export interface PluginExports { Datasource?: any; QueryCtrl?: any; + QueryEditor?: any; ConfigCtrl?: any; AnnotationsQueryCtrl?: any; VariableQueryEditor?: any; diff --git a/public/app/types/series.ts b/public/app/types/series.ts index 9fe68955da5..6f1795ef544 100644 --- a/public/app/types/series.ts +++ b/public/app/types/series.ts @@ -1,4 +1,4 @@ -import { PluginMeta } from './plugins'; +import { PluginMeta, PluginExports } from './plugins'; import { TimeSeries, TimeRange, RawTimeRange } from '@grafana/ui'; export interface DataQueryResponse { @@ -25,6 +25,10 @@ export interface DataQueryOptions { } export interface DataSourceApi { + name: string; + meta: PluginMeta; + pluginExports: PluginExports; + /** * min interval range */ From e08f61059bcbc60753268eae06aced2cc31e7f9d Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 15 Jan 2019 11:11:32 +0100 Subject: [PATCH 011/274] utils --- pkg/util/encoding.go | 8 ++++++++ pkg/util/ip_address.go | 27 +++++++++++++++++++++++++++ pkg/util/ip_address_test.go | 14 ++++++++++++++ 3 files changed, 49 insertions(+) create mode 100644 pkg/util/ip_address.go create mode 100644 pkg/util/ip_address_test.go diff --git a/pkg/util/encoding.go b/pkg/util/encoding.go index 0edb721e422..e82344d73f9 100644 --- a/pkg/util/encoding.go +++ b/pkg/util/encoding.go @@ -101,3 +101,11 @@ func DecodeBasicAuthHeader(header string) (string, string, error) { return userAndPass[0], userAndPass[1], nil } + +func RandomHex(n int) (string, error) { + bytes := make([]byte, n) + if _, err := rand.Read(bytes); err != nil { + return "", err + } + return hex.EncodeToString(bytes), nil +} diff --git a/pkg/util/ip_address.go b/pkg/util/ip_address.go new file mode 100644 index 00000000000..4e9a9378c6b --- /dev/null +++ b/pkg/util/ip_address.go @@ -0,0 +1,27 @@ +package util + +import ( + "net" + "strings" +) + +// ParseIPAddress parses an IP address and removes port and/or IPV6 format +func ParseIPAddress(input string) string { + var s string + lastIndex := strings.LastIndex(input, ":") + + if lastIndex != -1 { + s = input[:lastIndex] + } + + s = strings.Replace(s, "[", "", -1) + s = strings.Replace(s, "]", "", -1) + + ip := net.ParseIP(s) + + if ip.IsLoopback() { + return "127.0.0.1" + } + + return ip.String() +} diff --git a/pkg/util/ip_address_test.go b/pkg/util/ip_address_test.go new file mode 100644 index 00000000000..644340a5e82 --- /dev/null +++ b/pkg/util/ip_address_test.go @@ -0,0 +1,14 @@ +package util + +import ( + "testing" + + . "github.com/smartystreets/goconvey/convey" +) + +func TestParseIPAddress(t *testing.T) { + Convey("Test parse ip address", t, func() { + So(ParseIPAddress("192.168.0.140:456"), ShouldEqual, "192.168.0.140") + So(ParseIPAddress("[::1:456]"), ShouldEqual, "127.0.0.1") + }) +} From b0df7280be60be815078e572f5975a14521695bf Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 15 Jan 2019 15:15:17 +0100 Subject: [PATCH 012/274] begin user auth token implementation --- pkg/services/auth/auth_token.go | 170 +++++++++++++++ pkg/services/auth/auth_token_test.go | 206 ++++++++++++++++++ pkg/services/auth/model.go | 25 +++ .../sqlstore/migrations/migrations.go | 1 + .../migrations/user_auth_token_mig.go | 32 +++ 5 files changed, 434 insertions(+) create mode 100644 pkg/services/auth/auth_token.go create mode 100644 pkg/services/auth/auth_token_test.go create mode 100644 pkg/services/auth/model.go create mode 100644 pkg/services/sqlstore/migrations/user_auth_token_mig.go diff --git a/pkg/services/auth/auth_token.go b/pkg/services/auth/auth_token.go new file mode 100644 index 00000000000..aefcccadbd6 --- /dev/null +++ b/pkg/services/auth/auth_token.go @@ -0,0 +1,170 @@ +package auth + +import ( + "crypto/sha256" + "encoding/hex" + "time" + + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util" + macaron "gopkg.in/macaron.v1" + + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/services/sqlstore" +) + +func init() { + registry.RegisterService(&UserAuthTokenService{}) +} + +var now = time.Now + +// UserAuthTokenService are used for generating and validating user auth tokens +type UserAuthTokenService struct { + SQLStore *sqlstore.SqlStore `inject:""` + log log.Logger +} + +// Init this service +func (s *UserAuthTokenService) Init() error { + s.log = log.New("auth") + return nil +} + +const sessionCookieKey = "grafana_session" + +func (s *UserAuthTokenService) UserAuthenticatedHook(user *models.User, c *models.ReqContext) error { + userToken, err := s.CreateToken(user.Id, c.RemoteAddr(), c.Req.UserAgent()) + if err != nil { + return err + } + + c.Resp.Header().Del("Set-Cookie") + c.SetCookie(sessionCookieKey, userToken.unhashedToken, setting.AppSubUrl+"/", setting.Domain, false, true) + + return nil +} + +func (s *UserAuthTokenService) UserSignedOutHook(c *models.ReqContext) { + c.SetCookie(sessionCookieKey, "", -1, setting.AppSubUrl+"/", setting.Domain, false, true) +} + +func (s *UserAuthTokenService) RequestMiddleware() macaron.Handler { + return func(ctx *models.ReqContext) { + authToken := ctx.GetCookie(sessionCookieKey) + userToken, err := s.lookupToken(authToken) + if err != nil { + + } + + ctx.Next() + + refreshed, err := s.refreshToken(userToken, ctx.RemoteAddr(), ctx.Req.UserAgent()) + if err != nil { + + } + + if refreshed { + ctx.Resp.Header().Del("Set-Cookie") + ctx.SetCookie(sessionCookieKey, userToken.unhashedToken, setting.AppSubUrl+"/", setting.Domain, false, true) + } + } +} + +func (s *UserAuthTokenService) CreateToken(userId int64, clientIP, userAgent string) (*userAuthToken, error) { + clientIP = util.ParseIPAddress(clientIP) + token, err := util.RandomHex(16) + if err != nil { + return nil, err + } + + hashedToken := hashToken(token) + + userToken := userAuthToken{ + UserId: userId, + AuthToken: hashedToken, + PrevAuthToken: hashedToken, + ClientIp: clientIP, + UserAgent: userAgent, + RotatedAt: now().Unix(), + CreatedAt: now().Unix(), + UpdatedAt: now().Unix(), + SeenAt: 0, + AuthTokenSeen: false, + } + _, err = s.SQLStore.NewSession().Insert(&userToken) + if err != nil { + return nil, err + } + + userToken.unhashedToken = token + + return &userToken, nil +} + +func (s *UserAuthTokenService) lookupToken(unhashedToken string) (*userAuthToken, error) { + hashedToken := hashToken(unhashedToken) + + var userToken userAuthToken + exists, err := s.SQLStore.NewSession().Where("auth_token = ? OR prev_auth_token = ?", hashedToken, hashedToken).Get(&userToken) + if err != nil { + return nil, err + } + + if !exists { + return nil, ErrAuthTokenNotFound + } + + if userToken.AuthToken != hashedToken && userToken.PrevAuthToken == hashedToken && userToken.AuthTokenSeen { + userToken.AuthTokenSeen = false + expireBefore := now().Add(-1 * time.Minute).Unix() + affectedRows, err := s.SQLStore.NewSession().Where("id = ? AND prev_auth_token = ? AND rotated_at < ?", userToken.Id, userToken.PrevAuthToken, expireBefore).AllCols().Update(&userToken) + if err != nil { + return nil, err + } + + if affectedRows == 0 { + s.log.Debug("prev seen token unchanged", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent) + } else { + s.log.Debug("prev seen token", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent) + } + } + + if !userToken.AuthTokenSeen && userToken.AuthToken == hashedToken { + userTokenCopy := userToken + userTokenCopy.AuthTokenSeen = true + userTokenCopy.SeenAt = now().Unix() + affectedRows, err := s.SQLStore.NewSession().Where("id = ? AND auth_token = ?", userTokenCopy.Id, userTokenCopy.AuthToken).AllCols().Update(&userTokenCopy) + if err != nil { + return nil, err + } + + if affectedRows == 1 { + userToken = userTokenCopy + } + + if affectedRows == 0 { + s.log.Debug("seen wrong token", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent) + } else { + s.log.Debug("seen token", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent) + } + } + + userToken.unhashedToken = unhashedToken + + return &userToken, nil +} + +func (s *UserAuthTokenService) refreshToken(token *userAuthToken, clientIP, userAgent string) (bool, error) { + // lookup token in db + // refresh token if needed + + return false, nil +} + +func hashToken(token string) string { + hashBytes := sha256.Sum256([]byte(token + setting.SecretKey)) + return hex.EncodeToString(hashBytes[:]) +} diff --git a/pkg/services/auth/auth_token_test.go b/pkg/services/auth/auth_token_test.go new file mode 100644 index 00000000000..2e4618c10ed --- /dev/null +++ b/pkg/services/auth/auth_token_test.go @@ -0,0 +1,206 @@ +package auth + +import ( + "testing" + "time" + + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/services/sqlstore" + . "github.com/smartystreets/goconvey/convey" +) + +func TestUserAuthToken(t *testing.T) { + Convey("Test user auth token", t, func() { + ctx := createTestContext(t) + userAuthTokenService := ctx.tokenService + userID := int64(10) + + t := time.Date(2018, 12, 13, 13, 45, 0, 0, time.UTC) + now = func() time.Time { + return t + } + + Convey("When creating token", func() { + token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent") + So(err, ShouldBeNil) + So(token, ShouldNotBeNil) + So(token.AuthTokenSeen, ShouldBeFalse) + + Convey("When lookup unhashed token should return user auth token", func() { + lookupToken, err := userAuthTokenService.lookupToken(token.unhashedToken) + So(err, ShouldBeNil) + So(lookupToken, ShouldNotBeNil) + So(lookupToken.UserId, ShouldEqual, userID) + So(lookupToken.AuthTokenSeen, ShouldBeTrue) + + storedAuthToken, err := ctx.getAuthTokenByID(lookupToken.Id) + So(err, ShouldBeNil) + So(storedAuthToken, ShouldNotBeNil) + So(storedAuthToken.AuthTokenSeen, ShouldBeTrue) + }) + + Convey("When lookup hashed token should return user auth token not found error", func() { + lookupToken, err := userAuthTokenService.lookupToken(token.AuthToken) + So(err, ShouldEqual, ErrAuthTokenNotFound) + So(lookupToken, ShouldBeNil) + }) + }) + + Convey("expires correctly", func() { + token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent") + So(err, ShouldBeNil) + So(token, ShouldNotBeNil) + + _, err = userAuthTokenService.lookupToken(token.unhashedToken) + So(err, ShouldBeNil) + + token, err = ctx.getAuthTokenByID(token.Id) + So(err, ShouldBeNil) + + // set now (now - 23 hours) + _, err = userAuthTokenService.refreshToken(token, "192.168.10.11:1234", "some user agent") + So(err, ShouldBeNil) + + _, err = userAuthTokenService.lookupToken(token.unhashedToken) + So(err, ShouldBeNil) + + stillGood, err := userAuthTokenService.lookupToken(token.unhashedToken) + So(err, ShouldBeNil) + So(stillGood, ShouldNotBeNil) + + // set now (new - 2 hours) + notGood, err := userAuthTokenService.lookupToken(token.unhashedToken) + So(err, ShouldEqual, ErrAuthTokenNotFound) + So(notGood, ShouldBeNil) + }) + + Convey("can properly rotate tokens", func() { + token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent") + So(err, ShouldBeNil) + So(token, ShouldNotBeNil) + + prevToken := token.AuthToken + unhashedPrev := token.unhashedToken + + refreshed, err := userAuthTokenService.refreshToken(token, "192.168.10.12:1234", "a new user agent") + So(err, ShouldBeNil) + So(refreshed, ShouldBeFalse) + + ctx.markAuthTokenAsSeen(token.Id) + token, err = ctx.getAuthTokenByID(token.Id) + So(err, ShouldBeNil) + + // ability to auth using an old token + now = func() time.Time { + return t + } + + refreshed, err = userAuthTokenService.refreshToken(token, "192.168.10.12:1234", "a new user agent") + So(err, ShouldBeNil) + So(refreshed, ShouldBeTrue) + + unhashedToken := token.unhashedToken + + token, err = ctx.getAuthTokenByID(token.Id) + So(err, ShouldBeNil) + token.unhashedToken = unhashedToken + + So(token.RotatedAt, ShouldEqual, t.Unix()) + So(token.ClientIp, ShouldEqual, "192.168.10.12") + So(token.UserAgent, ShouldEqual, "a new user agent") + So(token.AuthTokenSeen, ShouldBeFalse) + So(token.SeenAt, ShouldEqual, 0) + So(token.PrevAuthToken, ShouldEqual, prevToken) + + lookedUp, err := userAuthTokenService.lookupToken(token.unhashedToken) + So(err, ShouldBeNil) + So(lookedUp, ShouldNotBeNil) + So(lookedUp.AuthTokenSeen, ShouldBeTrue) + So(lookedUp.SeenAt, ShouldEqual, t.Unix()) + + lookedUp, err = userAuthTokenService.lookupToken(unhashedPrev) + So(err, ShouldBeNil) + So(lookedUp, ShouldNotBeNil) + So(lookedUp.Id, ShouldEqual, token.Id) + + now = func() time.Time { + return t.Add(2 * time.Minute) + } + + lookedUp, err = userAuthTokenService.lookupToken(unhashedPrev) + So(err, ShouldBeNil) + So(lookedUp, ShouldNotBeNil) + + lookedUp, err = ctx.getAuthTokenByID(lookedUp.Id) + So(err, ShouldBeNil) + So(lookedUp, ShouldNotBeNil) + So(lookedUp.AuthTokenSeen, ShouldBeFalse) + + refreshed, err = userAuthTokenService.refreshToken(token, "192.168.10.12:1234", "a new user agent") + So(err, ShouldBeNil) + So(refreshed, ShouldBeTrue) + + token, err = ctx.getAuthTokenByID(token.Id) + So(err, ShouldBeNil) + So(token, ShouldNotBeNil) + So(token.SeenAt, ShouldEqual, 0) + }) + + Convey("keeps prev token valid for 1 minute after it is confirmed", func() { + + }) + + Convey("will not mark token unseen when prev and current are the same", func() { + + }) + + Reset(func() { + now = time.Now + }) + }) +} + +func createTestContext(t *testing.T) *testContext { + t.Helper() + + sqlstore := sqlstore.InitTestDB(t) + tokenService := &UserAuthTokenService{ + SQLStore: sqlstore, + log: log.New("test-logger"), + } + + return &testContext{ + sqlstore: sqlstore, + tokenService: tokenService, + } +} + +type testContext struct { + sqlstore *sqlstore.SqlStore + tokenService *UserAuthTokenService +} + +func (c *testContext) getAuthTokenByID(id int64) (*userAuthToken, error) { + sess := c.sqlstore.NewSession() + var t userAuthToken + found, err := sess.ID(id).Get(&t) + if err != nil || !found { + return nil, err + } + + return &t, nil +} + +func (c *testContext) markAuthTokenAsSeen(id int64) (bool, error) { + sess := c.sqlstore.NewSession() + res, err := sess.Exec("UPDATE user_auth_token SET auth_token_seen = ? WHERE id = ?", c.sqlstore.Dialect.BooleanStr(true), id) + if err != nil { + return false, err + } + + rowsAffected, err := res.RowsAffected() + if err != nil { + return false, err + } + return rowsAffected == 1, nil +} diff --git a/pkg/services/auth/model.go b/pkg/services/auth/model.go new file mode 100644 index 00000000000..a033b96be31 --- /dev/null +++ b/pkg/services/auth/model.go @@ -0,0 +1,25 @@ +package auth + +import ( + "errors" +) + +// Typed errors +var ( + ErrAuthTokenNotFound = errors.New("User auth token not found") +) + +type userAuthToken struct { + Id int64 + UserId int64 + AuthToken string + PrevAuthToken string + UserAgent string + ClientIp string + AuthTokenSeen bool + SeenAt int64 + RotatedAt int64 + CreatedAt int64 + UpdatedAt int64 + unhashedToken string `xorm:"-"` +} diff --git a/pkg/services/sqlstore/migrations/migrations.go b/pkg/services/sqlstore/migrations/migrations.go index 36cd8e5ed62..931259ec3ed 100644 --- a/pkg/services/sqlstore/migrations/migrations.go +++ b/pkg/services/sqlstore/migrations/migrations.go @@ -32,6 +32,7 @@ func AddMigrations(mg *Migrator) { addLoginAttemptMigrations(mg) addUserAuthMigrations(mg) addServerlockMigrations(mg) + addUserAuthTokenMigrations(mg) } func addMigrationLogMigrations(mg *Migrator) { diff --git a/pkg/services/sqlstore/migrations/user_auth_token_mig.go b/pkg/services/sqlstore/migrations/user_auth_token_mig.go new file mode 100644 index 00000000000..9794b7a78c7 --- /dev/null +++ b/pkg/services/sqlstore/migrations/user_auth_token_mig.go @@ -0,0 +1,32 @@ +package migrations + +import ( + . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" +) + +func addUserAuthTokenMigrations(mg *Migrator) { + userAuthTokenV1 := Table{ + Name: "user_auth_token", + Columns: []*Column{ + {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + {Name: "user_id", Type: DB_BigInt, Nullable: false}, + {Name: "auth_token", Type: DB_NVarchar, Length: 100, Nullable: false}, + {Name: "prev_auth_token", Type: DB_NVarchar, Length: 100, Nullable: false}, + {Name: "user_agent", Type: DB_NVarchar, Length: 255, Nullable: false}, + {Name: "client_ip", Type: DB_NVarchar, Length: 255, Nullable: false}, + {Name: "auth_token_seen", Type: DB_Bool, Nullable: false}, + {Name: "seen_at", Type: DB_Int, Nullable: true}, + {Name: "rotated_at", Type: DB_Int, Nullable: false}, + {Name: "created_at", Type: DB_Int, Nullable: false}, + {Name: "updated_at", Type: DB_Int, Nullable: false}, + }, + Indices: []*Index{ + {Cols: []string{"auth_token"}, Type: UniqueIndex}, + {Cols: []string{"prev_auth_token"}, Type: UniqueIndex}, + }, + } + + mg.AddMigration("create user auth token table", NewAddTableMigration(userAuthTokenV1)) + mg.AddMigration("add unique index user_auth_token.auth_token", NewAddIndexMigration(userAuthTokenV1, userAuthTokenV1.Indices[0])) + mg.AddMigration("add unique index user_auth_token.prev_auth_token", NewAddIndexMigration(userAuthTokenV1, userAuthTokenV1.Indices[1])) +} From 8764fb5aa6eee9c8df08e5fc23dec5d78d3f5682 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 15 Jan 2019 15:15:52 +0100 Subject: [PATCH 013/274] inject login/logout hooks --- pkg/api/api.go | 12 ++++++------ pkg/api/http_server.go | 17 ++++++++++------- pkg/api/login.go | 38 +++++++++++++++----------------------- pkg/api/login_oauth.go | 4 ++-- pkg/api/org_invite.go | 4 ++-- pkg/api/signup.go | 4 ++-- 6 files changed, 37 insertions(+), 42 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 0526ee80afe..07cb712f794 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -23,9 +23,9 @@ func (hs *HTTPServer) registerRoutes() { // not logged in views r.Get("/", reqSignedIn, hs.Index) - r.Get("/logout", Logout) - r.Post("/login", quota("session"), bind(dtos.LoginCommand{}), Wrap(LoginPost)) - r.Get("/login/:name", quota("session"), OAuthLogin) + r.Get("/logout", hs.Logout) + r.Post("/login", quota("session"), bind(dtos.LoginCommand{}), Wrap(hs.LoginPost)) + r.Get("/login/:name", quota("session"), hs.OAuthLogin) r.Get("/login", hs.LoginView) r.Get("/invite/:code", hs.Index) @@ -84,11 +84,11 @@ func (hs *HTTPServer) registerRoutes() { r.Get("/signup", hs.Index) r.Get("/api/user/signup/options", Wrap(GetSignUpOptions)) r.Post("/api/user/signup", quota("user"), bind(dtos.SignUpForm{}), Wrap(SignUp)) - r.Post("/api/user/signup/step2", bind(dtos.SignUpStep2Form{}), Wrap(SignUpStep2)) + r.Post("/api/user/signup/step2", bind(dtos.SignUpStep2Form{}), Wrap(hs.SignUpStep2)) // invited r.Get("/api/user/invite/:code", Wrap(GetInviteInfoByCode)) - r.Post("/api/user/invite/complete", bind(dtos.CompleteInviteForm{}), Wrap(CompleteInvite)) + r.Post("/api/user/invite/complete", bind(dtos.CompleteInviteForm{}), Wrap(hs.CompleteInvite)) // reset password r.Get("/user/password/send-reset-email", hs.Index) @@ -109,7 +109,7 @@ func (hs *HTTPServer) registerRoutes() { r.Delete("/api/snapshots/:key", reqEditorRole, Wrap(DeleteDashboardSnapshot)) // api renew session based on remember cookie - r.Get("/api/login/ping", quota("session"), LoginAPIPing) + r.Get("/api/login/ping", quota("session"), hs.LoginAPIPing) // authed api r.Group("/api", func(apiRoute routing.RouteRegister) { diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index d4d7b41bec5..600157878fe 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -11,6 +11,8 @@ import ( "path" "time" + "github.com/grafana/grafana/pkg/services/auth" + "github.com/grafana/grafana/pkg/api/routing" "github.com/prometheus/client_golang/prometheus" @@ -49,13 +51,14 @@ type HTTPServer struct { streamManager *live.StreamManager httpSrv *http.Server - RouteRegister routing.RouteRegister `inject:""` - Bus bus.Bus `inject:""` - RenderService rendering.Service `inject:""` - Cfg *setting.Cfg `inject:""` - HooksService *hooks.HooksService `inject:""` - CacheService *cache.CacheService `inject:""` - DatasourceCache datasources.CacheService `inject:""` + RouteRegister routing.RouteRegister `inject:""` + Bus bus.Bus `inject:""` + RenderService rendering.Service `inject:""` + Cfg *setting.Cfg `inject:""` + HooksService *hooks.HooksService `inject:""` + CacheService *cache.CacheService `inject:""` + DatasourceCache datasources.CacheService `inject:""` + AuthTokenService *auth.UserAuthTokenService `inject:""` } func (hs *HTTPServer) Init() error { diff --git a/pkg/api/login.go b/pkg/api/login.go index 05afc40e59a..f0902a60f58 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -9,7 +9,6 @@ import ( "github.com/grafana/grafana/pkg/login" "github.com/grafana/grafana/pkg/metrics" m "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/session" "github.com/grafana/grafana/pkg/setting" ) @@ -43,7 +42,7 @@ func (hs *HTTPServer) LoginView(c *m.ReqContext) { return } - if !tryLoginUsingRememberCookie(c) { + if !hs.tryLoginUsingRememberCookie(c) { c.HTML(200, ViewIndex, viewData) return } @@ -75,7 +74,7 @@ func tryOAuthAutoLogin(c *m.ReqContext) bool { return false } -func tryLoginUsingRememberCookie(c *m.ReqContext) bool { +func (hs *HTTPServer) tryLoginUsingRememberCookie(c *m.ReqContext) bool { // Check auto-login. uname := c.GetCookie(setting.CookieUserName) if len(uname) == 0 { @@ -111,12 +110,12 @@ func tryLoginUsingRememberCookie(c *m.ReqContext) bool { } isSucceed = true - loginUserWithUser(user, c) + hs.loginUserWithUser(user, c) return true } -func LoginAPIPing(c *m.ReqContext) { - if !tryLoginUsingRememberCookie(c) { +func (hs *HTTPServer) LoginAPIPing(c *m.ReqContext) { + if !hs.tryLoginUsingRememberCookie(c) { c.JsonApiErr(401, "Unauthorized", nil) return } @@ -124,7 +123,7 @@ func LoginAPIPing(c *m.ReqContext) { c.JsonOK("Logged in") } -func LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response { +func (hs *HTTPServer) LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response { if setting.DisableLoginForm { return Error(401, "Login is disabled", nil) } @@ -146,7 +145,7 @@ func LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response { user := authQuery.User - loginUserWithUser(user, c) + hs.loginUserWithUser(user, c) result := map[string]interface{}{ "message": "Logged in", @@ -162,27 +161,20 @@ func LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response { return JSON(200, result) } -func loginUserWithUser(user *m.User, c *m.ReqContext) { +func (hs *HTTPServer) loginUserWithUser(user *m.User, c *m.ReqContext) { if user == nil { - log.Error(3, "User login with nil user") + hs.log.Error("User login with nil user") } - c.Resp.Header().Del("Set-Cookie") - - days := 86400 * setting.LogInRememberDays - if days > 0 { - c.SetCookie(setting.CookieUserName, user.Login, days, setting.AppSubUrl+"/") - c.SetSuperSecureCookie(user.Rands+user.Password, setting.CookieRememberName, user.Login, days, setting.AppSubUrl+"/") + err := hs.AuthTokenService.UserAuthenticatedHook(user, c) + if err != nil { + hs.log.Error("User auth hook failed", err) } - - c.Session.RegenerateId(c.Context) - c.Session.Set(session.SESS_KEY_USERID, user.Id) } -func Logout(c *m.ReqContext) { - c.SetCookie(setting.CookieUserName, "", -1, setting.AppSubUrl+"/") - c.SetCookie(setting.CookieRememberName, "", -1, setting.AppSubUrl+"/") - c.Session.Destory(c.Context) +func (hs *HTTPServer) Logout(c *m.ReqContext) { + hs.AuthTokenService.UserSignedOutHook(c) + if setting.SignoutRedirectUrl != "" { c.Redirect(setting.SignoutRedirectUrl) } else { diff --git a/pkg/api/login_oauth.go b/pkg/api/login_oauth.go index fe4fa93b621..6013df8ea02 100644 --- a/pkg/api/login_oauth.go +++ b/pkg/api/login_oauth.go @@ -31,7 +31,7 @@ func GenStateString() string { return base64.URLEncoding.EncodeToString(rnd) } -func OAuthLogin(ctx *m.ReqContext) { +func (hs *HTTPServer) OAuthLogin(ctx *m.ReqContext) { if setting.OAuthService == nil { ctx.Handle(404, "OAuth not enabled", nil) return @@ -178,7 +178,7 @@ func OAuthLogin(ctx *m.ReqContext) { } // login - loginUserWithUser(cmd.Result, ctx) + hs.loginUserWithUser(cmd.Result, ctx) metrics.M_Api_Login_OAuth.Inc() diff --git a/pkg/api/org_invite.go b/pkg/api/org_invite.go index dfb2cf045ed..835b03a2cc9 100644 --- a/pkg/api/org_invite.go +++ b/pkg/api/org_invite.go @@ -148,7 +148,7 @@ func GetInviteInfoByCode(c *m.ReqContext) Response { }) } -func CompleteInvite(c *m.ReqContext, completeInvite dtos.CompleteInviteForm) Response { +func (hs *HTTPServer) CompleteInvite(c *m.ReqContext, completeInvite dtos.CompleteInviteForm) Response { query := m.GetTempUserByCodeQuery{Code: completeInvite.InviteCode} if err := bus.Dispatch(&query); err != nil { @@ -186,7 +186,7 @@ func CompleteInvite(c *m.ReqContext, completeInvite dtos.CompleteInviteForm) Res return rsp } - loginUserWithUser(user, c) + hs.loginUserWithUser(user, c) metrics.M_Api_User_SignUpCompleted.Inc() metrics.M_Api_User_SignUpInvite.Inc() diff --git a/pkg/api/signup.go b/pkg/api/signup.go index 200a3ebc9d1..fe577dd9ef9 100644 --- a/pkg/api/signup.go +++ b/pkg/api/signup.go @@ -51,7 +51,7 @@ func SignUp(c *m.ReqContext, form dtos.SignUpForm) Response { return JSON(200, util.DynMap{"status": "SignUpCreated"}) } -func SignUpStep2(c *m.ReqContext, form dtos.SignUpStep2Form) Response { +func (hs *HTTPServer) SignUpStep2(c *m.ReqContext, form dtos.SignUpStep2Form) Response { if !setting.AllowUserSignUp { return Error(401, "User signup is disabled", nil) } @@ -109,7 +109,7 @@ func SignUpStep2(c *m.ReqContext, form dtos.SignUpStep2Form) Response { apiResponse["code"] = "redirect-to-select-org" } - loginUserWithUser(user, c) + hs.loginUserWithUser(user, c) metrics.M_Api_User_SignUpCompleted.Inc() return JSON(200, apiResponse) From 58094faa12f8440083ea8a4c0aee0761470ee45b Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Tue, 15 Jan 2019 17:31:42 +0000 Subject: [PATCH 014/274] test and minor fix on mapping row --- .../components/FormGroup/FormGroup.test.tsx | 26 +++++++++++++++++++ .../__snapshots__/FormGroup.test.tsx.snap | 19 ++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 packages/grafana-ui/src/components/FormGroup/FormGroup.test.tsx create mode 100644 packages/grafana-ui/src/components/FormGroup/__snapshots__/FormGroup.test.tsx.snap diff --git a/packages/grafana-ui/src/components/FormGroup/FormGroup.test.tsx b/packages/grafana-ui/src/components/FormGroup/FormGroup.test.tsx new file mode 100644 index 00000000000..4f8b4be9540 --- /dev/null +++ b/packages/grafana-ui/src/components/FormGroup/FormGroup.test.tsx @@ -0,0 +1,26 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { FormGroup, Props } from './FormGroup'; + +const setup = (propOverrides?: object) => { + const props: Props = { + label: 'Test', + labelWidth: 11, + inputProps: { + value: 10, + onChange: jest.fn(), + }, + }; + + Object.assign(props, propOverrides); + + return shallow(); +}; + +describe('Render', () => { + it('should render component', () => { + const wrapper = setup(); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/packages/grafana-ui/src/components/FormGroup/__snapshots__/FormGroup.test.tsx.snap b/packages/grafana-ui/src/components/FormGroup/__snapshots__/FormGroup.test.tsx.snap new file mode 100644 index 00000000000..e88ff774981 --- /dev/null +++ b/packages/grafana-ui/src/components/FormGroup/__snapshots__/FormGroup.test.tsx.snap @@ -0,0 +1,19 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` +
+ + Test + + +
+`; From 83fbf52aac51fa6cc0c16eed9b48529b26fb489c Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Tue, 15 Jan 2019 17:33:42 +0000 Subject: [PATCH 015/274] fixing imports, minor fix on mapping row --- .../src/components/FormGroup/FormGroup.tsx | 2 +- .../grafana-ui/src/components/Label/Label.tsx | 2 +- .../SharedPreferences/SharedPreferences.tsx | 3 +-- .../datasources/settings/BasicSettings.tsx | 2 +- public/app/features/teams/TeamSettings.tsx | 2 +- public/app/plugins/panel/gauge/MappingRow.tsx | 19 +++++++++---------- 6 files changed, 14 insertions(+), 16 deletions(-) diff --git a/packages/grafana-ui/src/components/FormGroup/FormGroup.tsx b/packages/grafana-ui/src/components/FormGroup/FormGroup.tsx index ac761fa5d2c..a0088032079 100644 --- a/packages/grafana-ui/src/components/FormGroup/FormGroup.tsx +++ b/packages/grafana-ui/src/components/FormGroup/FormGroup.tsx @@ -1,7 +1,7 @@ import React, { SFC } from 'react'; import { Label } from '..'; -interface Props { +export interface Props { label: string; inputProps: {}; labelWidth?: number; diff --git a/packages/grafana-ui/src/components/Label/Label.tsx b/packages/grafana-ui/src/components/Label/Label.tsx index 5d60efa056a..b31ed45e32a 100644 --- a/packages/grafana-ui/src/components/Label/Label.tsx +++ b/packages/grafana-ui/src/components/Label/Label.tsx @@ -1,5 +1,5 @@ import React, { SFC, ReactNode } from 'react'; -import { Tooltip } from '@grafana/ui'; +import { Tooltip } from '..'; interface Props { tooltip?: string; diff --git a/public/app/core/components/SharedPreferences/SharedPreferences.tsx b/public/app/core/components/SharedPreferences/SharedPreferences.tsx index ca933332db9..0b11d32b668 100644 --- a/public/app/core/components/SharedPreferences/SharedPreferences.tsx +++ b/public/app/core/components/SharedPreferences/SharedPreferences.tsx @@ -1,7 +1,6 @@ import React, { PureComponent } from 'react'; -import { Label } from '../../../../../packages/grafana-ui/src/components/Label/Label'; -import { Select } from '@grafana/ui'; +import { Label, Select } from '@grafana/ui'; import { getBackendSrv, BackendSrv } from 'app/core/services/backend_srv'; import { DashboardSearchHit } from 'app/types'; diff --git a/public/app/features/datasources/settings/BasicSettings.tsx b/public/app/features/datasources/settings/BasicSettings.tsx index 55dc9b54211..21a548a5045 100644 --- a/public/app/features/datasources/settings/BasicSettings.tsx +++ b/public/app/features/datasources/settings/BasicSettings.tsx @@ -1,5 +1,5 @@ import React, { SFC } from 'react'; -import { Label } from '../../../../../packages/grafana-ui/src/components/Label/Label'; +import { Label } from '@grafana/ui'; import { Switch } from '../../../core/components/Switch/Switch'; export interface Props { diff --git a/public/app/features/teams/TeamSettings.tsx b/public/app/features/teams/TeamSettings.tsx index 3424f39d22c..22815dbb7ec 100644 --- a/public/app/features/teams/TeamSettings.tsx +++ b/public/app/features/teams/TeamSettings.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { connect } from 'react-redux'; -import { Label } from '../../../../packages/grafana-ui/src/components/Label/Label'; +import { Label } from '@grafana/ui'; import { SharedPreferences } from 'app/core/components/SharedPreferences/SharedPreferences'; import { updateTeam } from './state/actions'; import { getRouteParamsId } from 'app/core/selectors/location'; diff --git a/public/app/plugins/panel/gauge/MappingRow.tsx b/public/app/plugins/panel/gauge/MappingRow.tsx index b05da5514aa..91dff549677 100644 --- a/public/app/plugins/panel/gauge/MappingRow.tsx +++ b/public/app/plugins/panel/gauge/MappingRow.tsx @@ -81,16 +81,15 @@ export default class MappingRow extends PureComponent { }} inputWidth={8} /> - this.updateMapping, - onChange: event => this.onMappingTextChange(event), - value: text, - }} - inputWidth={10} - /> +
+ + +
); } From b3512f43a37953ba5dd4c7d1c4db47b611528d4e Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Wed, 16 Jan 2019 11:11:00 +0100 Subject: [PATCH 016/274] build: repo update testable and more robus. - adds script for integration testing - package path parameterized - more robust updates --- .circleci/config.yml | 8 +++--- scripts/build/update_repo/init-deb-repo.sh | 12 ++++++++ .../build/update_repo/test-update-deb-repo.sh | 5 ++++ scripts/build/update_repo/update-deb.sh | 28 +++++++++++-------- scripts/build/update_repo/update-rpm.sh | 12 ++++---- 5 files changed, 45 insertions(+), 20 deletions(-) create mode 100755 scripts/build/update_repo/init-deb-repo.sh create mode 100755 scripts/build/update_repo/test-update-deb-repo.sh diff --git a/.circleci/config.yml b/.circleci/config.yml index ec1fcfb411f..509dce3d761 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -370,10 +370,10 @@ jobs: command: './scripts/build/load-signing-key.sh' - run: name: Update Debian repository - command: './scripts/build/update_repo/update-deb.sh "enterprise" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG"' + command: './scripts/build/update_repo/update-deb.sh "enterprise" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG" "enterprise-dist"' - run: name: Update RPM repository - command: './scripts/build/update_repo/update-rpm.sh "enterprise" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG"' + command: './scripts/build/update_repo/update-rpm.sh "enterprise" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG" "enterprise-dist"' deploy-master: @@ -433,10 +433,10 @@ jobs: command: './scripts/build/load-signing-key.sh' - run: name: Update Debian repository - command: './scripts/build/update_repo/update-deb.sh "oss" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG"' + command: './scripts/build/update_repo/update-deb.sh "oss" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG" "dist"' - run: name: Update RPM repository - command: './scripts/build/update_repo/update-rpm.sh "oss" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG"' + command: './scripts/build/update_repo/update-rpm.sh "oss" "$GPG_KEY_PASSWORD" "$CIRCLE_TAG" "dist"' workflows: version: 2 diff --git a/scripts/build/update_repo/init-deb-repo.sh b/scripts/build/update_repo/init-deb-repo.sh new file mode 100755 index 00000000000..2b245dc2d42 --- /dev/null +++ b/scripts/build/update_repo/init-deb-repo.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +# Run this if you need to recreate the debian repository for some reason + +# Setup environment +cp scripts/build/update_repo/aptly.conf /etc/aptly.conf +mkdir -p /deb-repo/db \ + /deb-repo/repo \ + /deb-repo/tmp + +aptly repo create -distribution=stable -component=main grafana +aptly repo create -distribution=beta -component=main beta diff --git a/scripts/build/update_repo/test-update-deb-repo.sh b/scripts/build/update_repo/test-update-deb-repo.sh new file mode 100755 index 00000000000..f27e9bec265 --- /dev/null +++ b/scripts/build/update_repo/test-update-deb-repo.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash + +GPG_PASS=${1:-} + +./scripts/build/update_repo/update-deb.sh "oss" "$GPG_PASS" "v5.4.3" "dist" "grafana-testing-aptly-db" "grafana-testing-repo" diff --git a/scripts/build/update_repo/update-deb.sh b/scripts/build/update_repo/update-deb.sh index 89c5937b064..0f80de9674c 100755 --- a/scripts/build/update_repo/update-deb.sh +++ b/scripts/build/update_repo/update-deb.sh @@ -3,10 +3,14 @@ RELEASE_TYPE="${1:-}" GPG_PASS="${2:-}" RELEASE_TAG="${3:-}" +DIST_PATH="${4:-}" +GCP_DB_BUCKET="${5:-grafana-aptly-db}" +GCP_REPO_BUCKET="${6:-grafana-repo}" + REPO="grafana" -if [ -z "$RELEASE_TYPE" -o -z "$GPG_PASS" ]; then - echo "Both RELEASE_TYPE (arg 1) and GPG_PASS (arg 2) has to be set" +if [ -z "$RELEASE_TYPE" -o -z "$GPG_PASS" -o -z "$DIST_PATH" ]; then + echo "Both RELEASE_TYPE (arg 1), GPG_PASS (arg 2) and DIST_PATH (arg 4) has to be set" exit 1 fi @@ -28,30 +32,32 @@ mkdir -p /deb-repo/db \ /deb-repo/tmp # Download the database -gsutil -m rsync -r "gs://grafana-aptly-db/$RELEASE_TYPE" /deb-repo/db +gsutil -m rsync -r -d "gs://$GCP_DB_BUCKET/$RELEASE_TYPE" /deb-repo/db # Add the new release to the repo -aptly publish drop grafana filesystem:repo:grafana || true -aptly publish drop beta filesystem:repo:grafana || true -cp ./dist/*.deb /deb-repo/tmp +cp $DIST_PATH/*.deb /deb-repo/tmp rm /deb-repo/tmp/grafana_latest*.deb || true -aptly repo add "$REPO" ./dist +aptly repo add "$REPO" /deb-repo/tmp #adds too many packages in enterprise # Setup signing and sign the repo echo "allow-loopback-pinentry" > ~/.gnupg/gpg-agent.conf echo "pinentry-mode loopback" > ~/.gnupg/gpg.conf +pkill gpg-agent || true touch /tmp/sign-this +rm /tmp/sign-this.asc || true ./scripts/build/update_repo/unlock-gpg-key.sh "$GPG_PASS" rm /tmp/sign-this /tmp/sign-this.asc -aptly publish repo grafana filesystem:repo:grafana -aptly publish repo beta filesystem:repo:grafana +aptly publish update stable filesystem:repo:grafana +aptly publish update beta filesystem:repo:grafana # Update the repo and db on gcp -gsutil -m rsync -r -d /deb-repo/db "gs://grafana-aptly-db/$RELEASE_TYPE" -gsutil -m rsync -r -d /deb-repo/repo/grafana "gs://grafana-repo/$RELEASE_TYPE/deb" +## TODO: need to update this to push the binaries first and then the metadata so that we dont cache the binaries missing. + +gsutil -m rsync -r -d /deb-repo/db "gs://$GCP_DB_BUCKET/$RELEASE_TYPE" +gsutil -m rsync -r -d /deb-repo/repo/grafana "gs://$GCP_REPO_BUCKET/$RELEASE_TYPE/deb" # usage: # diff --git a/scripts/build/update_repo/update-rpm.sh b/scripts/build/update_repo/update-rpm.sh index caed3918216..7b28412df37 100755 --- a/scripts/build/update_repo/update-rpm.sh +++ b/scripts/build/update_repo/update-rpm.sh @@ -2,12 +2,13 @@ RELEASE_TYPE="${1:-}" GPG_PASS="${2:-}" - RELEASE_TAG="${3:-}" +DIST_PATH="${4:-}" + REPO="rpm" -if [ -z "$RELEASE_TYPE" -o -z "$GPG_PASS" ]; then - echo "Both RELEASE_TYPE (arg 1) and GPG_PASS (arg 2) has to be set" +if [ -z "$RELEASE_TYPE" -o -z "$GPG_PASS" -o -z "$DIST_PATH" ]; then + echo "Both RELEASE_TYPE (arg 1), GPG_PASS (arg 2) and DIST_PATH (arg 4) has to be set" exit 1 fi @@ -30,10 +31,11 @@ mkdir -p /rpm-repo gsutil -m rsync -r "$BUCKET" /rpm-repo # Add the new release to the repo -cp ./dist/*.rpm /rpm-repo +cp $DIST_PATH/*.rpm /rpm-repo # adds to many files for enterprise rm /rpm-repo/grafana-latest-1*.rpm || true cd /rpm-repo createrepo . +cd /go/src/github.com/grafana/grafana # Setup signing and sign the repo @@ -56,4 +58,4 @@ gsutil -m rsync -r -d /rpm-repo "$BUCKET" # gpgcheck=1 # gpgkey=https://packages.grafana.com/gpg.key # sslverify=1 -# sslcacert=/etc/pki/tls/certs/ca-bundle.crt \ No newline at end of file +# sslcacert=/etc/pki/tls/certs/ca-bundle.crt From 166e5edebd39ff6c8073f86f48ec7577b5a428a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 16 Jan 2019 14:00:29 +0100 Subject: [PATCH 017/274] wip: testing new query editor row design --- .../dashboard/panel_editor/QueriesTab.tsx | 63 +++++++++---------- .../dashboard/panel_editor/QueryEditorRow.tsx | 44 ++++++++++++- .../panel/partials/query_editor_row.html | 44 +------------ public/sass/components/_query_editor.scss | 56 +++++++++++++++-- 4 files changed, 123 insertions(+), 84 deletions(-) diff --git a/public/app/features/dashboard/panel_editor/QueriesTab.tsx b/public/app/features/dashboard/panel_editor/QueriesTab.tsx index 1c842e6572c..b1d2bd7284b 100644 --- a/public/app/features/dashboard/panel_editor/QueriesTab.tsx +++ b/public/app/features/dashboard/panel_editor/QueriesTab.tsx @@ -171,40 +171,39 @@ export class QueriesTab extends PureComponent { return ( <> - -
- {panel.targets.map((query, index) => ( - - ))} - -
-
- -
-
- {!isAddingMixed && ( - - )} - {isAddingMixed && this.renderMixedPicker()} -
+
+ {panel.targets.map((query, index) => ( + + ))} +
+
+
+
+ +
+
+ {!isAddingMixed && ( + + )} + {isAddingMixed && this.renderMixedPicker()}
- +
diff --git a/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx b/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx index 1028815cf08..def0e85f07b 100644 --- a/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx +++ b/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx @@ -1,5 +1,6 @@ // Libraries import React, { PureComponent } from 'react'; +import classNames from 'classnames'; // Utils & Services import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; @@ -21,6 +22,7 @@ interface Props { interface State { datasource: DataSourceApi | null; + isCollapsed: boolean; } export class QueryEditorRow extends PureComponent { @@ -29,6 +31,7 @@ export class QueryEditorRow extends PureComponent { state: State = { datasource: null, + isCollapsed: false, }; componentDidMount() { @@ -90,15 +93,51 @@ export class QueryEditorRow extends PureComponent { } } + onToggleCollapse = () => { + this.setState({ isCollapsed: !this.state.isCollapsed }); + }; + render() { - const { datasource } = this.state; + const { query } = this.props; + const { datasource, isCollapsed } = this.state; + const bodyClasses = classNames('query-editor-box__body gf-form-query', {hide: isCollapsed}); if (!datasource) { return null; } if (datasource.pluginExports.QueryCtrl) { - return
(this.element = element)} />; + return ( +
+
+
+ {isCollapsed && } + {!isCollapsed && } + {query.refId} +
+
+ + + + + +
+
+
+
(this.element = element)} /> +
+
+ ); } else if (datasource.pluginExports.QueryEditor) { const QueryEditor = datasource.pluginExports.QueryEditor; return ; @@ -119,4 +158,3 @@ export interface AngularQueryComponentScope { moveQuery: (query: DataQuery, direction: number) => void; datasource: DataSourceApi; } - diff --git a/public/app/features/panel/partials/query_editor_row.html b/public/app/features/panel/partials/query_editor_row.html index 34a86813d1d..fc2e3602630 100644 --- a/public/app/features/panel/partials/query_editor_row.html +++ b/public/app/features/panel/partials/query_editor_row.html @@ -1,44 +1,2 @@ -
- +
-
-
- -
-
- -
- -
- - - -
-
diff --git a/public/sass/components/_query_editor.scss b/public/sass/components/_query_editor.scss index 8b876624294..fe455df1bff 100644 --- a/public/sass/components/_query_editor.scss +++ b/public/sass/components/_query_editor.scss @@ -18,12 +18,6 @@ } .gf-form-query { - display: flex; - flex-direction: row; - flex-wrap: nowrap; - align-content: flex-start; - align-items: flex-start; - .gf-form, .gf-form-filler { margin-bottom: 2px; @@ -188,3 +182,53 @@ input[type='text'].tight-form-func-param { .rst-literal-block .rst-text { display: block; } + +.query-editor-box { + background: $page-bg; + margin-bottom: 2px; + + &:hover { + .query-editor-box__actions { + display: flex; + } + } +} + +.query-editor-box__header { + display: flex; + padding: 4px 0px 4px 8px; + position: relative; + height: 35px; +} + +.query-editor-box__ref-id { + font-weight: $font-weight-semi-bold; + color: $blue; + font-size: $font-size-md; + flex-grow: 1; + cursor: pointer; + display: flex; + align-items: center; + + i { + padding-right: 5px; + color: $text-muted; + position: relative; + } +} + +.query-editor-box__actions { + display: flex; + justify-content: flex-end; + display: none; +} + +.query-editor-box__action { + @include buttonBackground($btn-inverse-bg, $btn-inverse-bg-hl, $btn-inverse-text-color, $btn-inverse-text-shadow); + border: 1px solid $navbar-button-border; + margin-right: 3px; +} + + .query-editor-box__body { + padding: 10px 20px; + } From 4c40274313f38cf57a525c3fd780213e9ee0e0a2 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Wed, 16 Jan 2019 13:46:57 +0000 Subject: [PATCH 018/274] renaming after pr feedback --- .../{FormGroup.test.tsx => FormField.test.tsx} | 4 ++-- .../FormGroup/{FormGroup.tsx => FormField.tsx} | 10 +++++----- .../GfFormLabel.tsx => FormLabel/FormLabel.tsx} | 2 +- packages/grafana-ui/src/components/index.ts | 4 ++-- .../features/dashboard/panel_editor/QueryOptions.tsx | 4 ++-- public/app/plugins/panel/gauge/GaugeOptionsEditor.tsx | 6 +++--- public/app/plugins/panel/gauge/MappingRow.tsx | 8 ++++---- public/app/plugins/panel/gauge/ValueOptions.tsx | 8 ++++---- 8 files changed, 23 insertions(+), 23 deletions(-) rename packages/grafana-ui/src/components/FormGroup/{FormGroup.test.tsx => FormField.test.tsx} (82%) rename packages/grafana-ui/src/components/FormGroup/{FormGroup.tsx => FormField.tsx} (58%) rename packages/grafana-ui/src/components/{GfFormLabel/GfFormLabel.tsx => FormLabel/FormLabel.tsx} (81%) diff --git a/packages/grafana-ui/src/components/FormGroup/FormGroup.test.tsx b/packages/grafana-ui/src/components/FormGroup/FormField.test.tsx similarity index 82% rename from packages/grafana-ui/src/components/FormGroup/FormGroup.test.tsx rename to packages/grafana-ui/src/components/FormGroup/FormField.test.tsx index 4f8b4be9540..4474b0680c5 100644 --- a/packages/grafana-ui/src/components/FormGroup/FormGroup.test.tsx +++ b/packages/grafana-ui/src/components/FormGroup/FormField.test.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { FormGroup, Props } from './FormGroup'; +import { FormField, Props } from './FormField'; const setup = (propOverrides?: object) => { const props: Props = { @@ -14,7 +14,7 @@ const setup = (propOverrides?: object) => { Object.assign(props, propOverrides); - return shallow(); + return shallow(); }; describe('Render', () => { diff --git a/packages/grafana-ui/src/components/FormGroup/FormGroup.tsx b/packages/grafana-ui/src/components/FormGroup/FormField.tsx similarity index 58% rename from packages/grafana-ui/src/components/FormGroup/FormGroup.tsx rename to packages/grafana-ui/src/components/FormGroup/FormField.tsx index a0088032079..ae86d4115b0 100644 --- a/packages/grafana-ui/src/components/FormGroup/FormGroup.tsx +++ b/packages/grafana-ui/src/components/FormGroup/FormField.tsx @@ -1,9 +1,9 @@ -import React, { SFC } from 'react'; +import React, { InputHTMLAttributes, FunctionComponent } from 'react'; import { Label } from '..'; export interface Props { label: string; - inputProps: {}; + inputProps: InputHTMLAttributes; labelWidth?: number; inputWidth?: number; } @@ -14,7 +14,7 @@ const defaultProps = { inputWidth: 12, }; -const FormGroup: SFC = ({ label, labelWidth, inputProps, inputWidth }) => { +const FormField: FunctionComponent = ({ label, labelWidth, inputProps, inputWidth }) => { return (
@@ -23,5 +23,5 @@ const FormGroup: SFC = ({ label, labelWidth, inputProps, inputWidth }) => ); }; -FormGroup.defaultProps = defaultProps; -export { FormGroup }; +FormField.defaultProps = defaultProps; +export { FormField }; diff --git a/packages/grafana-ui/src/components/GfFormLabel/GfFormLabel.tsx b/packages/grafana-ui/src/components/FormLabel/FormLabel.tsx similarity index 81% rename from packages/grafana-ui/src/components/GfFormLabel/GfFormLabel.tsx rename to packages/grafana-ui/src/components/FormLabel/FormLabel.tsx index 8b80de64696..d6ac3da9394 100644 --- a/packages/grafana-ui/src/components/GfFormLabel/GfFormLabel.tsx +++ b/packages/grafana-ui/src/components/FormLabel/FormLabel.tsx @@ -9,7 +9,7 @@ interface Props { isInvalid?: boolean; } -export const GfFormLabel: SFC = ({ children, isFocused, isInvalid, className, htmlFor, ...rest }) => { +export const FormLabel: SFC = ({ children, isFocused, isInvalid, className, htmlFor, ...rest }) => { const classes = classNames('gf-form-label', className, { 'gf-form-label--is-focused': isFocused, 'gf-form-label--is-invalid': isInvalid, diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index ab0edf45ed0..3a29623838a 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -10,8 +10,8 @@ export { NoOptionsMessage } from './Select/NoOptionsMessage'; export { default as resetSelectStyles } from './Select/resetSelectStyles'; // Forms -export { GfFormLabel } from './GfFormLabel/GfFormLabel'; -export { FormGroup } from './FormGroup/FormGroup'; +export { FormLabel } from './FormLabel/FormLabel'; +export { FormField } from './FormGroup/FormField'; export { Label } from './Label/Label'; export { LoadingPlaceholder } from './LoadingPlaceholder/LoadingPlaceholder'; diff --git a/public/app/features/dashboard/panel_editor/QueryOptions.tsx b/public/app/features/dashboard/panel_editor/QueryOptions.tsx index fad70d92990..d6187a89b7b 100644 --- a/public/app/features/dashboard/panel_editor/QueryOptions.tsx +++ b/public/app/features/dashboard/panel_editor/QueryOptions.tsx @@ -10,7 +10,7 @@ import { Input } from 'app/core/components/Form'; import { EventsWithValidation } from 'app/core/components/Form/Input'; import { InputStatus } from 'app/core/components/Form/Input'; import DataSourceOption from './DataSourceOption'; -import { GfFormLabel } from '@grafana/ui'; +import { FormLabel } from '@grafana/ui'; // Types import { PanelModel } from '../panel_model'; @@ -164,7 +164,7 @@ export class QueryOptions extends PureComponent { {this.renderOptions()}
- Relative time + Relative time > { onToggleThresholdLabels = () => @@ -21,12 +21,12 @@ export default class GaugeOptionsEditor extends PureComponent - this.onMinValueChange(event), value: minValue }} /> - this.onMaxValueChange(event), value: maxValue }} diff --git a/public/app/plugins/panel/gauge/MappingRow.tsx b/public/app/plugins/panel/gauge/MappingRow.tsx index 91dff549677..47647b1b9ae 100644 --- a/public/app/plugins/panel/gauge/MappingRow.tsx +++ b/public/app/plugins/panel/gauge/MappingRow.tsx @@ -1,5 +1,5 @@ import React, { PureComponent } from 'react'; -import { FormGroup, Label, MappingType, RangeMap, Select, ValueMap } from '@grafana/ui'; +import { FormField, Label, MappingType, RangeMap, Select, ValueMap } from '@grafana/ui'; interface Props { mapping: ValueMap | RangeMap; @@ -61,7 +61,7 @@ export default class MappingRow extends PureComponent { if (type === MappingType.RangeToText) { return ( <> - { }} inputWidth={8} /> - { return ( <> - Unit
- - - Date: Wed, 16 Jan 2019 13:52:38 +0000 Subject: [PATCH 019/274] move styling --- .../{FormGroup => FormField}/FormField.test.tsx | 0 .../{FormGroup => FormField}/FormField.tsx | 2 +- .../src/components/FormField/_FormField.scss | 12 ++++++++++++ .../__snapshots__/FormField.test.tsx.snap} | 0 packages/grafana-ui/src/components/index.scss | 1 + packages/grafana-ui/src/components/index.ts | 2 +- 6 files changed, 15 insertions(+), 2 deletions(-) rename packages/grafana-ui/src/components/{FormGroup => FormField}/FormField.test.tsx (100%) rename packages/grafana-ui/src/components/{FormGroup => FormField}/FormField.tsx (95%) create mode 100644 packages/grafana-ui/src/components/FormField/_FormField.scss rename packages/grafana-ui/src/components/{FormGroup/__snapshots__/FormGroup.test.tsx.snap => FormField/__snapshots__/FormField.test.tsx.snap} (100%) diff --git a/packages/grafana-ui/src/components/FormGroup/FormField.test.tsx b/packages/grafana-ui/src/components/FormField/FormField.test.tsx similarity index 100% rename from packages/grafana-ui/src/components/FormGroup/FormField.test.tsx rename to packages/grafana-ui/src/components/FormField/FormField.test.tsx diff --git a/packages/grafana-ui/src/components/FormGroup/FormField.tsx b/packages/grafana-ui/src/components/FormField/FormField.tsx similarity index 95% rename from packages/grafana-ui/src/components/FormGroup/FormField.tsx rename to packages/grafana-ui/src/components/FormField/FormField.tsx index ae86d4115b0..aa026a74197 100644 --- a/packages/grafana-ui/src/components/FormGroup/FormField.tsx +++ b/packages/grafana-ui/src/components/FormField/FormField.tsx @@ -16,7 +16,7 @@ const defaultProps = { const FormField: FunctionComponent = ({ label, labelWidth, inputProps, inputWidth }) => { return ( -
+
diff --git a/packages/grafana-ui/src/components/FormField/_FormField.scss b/packages/grafana-ui/src/components/FormField/_FormField.scss new file mode 100644 index 00000000000..36955e2fca6 --- /dev/null +++ b/packages/grafana-ui/src/components/FormField/_FormField.scss @@ -0,0 +1,12 @@ +.form-field { + margin-bottom: $gf-form-margin; + display: flex; + flex-direction: row; + align-items: center; + text-align: left; + position: relative; + + &--grow { + flex-grow: 1; + } +} diff --git a/packages/grafana-ui/src/components/FormGroup/__snapshots__/FormGroup.test.tsx.snap b/packages/grafana-ui/src/components/FormField/__snapshots__/FormField.test.tsx.snap similarity index 100% rename from packages/grafana-ui/src/components/FormGroup/__snapshots__/FormGroup.test.tsx.snap rename to packages/grafana-ui/src/components/FormField/__snapshots__/FormField.test.tsx.snap diff --git a/packages/grafana-ui/src/components/index.scss b/packages/grafana-ui/src/components/index.scss index b894cf73c1a..eaf64561ae8 100644 --- a/packages/grafana-ui/src/components/index.scss +++ b/packages/grafana-ui/src/components/index.scss @@ -6,3 +6,4 @@ @import 'PanelOptionsGroup/PanelOptionsGroup'; @import 'PanelOptionsGrid/PanelOptionsGrid'; @import 'ColorPicker/ColorPicker'; +@import "FormField/FormField"; diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 3a29623838a..ac06c07951b 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -11,7 +11,7 @@ export { default as resetSelectStyles } from './Select/resetSelectStyles'; // Forms export { FormLabel } from './FormLabel/FormLabel'; -export { FormField } from './FormGroup/FormField'; +export { FormField } from './FormField/FormField'; export { Label } from './Label/Label'; export { LoadingPlaceholder } from './LoadingPlaceholder/LoadingPlaceholder'; From a558e76a68824ed9187859a79cce3fc8129b470d Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Wed, 16 Jan 2019 15:09:48 +0100 Subject: [PATCH 020/274] fix: Manually trigger a change-event when autofill is used in webkit-browsers #12133 --- public/app/core/core.ts | 1 + .../app/core/directives/autofill_event_fix.ts | 35 +++++++++++++++++++ public/app/partials/login.html | 2 +- public/sass/_grafana.scss | 1 + public/sass/utils/_hacks.scss | 11 ++++++ 5 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 public/app/core/directives/autofill_event_fix.ts create mode 100644 public/sass/utils/_hacks.scss diff --git a/public/app/core/core.ts b/public/app/core/core.ts index 6713d8bcd14..fb38cefd435 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -1,5 +1,6 @@ import './directives/dash_class'; import './directives/dropdown_typeahead'; +import './directives/autofill_event_fix'; import './directives/metric_segment'; import './directives/misc'; import './directives/ng_model_on_blur'; diff --git a/public/app/core/directives/autofill_event_fix.ts b/public/app/core/directives/autofill_event_fix.ts new file mode 100644 index 00000000000..51d278fe7c9 --- /dev/null +++ b/public/app/core/directives/autofill_event_fix.ts @@ -0,0 +1,35 @@ +import coreModule from '../core_module'; + +/** @ngInject */ +export function autofillEventFix($compile) { + return { + link: ($scope: any, elem: any) => { + const input = elem[0]; + const dispatchChangeEvent = () => { + const event = new Event('change'); + return input.dispatchEvent(event); + }; + const onAnimationStart = ({ animationName }: AnimationEvent) => { + switch (animationName) { + case 'onAutoFillStart': + return dispatchChangeEvent(); + case 'onAutoFillCancel': + return dispatchChangeEvent(); + } + return null; + }; + + // const onChange = (evt: Event) => console.log(evt); + + input.addEventListener('animationstart', onAnimationStart); + // input.addEventListener('change', onChange); + + $scope.$on('$destroy', () => { + input.removeEventListener('animationstart', onAnimationStart); + // input.removeEventListener('change', onChange); + }); + } + }; +} + +coreModule.directive('autofillEventFix', autofillEventFix); diff --git a/public/app/partials/login.html b/public/app/partials/login.html index f4237e7b1ec..d629244e0ae 100644 --- a/public/app/partials/login.html +++ b/public/app/partials/login.html @@ -9,7 +9,7 @@