diff --git a/package.json b/package.json index eab6fa53be1..8fff96ebae9 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "company": "Grafana Labs" }, "name": "grafana", - "version": "5.4.0", + "version": "5.4.1", "repository": { "type": "git", "url": "http://github.com/grafana/grafana.git" diff --git a/pkg/api/pluginproxy/ds_auth_provider.go b/pkg/api/pluginproxy/ds_auth_provider.go index edf43085c7c..5c5776eec07 100644 --- a/pkg/api/pluginproxy/ds_auth_provider.go +++ b/pkg/api/pluginproxy/ds_auth_provider.go @@ -51,7 +51,7 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route if token, err := tokenProvider.getAccessToken(data); err != nil { logger.Error("Failed to get access token", "error", err) } else { - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) } } @@ -60,7 +60,7 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route if token, err := tokenProvider.getJwtAccessToken(ctx, data); err != nil { logger.Error("Failed to get access token", "error", err) } else { - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) } } @@ -73,7 +73,7 @@ func ApplyRoute(ctx context.Context, req *http.Request, proxyPath string, route if err != nil { logger.Error("Failed to get default access token from meta data server", "error", err) } else { - req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken)) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token.AccessToken)) } } } diff --git a/pkg/api/pluginproxy/pluginproxy.go b/pkg/api/pluginproxy/pluginproxy.go index ffbe470cb20..66e4498c283 100644 --- a/pkg/api/pluginproxy/pluginproxy.go +++ b/pkg/api/pluginproxy/pluginproxy.go @@ -87,7 +87,7 @@ func NewApiPluginProxy(ctx *m.ReqContext, proxyPath string, route *plugins.AppPl } for key, value := range headers { - log.Trace("setting key %v value %v", key, value[0]) + log.Trace("setting key %v value ", key) req.Header.Set(key, value[0]) } } diff --git a/pkg/services/sqlstore/quota.go b/pkg/services/sqlstore/quota.go index 7005b341268..e90b7fec131 100644 --- a/pkg/services/sqlstore/quota.go +++ b/pkg/services/sqlstore/quota.go @@ -99,14 +99,14 @@ func UpdateOrgQuota(cmd *m.UpdateOrgQuotaCmd) error { return inTransaction(func(sess *DBSession) error { //Check if quota is already defined in the DB quota := m.Quota{ - Target: cmd.Target, - OrgId: cmd.OrgId, - Updated: time.Now(), + Target: cmd.Target, + OrgId: cmd.OrgId, } has, err := sess.Get("a) if err != nil { return err } + quota.Updated = time.Now() quota.Limit = cmd.Limit if !has { quota.Created = time.Now() @@ -201,14 +201,14 @@ func UpdateUserQuota(cmd *m.UpdateUserQuotaCmd) error { return inTransaction(func(sess *DBSession) error { //Check if quota is already defined in the DB quota := m.Quota{ - Target: cmd.Target, - UserId: cmd.UserId, - Updated: time.Now(), + Target: cmd.Target, + UserId: cmd.UserId, } has, err := sess.Get("a) if err != nil { return err } + quota.Updated = time.Now() quota.Limit = cmd.Limit if !has { quota.Created = time.Now() diff --git a/pkg/services/sqlstore/quota_test.go b/pkg/services/sqlstore/quota_test.go index 49e028e9cd3..976d54d10e2 100644 --- a/pkg/services/sqlstore/quota_test.go +++ b/pkg/services/sqlstore/quota_test.go @@ -2,6 +2,7 @@ package sqlstore import ( "testing" + "time" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" @@ -168,5 +169,69 @@ func TestQuotaCommandsAndQueries(t *testing.T) { So(query.Result.Limit, ShouldEqual, 5) So(query.Result.Used, ShouldEqual, 1) }) + + // related: https://github.com/grafana/grafana/issues/14342 + Convey("Should org quota updating is successful even if it called multiple time", func() { + orgCmd := m.UpdateOrgQuotaCmd{ + OrgId: orgId, + Target: "org_user", + Limit: 5, + } + err := UpdateOrgQuota(&orgCmd) + So(err, ShouldBeNil) + + query := m.GetOrgQuotaByTargetQuery{OrgId: orgId, Target: "org_user", Default: 1} + err = GetOrgQuotaByTarget(&query) + So(err, ShouldBeNil) + So(query.Result.Limit, ShouldEqual, 5) + + // XXX: resolution of `Updated` column is 1sec, so this makes delay + time.Sleep(1 * time.Second) + + orgCmd = m.UpdateOrgQuotaCmd{ + OrgId: orgId, + Target: "org_user", + Limit: 10, + } + err = UpdateOrgQuota(&orgCmd) + So(err, ShouldBeNil) + + query = m.GetOrgQuotaByTargetQuery{OrgId: orgId, Target: "org_user", Default: 1} + err = GetOrgQuotaByTarget(&query) + So(err, ShouldBeNil) + So(query.Result.Limit, ShouldEqual, 10) + }) + + // related: https://github.com/grafana/grafana/issues/14342 + Convey("Should user quota updating is successful even if it called multiple time", func() { + userQuotaCmd := m.UpdateUserQuotaCmd{ + UserId: userId, + Target: "org_user", + Limit: 5, + } + err := UpdateUserQuota(&userQuotaCmd) + So(err, ShouldBeNil) + + query := m.GetUserQuotaByTargetQuery{UserId: userId, Target: "org_user", Default: 1} + err = GetUserQuotaByTarget(&query) + So(err, ShouldBeNil) + So(query.Result.Limit, ShouldEqual, 5) + + // XXX: resolution of `Updated` column is 1sec, so this makes delay + time.Sleep(1 * time.Second) + + userQuotaCmd = m.UpdateUserQuotaCmd{ + UserId: userId, + Target: "org_user", + Limit: 10, + } + err = UpdateUserQuota(&userQuotaCmd) + So(err, ShouldBeNil) + + query = m.GetUserQuotaByTargetQuery{UserId: userId, Target: "org_user", Default: 1} + err = GetUserQuotaByTarget(&query) + So(err, ShouldBeNil) + So(query.Result.Limit, ShouldEqual, 10) + }) }) } diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go index dd026bbb79e..dfa03d2dfa9 100644 --- a/pkg/tsdb/cloudwatch/metric_find_query.go +++ b/pkg/tsdb/cloudwatch/metric_find_query.go @@ -101,7 +101,7 @@ func init() { "AWS/RDS": {"ActiveTransactions", "AuroraBinlogReplicaLag", "AuroraReplicaLag", "AuroraReplicaLagMaximum", "AuroraReplicaLagMinimum", "BinLogDiskUsage", "BlockedTransactions", "BufferCacheHitRatio", "BurstBalance", "CommitLatency", "CommitThroughput", "BinLogDiskUsage", "CPUCreditBalance", "CPUCreditUsage", "CPUUtilization", "DatabaseConnections", "DDLLatency", "DDLThroughput", "Deadlocks", "DeleteLatency", "DeleteThroughput", "DiskQueueDepth", "DMLLatency", "DMLThroughput", "EngineUptime", "FailedSqlStatements", "FreeableMemory", "FreeLocalStorage", "FreeStorageSpace", "InsertLatency", "InsertThroughput", "LoginFailures", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "NetworkThroughput", "Queries", "ReadIOPS", "ReadLatency", "ReadThroughput", "ReplicaLag", "ResultSetCacheHitRatio", "SelectLatency", "SelectThroughput", "SwapUsage", "TotalConnections", "UpdateLatency", "UpdateThroughput", "VolumeBytesUsed", "VolumeReadIOPS", "VolumeWriteIOPS", "WriteIOPS", "WriteLatency", "WriteThroughput"}, "AWS/Route53": {"ChildHealthCheckHealthyCount", "HealthCheckStatus", "HealthCheckPercentageHealthy", "ConnectionTime", "SSLHandshakeTime", "TimeToFirstByte"}, "AWS/S3": {"BucketSizeBytes", "NumberOfObjects", "AllRequests", "GetRequests", "PutRequests", "DeleteRequests", "HeadRequests", "PostRequests", "ListRequests", "BytesDownloaded", "BytesUploaded", "4xxErrors", "5xxErrors", "FirstByteLatency", "TotalRequestLatency"}, - "AWS/SES": {"Bounce", "Complaint", "Delivery", "Reject", "Send"}, + "AWS/SES": {"Bounce", "Complaint", "Delivery", "Reject", "Send", "Reputation.BounceRate", "Reputation.ComplaintRate"}, "AWS/SNS": {"NumberOfMessagesPublished", "PublishSize", "NumberOfNotificationsDelivered", "NumberOfNotificationsFailed"}, "AWS/SQS": {"NumberOfMessagesSent", "SentMessageSize", "NumberOfMessagesReceived", "NumberOfEmptyReceives", "NumberOfMessagesDeleted", "ApproximateAgeOfOldestMessage", "ApproximateNumberOfMessagesDelayed", "ApproximateNumberOfMessagesVisible", "ApproximateNumberOfMessagesNotVisible"}, "AWS/States": {"ExecutionTime", "ExecutionThrottled", "ExecutionsAborted", "ExecutionsFailed", "ExecutionsStarted", "ExecutionsSucceeded", "ExecutionsTimedOut", "ActivityRunTime", "ActivityScheduleTime", "ActivityTime", "ActivitiesFailed", "ActivitiesHeartbeatTimedOut", "ActivitiesScheduled", "ActivitiesScheduled", "ActivitiesSucceeded", "ActivitiesTimedOut", "LambdaFunctionRunTime", "LambdaFunctionScheduleTime", "LambdaFunctionTime", "LambdaFunctionsFailed", "LambdaFunctionsHeartbeatTimedOut", "LambdaFunctionsScheduled", "LambdaFunctionsStarted", "LambdaFunctionsSucceeded", "LambdaFunctionsTimedOut"}, diff --git a/pkg/tsdb/influxdb/query_part.go b/pkg/tsdb/influxdb/query_part.go index 77f565a8597..29a77f15617 100644 --- a/pkg/tsdb/influxdb/query_part.go +++ b/pkg/tsdb/influxdb/query_part.go @@ -32,6 +32,7 @@ func init() { renders["median"] = QueryDefinition{Renderer: functionRenderer} renders["sum"] = QueryDefinition{Renderer: functionRenderer} renders["mode"] = QueryDefinition{Renderer: functionRenderer} + renders["cumulative_sum"] = 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 08bcff9b727..76daf6446d8 100644 --- a/pkg/tsdb/influxdb/query_part_test.go +++ b/pkg/tsdb/influxdb/query_part_test.go @@ -23,6 +23,7 @@ func TestInfluxdbQueryPart(t *testing.T) { {mode: "alias", params: []string{"test"}, input: "mean(value)", expected: `mean(value) AS "test"`}, {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))`}, } queryContext := &tsdb.TsdbQuery{TimeRange: tsdb.NewTimeRange("5m", "now")} diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts index 7be28272f11..5609c058a27 100644 --- a/public/app/core/angular_wrappers.ts +++ b/public/app/core/angular_wrappers.ts @@ -16,7 +16,7 @@ export function registerAngularDirectives() { react2AngularDirective('searchResult', SearchResult, []); react2AngularDirective('tagFilter', TagFilter, [ 'tags', - ['onSelect', { watchDepth: 'reference' }], + ['onChange', { watchDepth: 'reference' }], ['tagOptions', { watchDepth: 'reference' }], ]); } diff --git a/public/app/core/components/PermissionList/AddPermission.tsx b/public/app/core/components/PermissionList/AddPermission.tsx index 71cc937ddfa..d6da7c68544 100644 --- a/public/app/core/components/PermissionList/AddPermission.tsx +++ b/public/app/core/components/PermissionList/AddPermission.tsx @@ -84,7 +84,7 @@ class AddPermissions extends Component { render() { const { onCancel } = this.props; const newItem = this.state; - const pickerClassName = 'width-20'; + const pickerClassName = 'min-width-20'; const isValid = this.isValid(); return (
diff --git a/public/app/core/components/Picker/UserPicker.tsx b/public/app/core/components/Picker/UserPicker.tsx index f78cf69bf5e..f80a3fc135f 100644 --- a/public/app/core/components/Picker/UserPicker.tsx +++ b/public/app/core/components/Picker/UserPicker.tsx @@ -40,7 +40,7 @@ export class UserPicker extends Component { .then(result => { return result.map(user => ({ id: user.userId, - label: `${user.login} - ${user.email}`, + label: user.login === user.email ? user.login : `${user.login} - ${user.email}`, avatarUrl: user.avatarUrl, login: user.login, })); diff --git a/public/app/core/components/TagFilter/TagFilter.tsx b/public/app/core/components/TagFilter/TagFilter.tsx index 14b6ef4932b..6a4203f8739 100644 --- a/public/app/core/components/TagFilter/TagFilter.tsx +++ b/public/app/core/components/TagFilter/TagFilter.tsx @@ -10,7 +10,7 @@ import ResetStyles from 'app/core/components/Picker/ResetStyles'; export interface Props { tags: string[]; tagOptions: () => any; - onSelect: (tag: string) => void; + onChange: (tags: string[]) => void; } export class TagFilter extends React.Component { @@ -18,12 +18,9 @@ export class TagFilter extends React.Component { constructor(props) { super(props); - - this.searchTags = this.searchTags.bind(this); - this.onChange = this.onChange.bind(this); } - searchTags(query) { + onLoadOptions = query => { return this.props.tagOptions().then(options => { return options.map(option => ({ value: option.term, @@ -31,18 +28,20 @@ export class TagFilter extends React.Component { count: option.count, })); }); - } + }; - onChange(newTags) { - this.props.onSelect(newTags); - } + onChange = (newTags: any[]) => { + this.props.onChange(newTags.map(tag => tag.value)); + }; render() { + const tags = this.props.tags.map(tag => ({ value: tag, label: tag, count: 0 })); + const selectOptions = { classNamePrefix: 'gf-form-select-box', isMulti: true, defaultOptions: true, - loadOptions: this.searchTags, + loadOptions: this.onLoadOptions, onChange: this.onChange, className: 'gf-form-input gf-form-input--form-dropdown', placeholder: 'Tags', @@ -50,7 +49,7 @@ export class TagFilter extends React.Component { noOptionsMessage: () => 'No tags found', getOptionValue: i => i.value, getOptionLabel: i => i.label, - value: this.props.tags, + value: tags, styles: ResetStyles, components: { Option: TagOption, diff --git a/public/app/core/components/colorpicker/SeriesColorPicker.tsx b/public/app/core/components/colorpicker/SeriesColorPicker.tsx index d6feaa31965..32b7554e38d 100644 --- a/public/app/core/components/colorpicker/SeriesColorPicker.tsx +++ b/public/app/core/components/colorpicker/SeriesColorPicker.tsx @@ -44,7 +44,7 @@ export class SeriesColorPicker extends React.Component { const drop = new Drop({ target: this.pickerElem, content: dropContentElem, - position: 'top center', + position: 'bottom center', classes: 'drop-popover', openOn: 'hover', hoverCloseDelay: 200, diff --git a/public/app/core/components/search/search.html b/public/app/core/components/search/search.html index 8723d5d0584..8a83ecbc205 100644 --- a/public/app/core/components/search/search.html +++ b/public/app/core/components/search/search.html @@ -41,7 +41,7 @@
- + diff --git a/public/app/core/components/search/search.ts b/public/app/core/components/search/search.ts index 322dd2bdf10..ff63ca5a8fe 100644 --- a/public/app/core/components/search/search.ts +++ b/public/app/core/components/search/search.ts @@ -25,8 +25,6 @@ export class SearchCtrl { appEvents.on('hide-dash-search', this.closeSearch.bind(this), $scope); this.initialFolderFilterTitle = 'All'; - this.getTags = this.getTags.bind(this); - this.onTagSelect = this.onTagSelect.bind(this); this.isEditor = contextSrv.isEditor; this.hasEditPermissionInFolders = contextSrv.hasEditPermissionInFolders; } @@ -162,7 +160,7 @@ export class SearchCtrl { const localSearchId = this.currentSearchId; const query = { ...this.query, - tag: this.query.tag.map(i => i.value), + tag: this.query.tag, }; return this.searchSrv.search(query).then(results => { @@ -195,14 +193,14 @@ export class SearchCtrl { evt.preventDefault(); } - getTags() { + getTags = () => { return this.searchSrv.getDashboardTags(); - } + }; - onTagSelect(newTags) { - this.query.tag = newTags; + onTagFiltersChanged = (tags: string[]) => { + this.query.tag = tags; this.search(); - } + }; clearSearchFilter() { this.query.tag = []; diff --git a/public/app/core/utils/kbn.ts b/public/app/core/utils/kbn.ts index 9caa5bf1a54..e0b98cb803c 100644 --- a/public/app/core/utils/kbn.ts +++ b/public/app/core/utils/kbn.ts @@ -590,8 +590,8 @@ kbn.valueFormats.flowcms = kbn.formatBuilders.fixedUnit('cms'); kbn.valueFormats.flowcfs = kbn.formatBuilders.fixedUnit('cfs'); kbn.valueFormats.flowcfm = kbn.formatBuilders.fixedUnit('cfm'); kbn.valueFormats.litreh = kbn.formatBuilders.fixedUnit('l/h'); -kbn.valueFormats.flowlpm = kbn.formatBuilders.decimalSIPrefix('l/min'); -kbn.valueFormats.flowmlpm = kbn.formatBuilders.decimalSIPrefix('mL/min', -1); +kbn.valueFormats.flowlpm = kbn.formatBuilders.fixedUnit('l/min'); +kbn.valueFormats.flowmlpm = kbn.formatBuilders.fixedUnit('mL/min'); // Angle kbn.valueFormats.degree = kbn.formatBuilders.fixedUnit('°'); diff --git a/public/app/features/dashboard/dashboard_model.ts b/public/app/features/dashboard/dashboard_model.ts index 3320783ec67..18a16d5c1d4 100644 --- a/public/app/features/dashboard/dashboard_model.ts +++ b/public/app/features/dashboard/dashboard_model.ts @@ -223,6 +223,8 @@ export class DashboardModel { } panelInitialized(panel: PanelModel) { + panel.initialized(); + if (!this.otherPanelInFullscreen(panel)) { panel.refresh(); } diff --git a/public/app/features/dashboard/panel_model.ts b/public/app/features/dashboard/panel_model.ts index dc8a509f2eb..737841be7e8 100644 --- a/public/app/features/dashboard/panel_model.ts +++ b/public/app/features/dashboard/panel_model.ts @@ -132,7 +132,7 @@ export class PanelModel { } } - panelInitialized() { + initialized() { this.events.emit('panel-initialized'); } diff --git a/public/app/features/panel/partials/soloPanel.html b/public/app/features/panel/partials/soloPanel.html index 0940e07afdd..644bbe74ffb 100644 --- a/public/app/features/panel/partials/soloPanel.html +++ b/public/app/features/panel/partials/soloPanel.html @@ -1,5 +1,4 @@ -
+
-
diff --git a/public/app/features/teams/TeamMembers.tsx b/public/app/features/teams/TeamMembers.tsx index f43dc44808f..433702fa0d5 100644 --- a/public/app/features/teams/TeamMembers.tsx +++ b/public/app/features/teams/TeamMembers.tsx @@ -115,7 +115,7 @@ export class TeamMembers extends PureComponent {
Add Team Member
- + {this.state.newTeamMember && (
@@ -152,7 +152,7 @@ exports[`Render should render team members 1`] = ` className="gf-form-inline" >
@@ -372,7 +372,7 @@ exports[`Render should render team members when sync enabled 1`] = ` className="gf-form-inline" > diff --git a/public/app/plugins/datasource/postgres/meta_query.ts b/public/app/plugins/datasource/postgres/meta_query.ts index fd13f3b4482..07ea3e51d87 100644 --- a/public/app/plugins/datasource/postgres/meta_query.ts +++ b/public/app/plugins/datasource/postgres/meta_query.ts @@ -151,8 +151,7 @@ table_schema IN ( buildDatatypeQuery(column: string) { let query = 'SELECT udt_name FROM information_schema.columns WHERE '; - query += this.buildSchemaConstraint(); - query += ' AND table_name = ' + this.quoteIdentAsLiteral(this.target.table); + query += this.buildTableConstraint(this.target.table); query += ' AND column_name = ' + this.quoteIdentAsLiteral(column); return query; } diff --git a/public/app/plugins/panel/graph/specs/time_region_manager.test.ts b/public/app/plugins/panel/graph/specs/time_region_manager.test.ts index 35e48897282..fc0b86d1b68 100644 --- a/public/app/plugins/panel/graph/specs/time_region_manager.test.ts +++ b/public/app/plugins/panel/graph/specs/time_region_manager.test.ts @@ -130,6 +130,33 @@ describe('TimeRegionManager', () => { }); }); + plotOptionsScenario('for time from/to region', ctx => { + const regions = [{ from: '00:00', to: '05:00', fill: true, colorMode: 'red' }]; + const from = moment('2018-12-01T00:00+01:00'); + const to = moment('2018-12-03T23:59+01:00'); + ctx.setup(regions, from, to); + + it('should add 3 markings', () => { + expect(ctx.options.grid.markings.length).toBe(3); + }); + + it('should add one fill between 00:00 and 05:00 each day', () => { + const markings = ctx.options.grid.markings; + + expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-12-01T01:00:00+01:00').format()); + expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-12-01T06:00:00+01:00').format()); + expect(markings[0].color).toBe(colorModes.red.color.fill); + + expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-12-02T01:00:00+01:00').format()); + expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-12-02T06:00:00+01:00').format()); + expect(markings[1].color).toBe(colorModes.red.color.fill); + + expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-12-03T01:00:00+01:00').format()); + expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-12-03T06:00:00+01:00').format()); + expect(markings[2].color).toBe(colorModes.red.color.fill); + }); + }); + plotOptionsScenario('for day of week from/to region', ctx => { const regions = [{ fromDayOfWeek: 7, toDayOfWeek: 7, fill: true, colorMode: 'red' }]; const from = moment('2018-01-01T18:45:05+01:00'); @@ -211,6 +238,42 @@ describe('TimeRegionManager', () => { }); }); + plotOptionsScenario('for day of week from/to time region', ctx => { + const regions = [{ fromDayOfWeek: 7, from: '23:00', toDayOfWeek: 1, to: '01:40', fill: true, colorMode: 'red' }]; + const from = moment('2018-12-07T12:51:19+01:00'); + const to = moment('2018-12-10T13:51:29+01:00'); + ctx.setup(regions, from, to); + + it('should add 1 marking', () => { + expect(ctx.options.grid.markings.length).toBe(1); + }); + + it('should add one fill between sunday 23:00 and monday 01:40', () => { + const markings = ctx.options.grid.markings; + + expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-12-10T00:00:00+01:00').format()); + expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-12-10T02:40:00+01:00').format()); + }); + }); + + plotOptionsScenario('for day of week from/to time region', ctx => { + const regions = [{ fromDayOfWeek: 6, from: '03:00', toDayOfWeek: 7, to: '02:00', fill: true, colorMode: 'red' }]; + const from = moment('2018-12-07T12:51:19+01:00'); + const to = moment('2018-12-10T13:51:29+01:00'); + ctx.setup(regions, from, to); + + it('should add 1 marking', () => { + expect(ctx.options.grid.markings.length).toBe(1); + }); + + it('should add one fill between saturday 03:00 and sunday 02:00', () => { + const markings = ctx.options.grid.markings; + + expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-12-08T04:00:00+01:00').format()); + expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-12-09T03:00:00+01:00').format()); + }); + }); + plotOptionsScenario('for day of week from/to time region with daylight saving time', ctx => { const regions = [{ fromDayOfWeek: 7, from: '20:00', toDayOfWeek: 7, to: '23:00', fill: true, colorMode: 'red' }]; const from = moment('2018-03-17T06:00:00+01:00'); diff --git a/public/app/plugins/panel/graph/time_region_manager.ts b/public/app/plugins/panel/graph/time_region_manager.ts index 95987e40dbe..e5ec27ee77a 100644 --- a/public/app/plugins/panel/graph/time_region_manager.ts +++ b/public/app/plugins/panel/graph/time_region_manager.ts @@ -87,6 +87,14 @@ export class TimeRegionManager { continue; } + if (timeRegion.from && !timeRegion.to) { + timeRegion.to = timeRegion.from; + } + + if (!timeRegion.from && timeRegion.to) { + timeRegion.from = timeRegion.to; + } + hRange = { from: this.parseTimeRange(timeRegion.from), to: this.parseTimeRange(timeRegion.to), @@ -108,21 +116,13 @@ export class TimeRegionManager { hRange.to.dayOfWeek = Number(timeRegion.toDayOfWeek); } - if (!hRange.from.h && hRange.to.h) { - hRange.from = hRange.to; - } - - if (hRange.from.h && !hRange.to.h) { - hRange.to = hRange.from; - } - - if (hRange.from.dayOfWeek && !hRange.from.h && !hRange.from.m) { + if (hRange.from.dayOfWeek && hRange.from.h === null && hRange.from.m === null) { hRange.from.h = 0; hRange.from.m = 0; hRange.from.s = 0; } - if (hRange.to.dayOfWeek && !hRange.to.h && !hRange.to.m) { + if (hRange.to.dayOfWeek && hRange.to.h === null && hRange.to.m === null) { hRange.to.h = 23; hRange.to.m = 59; hRange.to.s = 59; @@ -169,8 +169,16 @@ export class TimeRegionManager { fromEnd.add(hRange.to.h - hRange.from.h, 'hours'); } else if (hRange.from.h + hRange.to.h < 23) { fromEnd.add(hRange.to.h, 'hours'); + + while (fromEnd.hour() !== hRange.to.h) { + fromEnd.add(-1, 'hours'); + } } else { fromEnd.add(24 - hRange.from.h, 'hours'); + + while (fromEnd.hour() !== hRange.to.h) { + fromEnd.add(1, 'hours'); + } } fromEnd.set('minute', hRange.to.m); diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index 26ce6b7722d..f83151a0c46 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -107,7 +107,10 @@ class SingleStatCtrl extends MetricsPanelCtrl { } onDataReceived(dataList) { - const data: any = {}; + const data: any = { + scopedVars: _.extend({}, this.panel.scopedVars), + }; + if (dataList.length > 0 && dataList[0].type === 'table') { this.dataType = 'table'; const tableData = dataList.map(this.tableHandler.bind(this)); @@ -117,6 +120,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { this.series = dataList.map(this.seriesHandler.bind(this)); this.setValues(data); } + this.data = data; this.render(); } @@ -320,7 +324,6 @@ class SingleStatCtrl extends MetricsPanelCtrl { } // Add $__name variable for using in prefix or postfix - data.scopedVars = _.extend({}, this.panel.scopedVars); data.scopedVars['__name'] = { value: this.series[0].label }; } this.setValueMapping(data); diff --git a/public/sass/base/_type.scss b/public/sass/base/_type.scss index 2de8665f06a..1a005b0d511 100644 --- a/public/sass/base/_type.scss +++ b/public/sass/base/_type.scss @@ -199,7 +199,6 @@ small, mark, .mark { - padding: 0.2em; background: $alert-warning-bg; } diff --git a/public/sass/pages/_dashboard.scss b/public/sass/pages/_dashboard.scss index 125edac500f..0a578901bbd 100644 --- a/public/sass/pages/_dashboard.scss +++ b/public/sass/pages/_dashboard.scss @@ -19,16 +19,23 @@ div.flot-text { .panel { height: 100%; +} - &--solo { - position: fixed; - bottom: 0; - right: 0; - margin: 0; - .panel-container { - border: none; - z-index: $zindex-sidemenu + 1; - } +.panel-solo { + position: fixed; + bottom: 0; + right: 0; + margin: 0; + left: 0; + top: 0; + + .panel-container { + border: none; + } + + .panel-menu-toggle, + .panel-menu { + display: none; } } diff --git a/public/sass/utils/_widths.scss b/public/sass/utils/_widths.scss index 2000982f08d..b1213e6ea60 100644 --- a/public/sass/utils/_widths.scss +++ b/public/sass/utils/_widths.scss @@ -19,6 +19,12 @@ } } +@for $i from 1 through 30 { + .min-width-#{$i} { + min-width: ($spacer * $i) - $gf-form-margin !important; + } +} + @for $i from 1 through 30 { .offset-width-#{$i} { margin-left: ($spacer * $i) !important;