From dcb3a344d2153046bf8bd81b063c8c1242393620 Mon Sep 17 00:00:00 2001 From: Brian Gann Date: Tue, 12 Feb 2019 23:52:26 -0600 Subject: [PATCH 01/23] allow 90 percent of alertTimeout for rendering to complete vs 50 percent (cherry picked from commit c98b00c3024fde404af1d7772fc7cec62ec04c83) --- pkg/services/alerting/notifier.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index e1a550d48f4..59d459f122e 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -3,6 +3,7 @@ package alerting import ( "errors" "fmt" + "time" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/imguploader" @@ -126,7 +127,7 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) { renderOpts := rendering.Opts{ Width: 1000, Height: 500, - Timeout: alertTimeout / 2, + Timeout: time.Duration(float64(alertTimeout) * 0.9), OrgId: context.Rule.OrgId, OrgRole: m.ROLE_ADMIN, ConcurrentLimit: setting.AlertingRenderLimit, From c3e5a2c9681f56b5ea8be11a909d535dc15724d9 Mon Sep 17 00:00:00 2001 From: z0029c4 Date: Wed, 13 Feb 2019 17:55:31 +0530 Subject: [PATCH 02/23] added support for influxdb non_negative_difference function in tsdb for alerting (cherry picked from commit 8cb1e5b918c77c66ec077406ddbdcde9fa2a3905) --- pkg/tsdb/influxdb/query_part.go | 1 + pkg/tsdb/influxdb/query_part_test.go | 1 + 2 files changed, 2 insertions(+) diff --git a/pkg/tsdb/influxdb/query_part.go b/pkg/tsdb/influxdb/query_part.go index 29a77f15617..e81da28fd4d 100644 --- a/pkg/tsdb/influxdb/query_part.go +++ b/pkg/tsdb/influxdb/query_part.go @@ -33,6 +33,7 @@ func init() { renders["sum"] = QueryDefinition{Renderer: functionRenderer} renders["mode"] = QueryDefinition{Renderer: functionRenderer} renders["cumulative_sum"] = QueryDefinition{Renderer: functionRenderer} + renders["non_negative_difference"] = QueryDefinition{Renderer: functionRenderer} renders["holt_winters"] = QueryDefinition{ Renderer: functionRenderer, diff --git a/pkg/tsdb/influxdb/query_part_test.go b/pkg/tsdb/influxdb/query_part_test.go index 76daf6446d8..35ebbe0a13f 100644 --- a/pkg/tsdb/influxdb/query_part_test.go +++ b/pkg/tsdb/influxdb/query_part_test.go @@ -24,6 +24,7 @@ func TestInfluxdbQueryPart(t *testing.T) { {mode: "count", params: []string{}, input: "distinct(value)", expected: `count(distinct(value))`}, {mode: "mode", params: []string{}, input: "value", expected: `mode(value)`}, {mode: "cumulative_sum", params: []string{}, input: "mean(value)", expected: `cumulative_sum(mean(value))`}, + {mode: "non_negative_difference", params: []string{}, input: "max("value")", expected: `non_negative_difference(max("value"))`}, } queryContext := &tsdb.TsdbQuery{TimeRange: tsdb.NewTimeRange("5m", "now")} From 9e457077c6be59441254c1f116a33c797302abd6 Mon Sep 17 00:00:00 2001 From: z0029c4 Date: Wed, 13 Feb 2019 18:12:35 +0530 Subject: [PATCH 03/23] added support for influxdb non_negative_difference function in tsdb (cherry picked from commit 13974cdd28a636c13263ea6c81e1f1557e0f1c47) --- pkg/tsdb/influxdb/query_part_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/tsdb/influxdb/query_part_test.go b/pkg/tsdb/influxdb/query_part_test.go index 35ebbe0a13f..915c066b460 100644 --- a/pkg/tsdb/influxdb/query_part_test.go +++ b/pkg/tsdb/influxdb/query_part_test.go @@ -24,7 +24,7 @@ func TestInfluxdbQueryPart(t *testing.T) { {mode: "count", params: []string{}, input: "distinct(value)", expected: `count(distinct(value))`}, {mode: "mode", params: []string{}, input: "value", expected: `mode(value)`}, {mode: "cumulative_sum", params: []string{}, input: "mean(value)", expected: `cumulative_sum(mean(value))`}, - {mode: "non_negative_difference", params: []string{}, input: "max("value")", expected: `non_negative_difference(max("value"))`}, + {mode: "non_negative_difference", params: []string{}, input: "max(value)", expected: `non_negative_difference(max(value))`}, } queryContext := &tsdb.TsdbQuery{TimeRange: tsdb.NewTimeRange("5m", "now")} From 754be5a66ea5577881e64e816339879973a0a831 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 13 Feb 2019 16:36:59 +0100 Subject: [PATCH 04/23] Fixed issue with sass variables used from typescript, the prettier lowercases export variables (cherry picked from commit ef9e74fabb3dab6b3b93291bc8128f6075527ab2) --- public/app/features/dashboard/dashgrid/PanelChrome.tsx | 4 ++-- public/sass/_variables.scss.d.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index b050b1ff5e1..5c4ef430ce4 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -125,8 +125,8 @@ export class PanelChrome extends PureComponent { panelData={panelData} timeRange={timeRange} options={panel.getOptions(plugin.exports.PanelDefaults)} - width={width - 2 * variables.panelHorizontalPadding} - height={height - PANEL_HEADER_HEIGHT - variables.panelVerticalPadding} + width={width - 2 * variables.panelhorizontalpadding} + height={height - PANEL_HEADER_HEIGHT - variables.panelverticalpadding} renderCounter={renderCounter} onInterpolate={this.onInterpolate} /> diff --git a/public/sass/_variables.scss.d.ts b/public/sass/_variables.scss.d.ts index 5b44580a7aa..992dbb7bddf 100644 --- a/public/sass/_variables.scss.d.ts +++ b/public/sass/_variables.scss.d.ts @@ -1,6 +1,6 @@ export interface GrafanaVariables { - panelHorizontalPadding: number; - panelVerticalPadding: number; + panelhorizontalpadding: number; + panelverticalpadding: number; } declare const variables: GrafanaVariables; From d924d3f6c90fe98ad78a537b9d3d45fad92348f5 Mon Sep 17 00:00:00 2001 From: Maxim Neverov Date: Thu, 14 Feb 2019 16:31:15 +0100 Subject: [PATCH 05/23] Fix percent_diff calculation when points are nulls (cherry picked from commit 3c2f6094b2a90676558f4c4c58d2871bedd2b0df) --- pkg/services/alerting/conditions/reducer.go | 19 +++++++++++-------- .../alerting/conditions/reducer_test.go | 12 ++++++++++++ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/pkg/services/alerting/conditions/reducer.go b/pkg/services/alerting/conditions/reducer.go index 1e8ae792746..485ea43561b 100644 --- a/pkg/services/alerting/conditions/reducer.go +++ b/pkg/services/alerting/conditions/reducer.go @@ -131,14 +131,17 @@ func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) null.Float { break } } - // get the oldest point - points = points[0:i] - for i := 0; i < len(points); i++ { - if points[i][0].Valid { - allNull = false - val := (first - points[i][0].Float64) / points[i][0].Float64 * 100 - value = math.Abs(val) - break + + if i >= 1 { + // get the oldest point + points = points[0:i] + for i := 0; i < len(points); i++ { + if points[i][0].Valid { + allNull = false + val := (first - points[i][0].Float64) / points[i][0].Float64 * 100 + value = math.Abs(val) + break + } } } case "count_non_null": diff --git a/pkg/services/alerting/conditions/reducer_test.go b/pkg/services/alerting/conditions/reducer_test.go index 7f11fc498bd..70b03f1dba8 100644 --- a/pkg/services/alerting/conditions/reducer_test.go +++ b/pkg/services/alerting/conditions/reducer_test.go @@ -157,6 +157,18 @@ func TestSimpleReducer(t *testing.T) { result := testReducer("percent_diff", 30, 40, 40) So(result, ShouldEqual, float64(33.33333333333333)) }) + + Convey("percent_diff with only nulls", func() { + reducer := NewSimpleReducer("percent_diff") + series := &tsdb.TimeSeries{ + Name: "test time serie", + } + + series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 1)) + series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 2)) + + So(reducer.Reduce(series).Valid, ShouldEqual, false) + }) }) } From 688a8e286df3ab8b3d6262eded5dab79dd469521 Mon Sep 17 00:00:00 2001 From: Maxim Neverov Date: Thu, 14 Feb 2019 21:52:59 +0100 Subject: [PATCH 06/23] Extracted common code for diff calculation (cherry picked from commit 28eaac3a9c7082e8c496005c1cb66b4b70a4f82f) --- pkg/services/alerting/conditions/reducer.go | 87 +++++++++---------- .../alerting/conditions/reducer_test.go | 12 +++ 2 files changed, 51 insertions(+), 48 deletions(-) diff --git a/pkg/services/alerting/conditions/reducer.go b/pkg/services/alerting/conditions/reducer.go index 485ea43561b..f55545be311 100644 --- a/pkg/services/alerting/conditions/reducer.go +++ b/pkg/services/alerting/conditions/reducer.go @@ -95,55 +95,9 @@ func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) null.Float { } } case "diff": - var ( - points = series.Points - first float64 - i int - ) - // get the newest point - for i = len(points) - 1; i >= 0; i-- { - if points[i][0].Valid { - allNull = false - first = points[i][0].Float64 - break - } - } - // get the oldest point - points = points[0:i] - for i := 0; i < len(points); i++ { - if points[i][0].Valid { - allNull = false - value = first - points[i][0].Float64 - break - } - } + allNull, value = calculateDiff(series, allNull, value, diff) case "percent_diff": - var ( - points = series.Points - first float64 - i int - ) - // get the newest point - for i = len(points) - 1; i >= 0; i-- { - if points[i][0].Valid { - allNull = false - first = points[i][0].Float64 - break - } - } - - if i >= 1 { - // get the oldest point - points = points[0:i] - for i := 0; i < len(points); i++ { - if points[i][0].Valid { - allNull = false - val := (first - points[i][0].Float64) / points[i][0].Float64 * 100 - value = math.Abs(val) - break - } - } - } + allNull, value = calculateDiff(series, allNull, value, percentDiff) case "count_non_null": for _, v := range series.Points { if v[0].Valid { @@ -166,3 +120,40 @@ func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) null.Float { func NewSimpleReducer(typ string) *SimpleReducer { return &SimpleReducer{Type: typ} } + +func calculateDiff(series *tsdb.TimeSeries, allNull bool, value float64, fn func(float64, float64) float64) (bool, float64) { + var ( + points = series.Points + first float64 + i int + ) + // get the newest point + for i = len(points) - 1; i >= 0; i-- { + if points[i][0].Valid { + allNull = false + first = points[i][0].Float64 + break + } + } + if i >= 1 { + // get the oldest point + points = points[0:i] + for i := 0; i < len(points); i++ { + if points[i][0].Valid { + allNull = false + val := fn(first, points[i][0].Float64) + value = math.Abs(val) + break + } + } + } + return allNull, value +} + +var diff = func(newest, oldest float64) float64 { + return newest - oldest +} + +var percentDiff = func(newest, oldest float64) float64 { + return (newest - oldest) / oldest * 100 +} diff --git a/pkg/services/alerting/conditions/reducer_test.go b/pkg/services/alerting/conditions/reducer_test.go index 70b03f1dba8..d2c21771d0b 100644 --- a/pkg/services/alerting/conditions/reducer_test.go +++ b/pkg/services/alerting/conditions/reducer_test.go @@ -143,6 +143,18 @@ func TestSimpleReducer(t *testing.T) { So(result, ShouldEqual, float64(10)) }) + Convey("diff with only nulls", func() { + reducer := NewSimpleReducer("diff") + series := &tsdb.TimeSeries{ + Name: "test time serie", + } + + series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 1)) + series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), 2)) + + So(reducer.Reduce(series).Valid, ShouldEqual, false) + }) + Convey("percent_diff one point", func() { result := testReducer("percent_diff", 40) So(result, ShouldEqual, float64(0)) From d757c74442247e7f30275710fcd44231463c9ac7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 15 Feb 2019 11:03:24 +0100 Subject: [PATCH 07/23] Fixed dashboard navbar buttons being visible in fullscreen, fixes #15450 (cherry picked from commit 7b59b9cf4b428f255131c8af68d651e6c54972c2) --- public/sass/components/_navbar.scss | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/public/sass/components/_navbar.scss b/public/sass/components/_navbar.scss index a915d003eab..86494f9e47a 100644 --- a/public/sass/components/_navbar.scss +++ b/public/sass/components/_navbar.scss @@ -46,9 +46,8 @@ .navbar-button--add-panel, .navbar-button--star, - .navbar-button--tv, - .navbar-buttons--close { - display: flex; + .navbar-button--tv { + display: none; } } From acb329f2f1c7083e614cf5221658b33e42d2df4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 15 Feb 2019 11:23:31 +0100 Subject: [PATCH 08/23] fixed handling of alert urls with true flags, fixes #15454 (cherry picked from commit 7699706e65ffd48c03196907de6b89d2b0be1c7f) --- pkg/services/alerting/eval_context.go | 2 +- .../app/features/alerting/AlertRuleItem.tsx | 2 +- .../__snapshots__/AlertRuleItem.test.tsx.snap | 4 +-- .../containers/DashboardPage.test.tsx | 34 ++++++++++++++++++- .../dashboard/containers/DashboardPage.tsx | 6 ++-- 5 files changed, 40 insertions(+), 8 deletions(-) diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index 4db942e0a55..02b9955662f 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -104,7 +104,7 @@ func (c *EvalContext) GetDashboardUID() (*m.DashboardRef, error) { return c.dashboardRef, nil } -const urlFormat = "%s?fullscreen=true&edit=true&tab=alert&panelId=%d&orgId=%d" +const urlFormat = "%s?fullscreen&edit&tab=alert&panelId=%d&orgId=%d" func (c *EvalContext) GetRuleUrl() (string, error) { if c.IsTestRun { diff --git a/public/app/features/alerting/AlertRuleItem.tsx b/public/app/features/alerting/AlertRuleItem.tsx index 86bb0207460..3fec37d19b8 100644 --- a/public/app/features/alerting/AlertRuleItem.tsx +++ b/public/app/features/alerting/AlertRuleItem.tsx @@ -29,7 +29,7 @@ class AlertRuleItem extends PureComponent { 'fa-pause': rule.state !== 'paused', }); - const ruleUrl = `${rule.url}?panelId=${rule.panelId}&fullscreen=true&edit=true&tab=alert`; + const ruleUrl = `${rule.url}?panelId=${rule.panelId}&fullscreen&edit&tab=alert`; return (
  • diff --git a/public/app/features/alerting/__snapshots__/AlertRuleItem.test.tsx.snap b/public/app/features/alerting/__snapshots__/AlertRuleItem.test.tsx.snap index f686127ebf3..8e076ffd22e 100644 --- a/public/app/features/alerting/__snapshots__/AlertRuleItem.test.tsx.snap +++ b/public/app/features/alerting/__snapshots__/AlertRuleItem.test.tsx.snap @@ -21,7 +21,7 @@ exports[`Render should render component 1`] = ` className="alert-rule-item__name" > { expect(ctx.cleanUpDashboardMock.calls).toBe(1); }); }); + + describe('mapStateToProps with bool fullscreen', () => { + const props = mapStateToProps({ + location: { + routeParams: {}, + query: { + fullscreen: true, + edit: false, + }, + }, + dashboard: {}, + } as any); + + expect(props.urlFullscreen).toBe(true); + expect(props.urlEdit).toBe(false); + }); + + describe('mapStateToProps with string edit true', () => { + const props = mapStateToProps({ + location: { + routeParams: {}, + query: { + fullscreen: false, + edit: 'true', + }, + }, + dashboard: {}, + } as any); + + expect(props.urlFullscreen).toBe(false); + expect(props.urlEdit).toBe(true); + }); }); diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 27118e297b5..bdb601a692f 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -284,15 +284,15 @@ export class DashboardPage extends PureComponent { } } -const mapStateToProps = (state: StoreState) => ({ +export const mapStateToProps = (state: StoreState) => ({ urlUid: state.location.routeParams.uid, urlSlug: state.location.routeParams.slug, urlType: state.location.routeParams.type, editview: state.location.query.editview, urlPanelId: state.location.query.panelId, urlFolderId: state.location.query.folderId, - urlFullscreen: state.location.query.fullscreen === true, - urlEdit: state.location.query.edit === true, + urlFullscreen: !!state.location.query.fullscreen, + urlEdit: !!state.location.query.edit, initPhase: state.dashboard.initPhase, isInitSlow: state.dashboard.isInitSlow, initError: state.dashboard.initError, From 652ea3c08ec033773d4ff1b4038c9f0b0531269f Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 11 Feb 2019 16:44:09 +0100 Subject: [PATCH 09/23] Move error boundry from DataPanel to PanelChrome #15424, rebased and squashed PR hard move Revert "hard move" This reverts commit a2dad6157a0e77dbdae2f6c7440b55d6a40e3864. bubble error from datapanel to panelchrome remove comments implement show error in panelcorner fixed issue with updatePopperPosition using error callback from datapanel instead Fixes to error handling and clearing, also publishing of legacy events so old query editors work with react panels fully added another error message scenario Restored loading spinner to DataPanel Fixed merge issues (cherry picked from commit 125c087aab64b434dac03e577b39574fe841fed7) --- .../src/components/Tooltip/Popper.tsx | 20 +++-- packages/grafana-ui/src/types/datasource.ts | 10 +++ public/app/core/services/AngularLoader.ts | 4 +- .../features/dashboard/dashgrid/DataPanel.tsx | 73 ++++++------------ .../dashboard/dashgrid/PanelChrome.tsx | 47 +++++++++++- .../dashgrid/PanelHeader/PanelHeader.tsx | 6 +- .../PanelHeader/PanelHeaderCorner.tsx | 53 +++++++------ .../dashboard/panel_editor/QueryEditorRow.tsx | 75 ++++++++++++------- 8 files changed, 173 insertions(+), 115 deletions(-) diff --git a/packages/grafana-ui/src/components/Tooltip/Popper.tsx b/packages/grafana-ui/src/components/Tooltip/Popper.tsx index 93a896d97ed..cf4b9cdd653 100644 --- a/packages/grafana-ui/src/components/Tooltip/Popper.tsx +++ b/packages/grafana-ui/src/components/Tooltip/Popper.tsx @@ -35,8 +35,16 @@ interface Props extends React.HTMLAttributes { class Popper extends PureComponent { render() { - const { show, placement, onMouseEnter, onMouseLeave, className, wrapperClassName, renderArrow } = this.props; - const { content } = this.props; + const { + content, + show, + placement, + onMouseEnter, + onMouseLeave, + className, + wrapperClassName, + renderArrow, + } = this.props; return ( @@ -50,7 +58,7 @@ class Popper extends PureComponent { // TODO: move modifiers config to popper controller modifiers={{ preventOverflow: { enabled: true, boundariesElement: 'window' } }} > - {({ ref, style, placement, arrowProps, scheduleUpdate }) => { + {({ ref, style, placement, arrowProps }) => { return (
    { className={`${wrapperClassName}`} >
    - {typeof content === 'string' - ? content - : React.cloneElement(content, { - updatePopperPosition: scheduleUpdate, - })} + {typeof content === 'string' ? content : React.cloneElement(content)} {renderArrow && renderArrow({ arrowProps, diff --git a/packages/grafana-ui/src/types/datasource.ts b/packages/grafana-ui/src/types/datasource.ts index e34cf25dc01..a34f39b59c6 100644 --- a/packages/grafana-ui/src/types/datasource.ts +++ b/packages/grafana-ui/src/types/datasource.ts @@ -29,6 +29,16 @@ export interface DataQuery { datasource?: string | null; } +export interface DataQueryError { + data?: { + message?: string; + error?: string; + }; + message?: string; + status?: string; + statusText?: string; +} + export interface DataQueryOptions { timezone: string; range: TimeRange; diff --git a/public/app/core/services/AngularLoader.ts b/public/app/core/services/AngularLoader.ts index 54dd9a35767..d9b78e66cba 100644 --- a/public/app/core/services/AngularLoader.ts +++ b/public/app/core/services/AngularLoader.ts @@ -27,7 +27,9 @@ export class AngularLoader { compiledElem.remove(); }, digest: () => { - scope.$digest(); + if (!scope.$$phase) { + scope.$digest(); + } }, getScope: () => { return scope; diff --git a/public/app/features/dashboard/dashgrid/DataPanel.tsx b/public/app/features/dashboard/dashgrid/DataPanel.tsx index b81d66fa7f5..0675c7afa60 100644 --- a/public/app/features/dashboard/dashgrid/DataPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DataPanel.tsx @@ -1,8 +1,6 @@ // Library import React, { Component } from 'react'; -import { Tooltip } from '@grafana/ui'; -import ErrorBoundary from 'app/core/components/ErrorBoundary/ErrorBoundary'; // Services import { DatasourceSrv, getDatasourceSrv } from 'app/features/plugins/datasource_srv'; // Utils @@ -11,6 +9,7 @@ import kbn from 'app/core/utils/kbn'; import { DataQueryOptions, DataQueryResponse, + DataQueryError, LoadingState, PanelData, TableData, @@ -18,8 +17,6 @@ import { TimeSeries, } from '@grafana/ui'; -const DEFAULT_PLUGIN_ERROR = 'Error in plugin'; - interface RenderProps { loading: LoadingState; panelData: PanelData; @@ -38,12 +35,12 @@ export interface Props { maxDataPoints?: number; children: (r: RenderProps) => JSX.Element; onDataResponse?: (data: DataQueryResponse) => void; + onError: (message: string, error: DataQueryError) => void; } export interface State { isFirstLoad: boolean; loading: LoadingState; - errorMessage: string; response: DataQueryResponse; } @@ -61,7 +58,6 @@ export class DataPanel extends Component { this.state = { loading: LoadingState.NotStarted, - errorMessage: '', response: { data: [], }, @@ -100,6 +96,7 @@ export class DataPanel extends Component { widthPixels, maxDataPoints, onDataResponse, + onError, } = this.props; if (!isVisible) { @@ -111,7 +108,7 @@ export class DataPanel extends Component { return; } - this.setState({ loading: LoadingState.Loading, errorMessage: '' }); + this.setState({ loading: LoadingState.Loading }); try { const ds = await this.dataSourceSrv.get(datasource); @@ -150,18 +147,22 @@ export class DataPanel extends Component { isFirstLoad: false, }); } catch (err) { - console.log('Loading error', err); - this.onError('Request Error'); - } - }; + console.log('DataPanel error', err); - onError = (errorMessage: string) => { - if (this.state.loading !== LoadingState.Error || this.state.errorMessage !== errorMessage) { - this.setState({ - loading: LoadingState.Error, - isFirstLoad: false, - errorMessage: errorMessage, - }); + let message = 'Query error'; + + if (err.message) { + message = err.message; + } else if (err.data && err.data.message) { + message = err.data.message; + } else if (err.data && err.data.error) { + message = err.data.error; + } else if (err.status) { + message = `Query error: ${err.status} ${err.statusText}`; + } + + onError(message, err); + this.setState({ isFirstLoad: false }); } }; @@ -184,11 +185,10 @@ export class DataPanel extends Component { render() { const { queries } = this.props; const { loading, isFirstLoad } = this.state; - const panelData = this.getPanelData(); if (isFirstLoad && loading === LoadingState.Loading) { - return this.renderLoadingStates(); + return this.renderLoadingState(); } if (!queries.length) { @@ -201,46 +201,21 @@ export class DataPanel extends Component { return ( <> - {this.renderLoadingStates()} - - {({ error, errorInfo }) => { - if (errorInfo) { - this.onError(error.message || DEFAULT_PLUGIN_ERROR); - return null; - } - return ( - <> - {this.props.children({ - loading, - panelData, - })} - - ); - }} - + {this.renderLoadingState()} + {this.props.children({ loading, panelData })} ); } - private renderLoadingStates(): JSX.Element { - const { loading, errorMessage } = this.state; + private renderLoadingState(): JSX.Element { + const { loading } = this.state; if (loading === LoadingState.Loading) { return (
    ); - } else if (loading === LoadingState.Error) { - return ( - -
    - - -
    -
    - ); } - return null; } } diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index 5c4ef430ce4..5a993293946 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -8,6 +8,7 @@ import { getTimeSrv, TimeSrv } from '../services/TimeSrv'; // Components import { PanelHeader } from './PanelHeader/PanelHeader'; import { DataPanel } from './DataPanel'; +import ErrorBoundary from '../../../core/components/ErrorBoundary/ErrorBoundary'; // Utils import { applyPanelTimeOverrides, snapshotDataToPanelData } from 'app/features/dashboard/utils/panel'; @@ -17,11 +18,12 @@ import { profiler } from 'app/core/profiler'; // Types import { DashboardModel, PanelModel } from '../state'; import { PanelPlugin } from 'app/types'; -import { TimeRange, LoadingState, PanelData } from '@grafana/ui'; +import { DataQueryResponse, TimeRange, LoadingState, PanelData, DataQueryError } from '@grafana/ui'; import variables from 'sass/_variables.scss'; import templateSrv from 'app/features/templating/template_srv'; -import { DataQueryResponse } from '@grafana/ui/src'; + +const DEFAULT_PLUGIN_ERROR = 'Error in plugin'; export interface Props { panel: PanelModel; @@ -34,6 +36,7 @@ export interface State { renderCounter: number; timeInfo?: string; timeRange?: TimeRange; + errorMessage: string | null; } export class PanelChrome extends PureComponent { @@ -45,6 +48,7 @@ export class PanelChrome extends PureComponent { this.state = { refreshCounter: 0, renderCounter: 0, + errorMessage: null, }; } @@ -88,8 +92,33 @@ export class PanelChrome extends PureComponent { if (this.props.dashboard.isSnapshot()) { this.props.panel.snapshotData = dataQueryResponse.data; } + // clear error state (if any) + this.clearErrorState(); + + // This event is used by old query editors and panel editor options + this.props.panel.events.emit('data-received', dataQueryResponse.data); }; + onDataError = (message: string, error: DataQueryError) => { + if (this.state.errorMessage !== message) { + this.setState({ errorMessage: message }); + } + // this event is used by old query editors + this.props.panel.events.emit('data-error', error); + }; + + onPanelError = (message: string) => { + if (this.state.errorMessage !== message) { + this.setState({ errorMessage: message }); + } + }; + + clearErrorState() { + if (this.state.errorMessage) { + this.setState({ errorMessage: null }); + } + } + get isVisible() { return !this.props.dashboard.otherPanelInFullscreen(this.props.panel); } @@ -150,6 +179,7 @@ export class PanelChrome extends PureComponent { widthPixels={width} refreshCounter={refreshCounter} onDataResponse={this.onDataResponse} + onError={this.onDataError} > {({ loading, panelData }) => { return this.renderPanelPlugin(loading, panelData, width, height); @@ -164,7 +194,7 @@ export class PanelChrome extends PureComponent { render() { const { dashboard, panel } = this.props; - const { timeInfo } = this.state; + const { errorMessage, timeInfo } = this.state; const { transparent } = panel; const containerClassNames = `panel-container panel-container--absolute ${transparent ? 'panel-transparent' : ''}`; @@ -185,8 +215,17 @@ export class PanelChrome extends PureComponent { description={panel.description} scopedVars={panel.scopedVars} links={panel.links} + error={errorMessage} /> - {this.renderPanelBody(width, height)} + + {({ error, errorInfo }) => { + if (errorInfo) { + this.onPanelError(error.message || DEFAULT_PLUGIN_ERROR); + return null; + } + return this.renderPanelBody(width, height); + }} +
    ); }} diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx index 22766b4c8e6..2be32cd9955 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx @@ -2,7 +2,7 @@ import React, { Component } from 'react'; import classNames from 'classnames'; import { isEqual } from 'lodash'; -import PanelHeaderCorner from './PanelHeaderCorner'; +import { PanelHeaderCorner } from './PanelHeaderCorner'; import { PanelHeaderMenu } from './PanelHeaderMenu'; import templateSrv from 'app/features/templating/template_srv'; @@ -18,6 +18,7 @@ export interface Props { description?: string; scopedVars?: string; links?: []; + error?: string; } interface ClickCoordinates { @@ -71,7 +72,7 @@ export class PanelHeader extends Component { const isFullscreen = false; const isLoading = false; const panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen }); - const { panel, dashboard, timeInfo, scopedVars } = this.props; + const { panel, dashboard, timeInfo, scopedVars, error } = this.props; const title = templateSrv.replaceWithText(panel.title, scopedVars); return ( @@ -82,6 +83,7 @@ export class PanelHeader extends Component { description={panel.description} scopedVars={panel.scopedVars} links={panel.links} + error={error} />
    {isLoading && ( diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx index e1c21315bd2..63ebed1632c 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeaderCorner.tsx @@ -6,7 +6,7 @@ import templateSrv from 'app/features/templating/template_srv'; import { LinkSrv } from 'app/features/panel/panellinks/link_srv'; import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv'; -enum InfoModes { +enum InfoMode { Error = 'Error', Info = 'Info', Links = 'Links', @@ -18,18 +18,22 @@ interface Props { description?: string; scopedVars?: string; links?: []; + error?: string; } export class PanelHeaderCorner extends Component { timeSrv: TimeSrv = getTimeSrv(); getInfoMode = () => { - const { panel } = this.props; + const { panel, error } = this.props; + if (error) { + return InfoMode.Error; + } if (!!panel.description) { - return InfoModes.Info; + return InfoMode.Info; } if (panel.links && panel.links.length) { - return InfoModes.Links; + return InfoMode.Links; } return undefined; @@ -42,7 +46,7 @@ export class PanelHeaderCorner extends Component { const interpolatedMarkdown = templateSrv.replace(markdown, panel.scopedVars); const remarkableInterpolatedMarkdown = new Remarkable().render(interpolatedMarkdown); - const html = ( + return (
    {panel.links && @@ -62,30 +66,35 @@ export class PanelHeaderCorner extends Component { )}
    ); - - return html; }; + renderCornerType(infoMode: InfoMode, content: string | JSX.Element) { + const theme = infoMode === InfoMode.Error ? 'error' : 'info'; + return ( + +
    + + +
    +
    + ); + } + render() { - const infoMode: InfoModes | undefined = this.getInfoMode(); + const infoMode: InfoMode | undefined = this.getInfoMode(); if (!infoMode) { return null; } - return ( - <> - {infoMode === InfoModes.Info || infoMode === InfoModes.Links ? ( - -
    - - -
    -
    - ) : null} - - ); + if (infoMode === InfoMode.Error) { + return this.renderCornerType(infoMode, this.props.error); + } + + if (infoMode === InfoMode.Info) { + return this.renderCornerType(infoMode, this.getInfoContent()); + } + + return null; } } - -export default PanelHeaderCorner; diff --git a/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx b/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx index cf89175a5ee..0b8d2c39908 100644 --- a/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx +++ b/public/app/features/dashboard/panel_editor/QueryEditorRow.tsx @@ -28,28 +28,57 @@ interface State { loadedDataSourceValue: string | null | undefined; datasource: DataSourceApi | null; isCollapsed: boolean; - angularScope: AngularQueryComponentScope | null; + hasTextEditMode: boolean; } export class QueryEditorRow extends PureComponent { element: HTMLElement | null = null; + angularScope: AngularQueryComponentScope | null; angularQueryEditor: AngularComponent | null = null; state: State = { datasource: null, isCollapsed: false, - angularScope: null, loadedDataSourceValue: undefined, + hasTextEditMode: false, }; componentDidMount() { this.loadDatasource(); this.props.panel.events.on('refresh', this.onPanelRefresh); + this.props.panel.events.on('data-error', this.onPanelDataError); + this.props.panel.events.on('data-received', this.onPanelDataReceived); } + componentWillUnmount() { + this.props.panel.events.off('refresh', this.onPanelRefresh); + this.props.panel.events.off('data-error', this.onPanelDataError); + this.props.panel.events.off('data-received', this.onPanelDataReceived); + + if (this.angularQueryEditor) { + this.angularQueryEditor.destroy(); + } + } + + onPanelDataError = () => { + // Some query controllers listen to data error events and need a digest + if (this.angularQueryEditor) { + // for some reason this needs to be done in next tick + setTimeout(this.angularQueryEditor.digest); + } + }; + + onPanelDataReceived = () => { + // Some query controllers listen to data error events and need a digest + if (this.angularQueryEditor) { + // for some reason this needs to be done in next tick + setTimeout(this.angularQueryEditor.digest); + } + }; + onPanelRefresh = () => { - if (this.state.angularScope) { - this.state.angularScope.range = getTimeSrv().timeRange(); + if (this.angularScope) { + this.angularScope.range = getTimeSrv().timeRange(); } }; @@ -73,7 +102,11 @@ export class QueryEditorRow extends PureComponent { const dataSourceSrv = getDatasourceSrv(); const datasource = await dataSourceSrv.get(query.datasource || panel.datasource); - this.setState({ datasource, loadedDataSourceValue: this.props.dataSourceValue }); + this.setState({ + datasource, + loadedDataSourceValue: this.props.dataSourceValue, + hasTextEditMode: false, + }); } componentDidUpdate() { @@ -98,21 +131,14 @@ export class QueryEditorRow extends PureComponent { const scopeProps = { ctrl: this.getAngularQueryComponentScope() }; this.angularQueryEditor = loader.load(this.element, scopeProps, template); + this.angularScope = scopeProps.ctrl; // give angular time to compile setTimeout(() => { - this.setState({ angularScope: scopeProps.ctrl }); + this.setState({ hasTextEditMode: !!this.angularScope.toggleEditorMode }); }, 10); } - componentWillUnmount() { - this.props.panel.events.off('refresh', this.onPanelRefresh); - - if (this.angularQueryEditor) { - this.angularQueryEditor.destroy(); - } - } - onToggleCollapse = () => { this.setState({ isCollapsed: !this.state.isCollapsed }); }; @@ -138,10 +164,8 @@ export class QueryEditorRow extends PureComponent { } onToggleEditMode = () => { - const { angularScope } = this.state; - - if (angularScope && angularScope.toggleEditorMode) { - angularScope.toggleEditorMode(); + if (this.angularScope && this.angularScope.toggleEditorMode) { + this.angularScope.toggleEditorMode(); this.angularQueryEditor.digest(); } @@ -150,11 +174,6 @@ export class QueryEditorRow extends PureComponent { } }; - get hasTextEditMode() { - const { angularScope } = this.state; - return angularScope && angularScope.toggleEditorMode; - } - onRemoveQuery = () => { this.props.onRemoveQuery(this.props.query); }; @@ -171,10 +190,8 @@ export class QueryEditorRow extends PureComponent { }; renderCollapsedText(): string | null { - const { angularScope } = this.state; - - if (angularScope && angularScope.getCollapsedText) { - return angularScope.getCollapsedText(); + if (this.angularScope && this.angularScope.getCollapsedText) { + return this.angularScope.getCollapsedText(); } return null; @@ -182,7 +199,7 @@ export class QueryEditorRow extends PureComponent { render() { const { query, inMixedMode } = this.props; - const { datasource, isCollapsed } = this.state; + const { datasource, isCollapsed, hasTextEditMode } = this.state; const isDisabled = query.hide; const bodyClasses = classNames('query-editor-row__body gf-form-query', { @@ -212,7 +229,7 @@ export class QueryEditorRow extends PureComponent { {isCollapsed &&
    {this.renderCollapsedText()}
    }
    - {this.hasTextEditMode && ( + {hasTextEditMode && (
    )} diff --git a/public/app/features/dashboard/dashgrid/DataPanel.tsx b/public/app/features/dashboard/dashgrid/DataPanel.tsx index 0675c7afa60..9718e150e2a 100644 --- a/public/app/features/dashboard/dashgrid/DataPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DataPanel.tsx @@ -162,7 +162,7 @@ export class DataPanel extends Component { } onError(message, err); - this.setState({ isFirstLoad: false }); + this.setState({ isFirstLoad: false, loading: LoadingState.Error }); } }; @@ -187,7 +187,8 @@ export class DataPanel extends Component { const { loading, isFirstLoad } = this.state; const panelData = this.getPanelData(); - if (isFirstLoad && loading === LoadingState.Loading) { + // do not render component until we have first data + if (isFirstLoad && (loading === LoadingState.Loading || loading === LoadingState.NotStarted)) { return this.renderLoadingState(); } @@ -201,21 +202,17 @@ export class DataPanel extends Component { return ( <> - {this.renderLoadingState()} + {loading === LoadingState.Loading && this.renderLoadingState()} {this.props.children({ loading, panelData })} ); } private renderLoadingState(): JSX.Element { - const { loading } = this.state; - if (loading === LoadingState.Loading) { - return ( -
    - -
    - ); - } - return null; + return ( +
    + +
    + ); } } diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index 5a993293946..4f4e76b309d 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -139,7 +139,7 @@ export class PanelChrome extends PureComponent { renderPanelPlugin(loading: LoadingState, panelData: PanelData, width: number, height: number): JSX.Element { const { panel, plugin } = this.props; const { timeRange, renderCounter } = this.state; - const PanelComponent = plugin.exports.Panel; + const PanelComponent = plugin.exports.reactPanel.panel; // This is only done to increase a counter that is used by backend // image rendering (phantomjs/headless chrome) to know when to capture image @@ -153,7 +153,7 @@ export class PanelChrome extends PureComponent { loading={loading} panelData={panelData} timeRange={timeRange} - options={panel.getOptions(plugin.exports.PanelDefaults)} + options={panel.getOptions(plugin.exports.reactPanel.defaults)} width={width - 2 * variables.panelhorizontalpadding} height={height - PANEL_HEADER_HEIGHT - variables.panelverticalpadding} renderCounter={renderCounter} diff --git a/public/app/features/dashboard/dashgrid/PanelPluginNotFound.tsx b/public/app/features/dashboard/dashgrid/PanelPluginNotFound.tsx index 3f835bdbac2..4067f361f06 100644 --- a/public/app/features/dashboard/dashgrid/PanelPluginNotFound.tsx +++ b/public/app/features/dashboard/dashgrid/PanelPluginNotFound.tsx @@ -3,7 +3,7 @@ import _ from 'lodash'; import React, { PureComponent } from 'react'; // Types -import { PanelProps } from '@grafana/ui'; +import { PanelProps, ReactPanelPlugin } from '@grafana/ui'; import { PanelPlugin } from 'app/types'; interface Props { @@ -63,7 +63,7 @@ export function getPanelPluginNotFound(id: string): PanelPlugin { }, exports: { - Panel: NotFound, + reactPanel: new ReactPanelPlugin(NotFound), }, }; } diff --git a/public/app/features/dashboard/panel_editor/VisualizationTab.tsx b/public/app/features/dashboard/panel_editor/VisualizationTab.tsx index f9d8b3df607..8a904961a4f 100644 --- a/public/app/features/dashboard/panel_editor/VisualizationTab.tsx +++ b/public/app/features/dashboard/panel_editor/VisualizationTab.tsx @@ -50,33 +50,27 @@ export class VisualizationTab extends PureComponent { }; } - getPanelDefaultOptions = () => { + getReactPanelOptions = () => { const { panel, plugin } = this.props; - - if (plugin.exports.PanelDefaults) { - return panel.getOptions(plugin.exports.PanelDefaults.options); - } - - return panel.getOptions(plugin.exports.PanelDefaults); + return panel.getOptions(plugin.exports.reactPanel.defaults); }; renderPanelOptions() { const { plugin, angularPanel } = this.props; - const { PanelOptions } = plugin.exports; if (angularPanel) { return
    (this.element = element)} />; } - return ( - <> - {PanelOptions ? ( - - ) : ( -

    Visualization has no options

    - )} - - ); + if (plugin.exports.reactPanel) { + const PanelEditor = plugin.exports.reactPanel.editor; + + if (PanelEditor) { + return ; + } + } + + return

    Visualization has no options

    ; } componentDidMount() { diff --git a/public/app/features/dashboard/state/DashboardMigrator.test.ts b/public/app/features/dashboard/state/DashboardMigrator.test.ts index fdb309b5db5..e4b29eeddfc 100644 --- a/public/app/features/dashboard/state/DashboardMigrator.test.ts +++ b/public/app/features/dashboard/state/DashboardMigrator.test.ts @@ -127,7 +127,7 @@ describe('DashboardModel', () => { }); it('dashboard schema version should be set to latest', () => { - expect(model.schemaVersion).toBe(17); + expect(model.schemaVersion).toBe(18); }); it('graph thresholds should be migrated', () => { diff --git a/public/app/features/dashboard/state/DashboardMigrator.ts b/public/app/features/dashboard/state/DashboardMigrator.ts index ba631102b81..1aa310308d5 100644 --- a/public/app/features/dashboard/state/DashboardMigrator.ts +++ b/public/app/features/dashboard/state/DashboardMigrator.ts @@ -22,7 +22,7 @@ export class DashboardMigrator { let i, j, k, n; const oldVersion = this.dashboard.schemaVersion; const panelUpgrades = []; - this.dashboard.schemaVersion = 17; + this.dashboard.schemaVersion = 18; if (oldVersion === this.dashboard.schemaVersion) { return; @@ -387,6 +387,30 @@ export class DashboardMigrator { }); } + if (oldVersion < 18) { + // migrate change to gauge options + panelUpgrades.push(panel => { + if (panel['options-gauge']) { + panel.options = panel['options-gauge']; + panel.options.valueOptions = { + unit: panel.options.unit, + stat: panel.options.stat, + decimals: panel.options.decimals, + prefix: panel.options.prefix, + suffix: panel.options.suffix, + }; + // this options prop was due to a bug + delete panel.options.options; + delete panel.options.unit; + delete panel.options.stat; + delete panel.options.decimals; + delete panel.options.prefix; + delete panel.options.suffix; + delete panel['options-gauge']; + } + }); + } + if (panelUpgrades.length === 0) { return; } diff --git a/public/app/features/dashboard/state/PanelModel.test.ts b/public/app/features/dashboard/state/PanelModel.test.ts index a7e112c7ba5..d96838dc640 100644 --- a/public/app/features/dashboard/state/PanelModel.test.ts +++ b/public/app/features/dashboard/state/PanelModel.test.ts @@ -55,5 +55,19 @@ describe('PanelModel', () => { expect(model.alert).toBe(undefined); }); }); + + describe('get panel options', () => { + it('should apply defaults', () => { + model.options = { existingProp: 10 }; + const options = model.getOptions({ + defaultProp: true, + existingProp: 0, + }); + + expect(options.defaultProp).toBe(true); + expect(options.existingProp).toBe(10); + expect(model.options).toBe(options); + }); + }); }); }); diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts index 2c0ff674e8a..fda586d2776 100644 --- a/public/app/features/dashboard/state/PanelModel.ts +++ b/public/app/features/dashboard/state/PanelModel.ts @@ -3,7 +3,6 @@ import _ from 'lodash'; // Types import { Emitter } from 'app/core/utils/emitter'; -import { PANEL_OPTIONS_KEY_PREFIX } from 'app/core/constants'; import { DataQuery, TimeSeries } from '@grafana/ui'; import { TableData } from '@grafana/ui/src'; @@ -92,6 +91,7 @@ export class PanelModel { timeFrom?: any; timeShift?: any; hideTimeOverride?: any; + options: object; maxDataPoints?: number; interval?: string; @@ -105,8 +105,6 @@ export class PanelModel { hasRefreshed: boolean; events: Emitter; cacheTimeout?: any; - - // cache props between plugins cachedPluginOptions?: any; constructor(model) { @@ -134,20 +132,14 @@ export class PanelModel { } getOptions(panelDefaults) { - return _.defaultsDeep(this[this.getOptionsKey()] || {}, panelDefaults); + return _.defaultsDeep(this.options || {}, panelDefaults); } updateOptions(options: object) { - const update: any = {}; - update[this.getOptionsKey()] = options; - Object.assign(this, update); + this.options = options; this.render(); } - private getOptionsKey() { - return PANEL_OPTIONS_KEY_PREFIX + this.type; - } - getSaveModel() { const model: any = {}; for (const property in this) { @@ -240,14 +232,15 @@ export class PanelModel { // for angular panels only we need to remove all events and let angular panels do some cleanup if (fromAngularPanel) { this.destroy(); + } - for (const key of _.keys(this)) { - if (mustKeepProps[key]) { - continue; - } - - delete this[key]; + // remove panel type specific options + for (const key of _.keys(this)) { + if (mustKeepProps[key]) { + continue; } + + delete this[key]; } this.restorePanelOptions(pluginId); diff --git a/public/app/plugins/panel/gauge/GaugeOptionsEditor.tsx b/public/app/plugins/panel/gauge/GaugeOptionsBox.tsx similarity index 85% rename from public/app/plugins/panel/gauge/GaugeOptionsEditor.tsx rename to public/app/plugins/panel/gauge/GaugeOptionsBox.tsx index 50e2a344a9b..b5d6acca806 100644 --- a/public/app/plugins/panel/gauge/GaugeOptionsEditor.tsx +++ b/public/app/plugins/panel/gauge/GaugeOptionsBox.tsx @@ -1,9 +1,14 @@ +// Libraries import React, { PureComponent } from 'react'; -import { FormField, PanelOptionsProps, PanelOptionsGroup, Switch } from '@grafana/ui'; +// Components +import { Switch, PanelOptionsGroup } from '@grafana/ui'; + +// Types +import { FormField, PanelEditorProps } from '@grafana/ui'; import { GaugeOptions } from './types'; -export default class GaugeOptionsEditor extends PureComponent> { +export class GaugeOptionsBox extends PureComponent> { onToggleThresholdLabels = () => this.props.onChange({ ...this.props.options, showThresholdLabels: !this.props.options.showThresholdLabels }); diff --git a/public/app/plugins/panel/gauge/GaugePanel.tsx b/public/app/plugins/panel/gauge/GaugePanel.tsx index 5cb256ee1aa..e7e60a7c417 100644 --- a/public/app/plugins/panel/gauge/GaugePanel.tsx +++ b/public/app/plugins/panel/gauge/GaugePanel.tsx @@ -16,9 +16,10 @@ interface Props extends PanelProps {} export class GaugePanel extends PureComponent { render() { const { panelData, width, height, onInterpolate, options } = this.props; + const { valueOptions } = options; - const prefix = onInterpolate(options.prefix); - const suffix = onInterpolate(options.suffix); + const prefix = onInterpolate(valueOptions.prefix); + const suffix = onInterpolate(valueOptions.suffix); let value: TimeSeriesValue; if (panelData.timeSeries) { @@ -28,7 +29,7 @@ export class GaugePanel extends PureComponent { }); if (vmSeries[0]) { - value = vmSeries[0].stats[options.stat]; + value = vmSeries[0].stats[valueOptions.stat]; } else { value = null; } @@ -41,11 +42,18 @@ export class GaugePanel extends PureComponent { {theme => ( )} diff --git a/public/app/plugins/panel/gauge/GaugePanelOptions.tsx b/public/app/plugins/panel/gauge/GaugePanelEditor.tsx similarity index 52% rename from public/app/plugins/panel/gauge/GaugePanelOptions.tsx rename to public/app/plugins/panel/gauge/GaugePanelEditor.tsx index 84726ac88bf..63031f9d895 100644 --- a/public/app/plugins/panel/gauge/GaugePanelOptions.tsx +++ b/public/app/plugins/panel/gauge/GaugePanelEditor.tsx @@ -1,6 +1,6 @@ import React, { PureComponent } from 'react'; import { - PanelOptionsProps, + PanelEditorProps, ThresholdsEditor, Threshold, PanelOptionsGrid, @@ -8,29 +8,11 @@ import { ValueMapping, } from '@grafana/ui'; -import ValueOptions from 'app/plugins/panel/gauge/ValueOptions'; -import GaugeOptionsEditor from './GaugeOptionsEditor'; -import { GaugeOptions } from './types'; - -export const defaultProps = { - options: { - minValue: 0, - maxValue: 100, - prefix: '', - showThresholdMarkers: true, - showThresholdLabels: false, - suffix: '', - decimals: 0, - stat: 'avg', - unit: 'none', - valueMappings: [], - thresholds: [], - }, -}; - -export default class GaugePanelOptions extends PureComponent> { - static defaultProps = defaultProps; +import { SingleStatValueEditor } from 'app/plugins/panel/gauge/SingleStatValueEditor'; +import { GaugeOptionsBox } from './GaugeOptionsBox'; +import { GaugeOptions, SingleStatValueOptions } from './types'; +export class GaugePanelEditor extends PureComponent> { onThresholdsChanged = (thresholds: Threshold[]) => this.props.onChange({ ...this.props.options, @@ -43,14 +25,20 @@ export default class GaugePanelOptions extends PureComponent + this.props.onChange({ + ...this.props.options, + valueOptions, + }); + render() { const { onChange, options } = this.props; return ( <> - - + + diff --git a/public/app/plugins/panel/gauge/ValueOptions.tsx b/public/app/plugins/panel/gauge/SingleStatValueEditor.tsx similarity index 74% rename from public/app/plugins/panel/gauge/ValueOptions.tsx rename to public/app/plugins/panel/gauge/SingleStatValueEditor.tsx index 1fdccadddf2..86c177bb5e5 100644 --- a/public/app/plugins/panel/gauge/ValueOptions.tsx +++ b/public/app/plugins/panel/gauge/SingleStatValueEditor.tsx @@ -1,7 +1,12 @@ +// Libraries import React, { PureComponent } from 'react'; -import { FormField, FormLabel, PanelOptionsProps, PanelOptionsGroup, Select } from '@grafana/ui'; + +// Components import UnitPicker from 'app/core/components/Select/UnitPicker'; -import { GaugeOptions } from './types'; +import { FormField, FormLabel, PanelOptionsGroup, Select } from '@grafana/ui'; + +// Types +import { SingleStatValueOptions } from './types'; const statOptions = [ { value: 'min', label: 'Min' }, @@ -19,24 +24,40 @@ const statOptions = [ const labelWidth = 6; -export default class ValueOptions extends PureComponent> { - onUnitChange = unit => this.props.onChange({ ...this.props.options, unit: unit.value }); +export interface Props { + options: SingleStatValueOptions; + onChange: (valueOptions: SingleStatValueOptions) => void; +} +export class SingleStatValueEditor extends PureComponent { + onUnitChange = unit => this.props.onChange({ ...this.props.options, unit: unit.value }); onStatChange = stat => this.props.onChange({ ...this.props.options, stat: stat.value }); onDecimalChange = event => { if (!isNaN(event.target.value)) { - this.props.onChange({ ...this.props.options, decimals: event.target.value }); + this.props.onChange({ + ...this.props.options, + decimals: parseInt(event.target.value, 10), + }); + } else { + this.props.onChange({ + ...this.props.options, + decimals: null, + }); } }; onPrefixChange = event => this.props.onChange({ ...this.props.options, prefix: event.target.value }); - onSuffixChange = event => this.props.onChange({ ...this.props.options, suffix: event.target.value }); render() { const { stat, unit, decimals, prefix, suffix } = this.props.options; + let decimalsString = ''; + if (Number.isFinite(decimals)) { + decimalsString = decimals.toString(); + } + return (
    @@ -57,7 +78,7 @@ export default class ValueOptions extends PureComponent diff --git a/public/app/plugins/panel/gauge/module.tsx b/public/app/plugins/panel/gauge/module.tsx index 783e4825657..a32cb7cd538 100644 --- a/public/app/plugins/panel/gauge/module.tsx +++ b/public/app/plugins/panel/gauge/module.tsx @@ -1,4 +1,10 @@ -import GaugePanelOptions, { defaultProps } from './GaugePanelOptions'; -import { GaugePanel } from './GaugePanel'; +import { ReactPanelPlugin } from '@grafana/ui'; -export { GaugePanel as Panel, GaugePanelOptions as PanelOptions, defaultProps as PanelDefaults }; +import { GaugePanelEditor } from './GaugePanelEditor'; +import { GaugePanel } from './GaugePanel'; +import { GaugeOptions, defaults } from './types'; + +export const reactPanel = new ReactPanelPlugin(GaugePanel); + +reactPanel.setEditor(GaugePanelEditor); +reactPanel.setDefaults(defaults); diff --git a/public/app/plugins/panel/gauge/types.ts b/public/app/plugins/panel/gauge/types.ts index 42262178dc8..10dd475eff5 100644 --- a/public/app/plugins/panel/gauge/types.ts +++ b/public/app/plugins/panel/gauge/types.ts @@ -1,15 +1,35 @@ import { Threshold, ValueMapping } from '@grafana/ui'; export interface GaugeOptions { - decimals: number; valueMappings: ValueMapping[]; maxValue: number; minValue: number; - prefix: string; showThresholdLabels: boolean; showThresholdMarkers: boolean; - stat: string; - suffix: string; thresholds: Threshold[]; - unit: string; + valueOptions: SingleStatValueOptions; } + +export interface SingleStatValueOptions { + unit: string; + suffix: string; + stat: string; + prefix: string; + decimals?: number | null; +} + +export const defaults: GaugeOptions = { + minValue: 0, + maxValue: 100, + showThresholdMarkers: true, + showThresholdLabels: false, + valueOptions: { + prefix: '', + suffix: '', + decimals: null, + stat: 'avg', + unit: 'none', + }, + valueMappings: [], + thresholds: [], +}; diff --git a/public/app/plugins/panel/graph2/GraphPanelOptions.tsx b/public/app/plugins/panel/graph2/GraphPanelEditor.tsx similarity index 91% rename from public/app/plugins/panel/graph2/GraphPanelOptions.tsx rename to public/app/plugins/panel/graph2/GraphPanelEditor.tsx index a9c2d299589..80b17ccd5c4 100644 --- a/public/app/plugins/panel/graph2/GraphPanelOptions.tsx +++ b/public/app/plugins/panel/graph2/GraphPanelEditor.tsx @@ -3,10 +3,10 @@ import _ from 'lodash'; import React, { PureComponent } from 'react'; // Types -import { PanelOptionsProps, Switch } from '@grafana/ui'; +import { PanelEditorProps, Switch } from '@grafana/ui'; import { Options } from './types'; -export class GraphPanelOptions extends PureComponent> { +export class GraphPanelEditor extends PureComponent> { onToggleLines = () => { this.props.onChange({ ...this.props.options, showLines: !this.props.options.showLines }); }; diff --git a/public/app/plugins/panel/graph2/module.tsx b/public/app/plugins/panel/graph2/module.tsx index 762d5609541..a3a3fadf6bf 100644 --- a/public/app/plugins/panel/graph2/module.tsx +++ b/public/app/plugins/panel/graph2/module.tsx @@ -1,4 +1,4 @@ import { GraphPanel } from './GraphPanel'; -import { GraphPanelOptions } from './GraphPanelOptions'; +import { GraphPanelEditor } from './GraphPanelEditor'; -export { GraphPanel as Panel, GraphPanelOptions as PanelOptions }; +export { GraphPanel as Panel, GraphPanelEditor as PanelOptions }; diff --git a/public/app/plugins/panel/text2/module.tsx b/public/app/plugins/panel/text2/module.tsx index cc3ec016273..884a5927a19 100644 --- a/public/app/plugins/panel/text2/module.tsx +++ b/public/app/plugins/panel/text2/module.tsx @@ -1,5 +1,5 @@ import React, { PureComponent } from 'react'; -import { PanelProps } from '@grafana/ui'; +import { PanelProps, ReactPanelPlugin } from '@grafana/ui'; export class Text2 extends PureComponent { constructor(props: PanelProps) { @@ -11,4 +11,4 @@ export class Text2 extends PureComponent { } } -export { Text2 as Panel }; +export const reactPanel = new ReactPanelPlugin(Text2); From 3a36c750ddc90f09e5ce1eb7733c3c5641e43254 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 15 Feb 2019 10:49:10 +0100 Subject: [PATCH 11/23] Added missing strict type checking options to grafana/ui and fixed type errors (cherry picked from commit d6de40cbe69bf884632b3412b7875fc689665f26) --- .../grafana-ui/src/components/Graph/Graph.tsx | 2 +- packages/grafana-ui/src/themes/index.ts | 2 +- .../valueFormats/arithmeticFormatters.ts | 14 ++--- .../utils/valueFormats/dateTimeFormatters.ts | 63 ++++++++++++------- .../utils/valueFormats/symbolFormatters.ts | 4 +- .../src/utils/valueFormats/valueFormats.ts | 47 ++++++++------ packages/grafana-ui/tsconfig.json | 14 ++--- 7 files changed, 85 insertions(+), 61 deletions(-) diff --git a/packages/grafana-ui/src/components/Graph/Graph.tsx b/packages/grafana-ui/src/components/Graph/Graph.tsx index ad038cebcda..d380ad26b68 100644 --- a/packages/grafana-ui/src/components/Graph/Graph.tsx +++ b/packages/grafana-ui/src/components/Graph/Graph.tsx @@ -22,7 +22,7 @@ export class Graph extends PureComponent { showBars: false, }; - element: HTMLElement | null; + element: HTMLElement | null = null; componentDidUpdate() { this.draw(); diff --git a/packages/grafana-ui/src/themes/index.ts b/packages/grafana-ui/src/themes/index.ts index c0d9a4f2d32..1d8d2f62606 100644 --- a/packages/grafana-ui/src/themes/index.ts +++ b/packages/grafana-ui/src/themes/index.ts @@ -6,7 +6,7 @@ let themeMock: ((name?: string) => GrafanaTheme) | null; export let getTheme = (name?: string) => (themeMock && themeMock(name)) || (name === 'light' ? lightTheme : darkTheme); -export const mockTheme = (mock: (name: string) => GrafanaTheme) => { +export const mockTheme = (mock: (name?: string) => GrafanaTheme) => { themeMock = mock; return () => { themeMock = null; diff --git a/packages/grafana-ui/src/utils/valueFormats/arithmeticFormatters.ts b/packages/grafana-ui/src/utils/valueFormats/arithmeticFormatters.ts index fa9daf0fb97..8ff5a1ff5e7 100644 --- a/packages/grafana-ui/src/utils/valueFormats/arithmeticFormatters.ts +++ b/packages/grafana-ui/src/utils/valueFormats/arithmeticFormatters.ts @@ -1,20 +1,20 @@ -import { toFixed } from './valueFormats'; +import { toFixed, DecimalCount } from './valueFormats'; -export function toPercent(size: number, decimals: number) { +export function toPercent(size: number, decimals: DecimalCount) { if (size === null) { return ''; } return toFixed(size, decimals) + '%'; } -export function toPercentUnit(size: number, decimals: number) { +export function toPercentUnit(size: number, decimals: DecimalCount) { if (size === null) { return ''; } return toFixed(100 * size, decimals) + '%'; } -export function toHex0x(value: number, decimals: number) { +export function toHex0x(value: number, decimals: DecimalCount) { if (value == null) { return ''; } @@ -25,7 +25,7 @@ export function toHex0x(value: number, decimals: number) { return '0x' + hexString; } -export function toHex(value: number, decimals: number) { +export function toHex(value: number, decimals: DecimalCount) { if (value == null) { return ''; } @@ -34,9 +34,9 @@ export function toHex(value: number, decimals: number) { .toUpperCase(); } -export function sci(value: number, decimals: number) { +export function sci(value: number, decimals: DecimalCount) { if (value == null) { return ''; } - return value.toExponential(decimals); + return value.toExponential(decimals as number); } diff --git a/packages/grafana-ui/src/utils/valueFormats/dateTimeFormatters.ts b/packages/grafana-ui/src/utils/valueFormats/dateTimeFormatters.ts index 1e07857eb66..819b73586d5 100644 --- a/packages/grafana-ui/src/utils/valueFormats/dateTimeFormatters.ts +++ b/packages/grafana-ui/src/utils/valueFormats/dateTimeFormatters.ts @@ -1,4 +1,4 @@ -import { toFixed, toFixedScaled } from './valueFormats'; +import { toFixed, toFixedScaled, DecimalCount } from './valueFormats'; import moment from 'moment'; interface IntervalsInSeconds { @@ -27,7 +27,7 @@ const INTERVALS_IN_SECONDS: IntervalsInSeconds = { [Interval.Millisecond]: 0.001, }; -export function toNanoSeconds(size: number, decimals: number, scaledDecimals: number) { +export function toNanoSeconds(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) { if (size === null) { return ''; } @@ -45,7 +45,7 @@ export function toNanoSeconds(size: number, decimals: number, scaledDecimals: nu } } -export function toMicroSeconds(size: number, decimals: number, scaledDecimals: number) { +export function toMicroSeconds(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) { if (size === null) { return ''; } @@ -59,7 +59,7 @@ export function toMicroSeconds(size: number, decimals: number, scaledDecimals: n } } -export function toMilliSeconds(size: number, decimals: number, scaledDecimals: number) { +export function toMilliSeconds(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) { if (size === null) { return ''; } @@ -83,22 +83,29 @@ export function toMilliSeconds(size: number, decimals: number, scaledDecimals: n return toFixedScaled(size / 31536000000, decimals, scaledDecimals, 10, ' year'); } -export function toSeconds(size: number, decimals: number, scaledDecimals: number) { +export function trySubstract(value1: DecimalCount, value2: DecimalCount): DecimalCount { + if (value1 !== null && value1 !== undefined && value2 !== null && value2 !== undefined) { + return value1 - value2; + } + return undefined; +} + +export function toSeconds(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) { if (size === null) { return ''; } // Less than 1 µs, divide in ns if (Math.abs(size) < 0.000001) { - return toFixedScaled(size * 1e9, decimals, scaledDecimals - decimals, -9, ' ns'); + return toFixedScaled(size * 1e9, decimals, trySubstract(scaledDecimals, decimals), -9, ' ns'); } // Less than 1 ms, divide in µs if (Math.abs(size) < 0.001) { - return toFixedScaled(size * 1e6, decimals, scaledDecimals - decimals, -6, ' µs'); + return toFixedScaled(size * 1e6, decimals, trySubstract(scaledDecimals, decimals), -6, ' µs'); } // Less than 1 second, divide in ms if (Math.abs(size) < 1) { - return toFixedScaled(size * 1e3, decimals, scaledDecimals - decimals, -3, ' ms'); + return toFixedScaled(size * 1e3, decimals, trySubstract(scaledDecimals, decimals), -3, ' ms'); } if (Math.abs(size) < 60) { @@ -120,7 +127,7 @@ export function toSeconds(size: number, decimals: number, scaledDecimals: number return toFixedScaled(size / 3.15569e7, decimals, scaledDecimals, 7, ' year'); } -export function toMinutes(size: number, decimals: number, scaledDecimals: number) { +export function toMinutes(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) { if (size === null) { return ''; } @@ -138,7 +145,7 @@ export function toMinutes(size: number, decimals: number, scaledDecimals: number } } -export function toHours(size: number, decimals: number, scaledDecimals: number) { +export function toHours(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) { if (size === null) { return ''; } @@ -154,7 +161,7 @@ export function toHours(size: number, decimals: number, scaledDecimals: number) } } -export function toDays(size: number, decimals: number, scaledDecimals: number) { +export function toDays(size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) { if (size === null) { return ''; } @@ -168,13 +175,15 @@ export function toDays(size: number, decimals: number, scaledDecimals: number) { } } -export function toDuration(size: number, decimals: number, timeScale: Interval): string { +export function toDuration(size: number, decimals: DecimalCount, timeScale: Interval): string { if (size === null) { return ''; } + if (size === 0) { return '0 ' + timeScale + 's'; } + if (size < 0) { return toDuration(-size, decimals, timeScale) + ' ago'; } @@ -189,14 +198,22 @@ export function toDuration(size: number, decimals: number, timeScale: Interval): { long: Interval.Second }, { long: Interval.Millisecond }, ]; + // convert $size to milliseconds // intervals_in_seconds uses seconds (duh), convert them to milliseconds here to minimize floating point errors size *= INTERVALS_IN_SECONDS[timeScale] * 1000; const strings = []; + // after first value >= 1 print only $decimals more let decrementDecimals = false; - for (let i = 0; i < units.length && decimals >= 0; i++) { + let decimalsCount = 0; + + if (decimals !== null || decimals !== undefined) { + decimalsCount = decimals as number; + } + + for (let i = 0; i < units.length && decimalsCount >= 0; i++) { const interval = INTERVALS_IN_SECONDS[units[i].long] * 1000; const value = size / interval; if (value >= 1 || decrementDecimals) { @@ -205,14 +222,14 @@ export function toDuration(size: number, decimals: number, timeScale: Interval): const unit = units[i].long + (floor !== 1 ? 's' : ''); strings.push(floor + ' ' + unit); size = size % interval; - decimals--; + decimalsCount--; } } return strings.join(', '); } -export function toClock(size: number, decimals?: number) { +export function toClock(size: number, decimals?: DecimalCount) { if (size === null) { return ''; } @@ -257,11 +274,11 @@ export function toClock(size: number, decimals?: number) { return format ? `${hours}:${moment.utc(size).format(format)}` : hours; } -export function toDurationInMilliseconds(size: number, decimals: number) { +export function toDurationInMilliseconds(size: number, decimals: DecimalCount) { return toDuration(size, decimals, Interval.Millisecond); } -export function toDurationInSeconds(size: number, decimals: number) { +export function toDurationInSeconds(size: number, decimals: DecimalCount) { return toDuration(size, decimals, Interval.Second); } @@ -276,19 +293,19 @@ export function toDurationInHoursMinutesSeconds(size: number) { return strings.join(':'); } -export function toTimeTicks(size: number, decimals: number, scaledDecimals: number) { +export function toTimeTicks(size: number, decimals: DecimalCount, scaledDecimals: DecimalCount) { return toSeconds(size, decimals, scaledDecimals); } -export function toClockMilliseconds(size: number, decimals: number) { +export function toClockMilliseconds(size: number, decimals: DecimalCount) { return toClock(size, decimals); } -export function toClockSeconds(size: number, decimals: number) { +export function toClockSeconds(size: number, decimals: DecimalCount) { return toClock(size * 1000, decimals); } -export function dateTimeAsIso(value: number, decimals: number, scaledDecimals: number, isUtc: boolean) { +export function dateTimeAsIso(value: number, decimals: DecimalCount, scaledDecimals: DecimalCount, isUtc?: boolean) { const time = isUtc ? moment.utc(value) : moment(value); if (moment().isSame(value, 'day')) { @@ -297,7 +314,7 @@ export function dateTimeAsIso(value: number, decimals: number, scaledDecimals: n return time.format('YYYY-MM-DD HH:mm:ss'); } -export function dateTimeAsUS(value: number, decimals: number, scaledDecimals: number, isUtc: boolean) { +export function dateTimeAsUS(value: number, decimals: DecimalCount, scaledDecimals: DecimalCount, isUtc?: boolean) { const time = isUtc ? moment.utc(value) : moment(value); if (moment().isSame(value, 'day')) { @@ -306,7 +323,7 @@ export function dateTimeAsUS(value: number, decimals: number, scaledDecimals: nu return time.format('MM/DD/YYYY h:mm:ss a'); } -export function dateTimeFromNow(value: number, decimals: number, scaledDecimals: number, isUtc: boolean) { +export function dateTimeFromNow(value: number, decimals: DecimalCount, scaledDecimals: DecimalCount, isUtc?: boolean) { const time = isUtc ? moment.utc(value) : moment(value); return time.fromNow(); } diff --git a/packages/grafana-ui/src/utils/valueFormats/symbolFormatters.ts b/packages/grafana-ui/src/utils/valueFormats/symbolFormatters.ts index 66808143daa..da8198daf5a 100644 --- a/packages/grafana-ui/src/utils/valueFormats/symbolFormatters.ts +++ b/packages/grafana-ui/src/utils/valueFormats/symbolFormatters.ts @@ -1,9 +1,9 @@ -import { scaledUnits } from './valueFormats'; +import { scaledUnits, DecimalCount } from './valueFormats'; export function currency(symbol: string) { const units = ['', 'K', 'M', 'B', 'T']; const scaler = scaledUnits(1000, units); - return (size: number, decimals: number, scaledDecimals: number) => { + return (size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) => { if (size === null) { return ''; } diff --git a/packages/grafana-ui/src/utils/valueFormats/valueFormats.ts b/packages/grafana-ui/src/utils/valueFormats/valueFormats.ts index 0a56ce58e5b..d626a241a79 100644 --- a/packages/grafana-ui/src/utils/valueFormats/valueFormats.ts +++ b/packages/grafana-ui/src/utils/valueFormats/valueFormats.ts @@ -1,8 +1,15 @@ import { getCategories } from './categories'; -type ValueFormatter = (value: number, decimals?: number, scaledDecimals?: number, isUtc?: boolean) => string; +export type DecimalCount = number | null | undefined; -interface ValueFormat { +export type ValueFormatter = ( + value: number, + decimals?: DecimalCount, + scaledDecimals?: DecimalCount, + isUtc?: boolean +) => string; + +export interface ValueFormat { name: string; id: string; fn: ValueFormatter; @@ -22,7 +29,7 @@ let categories: ValueFormatCategory[] = []; const index: ValueFormatterIndex = {}; let hasBuiltIndex = false; -export function toFixed(value: number, decimals?: number): string { +export function toFixed(value: number, decimals?: DecimalCount): string { if (value === null) { return ''; } @@ -50,20 +57,24 @@ export function toFixed(value: number, decimals?: number): string { export function toFixedScaled( value: number, - decimals: number, - scaledDecimals: number, - additionalDecimals: number, - ext: string + decimals?: DecimalCount, + scaledDecimals?: DecimalCount, + additionalDecimals?: DecimalCount, + ext?: string ) { - if (scaledDecimals === null) { - return toFixed(value, decimals) + ext; - } else { - return toFixed(value, scaledDecimals + additionalDecimals) + ext; + if (scaledDecimals) { + if (additionalDecimals) { + return toFixed(value, scaledDecimals + additionalDecimals) + ext; + } else { + return toFixed(value, scaledDecimals) + ext; + } } + + return toFixed(value, decimals) + ext; } -export function toFixedUnit(unit: string) { - return (size: number, decimals: number) => { +export function toFixedUnit(unit: string): ValueFormatter { + return (size: number, decimals?: DecimalCount) => { if (size === null) { return ''; } @@ -75,7 +86,7 @@ export function toFixedUnit(unit: string) { // numeric factor. Repeatedly scales the value down by the factor until it is // less than the factor in magnitude, or the end of the array is reached. export function scaledUnits(factor: number, extArray: string[]) { - return (size: number, decimals: number, scaledDecimals: number) => { + return (size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) => { if (size === null) { return ''; } @@ -92,7 +103,7 @@ export function scaledUnits(factor: number, extArray: string[]) { } } - if (steps > 0 && scaledDecimals !== null) { + if (steps > 0 && scaledDecimals !== null && scaledDecimals !== undefined) { decimals = scaledDecimals + 3 * steps; } @@ -100,17 +111,17 @@ export function scaledUnits(factor: number, extArray: string[]) { }; } -export function locale(value: number, decimals: number) { +export function locale(value: number, decimals: DecimalCount) { if (value == null) { return ''; } - return value.toLocaleString(undefined, { maximumFractionDigits: decimals }); + return value.toLocaleString(undefined, { maximumFractionDigits: decimals as number }); } export function simpleCountUnit(symbol: string) { const units = ['', 'K', 'M', 'B', 'T']; const scaler = scaledUnits(1000, units); - return (size: number, decimals: number, scaledDecimals: number) => { + return (size: number, decimals?: DecimalCount, scaledDecimals?: DecimalCount) => { if (size === null) { return ''; } diff --git a/packages/grafana-ui/tsconfig.json b/packages/grafana-ui/tsconfig.json index 22d336b85f5..1dfe6e0b44c 100644 --- a/packages/grafana-ui/tsconfig.json +++ b/packages/grafana-ui/tsconfig.json @@ -1,21 +1,17 @@ { "extends": "../../tsconfig.json", - "include": [ - "src/**/*.ts", - "src/**/*.tsx" - ], - "exclude": [ - "dist", - "node_modules" - ], + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": ["dist", "node_modules"], "compilerOptions": { "rootDirs": [".", "stories"], "module": "esnext", "outDir": "dist", "declaration": true, + "strict": true, + "alwaysStrict": true, "noImplicitAny": true, "strictNullChecks": true, "typeRoots": ["./node_modules/@types", "types"], "skipLibCheck": true // Temp workaround for Duplicate identifier tsc errors - }, + } } From d45fc0cadd582402fc8c14f28575420c7b2d9e3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 16 Feb 2019 15:45:19 +0100 Subject: [PATCH 12/23] Fixed prettier issue (#15471) Fixed prettier CI issue that caused build failures (cherry picked from commit 2d5fd7fdfd78a930bddff5ae4d48f77f03ee8798) --- package.json | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 19939528425..601681f54d4 100644 --- a/package.json +++ b/package.json @@ -121,7 +121,7 @@ "jest": "jest --notify --watch", "api-tests": "jest --notify --watch --config=tests/api/jest.js", "storybook": "cd packages/grafana-ui && yarn storybook", - "prettier:check": "prettier -- --list-different \"**/*.{ts,tsx,scss}\"" + "prettier:check": "prettier --list-different \"**/*.{ts,tsx,scss}\"" }, "husky": { "hooks": { @@ -129,14 +129,8 @@ } }, "lint-staged": { - "*.{ts,tsx,json,scss}": [ - "prettier --write", - "git add" - ], - "*pkg/**/*.go": [ - "gofmt -w -s", - "git add" - ] + "*.{ts,tsx,json,scss}": ["prettier --write", "git add"], + "*pkg/**/*.go": ["gofmt -w -s", "git add"] }, "prettier": { "trailingComma": "es5", @@ -201,12 +195,7 @@ "**/@types/react": "16.7.6" }, "workspaces": { - "packages": [ - "packages/*" - ], - "nohoist": [ - "**/@types/*", - "**/@types/*/**" - ] + "packages": ["packages/*"], + "nohoist": ["**/@types/*", "**/@types/*/**"] } } From b229b162596ee64fc2b2ccc2c144641fa7c45f99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 17 Feb 2019 08:11:57 +0100 Subject: [PATCH 13/23] Fixed issue with PanelHeader and grid-drag-handle class still being applied in fullscreen, fixes #15480 (cherry picked from commit 89ad52598610632f3370add4f0853b9ecacf80ef) --- .../features/dashboard/dashgrid/DashboardPanel.tsx | 4 ++-- .../app/features/dashboard/dashgrid/PanelChrome.tsx | 4 +++- .../dashboard/dashgrid/PanelHeader/PanelHeader.tsx | 11 +++-------- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx index 8f52a9d18db..9aeddd5a0d9 100644 --- a/public/app/features/dashboard/dashgrid/DashboardPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardPanel.tsx @@ -131,10 +131,10 @@ export class DashboardPanel extends PureComponent { }; renderReactPanel() { - const { dashboard, panel } = this.props; + const { dashboard, panel, isFullscreen } = this.props; const { plugin } = this.state; - return ; + return ; } renderAngularPanel() { diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index 4f4e76b309d..23c92b23837 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -29,6 +29,7 @@ export interface Props { panel: PanelModel; dashboard: DashboardModel; plugin: PanelPlugin; + isFullscreen: boolean; } export interface State { @@ -193,7 +194,7 @@ export class PanelChrome extends PureComponent { }; render() { - const { dashboard, panel } = this.props; + const { dashboard, panel, isFullscreen } = this.props; const { errorMessage, timeInfo } = this.state; const { transparent } = panel; @@ -216,6 +217,7 @@ export class PanelChrome extends PureComponent { scopedVars={panel.scopedVars} links={panel.links} error={errorMessage} + isFullscreen={isFullscreen} /> {({ error, errorInfo }) => { diff --git a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx index 2be32cd9955..5d10949be36 100644 --- a/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx +++ b/public/app/features/dashboard/dashgrid/PanelHeader/PanelHeader.tsx @@ -19,6 +19,7 @@ export interface Props { scopedVars?: string; links?: []; error?: string; + isFullscreen: boolean; } interface ClickCoordinates { @@ -69,10 +70,9 @@ export class PanelHeader extends Component { }; render() { - const isFullscreen = false; - const isLoading = false; + const { panel, dashboard, timeInfo, scopedVars, error, isFullscreen } = this.props; + const panelHeaderClass = classNames({ 'panel-header': true, 'grid-drag-handle': !isFullscreen }); - const { panel, dashboard, timeInfo, scopedVars, error } = this.props; const title = templateSrv.replaceWithText(panel.title, scopedVars); return ( @@ -86,11 +86,6 @@ export class PanelHeader extends Component { error={error} />
    - {isLoading && ( - - - - )}
    From 5091e06e4ce3c3579baa2d2d5fa8ddbf997eb7df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 17 Feb 2019 17:35:46 +0100 Subject: [PATCH 14/23] Removed primary class from Add Query button, and changed name of Panel Options tab o General Options (cherry picked from commit bf826d7c81ef9db2cd359926d2e32e82c25e6169) --- public/app/features/dashboard/panel_editor/GeneralTab.tsx | 2 +- public/app/features/dashboard/panel_editor/PanelEditor.tsx | 2 +- public/app/features/dashboard/panel_editor/QueriesTab.tsx | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/public/app/features/dashboard/panel_editor/GeneralTab.tsx b/public/app/features/dashboard/panel_editor/GeneralTab.tsx index d91737195f1..01a6e39cedb 100644 --- a/public/app/features/dashboard/panel_editor/GeneralTab.tsx +++ b/public/app/features/dashboard/panel_editor/GeneralTab.tsx @@ -44,7 +44,7 @@ export class GeneralTab extends PureComponent { render() { return ( - +
    (this.element = element)} /> ); diff --git a/public/app/features/dashboard/panel_editor/PanelEditor.tsx b/public/app/features/dashboard/panel_editor/PanelEditor.tsx index 74870b25f07..1bc42a2fd88 100644 --- a/public/app/features/dashboard/panel_editor/PanelEditor.tsx +++ b/public/app/features/dashboard/panel_editor/PanelEditor.tsx @@ -45,7 +45,7 @@ interface PanelEditorTab { const panelEditorTabTexts = { [PanelEditorTabIds.Queries]: 'Queries', [PanelEditorTabIds.Visualization]: 'Visualization', - [PanelEditorTabIds.Advanced]: 'Panel Options', + [PanelEditorTabIds.Advanced]: 'General', [PanelEditorTabIds.Alert]: 'Alert', }; diff --git a/public/app/features/dashboard/panel_editor/QueriesTab.tsx b/public/app/features/dashboard/panel_editor/QueriesTab.tsx index d46ff020906..bef23c03496 100644 --- a/public/app/features/dashboard/panel_editor/QueriesTab.tsx +++ b/public/app/features/dashboard/panel_editor/QueriesTab.tsx @@ -135,7 +135,7 @@ export class QueriesTab extends PureComponent {
    {!isAddingMixed && ( - )} From 6fc3c6a7ed1b7407ab06154488fb7f79add4a186 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Mon, 18 Feb 2019 11:08:42 +0100 Subject: [PATCH 15/23] Fix typo in view mode cykle button (cherry picked from commit 75dd7d00360d337c6b8a5cf0023249cb2e33a8ae) --- public/app/features/dashboard/components/DashNav/DashNav.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx index 6db07b5d42e..8806e35c6b6 100644 --- a/public/app/features/dashboard/components/DashNav/DashNav.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx @@ -248,7 +248,7 @@ export class DashNav extends PureComponent {
    Date: Mon, 18 Feb 2019 11:33:16 +0100 Subject: [PATCH 16/23] Remove maxDataPoints and interval props from props to remember in panel model (cherry picked from commit 8e035676e154f60f29f93a85e9548c1192e76d0f) --- public/app/features/dashboard/state/PanelModel.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts index fda586d2776..d69e767a484 100644 --- a/public/app/features/dashboard/state/PanelModel.ts +++ b/public/app/features/dashboard/state/PanelModel.ts @@ -46,8 +46,6 @@ const mustKeepProps: { [str: string]: boolean } = { timeFrom: true, timeShift: true, hideTimeOverride: true, - maxDataPoints: true, - interval: true, description: true, links: true, fullscreen: true, From 9d5529c453b8edf62518deb1d45855be84483ef8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Mon, 18 Feb 2019 11:40:25 +0100 Subject: [PATCH 17/23] Fixes #15477 (cherry picked from commit 92972eed7b78a30fbe4573e204288e6774d2cd3f) --- .../dashboard/state/PanelModel.test.ts | 29 +++++++++++++++++++ .../features/dashboard/state/PanelModel.ts | 21 ++++++++++++-- 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/public/app/features/dashboard/state/PanelModel.test.ts b/public/app/features/dashboard/state/PanelModel.test.ts index d96838dc640..079946b1521 100644 --- a/public/app/features/dashboard/state/PanelModel.test.ts +++ b/public/app/features/dashboard/state/PanelModel.test.ts @@ -10,6 +10,20 @@ describe('PanelModel', () => { type: 'table', showColumns: true, targets: [{ refId: 'A' }, { noRefId: true }], + options: { + thresholds: [ + { + color: '#F2495C', + index: 1, + value: 50, + }, + { + color: '#73BF69', + index: 0, + value: null, + }, + ], + }, }); }); @@ -35,6 +49,21 @@ describe('PanelModel', () => { expect(saveModel.events).toBe(undefined); }); + it('should restore -Infinity value for base threshold', () => { + expect(model.options.thresholds).toEqual([ + { + color: '#F2495C', + index: 1, + value: 50, + }, + { + color: '#73BF69', + index: 0, + value: -Infinity, + }, + ]); + }); + describe('when changing panel type', () => { beforeEach(() => { model.changeType('graph', true); diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts index d69e767a484..ac3722d61c2 100644 --- a/public/app/features/dashboard/state/PanelModel.ts +++ b/public/app/features/dashboard/state/PanelModel.ts @@ -3,7 +3,7 @@ import _ from 'lodash'; // Types import { Emitter } from 'app/core/utils/emitter'; -import { DataQuery, TimeSeries } from '@grafana/ui'; +import { DataQuery, TimeSeries, Threshold } from '@grafana/ui'; import { TableData } from '@grafana/ui/src'; export interface GridPos { @@ -89,7 +89,9 @@ export class PanelModel { timeFrom?: any; timeShift?: any; hideTimeOverride?: any; - options: object; + options: { + [key: string]: any; + }; maxDataPoints?: number; interval?: string; @@ -117,6 +119,8 @@ export class PanelModel { _.defaultsDeep(this, _.cloneDeep(defaults)); // queries must have refId this.ensureQueryIds(); + + this.restoreInfintyForThresholds(); } ensureQueryIds() { @@ -129,6 +133,19 @@ export class PanelModel { } } + restoreInfintyForThresholds() { + if (this.options && this.options.thresholds) { + this.options.thresholds = this.options.thresholds.map((threshold: Threshold) => { + // JSON serialization of -Infinity is 'null' so lets convert it back to -Infinity + if (threshold.index === 0 && threshold.value === null) { + return { ...threshold, value: -Infinity }; + } + + return threshold; + }); + } + } + getOptions(panelDefaults) { return _.defaultsDeep(this.options || {}, panelDefaults); } From 1ce7ef7ae02febb66bd0ce1ad7a72d6019b67707 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Mon, 18 Feb 2019 13:51:43 +0100 Subject: [PATCH 18/23] cli: chmod 755 for backend plugin binaries Fixes #15500. Does a simple filename check if the binary names ends with _linux_amd64 or _darwin_amd64 then sets the file mode to 755. (cherry picked from commit 56c965e5df563fa19dc0124fb2f58e7890c3b29b) --- .../grafana-cli/commands/install_command.go | 20 +++++++-- .../commands/install_command_test.go | 41 ++++++++++++++++++ ...18fa4da8096a952608a7e4c7782b4260b41bcf.zip | Bin 0 -> 910 bytes 3 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 pkg/cmd/grafana-cli/commands/testdata/grafana-simple-json-datasource-ec18fa4da8096a952608a7e4c7782b4260b41bcf.zip diff --git a/pkg/cmd/grafana-cli/commands/install_command.go b/pkg/cmd/grafana-cli/commands/install_command.go index f88bb9bbfff..d758633fea5 100644 --- a/pkg/cmd/grafana-cli/commands/install_command.go +++ b/pkg/cmd/grafana-cli/commands/install_command.go @@ -57,6 +57,8 @@ func installCommand(c CommandLine) error { return InstallPlugin(pluginToInstall, version, c) } +// InstallPlugin downloads the plugin code as a zip file from the Grafana.com API +// and then extracts the zip into the plugins directory. func InstallPlugin(pluginName, version string, c CommandLine) error { pluginFolder := c.PluginDirectory() downloadURL := c.PluginURL() @@ -152,6 +154,10 @@ func downloadFile(pluginName, filePath, url string) (err error) { return err } + return extractFiles(body, pluginName, filePath) +} + +func extractFiles(body []byte, pluginName string, filePath string) error { r, err := zip.NewReader(bytes.NewReader(body), int64(len(body))) if err != nil { return err @@ -161,12 +167,18 @@ func downloadFile(pluginName, filePath, url string) (err error) { if zf.FileInfo().IsDir() { err := os.Mkdir(newFile, 0777) - if PermissionsError(err) { + if permissionsError(err) { return fmt.Errorf(permissionsDeniedMessage, newFile) } } else { - dst, err := os.Create(newFile) - if PermissionsError(err) { + fileMode := zf.Mode() + + if strings.HasSuffix(newFile, "_linux_amd64") || strings.HasSuffix(newFile, "_darwin_amd64") { + fileMode = os.FileMode(0755) + } + + dst, err := os.OpenFile(newFile, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fileMode) + if permissionsError(err) { return fmt.Errorf(permissionsDeniedMessage, newFile) } @@ -184,6 +196,6 @@ func downloadFile(pluginName, filePath, url string) (err error) { return nil } -func PermissionsError(err error) bool { +func permissionsError(err error) bool { return err != nil && strings.Contains(err.Error(), "permission denied") } diff --git a/pkg/cmd/grafana-cli/commands/install_command_test.go b/pkg/cmd/grafana-cli/commands/install_command_test.go index 52b329adf7f..3554dda82a9 100644 --- a/pkg/cmd/grafana-cli/commands/install_command_test.go +++ b/pkg/cmd/grafana-cli/commands/install_command_test.go @@ -1,6 +1,8 @@ package commands import ( + "io/ioutil" + "os" "testing" . "github.com/smartystreets/goconvey/convey" @@ -37,3 +39,42 @@ func TestFoldernameReplacement(t *testing.T) { }) }) } + +func TestExtractFiles(t *testing.T) { + Convey("Should preserve file permissions for plugin backend binaries for linux and darwin", t, func() { + err := os.RemoveAll("testdata/fake-plugins-dir") + So(err, ShouldBeNil) + + err = os.MkdirAll("testdata/fake-plugins-dir", 0774) + So(err, ShouldBeNil) + + body, err := ioutil.ReadFile("testdata/grafana-simple-json-datasource-ec18fa4da8096a952608a7e4c7782b4260b41bcf.zip") + So(err, ShouldBeNil) + + err = extractFiles(body, "grafana-simple-json-datasource", "testdata/fake-plugins-dir") + So(err, ShouldBeNil) + + //File in zip has permissions 777 + fileInfo, err := os.Stat("testdata/fake-plugins-dir/grafana-simple-json-datasource/simple-plugin_darwin_amd64") + So(err, ShouldBeNil) + So(fileInfo.Mode().String(), ShouldEqual, "-rwxr-xr-x") + + //File in zip has permission 664 + fileInfo, err = os.Stat("testdata/fake-plugins-dir/grafana-simple-json-datasource/simple-plugin_linux_amd64") + So(err, ShouldBeNil) + So(fileInfo.Mode().String(), ShouldEqual, "-rwxr-xr-x") + + //File in zip has permission 644 + fileInfo, err = os.Stat("testdata/fake-plugins-dir/grafana-simple-json-datasource/simple-plugin_windows_amd64.exe") + So(err, ShouldBeNil) + So(fileInfo.Mode().String(), ShouldEqual, "-rw-r--r--") + + //File in zip has permission 755 + fileInfo, err = os.Stat("testdata/fake-plugins-dir/grafana-simple-json-datasource/non-plugin-binary") + So(err, ShouldBeNil) + So(fileInfo.Mode().String(), ShouldEqual, "-rwxr-xr-x") + + err = os.RemoveAll("testdata/fake-plugins-dir") + So(err, ShouldBeNil) + }) +} diff --git a/pkg/cmd/grafana-cli/commands/testdata/grafana-simple-json-datasource-ec18fa4da8096a952608a7e4c7782b4260b41bcf.zip b/pkg/cmd/grafana-cli/commands/testdata/grafana-simple-json-datasource-ec18fa4da8096a952608a7e4c7782b4260b41bcf.zip new file mode 100644 index 0000000000000000000000000000000000000000..f9263ab3e78a36cf40339c296db344db1ed2e4c7 GIT binary patch literal 910 zcmWIWW@h1H0D((oL4IHclwf6$VMxg=F3}GS;bdTz*_jms!lf1542&!C$rL;z0h zDlqNEnYjfysk#L@rRkY@@#UF$Df#8a@rk)9W+r;66{&F3qe(Gc4!h|&nR%rZ5VMIh z_avI9#bD;<<>x`2tecdXmsnH@HjfMFMv~kq4>J!n6jBn4fB`{_Lm8Rmm~q9A1T>}u z82&nfn9$f_g~S#{OyM?4g#l(11H+O=dzewEZpIRGq}wEiWYgQmx!7$&iABP8V8$WB zH*Xui!tB5jqd>DkF-k&6$)mX6l@S`~s3C Date: Mon, 18 Feb 2019 15:04:26 +0100 Subject: [PATCH 19/23] Updated explore icon and style tweaks Lowered icon size and improved paddings, tried to align placement between dashboard and explore (cherry picked from commit df170aee166ee1ef28cfe562bf51fa22a9a188a7) --- pkg/api/index.go | 2 +- .../app/features/explore/ExploreToolbar.tsx | 16 ++++++------ public/img/icons_dark_theme/icon_explore.svg | 24 ++++++++++++++++++ public/img/icons_light_theme/icon_explore.svg | 24 ++++++++++++++++++ public/sass/base/_icons.scss | 7 ++++++ public/sass/components/_navbar.scss | 24 ++++++++---------- public/sass/components/_sidemenu.scss | 2 +- public/sass/pages/_explore.scss | 25 +------------------ 8 files changed, 76 insertions(+), 48 deletions(-) create mode 100644 public/img/icons_dark_theme/icon_explore.svg create mode 100644 public/img/icons_light_theme/icon_explore.svg diff --git a/pkg/api/index.go b/pkg/api/index.go index e90db84016d..248ebf63f0f 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -145,7 +145,7 @@ func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, er Text: "Explore", Id: "explore", SubTitle: "Explore your data", - Icon: "fa fa-rocket", + Icon: "gicon gicon-explore", Url: setting.AppSubUrl + "/explore", }) } diff --git a/public/app/features/explore/ExploreToolbar.tsx b/public/app/features/explore/ExploreToolbar.tsx index 786998c96c1..adcd0c8afa8 100644 --- a/public/app/features/explore/ExploreToolbar.tsx +++ b/public/app/features/explore/ExploreToolbar.tsx @@ -103,18 +103,16 @@ export class UnConnectedExploreToolbar extends PureComponent {
    {exploreId === 'left' && ( - + Explore )}
    -
    - {exploreId === 'right' && ( - - - - )} -
    + {exploreId === 'right' && ( +
    + + + )}
    @@ -156,7 +154,7 @@ export class UnConnectedExploreToolbar extends PureComponent { splitted, title: 'Run Query', onClick: this.onRunQuery, - buttonClassName: 'navbar-button--primary', + buttonClassName: 'navbar-button--secondary', iconClassName: loading ? 'fa fa-spinner fa-fw fa-spin run-icon' : 'fa fa-level-down fa-fw run-icon', iconSide: IconSide.right, })} diff --git a/public/img/icons_dark_theme/icon_explore.svg b/public/img/icons_dark_theme/icon_explore.svg new file mode 100644 index 00000000000..8d7e1b93fce --- /dev/null +++ b/public/img/icons_dark_theme/icon_explore.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + diff --git a/public/img/icons_light_theme/icon_explore.svg b/public/img/icons_light_theme/icon_explore.svg new file mode 100644 index 00000000000..8732b2d9ae8 --- /dev/null +++ b/public/img/icons_light_theme/icon_explore.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + diff --git a/public/sass/base/_icons.scss b/public/sass/base/_icons.scss index 2d0cb394da1..8d171673f6b 100644 --- a/public/sass/base/_icons.scss +++ b/public/sass/base/_icons.scss @@ -192,6 +192,10 @@ background-image: url('../img/icons_#{$theme-name}_theme/icon_zoom_out.svg'); } +.gicon-explore { + background-image: url('../img/icons_#{$theme-name}_theme/icon_explore.svg'); +} + .sidemenu { .gicon-dashboard { background-image: url('../img/icons_dark_theme/icon_dashboard.svg'); @@ -205,6 +209,9 @@ .gicon-question { background-image: url('../img/icons_dark_theme/icon_question.svg'); } + .gicon-explore { + background-image: url('../img/icons_dark_theme/icon_explore.svg'); + } } .fa--permissions-list { diff --git a/public/sass/components/_navbar.scss b/public/sass/components/_navbar.scss index 86494f9e47a..b96f7bb7fc5 100644 --- a/public/sass/components/_navbar.scss +++ b/public/sass/components/_navbar.scss @@ -1,9 +1,8 @@ .navbar { position: relative; - padding-left: 20px; z-index: $zindex-navbar-fixed; height: $navbarHeight; - padding-right: 20px; + padding: 0 20px 0 50px; display: flex; flex-grow: 1; border-bottom: 1px solid transparent; @@ -57,15 +56,14 @@ white-space: nowrap; display: block; margin: 0; - color: darken($link-color, 5%); + color: $headings-color; font-size: $font-size-lg; - padding-left: 1rem; min-height: $navbarHeight; line-height: $navbarHeight; .fa-caret-down { font-size: 60%; - padding-left: 0.2rem; + padding-left: 6px; } &--search { @@ -73,12 +71,12 @@ } .gicon { + top: -2px; position: relative; - top: -1px; - font-size: 19px; + font-size: 17px; line-height: 8px; opacity: 0.75; - margin-right: 13px; + margin-right: 10px; display: none; } @@ -123,7 +121,7 @@ height: 30px; color: $text-muted; border: 1px solid $navbar-button-border; - margin-right: 3px; + margin-left: 3px; white-space: nowrap; .gicon { @@ -152,19 +150,19 @@ } } - &--primary { - @include buttonBackground($btn-primary-bg, $btn-primary-bg-hl); + &--secondary { + @include buttonBackground($btn-secondary-bg, $btn-secondary-bg-hl); } } @include media-breakpoint-up(sm) { .navbar { - padding-left: 50px; + padding-left: 60px; } .sidemenu-open { .navbar { - padding-left: 15px; + padding-left: 20px; margin-left: 0; } } diff --git a/public/sass/components/_sidemenu.scss b/public/sass/components/_sidemenu.scss index 44941540598..f30bdb5c79e 100644 --- a/public/sass/components/_sidemenu.scss +++ b/public/sass/components/_sidemenu.scss @@ -253,7 +253,7 @@ li.sidemenu-org-switcher { } .sidemenu__logo_small_breakpoint { - padding: 16px 10px 26px; + padding: 14px 10px 26px 13px; display: flex; flex-direction: row; justify-content: space-between; diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss index 151130a69e2..50d2c188862 100644 --- a/public/sass/pages/_explore.scss +++ b/public/sass/pages/_explore.scss @@ -30,12 +30,6 @@ padding: 0; margin-left: 0; } - - .explore-toolbar-header-title { - .navbar-page-btn { - padding-left: 0; - } - } } .explore-toolbar { @@ -87,22 +81,9 @@ align-items: center; } -.explore-toolbar-header-title { - color: darken($link-color, 5%); - - .navbar-page-btn { - padding-left: $dashboard-padding; - } - - .fa { - font-size: 100%; - opacity: 0.75; - margin-right: 0.5em; - } -} - .explore-toolbar-header-close { margin-left: auto; + color: $text-color-weak; } .explore-toolbar-content { @@ -156,7 +137,6 @@ .sidemenu-open { .explore-toolbar-header-title { .navbar-page-btn { - padding-left: 0; margin-left: 0; } } @@ -164,7 +144,6 @@ .explore-toolbar-header-title { .navbar-page-btn { - padding-left: 0; margin-left: $dashboard-padding; } } @@ -185,7 +164,6 @@ .sidemenu-open { .explore-toolbar-header-title { .navbar-page-btn { - padding-left: 0; margin-left: $dashboard-padding; } } @@ -193,7 +171,6 @@ .explore-toolbar-header-title { .navbar-page-btn { - padding-left: 0; margin-left: $dashboard-padding; } } From a32d5ed16a201e2593c7fe0eef83124bb9a9eff1 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 18 Feb 2019 15:31:47 +0100 Subject: [PATCH 20/23] Fixing array direction, adding simple render test, fixes #15478 Fixed unit test and updated gauge Added migration for threshold order (cherry picked from commit 93e8edfdddebc1bdf3ecd2d192afc94b2d113b60) --- .../src/components/Gauge/Gauge.test.tsx | 4 +- .../grafana-ui/src/components/Gauge/Gauge.tsx | 7 +- .../ThresholdsEditor.test.tsx | 65 +++++++++++-------- .../ThresholdsEditor/ThresholdsEditor.tsx | 52 ++++++++------- .../ThresholdsEditor.test.tsx.snap | 7 ++ .../dashboard/state/DashboardMigrator.ts | 6 ++ 6 files changed, 84 insertions(+), 57 deletions(-) create mode 100644 packages/grafana-ui/src/components/ThresholdsEditor/__snapshots__/ThresholdsEditor.test.tsx.snap diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx index 5e5f477a733..70e29abc221 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx @@ -84,9 +84,9 @@ describe('Get thresholds formatted', () => { it('should get the correct formatted values when thresholds are added', () => { const { instance } = setup({ thresholds: [ - { index: 2, value: 75, color: '#6ED0E0' }, - { index: 1, value: 50, color: '#EAB839' }, { index: 0, value: -Infinity, color: '#7EB26D' }, + { index: 1, value: 50, color: '#EAB839' }, + { index: 2, value: 75, color: '#6ED0E0' }, ], }); diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.tsx index b8c257f4138..2ec0517e96a 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.tsx @@ -98,16 +98,15 @@ export class Gauge extends PureComponent { getFormattedThresholds() { const { maxValue, minValue, thresholds, theme } = this.props; - const thresholdsSortedByIndex = [...thresholds].sort((t1, t2) => t1.index - t2.index); - const lastThreshold = thresholdsSortedByIndex[thresholdsSortedByIndex.length - 1]; + const lastThreshold = thresholds[thresholds.length - 1]; return [ - ...thresholdsSortedByIndex.map(threshold => { + ...thresholds.map(threshold => { if (threshold.index === 0) { return { value: minValue, color: getColorFromHexRgbOrName(threshold.color, theme.type) }; } - const previousThreshold = thresholdsSortedByIndex[threshold.index - 1]; + const previousThreshold = thresholds[threshold.index - 1]; return { value: threshold.value, color: getColorFromHexRgbOrName(previousThreshold.color, theme.type) }; }), { value: maxValue, color: getColorFromHexRgbOrName(lastThreshold.color, theme.type) }, diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx index 2b6af67df22..ea94537c429 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx @@ -1,9 +1,8 @@ import React, { ChangeEvent } from 'react'; -import { shallow } from 'enzyme'; - +import { mount } from 'enzyme'; import { ThresholdsEditor, Props } from './ThresholdsEditor'; -const setup = (propOverrides?: object) => { +const setup = (propOverrides?: Partial) => { const props: Props = { onChange: jest.fn(), thresholds: [], @@ -11,12 +10,26 @@ const setup = (propOverrides?: object) => { Object.assign(props, propOverrides); - return shallow().instance() as ThresholdsEditor; + const wrapper = mount(); + const instance = wrapper.instance() as ThresholdsEditor; + + return { + instance, + wrapper, + }; }; +describe('Render', () => { + it('should render with base threshold', () => { + const { wrapper } = setup(); + + expect(wrapper).toMatchSnapshot(); + }); +}); + describe('Initialization', () => { it('should add a base threshold if missing', () => { - const instance = setup(); + const { instance } = setup(); expect(instance.state.thresholds).toEqual([{ index: 0, value: -Infinity, color: '#7EB26D' }]); }); @@ -24,7 +37,7 @@ describe('Initialization', () => { describe('Add threshold', () => { it('should not add threshold at index 0', () => { - const instance = setup(); + const { instance } = setup(); instance.onAddThreshold(0); @@ -32,32 +45,32 @@ describe('Add threshold', () => { }); it('should add threshold', () => { - const instance = setup(); + const { instance } = setup(); instance.onAddThreshold(1); expect(instance.state.thresholds).toEqual([ - { index: 1, value: 50, color: '#EAB839' }, { index: 0, value: -Infinity, color: '#7EB26D' }, + { index: 1, value: 50, color: '#EAB839' }, ]); }); it('should add another threshold above a first', () => { - const instance = setup({ + const { instance } = setup({ thresholds: [{ index: 0, value: -Infinity, color: '#7EB26D' }, { index: 1, value: 50, color: '#EAB839' }], }); instance.onAddThreshold(2); expect(instance.state.thresholds).toEqual([ - { index: 2, value: 75, color: '#6ED0E0' }, - { index: 1, value: 50, color: '#EAB839' }, { index: 0, value: -Infinity, color: '#7EB26D' }, + { index: 1, value: 50, color: '#EAB839' }, + { index: 2, value: 75, color: '#6ED0E0' }, ]); }); it('should add another threshold between first and second index', () => { - const instance = setup({ + const { instance } = setup({ thresholds: [ { index: 0, value: -Infinity, color: '#7EB26D' }, { index: 1, value: 50, color: '#EAB839' }, @@ -68,10 +81,10 @@ describe('Add threshold', () => { instance.onAddThreshold(2); expect(instance.state.thresholds).toEqual([ - { index: 3, value: 75, color: '#6ED0E0' }, - { index: 2, value: 62.5, color: '#EF843C' }, - { index: 1, value: 50, color: '#EAB839' }, { index: 0, value: -Infinity, color: '#7EB26D' }, + { index: 1, value: 50, color: '#EAB839' }, + { index: 2, value: 62.5, color: '#EF843C' }, + { index: 3, value: 75, color: '#6ED0E0' }, ]); }); }); @@ -83,7 +96,7 @@ describe('Remove threshold', () => { { index: 1, value: 50, color: '#EAB839' }, { index: 2, value: 75, color: '#6ED0E0' }, ]; - const instance = setup({ thresholds }); + const { instance } = setup({ thresholds }); instance.onRemoveThreshold(thresholds[0]); @@ -96,9 +109,7 @@ describe('Remove threshold', () => { { index: 1, value: 50, color: '#EAB839' }, { index: 2, value: 75, color: '#6ED0E0' }, ]; - const instance = setup({ - thresholds, - }); + const { instance } = setup({ thresholds }); instance.onRemoveThreshold(thresholds[1]); @@ -116,7 +127,7 @@ describe('change threshold value', () => { { index: 1, value: 50, color: '#EAB839' }, { index: 2, value: 75, color: '#6ED0E0' }, ]; - const instance = setup({ thresholds }); + const { instance } = setup({ thresholds }); const mockEvent = ({ target: { value: '12' } } as any) as ChangeEvent; @@ -126,7 +137,7 @@ describe('change threshold value', () => { }); it('should update value', () => { - const instance = setup(); + const { instance } = setup(); const thresholds = [ { index: 0, value: -Infinity, color: '#7EB26D' }, { index: 1, value: 50, color: '#EAB839' }, @@ -150,24 +161,24 @@ describe('change threshold value', () => { }); describe('on blur threshold value', () => { - it('should resort rows and update indexes', () => { - const instance = setup(); + it.only('should resort rows and update indexes', () => { + const { instance } = setup(); const thresholds = [ { index: 0, value: -Infinity, color: '#7EB26D' }, { index: 1, value: 78, color: '#EAB839' }, { index: 2, value: 75, color: '#6ED0E0' }, ]; - instance.state = { + instance.setState({ thresholds, - }; + }); instance.onBlur(); expect(instance.state.thresholds).toEqual([ - { index: 2, value: 78, color: '#EAB839' }, - { index: 1, value: 75, color: '#6ED0E0' }, { index: 0, value: -Infinity, color: '#7EB26D' }, + { index: 1, value: 75, color: '#6ED0E0' }, + { index: 2, value: 78, color: '#EAB839' }, ]); }); }); diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx index f4db23d6656..475e69b4de8 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx @@ -1,7 +1,7 @@ import React, { PureComponent, ChangeEvent } from 'react'; import { Threshold } from '../../types'; -import { ColorPicker } from '../ColorPicker/ColorPicker'; -import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup'; +import { ColorPicker } from '..'; +import { PanelOptionsGroup } from '..'; import { colors } from '../../utils'; import { getColorFromHexRgbOrName, ThemeContext } from '@grafana/ui'; @@ -54,16 +54,16 @@ export class ThresholdsEditor extends PureComponent { const value = afterThresholdValue - (afterThresholdValue - beforeThresholdValue) / 2; // Set a color - const color = colors.filter(c => newThresholds.some(t => t.color === c) === false)[0]; + const color = colors.filter(c => !newThresholds.some(t => t.color === c))[0]; this.setState( { thresholds: this.sortThresholds([ ...newThresholds, { + color, index, value: value as number, - color, }, ]), }, @@ -137,10 +137,11 @@ export class ThresholdsEditor extends PureComponent { onBlur = () => { this.setState(prevState => { const sortThresholds = this.sortThresholds([...prevState.thresholds]); - let index = sortThresholds.length - 1; + let index = 0; sortThresholds.forEach(t => { - t.index = index--; + t.index = index++; }); + return { thresholds: sortThresholds }; }); @@ -153,12 +154,13 @@ export class ThresholdsEditor extends PureComponent { sortThresholds = (thresholds: Threshold[]) => { return thresholds.sort((t1, t2) => { - return t2.value - t1.value; + return t1.value - t2.value; }); }; renderInput = (threshold: Threshold) => { const value = threshold.index === 0 ? 'Base' : threshold.value; + return (
    @@ -190,30 +192,32 @@ export class ThresholdsEditor extends PureComponent { render() { const { thresholds } = this.state; - return ( {theme => { return (
    - {thresholds.map((threshold, index) => { - return ( -
    -
    this.onAddThreshold(threshold.index + 1)} - > - + {thresholds + .slice(0) + .reverse() + .map((threshold, index) => { + return ( +
    +
    this.onAddThreshold(threshold.index + 1)} + > + +
    +
    +
    {this.renderInput(threshold)}
    -
    -
    {this.renderInput(threshold)}
    -
    - ); - })} + ); + })}
    ); diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/__snapshots__/ThresholdsEditor.test.tsx.snap b/packages/grafana-ui/src/components/ThresholdsEditor/__snapshots__/ThresholdsEditor.test.tsx.snap new file mode 100644 index 00000000000..b0dc025090b --- /dev/null +++ b/packages/grafana-ui/src/components/ThresholdsEditor/__snapshots__/ThresholdsEditor.test.tsx.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render with base threshold 1`] = ` + + + +`; diff --git a/public/app/features/dashboard/state/DashboardMigrator.ts b/public/app/features/dashboard/state/DashboardMigrator.ts index 1aa310308d5..c43f4c491c0 100644 --- a/public/app/features/dashboard/state/DashboardMigrator.ts +++ b/public/app/features/dashboard/state/DashboardMigrator.ts @@ -399,6 +399,12 @@ export class DashboardMigrator { prefix: panel.options.prefix, suffix: panel.options.suffix, }; + + // correct order + if (panel.options.thresholds) { + panel.options.thresholds.reverse(); + } + // this options prop was due to a bug delete panel.options.options; delete panel.options.unit; From 56f75e9e9d47a4d69caa9df505b14f603aeeb7fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 18 Feb 2019 21:29:42 +0100 Subject: [PATCH 21/23] Fixed navbar backbutton padding (cherry picked from commit 13e27601df3a13105da437eb3f8b16e9164907ff) --- public/sass/components/_navbar.scss | 4 ++-- public/sass/pages/_explore.scss | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/public/sass/components/_navbar.scss b/public/sass/components/_navbar.scss index b96f7bb7fc5..a86b8c450a0 100644 --- a/public/sass/components/_navbar.scss +++ b/public/sass/components/_navbar.scss @@ -162,7 +162,7 @@ .sidemenu-open { .navbar { - padding-left: 20px; + padding-left: 25px; margin-left: 0; } } @@ -178,7 +178,7 @@ display: flex; height: $navbarHeight; align-items: center; - padding-left: 7px; + padding-right: 13px; } .navbar-edit__back-btn { diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss index 50d2c188862..9b8f18e8681 100644 --- a/public/sass/pages/_explore.scss +++ b/public/sass/pages/_explore.scss @@ -38,7 +38,7 @@ flex-flow: row wrap; justify-content: flex-start; height: auto; - padding: 0px $dashboard-padding; + padding: 0px $dashboard-padding 0 25px; border-bottom: 1px solid #0000; transition-duration: 0.35s; transition-timing-function: ease-in-out; From 82cf6b951a2ae355bfbcb45a2e51f52314157779 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 19 Feb 2019 07:48:54 +0100 Subject: [PATCH 22/23] Fixes #15505 (cherry picked from commit 7f04848dea98282a97f6f6a9227dc2d96f05cbf4) --- .../ThresholdsEditor/ThresholdsEditor.tsx | 35 +++++++++++-------- .../ThresholdsEditor/_ThresholdsEditor.scss | 2 +- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx index 475e69b4de8..d613a911b03 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx @@ -159,8 +159,6 @@ export class ThresholdsEditor extends PureComponent { }; renderInput = (threshold: Threshold) => { - const value = threshold.index === 0 ? 'Base' : threshold.value; - return (
    @@ -171,21 +169,28 @@ export class ThresholdsEditor extends PureComponent {
    )}
    -
    - this.onChangeThresholdValue(event, threshold)} - value={value} - onBlur={this.onBlur} - readOnly={threshold.index === 0} - /> -
    - {threshold.index > 0 && ( -
    this.onRemoveThreshold(threshold)}> - + {threshold.index === 0 && ( +
    +
    )} + {threshold.index > 0 && ( + <> +
    + this.onChangeThresholdValue(event, threshold)} + value={threshold.value} + onBlur={this.onBlur} + readOnly={threshold.index === 0} + /> +
    +
    this.onRemoveThreshold(threshold)}> + +
    + + )}
    ); }; diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss b/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss index 8ef59bf08af..af70fd86f7a 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss +++ b/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss @@ -43,7 +43,7 @@ } .thresholds-row-input { - margin-top: 49px; + margin-top: 44px; margin-left: 2px; } From 57977c9db6fbd91261382b06a4d1abe246d93067 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Tue, 19 Feb 2019 11:00:15 +0100 Subject: [PATCH 23/23] release 6.0.0-beta3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 601681f54d4..afc51093823 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "company": "Grafana Labs" }, "name": "grafana", - "version": "6.0.0-beta2", + "version": "6.0.0-beta3", "repository": { "type": "git", "url": "http://github.com/grafana/grafana.git"