mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
feat(alerting): progress on email notifications
This commit is contained in:
parent
6cb1dafb1d
commit
0d9b98da6d
@ -1,31 +1,12 @@
|
||||
<!-- This email is sent when an existing user is added to an organization -->
|
||||
[[Subject .Subject "Grafana Alert: [[.Severity]] [[.RuleName]]"]]
|
||||
|
||||
[[Subject .Subject "Grafana Alert: [ [[.State]] ] [[.Name]]" ]]
|
||||
<br>
|
||||
<br>
|
||||
|
||||
Alertstate: [[.State]]<br />
|
||||
[[.AlertPageUrl]]<br />
|
||||
[[.DashboardLink]]<br />
|
||||
[[.Description]]<br />
|
||||
Alert rule: [[.RuleName]]<br>
|
||||
Alert state: [[.RuleState]]<br>
|
||||
|
||||
[[if eq .State "Ok"]]
|
||||
Everything is Ok
|
||||
[[end]]
|
||||
<a href="[[.RuleLink]]">Link to alert rule</a>
|
||||
|
||||
<img src="[[.DashboardImage]]" />
|
||||
<br>
|
||||
|
||||
[[if ne .State "Ok" ]]
|
||||
<table class="row">
|
||||
<tr>
|
||||
<td class="expander">Serie</td>
|
||||
<td class="expander">State</td>
|
||||
<td class="expander">Actual value</td>
|
||||
</tr>
|
||||
[[ range $ta := .TriggeredAlerts]]
|
||||
<tr>
|
||||
<td class="expander">[[$ta.Name]]</td>
|
||||
<td class="expander">[[$ta.State]]</td>
|
||||
<td class="expander">[[$ta.ActualValue]]</td>
|
||||
</tr>
|
||||
[[end]]
|
||||
</table>
|
||||
[[end]]
|
||||
|
@ -104,7 +104,7 @@ func AlertTest(c *middleware.Context, dto dtos.AlertTestCommand) Response {
|
||||
dtoRes.Logs = append(dtoRes.Logs, &dtos.AlertTestResultLog{Message: log.Message, Data: log.Data})
|
||||
}
|
||||
|
||||
dtoRes.Timing = fmt.Sprintf("%1.3fs", res.GetDurationSeconds())
|
||||
dtoRes.TimeMs = fmt.Sprintf("%1.3fms", res.GetDurationMs())
|
||||
|
||||
return Json(200, dtoRes)
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ type AlertTestCommand struct {
|
||||
|
||||
type AlertTestResult struct {
|
||||
Firing bool `json:"firing"`
|
||||
Timing string `json:"timing"`
|
||||
TimeMs string `json:"timeMs"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Logs []*AlertTestResultLog `json:"logs,omitempty"`
|
||||
}
|
||||
|
@ -116,7 +116,9 @@ func getFilters(filterStrArray []string) map[string]log15.Lvl {
|
||||
|
||||
for _, filterStr := range filterStrArray {
|
||||
parts := strings.Split(filterStr, ":")
|
||||
filterMap[parts[0]] = getLogLevelFromString(parts[1])
|
||||
if len(parts) > 1 {
|
||||
filterMap[parts[0]] = getLogLevelFromString(parts[1])
|
||||
}
|
||||
}
|
||||
|
||||
return filterMap
|
||||
|
@ -60,6 +60,8 @@ func NewAlertRuleFromDBModel(ruleDef *m.Alert) (*AlertRule, error) {
|
||||
model := &AlertRule{}
|
||||
model.Id = ruleDef.Id
|
||||
model.OrgId = ruleDef.OrgId
|
||||
model.DashboardId = ruleDef.DashboardId
|
||||
model.PanelId = ruleDef.PanelId
|
||||
model.Name = ruleDef.Name
|
||||
model.Description = ruleDef.Description
|
||||
model.Frequency = ruleDef.Frequency
|
||||
@ -67,8 +69,11 @@ func NewAlertRuleFromDBModel(ruleDef *m.Alert) (*AlertRule, error) {
|
||||
model.State = ruleDef.State
|
||||
|
||||
for _, v := range ruleDef.Settings.Get("notifications").MustArray() {
|
||||
if id, ok := v.(int64); ok {
|
||||
model.Notifications = append(model.Notifications, int64(id))
|
||||
jsonModel := simplejson.NewFromAny(v)
|
||||
if id, err := jsonModel.Get("id").Int64(); err != nil {
|
||||
return nil, AlertValidationError{Reason: "Invalid notification schema"}
|
||||
} else {
|
||||
model.Notifications = append(model.Notifications, id)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -49,8 +49,12 @@ func TestAlertRuleModel(t *testing.T) {
|
||||
},
|
||||
"reducer": {"type": "avg", "params": []},
|
||||
"evaluator": {"type": ">", "params": [100]}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"notifications": [
|
||||
{"id": 1134},
|
||||
{"id": 22}
|
||||
]
|
||||
}
|
||||
`
|
||||
|
||||
@ -91,6 +95,10 @@ func TestAlertRuleModel(t *testing.T) {
|
||||
So(evaluator.Type, ShouldEqual, ">")
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Can read notifications", func() {
|
||||
So(len(alertRule.Notifications), ShouldEqual, 2)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ type HandlerImpl struct {
|
||||
|
||||
func NewHandler() *HandlerImpl {
|
||||
return &HandlerImpl{
|
||||
log: log.New("alerting.executor"),
|
||||
log: log.New("alerting.handler"),
|
||||
alertJobTimeout: time.Second * 5,
|
||||
}
|
||||
}
|
||||
@ -33,7 +33,7 @@ func (e *HandlerImpl) Execute(context *AlertResultContext) {
|
||||
context.EndTime = time.Now()
|
||||
e.log.Debug("Job Execution timeout", "alertId", context.Rule.Id)
|
||||
case <-context.DoneChan:
|
||||
e.log.Debug("Job Execution done", "timing", context.GetDurationSeconds(), "alertId", context.Rule.Id, "firing", context.Firing)
|
||||
e.log.Debug("Job Execution done", "timeMs", context.GetDurationMs(), "alertId", context.Rule.Id, "firing", context.Firing)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -42,8 +42,8 @@ type AlertResultContext struct {
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func (a *AlertResultContext) GetDurationSeconds() float64 {
|
||||
return float64(a.EndTime.Nanosecond()-a.StartTime.Nanosecond()) / float64(1000000000)
|
||||
func (a *AlertResultContext) GetDurationMs() float64 {
|
||||
return float64(a.EndTime.Nanosecond()-a.StartTime.Nanosecond()) / float64(1000000)
|
||||
}
|
||||
|
||||
func NewAlertResultContext(rule *AlertRule) *AlertResultContext {
|
||||
|
@ -23,6 +23,8 @@ func NewRootNotifier() *RootNotifier {
|
||||
}
|
||||
|
||||
func (n *RootNotifier) Notify(context *AlertResultContext) {
|
||||
n.log.Info("Sending notifications for", "ruleId", context.Rule.Id)
|
||||
|
||||
notifiers, err := n.getNotifiers(context.Rule.OrgId, context.Rule.Notifications)
|
||||
if err != nil {
|
||||
n.log.Error("Failed to read notifications", "error", err)
|
||||
@ -70,20 +72,22 @@ type EmailNotifier struct {
|
||||
}
|
||||
|
||||
func (this *EmailNotifier) Notify(context *AlertResultContext) {
|
||||
this.log.Info("Sending alert notification to %v", this.Addresses)
|
||||
this.log.Info("Sending alert notification to", "addresses", this.Addresses)
|
||||
|
||||
slugQuery := &m.GetDashboardSlugByIdQuery{Id: context.Rule.DashboardId}
|
||||
if err := bus.Dispatch(slugQuery); err != nil {
|
||||
this.log.Error("Failed to load dashboard", "error", err)
|
||||
return
|
||||
}
|
||||
dashboardSlug := slugQuery.Result
|
||||
|
||||
ruleLink := fmt.Sprintf("%sdashboard/db/%s?fullscreen&edit&tab=alert&panelId=%d", setting.AppUrl, slugQuery.Result, context.Rule.PanelId)
|
||||
|
||||
cmd := &m.SendEmailCommand{
|
||||
Data: map[string]interface{}{
|
||||
"RuleName": context.Rule.Name,
|
||||
"Severity": context.Rule.Severity,
|
||||
"RuleLink": setting.ToAbsUrl("dashboard/db/" + dashboardSlug),
|
||||
"RuleState": context.Rule.State,
|
||||
"RuleName": context.Rule.Name,
|
||||
"Severity": context.Rule.Severity,
|
||||
"RuleLink": ruleLink,
|
||||
},
|
||||
To: this.Addresses,
|
||||
Template: "alert_notification.html",
|
||||
@ -91,7 +95,7 @@ func (this *EmailNotifier) Notify(context *AlertResultContext) {
|
||||
|
||||
err := bus.Dispatch(cmd)
|
||||
if err != nil {
|
||||
this.log.Error("Failed tosend alert notification email", "error", err)
|
||||
this.log.Error("Failed to send alert notification email", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -18,10 +18,13 @@ type AlertRuleReader struct {
|
||||
serverID string
|
||||
serverPosition int
|
||||
clusterSize int
|
||||
log log.Logger
|
||||
}
|
||||
|
||||
func NewRuleReader() *AlertRuleReader {
|
||||
ruleReader := &AlertRuleReader{}
|
||||
ruleReader := &AlertRuleReader{
|
||||
log: log.New("alerting.ruleReader"),
|
||||
}
|
||||
|
||||
go ruleReader.initReader()
|
||||
return ruleReader
|
||||
@ -40,17 +43,19 @@ func (arr *AlertRuleReader) initReader() {
|
||||
|
||||
func (arr *AlertRuleReader) Fetch() []*AlertRule {
|
||||
cmd := &m.GetAllAlertsQuery{}
|
||||
err := bus.Dispatch(cmd)
|
||||
|
||||
if err != nil {
|
||||
log.Error(1, "Alerting: ruleReader.fetch(): Could not load alerts", err)
|
||||
if err := bus.Dispatch(cmd); err != nil {
|
||||
arr.log.Error("Could not load alerts", "error", err)
|
||||
return []*AlertRule{}
|
||||
}
|
||||
|
||||
res := make([]*AlertRule, len(cmd.Result))
|
||||
for i, ruleDef := range cmd.Result {
|
||||
model, _ := NewAlertRuleFromDBModel(ruleDef)
|
||||
res[i] = model
|
||||
res := make([]*AlertRule, 0)
|
||||
for _, ruleDef := range cmd.Result {
|
||||
if model, err := NewAlertRuleFromDBModel(ruleDef); err != nil {
|
||||
arr.log.Error("Could not build alert model for rule", "ruleId", ruleDef.Id, "error", err)
|
||||
} else {
|
||||
res = append(res, model)
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
|
@ -17,7 +17,8 @@ type ResultHandlerImpl struct {
|
||||
|
||||
func NewResultHandler() *ResultHandlerImpl {
|
||||
return &ResultHandlerImpl{
|
||||
log: log.New("alerting.resultHandler"),
|
||||
log: log.New("alerting.resultHandler"),
|
||||
notifier: NewRootNotifier(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -47,7 +48,6 @@ func (handler *ResultHandlerImpl) Handle(result *AlertResultContext) {
|
||||
}
|
||||
|
||||
result.Rule.State = newState
|
||||
//handler.log.Debug("will notify about new state", "new state", result.State)
|
||||
//handler.notifier.Notify(result)
|
||||
handler.notifier.Notify(result)
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package sqlstore
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-xorm/xorm"
|
||||
@ -63,8 +64,10 @@ func getAlertNotifications(query *m.GetAlertNotificationsQuery, sess *xorm.Sessi
|
||||
}
|
||||
|
||||
if len(query.Ids) > 0 {
|
||||
sql.WriteString(` AND alert_notification.id IN (?)`)
|
||||
params = append(params, query.Ids)
|
||||
sql.WriteString(` AND alert_notification.id IN (?` + strings.Repeat(",?", len(query.Ids)-1) + ")")
|
||||
for _, v := range query.Ids {
|
||||
params = append(params, v)
|
||||
}
|
||||
}
|
||||
|
||||
results := make([]*m.AlertNotification, 0)
|
||||
|
@ -115,6 +115,11 @@ function (angular, _, $) {
|
||||
}
|
||||
}
|
||||
|
||||
// if no edit state cleanup tab parm
|
||||
if (!this.state.edit) {
|
||||
delete this.state.tab;
|
||||
}
|
||||
|
||||
$location.search(this.serializeToUrl());
|
||||
this.syncState();
|
||||
};
|
||||
|
@ -95,10 +95,10 @@ export class PanelCtrl {
|
||||
this.editModeInitiated = true;
|
||||
this.events.emit('init-edit-mode', null);
|
||||
|
||||
var routeParams = this.$injector.get('$routeParams');
|
||||
if (routeParams.editorTab) {
|
||||
var urlTab = (this.$injector.get('$routeParams').tab || '').toLowerCase();
|
||||
if (urlTab) {
|
||||
this.editorTabs.forEach((tab, i) => {
|
||||
if (tab.title === routeParams.editorTab) {
|
||||
if (tab.title.toLowerCase() === urlTab) {
|
||||
this.editorTabIndex = i;
|
||||
}
|
||||
});
|
||||
@ -109,7 +109,7 @@ export class PanelCtrl {
|
||||
this.editorTabIndex = newIndex;
|
||||
var route = this.$injector.get('$route');
|
||||
|
||||
route.current.params.editorTab = this.editorTabs[newIndex].title;
|
||||
route.current.params.tab = this.editorTabs[newIndex].title.toLowerCase();
|
||||
route.updateParams();
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,6 @@
|
||||
///<reference path="../../../headers/common.d.ts" />
|
||||
|
||||
import _ from 'lodash';
|
||||
import $ from 'jquery';
|
||||
import angular from 'angular';
|
||||
|
||||
import {
|
||||
QueryPartDef,
|
||||
@ -28,7 +26,6 @@ var reducerAvgDef = new QueryPartDef({
|
||||
export class AlertTabCtrl {
|
||||
panel: any;
|
||||
panelCtrl: any;
|
||||
metricTargets;
|
||||
testing: boolean;
|
||||
testResult: any;
|
||||
|
||||
@ -50,37 +47,57 @@ export class AlertTabCtrl {
|
||||
{text: 'Warning', value: 'warning'},
|
||||
];
|
||||
addNotificationSegment;
|
||||
notifications;
|
||||
alertNotifications;
|
||||
|
||||
/** @ngInject */
|
||||
constructor($scope, private $timeout, private backendSrv, private dashboardSrv, private uiSegmentSrv) {
|
||||
constructor(private $scope, private $timeout, private backendSrv, private dashboardSrv, private uiSegmentSrv) {
|
||||
this.panelCtrl = $scope.ctrl;
|
||||
this.panel = this.panelCtrl.panel;
|
||||
$scope.ctrl = this;
|
||||
this.$scope.ctrl = this;
|
||||
}
|
||||
|
||||
this.metricTargets = this.panel.targets.map(val => val);
|
||||
this.addNotificationSegment = uiSegmentSrv.newPlusButton();
|
||||
$onInit() {
|
||||
this.addNotificationSegment = this.uiSegmentSrv.newPlusButton();
|
||||
|
||||
this.initModel();
|
||||
|
||||
// set panel alert edit mode
|
||||
$scope.$on("$destroy", () => {
|
||||
this.$scope.$on("$destroy", () => {
|
||||
this.panelCtrl.editingAlert = false;
|
||||
this.panelCtrl.render();
|
||||
});
|
||||
}
|
||||
|
||||
getNotifications() {
|
||||
// build notification model
|
||||
this.notifications = [];
|
||||
this.alertNotifications = [];
|
||||
|
||||
return this.backendSrv.get('/api/alert-notifications').then(res => {
|
||||
return res.map(item => {
|
||||
return this.uiSegmentSrv.newSegment(item.name);
|
||||
this.notifications = res;
|
||||
|
||||
_.each(this.alert.notifications, item => {
|
||||
var model = _.findWhere(this.notifications, {id: item.id});
|
||||
if (model) {
|
||||
this.alertNotifications.push(model);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getNotifications() {
|
||||
return Promise.resolve(this.notifications.map(item => {
|
||||
return this.uiSegmentSrv.newSegment(item.name);
|
||||
}));
|
||||
}
|
||||
|
||||
notificationAdded() {
|
||||
this.alert.notifications.push({
|
||||
name: this.addNotificationSegment.value
|
||||
});
|
||||
var model = _.findWhere(this.notifications, {name: this.addNotificationSegment.value});
|
||||
if (!model) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.alertNotifications.push({name: model.name});
|
||||
this.alert.notifications.push({id: model.id});
|
||||
|
||||
// reset plus button
|
||||
this.addNotificationSegment.value = this.uiSegmentSrv.newPlusButton().value;
|
||||
@ -89,6 +106,7 @@ export class AlertTabCtrl {
|
||||
|
||||
removeNotification(index) {
|
||||
this.alert.notifications.splice(index, 1);
|
||||
this.alertNotifications.splice(index, 1);
|
||||
}
|
||||
|
||||
initModel() {
|
||||
|
@ -132,7 +132,7 @@ class GraphCtrl extends MetricsPanelCtrl {
|
||||
this.addEditorTab('Display', 'public/app/plugins/panel/graph/tab_display.html', 4);
|
||||
|
||||
if (config.alertingEnabled) {
|
||||
this.addEditorTab('Alerting', graphAlertEditor, 5);
|
||||
this.addEditorTab('Alert', graphAlertEditor, 5);
|
||||
}
|
||||
|
||||
this.logScales = {
|
||||
|
@ -99,7 +99,7 @@
|
||||
<h5 class="section-heading">Notifications</h5>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label" ng-repeat="nc in ctrl.alert.notifications">
|
||||
<span class="gf-form-label" ng-repeat="nc in ctrl.alertNotifications">
|
||||
{{nc.name}}
|
||||
<i class="fa fa-remove pointer" ng-click="ctrl.removeNotification($index)"></i>
|
||||
</span>
|
||||
|
@ -113,37 +113,18 @@ color: #FFFFFF !important;
|
||||
<table class="container" style="border-collapse: collapse; border-spacing: 0; margin: 0 auto; padding: 0; text-align: inherit; vertical-align: top; width: 580px">
|
||||
<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
|
||||
<td style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: left; vertical-align: top; word-break: break-word" align="left" valign="top">
|
||||
|
||||
{{Subject .Subject "Grafana Alert: {{.Severity}} {{.RuleName}}"}}
|
||||
|
||||
{{Subject .Subject "Grafana Alert: [ {{.State}} ] {{.Name}}" }}
|
||||
<br />
|
||||
<br />
|
||||
|
||||
Alertstate: {{.State}}<br />
|
||||
{{.AlertPageUrl}}<br />
|
||||
{{.DashboardLink}}<br />
|
||||
{{.Description}}<br />
|
||||
Alert rule: {{.RuleName}}<br />
|
||||
Alert state: {{.RuleState}}<br />
|
||||
|
||||
{{if eq .State "Ok"}}
|
||||
Everything is Ok
|
||||
{{end}}
|
||||
<a href="{{.RuleLink}}" style="color: #E67612; text-decoration: none">Link to alert rule</a>
|
||||
|
||||
{{if ne .State "Ok" }}
|
||||
<img src="{{.DashboardImage}}" style="-ms-interpolation-mode: bicubic; clear: both; display: block; float: left; max-width: 100%; outline: none; text-decoration: none; width: auto" align="left" />
|
||||
<br />
|
||||
|
||||
<table class="row" style="border-collapse: collapse; border-spacing: 0; display: block; padding: 0px; position: relative; text-align: left; vertical-align: top; width: 100%">
|
||||
<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
|
||||
<td class="expander" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: left; vertical-align: top; visibility: hidden; width: 0px; word-break: break-word" align="left" valign="top">Serie</td>
|
||||
<td class="expander" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: left; vertical-align: top; visibility: hidden; width: 0px; word-break: break-word" align="left" valign="top">State</td>
|
||||
<td class="expander" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: left; vertical-align: top; visibility: hidden; width: 0px; word-break: break-word" align="left" valign="top">Actual value</td>
|
||||
</tr>
|
||||
{{ range $ta := .TriggeredAlerts}}
|
||||
<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
|
||||
<td class="expander" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: left; vertical-align: top; visibility: hidden; width: 0px; word-break: break-word" align="left" valign="top">{{$ta.Name}}</td>
|
||||
<td class="expander" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: left; vertical-align: top; visibility: hidden; width: 0px; word-break: break-word" align="left" valign="top">{{$ta.State}}</td>
|
||||
<td class="expander" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0; text-align: left; vertical-align: top; visibility: hidden; width: 0px; word-break: break-word" align="left" valign="top">{{$ta.ActualValue}}</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
{{end}}
|
||||
|
||||
|
||||
<table class="row footer" style="border-collapse: collapse; border-spacing: 0; display: block; margin-top: 20px; padding: 0px; position: relative; text-align: left; vertical-align: top; width: 100%">
|
||||
|
@ -149,7 +149,7 @@ color: #FFFFFF !important;
|
||||
<td class="center" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0px 0px 10px; text-align: center; vertical-align: top; word-break: break-word" align="center" valign="top">
|
||||
<table class="better-button" align="center" border="0" cellspacing="0" cellpadding="0" style="border-collapse: collapse; border-spacing: 0; margin-bottom: 20px; margin-top: 10px; padding: 0; text-align: left; vertical-align: top">
|
||||
<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
|
||||
<td align="center" class="better-button" bgcolor="#ff8f2b" style="-moz-border-radius: 2px; -moz-hyphens: auto; -webkit-border-radius: 2px; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; border-radius: 2px; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0px; text-align: left; vertical-align: top; word-break: break-word" valign="top"><a href="{{.AppUrl}}" target="_blank" style="-moz-border-radius: 2px; -webkit-border-radius: 2px; border-radius: 2px; border: 1px solid #ff8f2b; color: #FFF; display: inline-block; padding: 12px 25px; text-decoration: none">Log in now</a></td>
|
||||
<td align="center" class="better-button" bgcolor="#ff8f2b" style="-moz-border-radius: 2px; -moz-hyphens: auto; -webkit-border-radius: 2px; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; border-radius: 2px; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0px; text-align: left; vertical-align: top; word-break: break-word" valign="top"><a href="{{.AppUrl}}" target="_blank" style="-moz-border-radius: 2px; -webkit-border-radius: 2px; border: 1px solid #ff8f2b; border-radius: 2px; color: #FFF; display: inline-block; padding: 12px 25px; text-decoration: none">Log in now</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
|
@ -147,7 +147,7 @@ color: #FFFFFF !important;
|
||||
<td class="center" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0px 0px 10px; text-align: center; vertical-align: top; word-break: break-word" align="center" valign="top">
|
||||
<table class="better-button" align="center" border="0" cellspacing="0" cellpadding="0" style="border-collapse: collapse; border-spacing: 0; margin-bottom: 20px; margin-top: 10px; padding: 0; text-align: left; vertical-align: top">
|
||||
<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
|
||||
<td align="center" class="better-button" bgcolor="#ff8f2b" style="-moz-border-radius: 2px; -moz-hyphens: auto; -webkit-border-radius: 2px; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; border-radius: 2px; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0px; text-align: left; vertical-align: top; word-break: break-word" valign="top"><a href="{{.LinkUrl}}" target="_blank" style="-moz-border-radius: 2px; -webkit-border-radius: 2px; border-radius: 2px; border: 1px solid #ff8f2b; color: #FFF; display: inline-block; padding: 12px 25px; text-decoration: none">Accept Invitation</a></td>
|
||||
<td align="center" class="better-button" bgcolor="#ff8f2b" style="-moz-border-radius: 2px; -moz-hyphens: auto; -webkit-border-radius: 2px; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; border-radius: 2px; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0px; text-align: left; vertical-align: top; word-break: break-word" valign="top"><a href="{{.LinkUrl}}" target="_blank" style="-moz-border-radius: 2px; -webkit-border-radius: 2px; border: 1px solid #ff8f2b; border-radius: 2px; color: #FFF; display: inline-block; padding: 12px 25px; text-decoration: none">Accept Invitation</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
|
@ -148,7 +148,7 @@ color: #FFFFFF !important;
|
||||
<td class="center" style="-moz-hyphens: auto; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0px 0px 10px; text-align: center; vertical-align: top; word-break: break-word" align="center" valign="top">
|
||||
<table class="better-button" align="center" border="0" cellspacing="0" cellpadding="0" style="border-collapse: collapse; border-spacing: 0; margin-bottom: 20px; margin-top: 10px; padding: 0; text-align: left; vertical-align: top">
|
||||
<tr style="padding: 0; text-align: left; vertical-align: top" align="left">
|
||||
<td align="center" class="better-button" bgcolor="#ff8f2b" style="-moz-border-radius: 2px; -moz-hyphens: auto; -webkit-border-radius: 2px; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; border-radius: 2px; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0px; text-align: left; vertical-align: top; word-break: break-word" valign="top"><a href="{{.SignUpUrl}}" target="_blank" style="-moz-border-radius: 2px; -webkit-border-radius: 2px; border-radius: 2px; border: 1px solid #ff8f2b; color: #FFF; display: inline-block; padding: 12px 25px; text-decoration: none">Complete Sign Up</a></td>
|
||||
<td align="center" class="better-button" bgcolor="#ff8f2b" style="-moz-border-radius: 2px; -moz-hyphens: auto; -webkit-border-radius: 2px; -webkit-font-smoothing: antialiased; -webkit-hyphens: auto; -webkit-text-size-adjust: none; border-collapse: collapse !important; border-radius: 2px; color: #222222; font-family: 'Open Sans', 'Helvetica Neue', 'Helvetica', Helvetica, Arial, sans-serif; font-size: 14px; font-weight: normal; hyphens: auto; line-height: 19px; margin: 0; padding: 0px; text-align: left; vertical-align: top; word-break: break-word" valign="top"><a href="{{.SignUpUrl}}" target="_blank" style="-moz-border-radius: 2px; -webkit-border-radius: 2px; border: 1px solid #ff8f2b; border-radius: 2px; color: #FFF; display: inline-block; padding: 12px 25px; text-decoration: none">Complete Sign Up</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
|
Loading…
Reference in New Issue
Block a user