Browse Source

Merge branch 'master' into solo-panel-rewrite

Torkel Ödegaard 6 năm trước cách đây
mục cha
commit
c4f55fecbe

+ 1 - 0
.circleci/config.yml

@@ -333,6 +333,7 @@ jobs:
     docker:
     - image: grafana/grafana-ci-deploy:1.2.0
     steps:
+      - checkout
       - attach_workspace:
          at: .
       - run:

+ 3 - 0
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

+ 1 - 1
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', () => ({

+ 17 - 0
packages/grafana-ui/src/types/series.ts → 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;
+}

+ 2 - 2
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 {

+ 1 - 1
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';

+ 6 - 1
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<T = any> {
   onInterpolate: InterpolateFunction;
 }
 
+export interface PanelData {
+  timeSeries?: TimeSeries[];
+  tableData?: TableData;
+}
+
 export interface PanelOptionsProps<T = any> {
   options: T;
   onChange: (options: T) => void;

+ 120 - 24
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("\n<b>Error message:</b> %s", evalContext.Error.Error())
 	}
-	if evalContext.ImagePublicUrl != "" {
-		message += fmt.Sprintf("\n<a href=\"%s\">Show graph image</a>", 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))
-	}
-	if this.Device != "" {
-		q.Add("device", this.Device)
-	}
-	if this.Sound != "default" {
-		q.Add("sound", this.Sound)
+	headers, uploadBody, err := this.genPushoverBody(evalContext, message, ruleUrl)
+	if err != nil {
+		this.log.Error("Failed to generate body for pushover", "error", err)
+		return err
 	}
-	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
+}

+ 1 - 1
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()

+ 2 - 2
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,

+ 1 - 1
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',

+ 30 - 5
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<Props, State> {
 
       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<Props, State> {
     }
   };
 
+  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<Props, State> {
             return (
               <>
                 {this.props.children({
-                  timeSeries,
                   loading,
+                  panelData,
                 })}
               </>
             );

+ 3 - 5
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<Props, State> {
                 scopedVars={panel.scopedVars}
                 links={panel.links}
               />
-
               {panel.snapshotData ? (
                 this.renderPanel(false, panel.snapshotData, width, height)
               ) : (
@@ -152,8 +150,8 @@ export class PanelChrome extends PureComponent<Props, State> {
                   refreshCounter={refreshCounter}
                   onDataResponse={this.onDataResponse}
                 >
-                  {({ loading, timeSeries }) => {
-                    return this.renderPanel(loading, timeSeries, width, height);
+                  {({ loading, panelData }) => {
+                    return this.renderPanel(loading, panelData.timeSeries, width, height);
                   }}
                 </DataPanel>
               )}

+ 0 - 11
public/app/features/dashboard/panel_editor/PanelEditor.tsx

@@ -101,17 +101,6 @@ export class PanelEditor extends PureComponent<PanelEditorProps> {
 
     return (
       <div className="panel-editor-container__editor">
-        {
-          // <div className="panel-editor__close">
-          //   <i className="fa fa-arrow-left" />
-          // </div>
-          // <div className="panel-editor-resizer">
-          //   <div className="panel-editor-resizer__handle">
-          //     <div className="panel-editor-resizer__handle-dots" />
-          //   </div>
-          // </div>
-        }
-
         <div className="panel-editor-tabs">
           {tabs.map(tab => {
             return <TabItem tab={tab} activeTab={activeTab} onClick={this.onChangeTab} key={tab.id} />;

+ 2 - 1
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;

+ 1 - 0
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)];
 

+ 19 - 0
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' }];

+ 4 - 4
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,
   };
 }