diff --git a/.circleci/config.yml b/.circleci/config.yml index 209cf5c98cc..8144956773b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -333,6 +333,7 @@ jobs: docker: - image: grafana/grafana-ci-deploy:1.2.0 steps: + - checkout - attach_workspace: at: . - run: diff --git a/CHANGELOG.md b/CHANGELOG.md index e6fdc78842a..7164f5d99a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # 6.0.0-beta2 (unreleased) +### Minor +* **Pushover**: Adds support for images in pushover notifier [#10780](https://github.com/grafana/grafana/issues/10780), thx [@jpenalbae](https://github.com/jpenalbae) + # 6.0.0-beta1 (2019-01-30) ### New Features diff --git a/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx b/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx index d4051b5ea22..1d2151a0627 100644 --- a/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx +++ b/packages/grafana-ui/src/components/Gauge/Gauge.test.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { Gauge, Props } from './Gauge'; -import { TimeSeriesVMs } from '../../types/series'; +import { TimeSeriesVMs } from '../../types/data'; import { ValueMapping, MappingType } from '../../types'; jest.mock('jquery', () => ({ diff --git a/packages/grafana-ui/src/types/series.ts b/packages/grafana-ui/src/types/data.ts similarity index 81% rename from packages/grafana-ui/src/types/series.ts rename to packages/grafana-ui/src/types/data.ts index 5cad1e4a72a..1e4ccba3948 100644 --- a/packages/grafana-ui/src/types/series.ts +++ b/packages/grafana-ui/src/types/data.ts @@ -52,3 +52,20 @@ export interface TimeSeriesVMs { [index: number]: TimeSeriesVM; length: number; } + +interface Column { + text: string; + title?: string; + type?: string; + sort?: boolean; + desc?: boolean; + filterable?: boolean; + unit?: string; +} + +export interface TableData { + columns: Column[]; + rows: any[]; + type: string; + columnMap: any; +} diff --git a/packages/grafana-ui/src/types/datasource.ts b/packages/grafana-ui/src/types/datasource.ts index ffcbbb5fe64..44d38ff20e2 100644 --- a/packages/grafana-ui/src/types/datasource.ts +++ b/packages/grafana-ui/src/types/datasource.ts @@ -1,9 +1,9 @@ import { TimeRange, RawTimeRange } from './time'; -import { TimeSeries } from './series'; import { PluginMeta } from './plugin'; +import { TableData, TimeSeries } from './data'; export interface DataQueryResponse { - data: TimeSeries[]; + data: TimeSeries[] | [TableData]; } export interface DataQuery { diff --git a/packages/grafana-ui/src/types/index.ts b/packages/grafana-ui/src/types/index.ts index 575c749e07e..e23b5e63af8 100644 --- a/packages/grafana-ui/src/types/index.ts +++ b/packages/grafana-ui/src/types/index.ts @@ -1,4 +1,4 @@ -export * from './series'; +export * from './data'; export * from './time'; export * from './panel'; export * from './plugin'; diff --git a/packages/grafana-ui/src/types/panel.ts b/packages/grafana-ui/src/types/panel.ts index f2a699839b8..ad09b3aba9f 100644 --- a/packages/grafana-ui/src/types/panel.ts +++ b/packages/grafana-ui/src/types/panel.ts @@ -1,4 +1,4 @@ -import { TimeSeries, LoadingState } from './series'; +import { TimeSeries, LoadingState, TableData } from './data'; import { TimeRange } from './time'; export type InterpolateFunction = (value: string, format?: string | Function) => string; @@ -14,6 +14,11 @@ export interface PanelProps { onInterpolate: InterpolateFunction; } +export interface PanelData { + timeSeries?: TimeSeries[]; + tableData?: TableData; +} + export interface PanelOptionsProps { options: T; onChange: (options: T) => void; diff --git a/pkg/services/alerting/notifiers/pushover.go b/pkg/services/alerting/notifiers/pushover.go index 55dc02c5f4a..6581328b745 100644 --- a/pkg/services/alerting/notifiers/pushover.go +++ b/pkg/services/alerting/notifiers/pushover.go @@ -1,8 +1,11 @@ package notifiers import ( + "bytes" "fmt" - "net/url" + "io" + "mime/multipart" + "os" "strconv" "github.com/grafana/grafana/pkg/bus" @@ -91,6 +94,7 @@ func NewPushoverNotifier(model *m.AlertNotification) (alerting.Notifier, error) retry, _ := strconv.Atoi(model.Settings.Get("retry").MustString()) expire, _ := strconv.Atoi(model.Settings.Get("expire").MustString()) sound := model.Settings.Get("sound").MustString() + uploadImage := model.Settings.Get("uploadImage").MustBool(true) if userKey == "" { return nil, alerting.ValidationError{Reason: "User key not given"} @@ -107,6 +111,7 @@ func NewPushoverNotifier(model *m.AlertNotification) (alerting.Notifier, error) Expire: expire, Device: device, Sound: sound, + Upload: uploadImage, log: log.New("alerting.notifier.pushover"), }, nil } @@ -120,6 +125,7 @@ type PushoverNotifier struct { Expire int Device string Sound string + Upload bool log log.Logger } @@ -140,38 +146,22 @@ func (this *PushoverNotifier) Notify(evalContext *alerting.EvalContext) error { if evalContext.Error != nil { message += fmt.Sprintf("\nError message: %s", evalContext.Error.Error()) } - if evalContext.ImagePublicUrl != "" { - message += fmt.Sprintf("\nShow graph image", evalContext.ImagePublicUrl) - } + if message == "" { message = "Notification message missing (Set a notification message to replace this text.)" } - q := url.Values{} - q.Add("user", this.UserKey) - q.Add("token", this.ApiToken) - q.Add("priority", strconv.Itoa(this.Priority)) - if this.Priority == 2 { - q.Add("retry", strconv.Itoa(this.Retry)) - q.Add("expire", strconv.Itoa(this.Expire)) + headers, uploadBody, err := this.genPushoverBody(evalContext, message, ruleUrl) + if err != nil { + this.log.Error("Failed to generate body for pushover", "error", err) + return err } - if this.Device != "" { - q.Add("device", this.Device) - } - if this.Sound != "default" { - q.Add("sound", this.Sound) - } - q.Add("title", evalContext.GetNotificationTitle()) - q.Add("url", ruleUrl) - q.Add("url_title", "Show dashboard with alert") - q.Add("message", message) - q.Add("html", "1") cmd := &m.SendWebhookSync{ Url: PUSHOVER_ENDPOINT, HttpMethod: "POST", - HttpHeader: map[string]string{"Content-Type": "application/x-www-form-urlencoded"}, - Body: q.Encode(), + HttpHeader: headers, + Body: uploadBody.String(), } if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { @@ -181,3 +171,109 @@ func (this *PushoverNotifier) Notify(evalContext *alerting.EvalContext) error { return nil } + +func (this *PushoverNotifier) genPushoverBody(evalContext *alerting.EvalContext, message string, ruleUrl string) (map[string]string, bytes.Buffer, error) { + var b bytes.Buffer + var err error + w := multipart.NewWriter(&b) + + // Add image only if requested and available + if this.Upload && evalContext.ImageOnDiskPath != "" { + f, err := os.Open(evalContext.ImageOnDiskPath) + if err != nil { + return nil, b, err + } + defer f.Close() + + fw, err := w.CreateFormFile("attachment", evalContext.ImageOnDiskPath) + if err != nil { + return nil, b, err + } + + _, err = io.Copy(fw, f) + if err != nil { + return nil, b, err + } + } + + // Add the user token + err = w.WriteField("user", this.UserKey) + if err != nil { + return nil, b, err + } + + // Add the api token + err = w.WriteField("token", this.ApiToken) + if err != nil { + return nil, b, err + } + + // Add priority + err = w.WriteField("priority", strconv.Itoa(this.Priority)) + if err != nil { + return nil, b, err + } + + if this.Priority == 2 { + err = w.WriteField("retry", strconv.Itoa(this.Retry)) + if err != nil { + return nil, b, err + } + + err = w.WriteField("expire", strconv.Itoa(this.Expire)) + if err != nil { + return nil, b, err + } + } + + // Add device + if this.Device != "" { + err = w.WriteField("device", this.Device) + if err != nil { + return nil, b, err + } + } + + // Add sound + if this.Sound != "default" { + err = w.WriteField("sound", this.Sound) + if err != nil { + return nil, b, err + } + } + + // Add title + err = w.WriteField("title", evalContext.GetNotificationTitle()) + if err != nil { + return nil, b, err + } + + // Add URL + err = w.WriteField("url", ruleUrl) + if err != nil { + return nil, b, err + } + // Add URL title + err = w.WriteField("url_title", "Show dashboard with alert") + if err != nil { + return nil, b, err + } + + // Add message + err = w.WriteField("message", message) + if err != nil { + return nil, b, err + } + + // Mark as html message + err = w.WriteField("html", "1") + if err != nil { + return nil, b, err + } + + w.Close() + headers := map[string]string{ + "Content-Type": w.FormDataContentType(), + } + return headers, b, nil +} diff --git a/pkg/services/auth/auth_token.go b/pkg/services/auth/auth_token.go index 7e9433c2d70..db4d9d18624 100644 --- a/pkg/services/auth/auth_token.go +++ b/pkg/services/auth/auth_token.go @@ -151,7 +151,7 @@ func (s *UserAuthTokenServiceImpl) CreateToken(userId int64, clientIP, userAgent func (s *UserAuthTokenServiceImpl) LookupToken(unhashedToken string) (*userAuthToken, error) { hashedToken := hashToken(unhashedToken) if setting.Env == setting.DEV { - s.log.Info("looking up token", "unhashed", unhashedToken, "hashed", hashedToken) + s.log.Debug("looking up token", "unhashed", unhashedToken, "hashed", hashedToken) } expireBefore := getTime().Add(time.Duration(-86400*s.Cfg.LoginCookieMaxDays) * time.Second).Unix() diff --git a/pkg/services/provisioning/notifiers/alert_notifications.go b/pkg/services/provisioning/notifiers/alert_notifications.go index 514f11379c8..7b595a3d32f 100644 --- a/pkg/services/provisioning/notifiers/alert_notifications.go +++ b/pkg/services/provisioning/notifiers/alert_notifications.go @@ -92,7 +92,7 @@ func (dc *NotificationProvisioner) mergeNotifications(notificationToMerge []*not } if cmd.Result == nil { - dc.log.Info("Inserting alert notification from configuration ", "name", notification.Name, "uid", notification.Uid) + dc.log.Debug("inserting alert notification from configuration", "name", notification.Name, "uid", notification.Uid) insertCmd := &models.CreateAlertNotificationCommand{ Uid: notification.Uid, Name: notification.Name, @@ -109,7 +109,7 @@ func (dc *NotificationProvisioner) mergeNotifications(notificationToMerge []*not return err } } else { - dc.log.Info("Updating alert notification from configuration", "name", notification.Name) + dc.log.Debug("updating alert notification from configuration", "name", notification.Name) updateCmd := &models.UpdateAlertNotificationWithUidCommand{ Uid: notification.Uid, Name: notification.Name, diff --git a/public/app/features/dashboard/components/RowOptions/RowOptionsCtrl.ts b/public/app/features/dashboard/components/RowOptions/RowOptionsCtrl.ts index a855791f1ea..d2526c92cd0 100644 --- a/public/app/features/dashboard/components/RowOptions/RowOptionsCtrl.ts +++ b/public/app/features/dashboard/components/RowOptions/RowOptionsCtrl.ts @@ -24,7 +24,7 @@ export class RowOptionsCtrl { export function rowOptionsDirective() { return { restrict: 'E', - templateUrl: 'public/app/features/dashboard/partials/row_options.html', + templateUrl: 'public/app/features/dashboard/components/RowOptions/template.html', controller: RowOptionsCtrl, bindToController: true, controllerAs: 'ctrl', diff --git a/public/app/features/dashboard/dashgrid/DataPanel.tsx b/public/app/features/dashboard/dashgrid/DataPanel.tsx index e15ff8d4c0d..353c474bcd9 100644 --- a/public/app/features/dashboard/dashgrid/DataPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DataPanel.tsx @@ -8,13 +8,21 @@ import { DatasourceSrv, getDatasourceSrv } from 'app/features/plugins/datasource // Utils import kbn from 'app/core/utils/kbn'; // Types -import { DataQueryOptions, DataQueryResponse, LoadingState, TimeRange, TimeSeries } from '@grafana/ui/src/types'; +import { + DataQueryOptions, + DataQueryResponse, + LoadingState, + PanelData, + TableData, + TimeRange, + TimeSeries, +} from '@grafana/ui'; const DEFAULT_PLUGIN_ERROR = 'Error in plugin'; interface RenderProps { loading: LoadingState; - timeSeries: TimeSeries[]; + panelData: PanelData; } export interface Props { @@ -129,6 +137,7 @@ export class DataPanel extends Component { console.log('Issuing DataPanel query', queryOptions); const resp = await ds.query(queryOptions); + console.log('Issuing DataPanel query Resp', resp); if (this.isUnmounted) { @@ -160,11 +169,27 @@ export class DataPanel extends Component { } }; + getPanelData = () => { + const { response } = this.state; + + if (response.data.length > 0 && (response.data[0] as TableData).type === 'table') { + return { + tableData: response.data[0] as TableData, + timeSeries: null, + }; + } + + return { + timeSeries: response.data as TimeSeries[], + tableData: null, + }; + }; + render() { const { queries } = this.props; - const { response, loading, isFirstLoad } = this.state; + const { loading, isFirstLoad } = this.state; - const timeSeries = response.data; + const panelData = this.getPanelData(); if (isFirstLoad && loading === LoadingState.Loading) { return this.renderLoadingStates(); @@ -190,8 +215,8 @@ export class DataPanel extends Component { return ( <> {this.props.children({ - timeSeries, loading, + panelData, })} ); diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index bdb6aca870a..68b53714504 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -14,8 +14,7 @@ import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel'; import { PANEL_HEADER_HEIGHT } from 'app/core/constants'; // Types -import { PanelModel } from '../state/PanelModel'; -import { DashboardModel } from '../state/DashboardModel'; +import { DashboardModel, PanelModel } from '../state'; import { PanelPlugin } from 'app/types'; import { TimeRange } from '@grafana/ui'; @@ -139,7 +138,6 @@ export class PanelChrome extends PureComponent { scopedVars={panel.scopedVars} links={panel.links} /> - {panel.snapshotData ? ( this.renderPanel(false, panel.snapshotData, width, height) ) : ( @@ -152,8 +150,8 @@ export class PanelChrome extends PureComponent { refreshCounter={refreshCounter} onDataResponse={this.onDataResponse} > - {({ loading, timeSeries }) => { - return this.renderPanel(loading, timeSeries, width, height); + {({ loading, panelData }) => { + return this.renderPanel(loading, panelData.timeSeries, width, height); }} )} diff --git a/public/app/features/dashboard/panel_editor/PanelEditor.tsx b/public/app/features/dashboard/panel_editor/PanelEditor.tsx index 7b8097b9f65..d7aafb89e55 100644 --- a/public/app/features/dashboard/panel_editor/PanelEditor.tsx +++ b/public/app/features/dashboard/panel_editor/PanelEditor.tsx @@ -101,17 +101,6 @@ export class PanelEditor extends PureComponent { return (
- { - //
- // - //
- //
- //
- //
- //
- //
- } -
{tabs.map(tab => { return ; diff --git a/public/app/features/dashboard/state/PanelModel.ts b/public/app/features/dashboard/state/PanelModel.ts index 6f85e7a7a3c..0e5e48cec01 100644 --- a/public/app/features/dashboard/state/PanelModel.ts +++ b/public/app/features/dashboard/state/PanelModel.ts @@ -5,6 +5,7 @@ import _ from 'lodash'; 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'; export interface GridPos { x: number; @@ -87,7 +88,7 @@ export class PanelModel { datasource: string; thresholds?: any; - snapshotData?: TimeSeries[]; + snapshotData?: TimeSeries[] | [TableData]; timeFrom?: any; timeShift?: any; hideTimeOverride?: any; diff --git a/public/app/plugins/datasource/elasticsearch/pipeline_variables.ts b/public/app/plugins/datasource/elasticsearch/pipeline_variables.ts index 94e0e78d686..26c886043b2 100644 --- a/public/app/plugins/datasource/elasticsearch/pipeline_variables.ts +++ b/public/app/plugins/datasource/elasticsearch/pipeline_variables.ts @@ -22,6 +22,7 @@ const newVariable = index => { }; export class ElasticPipelineVariablesCtrl { + /** @ngInject */ constructor($scope) { $scope.variables = $scope.variables || [newVariable(1)]; 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 fc0b86d1b68..691247a0f75 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 @@ -43,6 +43,25 @@ describe('TimeRegionManager', () => { }); } + describe('When colors missing in config', () => { + plotOptionsScenario('should not throw an error when fillColor is undefined', ctx => { + const regions = [ + { fromDayOfWeek: 1, toDayOfWeek: 1, fill: true, line: true, lineColor: '#ffffff', colorMode: 'custom' }, + ]; + const from = moment('2018-01-01T00:00:00+01:00'); + const to = moment('2018-01-01T23:59:00+01:00'); + expect(() => ctx.setup(regions, from, to)).not.toThrow(); + }); + plotOptionsScenario('should not throw an error when lineColor is undefined', ctx => { + const regions = [ + { fromDayOfWeek: 1, toDayOfWeek: 1, fill: true, fillColor: '#ffffff', line: true, colorMode: 'custom' }, + ]; + const from = moment('2018-01-01T00:00:00+01:00'); + const to = moment('2018-01-01T23:59:00+01:00'); + expect(() => ctx.setup(regions, from, to)).not.toThrow(); + }); + }); + describe('When creating plot markings using local time', () => { plotOptionsScenario('for day of week region', ctx => { const regions = [{ fromDayOfWeek: 1, toDayOfWeek: 1, fill: true, line: true, colorMode: 'red' }]; diff --git a/public/app/plugins/panel/graph/time_region_manager.ts b/public/app/plugins/panel/graph/time_region_manager.ts index be5de722fe2..2917583ff36 100644 --- a/public/app/plugins/panel/graph/time_region_manager.ts +++ b/public/app/plugins/panel/graph/time_region_manager.ts @@ -50,8 +50,8 @@ function getColor(timeRegion, theme: GrafanaTheme): TimeRegionColorDefinition { if (timeRegion.colorMode === 'custom') { return { - fill: getColorFromHexRgbOrName(timeRegion.fillColor, theme), - line: getColorFromHexRgbOrName(timeRegion.lineColor, theme), + fill: timeRegion.fill && timeRegion.fillColor ? getColorFromHexRgbOrName(timeRegion.fillColor, theme) : null, + line: timeRegion.line && timeRegion.lineColor ? getColorFromHexRgbOrName(timeRegion.lineColor, theme) : null, }; } @@ -62,8 +62,8 @@ function getColor(timeRegion, theme: GrafanaTheme): TimeRegionColorDefinition { } return { - fill: getColorFromHexRgbOrName(colorMode.color.fill, theme), - line: getColorFromHexRgbOrName(colorMode.color.line, theme), + fill: timeRegion.fill ? getColorFromHexRgbOrName(colorMode.color.fill, theme) : null, + line: timeRegion.fill ? getColorFromHexRgbOrName(colorMode.color.line, theme) : null, }; }