Merge branch 'master' into memberlist

This commit is contained in:
Stuart Nelson 2018-02-13 10:47:17 +01:00
commit a552afd998
18 changed files with 395 additions and 173 deletions

View File

@ -1,3 +1,20 @@
## 0.14.0 / 2018-02-12
* [ENHANCEMENT] [amtool] Silence update support dwy suffixes to expire flag (#1197)
* [ENHANCEMENT] Allow templating PagerDuty receiver severity (#1214)
* [ENHANCEMENT] Include receiver name in failed notifications log messages (#1207)
* [ENHANCEMENT] Allow global opsgenie api key (#1208)
* [ENHANCEMENT] Add mesh metrics (#1225)
* [ENHANCEMENT] Add Class field to PagerDuty; add templating to PagerDuty-CEF fields (#1231)
* [BUGFIX] Don't notify of resolved alerts if none were reported firing (#1198)
* [BUGFIX] Notify only when new firing alerts are added (#1205)
* [BUGFIX] [mesh] Fix pending connections never set to established (#1204)
* [BUGFIX] Allow OpsGenie notifier to have empty team fields (#1224)
* [BUGFIX] Don't count alerts with EndTime in the future as resolved (#1233)
* [BUGFIX] Speed up re-rendering of Silence UI (#1235)
* [BUGFIX] Forbid 0 value for group_interval and repeat_interval (#1230)
* [BUGFIX] Fix WeChat agentid issue (#1229)
## 0.13.0 / 2018-01-12
* [CHANGE] Switch cmd/alertmanager to kingpin (#974)

View File

@ -1 +1 @@
0.13.0
0.14.0

View File

@ -473,7 +473,8 @@ func (api *API) insertAlerts(w http.ResponseWriter, r *http.Request, alerts ...*
if alert.EndsAt.IsZero() {
alert.Timeout = true
alert.EndsAt = now.Add(resolveTimeout)
}
if alert.EndsAt.After(time.Now()) {
numReceivedAlerts.WithLabelValues("firing").Inc()
} else {
numReceivedAlerts.WithLabelValues("resolved").Inc()

View File

@ -243,24 +243,27 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
}
}
for _, wcc := range rcv.WechatConfigs {
wcc.APIURL = c.Global.WeChatAPIURL
if wcc.APIURL == "" {
if c.Global.WeChatAPIURL == "" {
return fmt.Errorf("no global Wechat URL set")
}
wcc.APIURL = c.Global.WeChatAPIURL
}
wcc.APISecret = c.Global.WeChatAPISecret
if wcc.APISecret == "" {
if c.Global.WeChatAPISecret == "" {
return fmt.Errorf("no global Wechat ApiSecret set")
}
wcc.APISecret = c.Global.WeChatAPISecret
}
if wcc.CorpID == "" {
if c.Global.WeChatAPICorpID == "" {
return fmt.Errorf("no global Wechat CorpID set")
}
wcc.CorpID = c.Global.WeChatAPICorpID
}
if !strings.HasSuffix(wcc.APIURL, "/") {
wcc.APIURL += "/"
}
@ -421,6 +424,13 @@ func (r *Route) UnmarshalYAML(unmarshal func(interface{}) error) error {
groupBy[ln] = struct{}{}
}
if r.GroupInterval != nil && time.Duration(*r.GroupInterval) == time.Duration(0) {
return fmt.Errorf("group_interval cannot be zero")
}
if r.RepeatInterval != nil && time.Duration(*r.RepeatInterval) == time.Duration(0) {
return fmt.Errorf("repeat_interval cannot be zero")
}
return checkOverflow(r.XXX, "route")
}

View File

@ -73,7 +73,7 @@ receivers:
expected := "notification config name \"team-X\" is not unique"
if err == nil {
t.Fatalf("no error returned, expeceted:\n%q", expected)
t.Fatalf("no error returned, expected:\n%q", expected)
}
if err.Error() != expected {
t.Errorf("\nexpected:\n%q\ngot:\n%q", expected, err.Error())
@ -94,7 +94,7 @@ receivers:
expected := "undefined receiver \"team-X\" used in route"
if err == nil {
t.Fatalf("no error returned, expeceted:\n%q", expected)
t.Fatalf("no error returned, expected:\n%q", expected)
}
if err.Error() != expected {
t.Errorf("\nexpected:\n%q\ngot:\n%q", expected, err.Error())
@ -114,7 +114,7 @@ receivers:
expected := "missing name in receiver"
if err == nil {
t.Fatalf("no error returned, expeceted:\n%q", expected)
t.Fatalf("no error returned, expected:\n%q", expected)
}
if err.Error() != expected {
t.Errorf("\nexpected:\n%q\ngot:\n%q", expected, err.Error())
@ -135,7 +135,7 @@ receivers:
expected := "duplicated label \"cluster\" in group_by"
if err == nil {
t.Fatalf("no error returned, expeceted:\n%q", expected)
t.Fatalf("no error returned, expected:\n%q", expected)
}
if err.Error() != expected {
t.Errorf("\nexpected:\n%q\ngot:\n%q", expected, err.Error())
@ -153,7 +153,7 @@ receivers:
expected := "no routes provided"
if err == nil {
t.Fatalf("no error returned, expeceted:\n%q", expected)
t.Fatalf("no error returned, expected:\n%q", expected)
}
if err.Error() != expected {
t.Errorf("\nexpected:\n%q\ngot:\n%q", expected, err.Error())
@ -176,7 +176,7 @@ receivers:
expected := "root route must not have any matchers"
if err == nil {
t.Fatalf("no error returned, expeceted:\n%q", expected)
t.Fatalf("no error returned, expected:\n%q", expected)
}
if err.Error() != expected {
t.Errorf("\nexpected:\n%q\ngot:\n%q", expected, err.Error())
@ -198,7 +198,7 @@ receivers:
expected := "cannot have continue in root route"
if err == nil {
t.Fatalf("no error returned, expeceted:\n%q", expected)
t.Fatalf("no error returned, expected:\n%q", expected)
}
if err.Error() != expected {
t.Errorf("\nexpected:\n%q\ngot:\n%q", expected, err.Error())
@ -206,6 +206,48 @@ receivers:
}
func TestGroupIntervalIsGreaterThanZero(t *testing.T) {
in := `
route:
receiver: team-X-mails
group_interval: 0s
receivers:
- name: 'team-X-mails'
`
_, err := Load(in)
expected := "group_interval cannot be zero"
if err == nil {
t.Fatalf("no error returned, expected:\n%q", expected)
}
if err.Error() != expected {
t.Errorf("\nexpected:\n%q\ngot:\n%q", expected, err.Error())
}
}
func TestRepeatIntervalIsGreaterThanZero(t *testing.T) {
in := `
route:
receiver: team-X-mails
repeat_interval: 0s
receivers:
- name: 'team-X-mails'
`
_, err := Load(in)
expected := "repeat_interval cannot be zero"
if err == nil {
t.Fatalf("no error returned, expected:\n%q", expected)
}
if err.Error() != expected {
t.Errorf("\nexpected:\n%q\ngot:\n%q", expected, err.Error())
}
}
func TestHideConfigSecrets(t *testing.T) {
c, _, err := LoadFile("testdata/conf.good.yml")
if err != nil {

View File

@ -102,13 +102,11 @@ var (
VSendResolved: true,
},
Message: `{{ template "wechat.default.message" . }}`,
APIURL: `{{ template "wechat.default.api_url" . }}`,
APISecret: `{{ template "wechat.default.api_secret" . }}`,
ToUser: `{{ template "wechat.default.to_user" . }}`,
ToParty: `{{ template "wechat.default.to_party" . }}`,
ToTag: `{{ template "wechat.default.to_tag" . }}`,
AgentID: `{{ template "wechat.default.agent_id" . }}`,
// TODO: Add a details field with all the alerts.
}
// DefaultVictorOpsConfig defines default values for VictorOps configurations.
@ -203,6 +201,7 @@ type PagerdutyConfig struct {
Description string `yaml:"description,omitempty" json:"description,omitempty"`
Details map[string]string `yaml:"details,omitempty" json:"details,omitempty"`
Severity string `yaml:"severity,omitempty" json:"severity,omitempty"`
Class string `yaml:"class,omitempty" json:"class,omitempty"`
Component string `yaml:"component,omitempty" json:"component,omitempty"`
Group string `yaml:"group,omitempty" json:"group,omitempty"`

View File

@ -129,6 +129,7 @@ api_secret: ''
}
func TestWechatCorpIDIsPresent(t *testing.T) {
in := `
api_secret: 'api_secret'
corp_id: ''
`
var cfg WechatConfig

View File

@ -460,6 +460,7 @@ type pagerDutyPayload struct {
Source string `json:"source"`
Severity string `json:"severity"`
Timestamp string `json:"timestamp,omitempty"`
Class string `json:"class,omitempty"`
Component string `json:"component,omitempty"`
Group string `json:"group,omitempty"`
CustomDetails map[string]string `json:"custom_details,omitempty"`
@ -508,8 +509,9 @@ func (n *PagerDuty) notifyV2(ctx context.Context, eventType, key string, tmpl fu
Source: tmpl(n.conf.Client),
Severity: tmpl(n.conf.Severity),
CustomDetails: details,
Component: n.conf.Component,
Group: n.conf.Group,
Class: tmpl(n.conf.Class),
Component: tmpl(n.conf.Component),
Group: tmpl(n.conf.Group),
}
}
@ -797,36 +799,33 @@ type Wechat struct {
conf *config.WechatConfig
tmpl *template.Template
logger log.Logger
accessToken string
accessTokenAt time.Time
}
// Wechat AccessToken with corpid and corpsecret.
type WechatToken struct {
AccessToken string `json:"access_token"`
// Catches all undefined fields and must be empty after parsing.
XXX map[string]interface{} `json:"-"`
}
type weChatMessage struct {
Text weChatMessageContent `yaml:"text,omitempty" json:"text,omitempty"`
ToUser string `yaml:"touser,omitempty" json:"touser,omitempty"`
ToParty string `yaml:"toparty,omitempty" json:"toparty,omitempty"`
Totag string `yaml:"totag,omitempty" json:"totag,omitempty"`
AgentID string `yaml:"agentid,omitempty" json:"agentid,omitempty"`
Safe string `yaml:"safe,omitempty" json:"safe,omitempty"`
Type string `yaml:"msgtype,omitempty" json:"msgtype,omitempty"`
}
type weChatMessageContent struct {
Content string `json:"content"`
}
type weChatCreateMessage struct {
Text weChatMessage `yaml:"text,omitempty" json:"text,omitempty"`
ToUser string `yaml:"touser,omitempty" json:"touser,omitempty"`
ToParty string `yaml:"toparty,omitempty" json:"toparty,omitempty"`
Totag string `yaml:"totag,omitempty" json:"totag,omitempty"`
AgentID string `yaml:"agentid,omitempty" json:"agentid,omitempty"`
Safe string `yaml:"safe,omitempty" json:"safe,omitempty"`
Type string `yaml:"msgtype,omitempty" json:"msgtype,omitempty"`
}
type weChatCloseMessage struct {
Text weChatMessage `yaml:"text,omitempty" json:"text,omitempty"`
ToUser string `yaml:"touser,omitempty" json:"touser,omitempty"`
ToParty string `yaml:"toparty,omitempty" json:"toparty,omitempty"`
Totag string `yaml:"totag,omitempty" json:"totag,omitempty"`
AgentID string `yaml:"agentid,omitempty" json:"agentid,omitempty"`
Safe string `yaml:"safe,omitempty" json:"safe,omitempty"`
Type string `yaml:"msgtype,omitempty" json:"msgtype,omitempty"`
}
type weChatErrorResponse struct {
type weChatResponse struct {
Code int `json:"code"`
Error string `json:"error"`
}
@ -842,84 +841,114 @@ func (n *Wechat) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
if !ok {
return false, fmt.Errorf("group key missing")
}
data := n.tmpl.Data(receiverName(ctx, n.logger), groupLabels(ctx, n.logger), as...)
level.Debug(n.logger).Log("msg", "Notifying Wechat", "incident", key)
data := n.tmpl.Data(receiverName(ctx, n.logger), groupLabels(ctx, n.logger), as...)
var err error
tmpl := tmplText(n.tmpl, data, &err)
if err != nil {
return false, err
}
var (
msg interface{}
apiURL string
apiMsg = weChatMessage{
// Refresh AccessToken over 2 hours
if n.accessToken == "" || time.Now().Sub(n.accessTokenAt) > 2*time.Hour {
parameters := url.Values{}
parameters.Add("corpsecret", tmpl(string(n.conf.APISecret)))
parameters.Add("corpid", tmpl(string(n.conf.CorpID)))
apiURL := n.conf.APIURL + "gettoken"
u, err := url.Parse(apiURL)
if err != nil {
return false, err
}
u.RawQuery = parameters.Encode()
level.Debug(n.logger).Log("msg", "Sending Wechat message", "incident", key, "url", u.String())
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
if err != nil {
return true, err
}
req.Header.Set("Content-Type", contentTypeJSON)
resp, err := http.DefaultClient.Do(req.WithContext(ctx))
if err != nil {
return true, err
}
defer resp.Body.Close()
var wechatToken WechatToken
if err := json.NewDecoder(resp.Body).Decode(&wechatToken); err != nil {
return false, err
}
if wechatToken.AccessToken == "" {
return false, fmt.Errorf("invalid APISecret for CorpID: %s", n.conf.CorpID)
}
// Cache accessToken
n.accessToken = wechatToken.AccessToken
n.accessTokenAt = time.Now()
}
msg := &weChatMessage{
Text: weChatMessageContent{
Content: tmpl(n.conf.Message),
}
alerts = types.Alerts(as...)
)
parameters := url.Values{}
parameters.Add("corpsecret", tmpl(string(n.conf.APISecret)))
parameters.Add("corpid", tmpl(string(n.conf.CorpID)))
apiURL = n.conf.APIURL + "gettoken"
u, err := url.Parse(apiURL)
if err != nil {
return false, err
}
u.RawQuery = parameters.Encode()
level.Debug(n.logger).Log("msg", "Sending Wechat message", "incident", key, "url", u.String())
resp, err := ctxhttp.Get(ctx, http.DefaultClient, u.String())
if err != nil {
return true, err
}
defer resp.Body.Close()
var wechatToken WechatToken
if err := json.NewDecoder(resp.Body).Decode(&wechatToken); err != nil {
return false, err
}
postMessageURL := n.conf.APIURL + "message/send?access_token=" + wechatToken.AccessToken
switch alerts.Status() {
case model.AlertResolved:
msg = &weChatCloseMessage{Text: apiMsg,
ToUser: tmpl(n.conf.ToUser),
ToParty: tmpl(n.conf.ToParty),
Totag: tmpl(n.conf.ToTag),
AgentID: tmpl(n.conf.AgentID),
Type: "text",
Safe: "0"}
default:
msg = &weChatCreateMessage{
Text: weChatMessage{
Content: tmpl(n.conf.Message),
},
ToUser: tmpl(n.conf.ToUser),
ToParty: tmpl(n.conf.ToParty),
Totag: tmpl(n.conf.ToTag),
AgentID: tmpl(n.conf.AgentID),
Type: "text",
Safe: "0",
}
},
ToUser: n.conf.ToUser,
ToParty: n.conf.ToParty,
Totag: n.conf.ToTag,
AgentID: n.conf.AgentID,
Type: "text",
Safe: "0",
}
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(msg); err != nil {
return false, err
}
resp, err = ctxhttp.Post(ctx, http.DefaultClient, postMessageURL, contentTypeJSON, &buf)
postMessageURL := n.conf.APIURL + "message/send?access_token=" + n.accessToken
req, err := http.NewRequest(http.MethodPost, postMessageURL, &buf)
if err != nil {
return true, err
}
resp, err := http.DefaultClient.Do(req.WithContext(ctx))
if err != nil {
return true, err
}
defer resp.Body.Close()
body, _ := ioutil.ReadAll(resp.Body)
level.Debug(n.logger).Log("msg", "response: "+string(body), "incident", key)
defer resp.Body.Close()
return n.retry(resp.StatusCode)
}
func (n *Wechat) retry(statusCode int) (bool, error) {
// https://work.weixin.qq.com/api/doc#10649
if statusCode/100 == 5 || statusCode == 429 {
return true, fmt.Errorf("unexpected status code %v", statusCode)
} else if statusCode/100 != 2 {
return false, fmt.Errorf("unexpected status code %v", statusCode)
}
return false, nil
if resp.StatusCode != 200 {
return true, fmt.Errorf("unexpected status code %v", resp.StatusCode)
} else {
var weResp weChatResponse
if err := json.Unmarshal(body, &weResp); err != nil {
return true, err
}
// https://work.weixin.qq.com/api/doc#10649
if weResp.Code == 0 {
return false, nil
}
// AccessToken is expired
if weResp.Code == 42001 {
n.accessToken = ""
return true, errors.New(weResp.Error)
}
return false, errors.New(weResp.Error)
}
}
// OpsGenie implements a Notifier for OpsGenie notifications.

View File

@ -10,12 +10,13 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/net/context"
"github.com/prometheus/common/model"
"github.com/prometheus/alertmanager/types"
"io/ioutil"
"net/url"
"github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/template"
"net/url"
"io/ioutil"
"github.com/prometheus/alertmanager/types"
"github.com/prometheus/common/model"
)
func TestWebhookRetry(t *testing.T) {
@ -63,15 +64,6 @@ func TestHipchatRetry(t *testing.T) {
}
}
func TestWechatRetry(t *testing.T) {
notifier := new(Wechat)
retryCodes := append(defaultRetryCodes(), http.StatusTooManyRequests)
for statusCode, expected := range retryTests(retryCodes) {
actual, _ := notifier.retry(statusCode)
require.Equal(t, expected, actual, fmt.Sprintf("error on status %d", statusCode))
}
}
func TestOpsGenieRetry(t *testing.T) {
notifier := new(OpsGenie)
@ -200,7 +192,7 @@ func createTmpl(t *testing.T) *template.Template {
return tmpl
}
func readBody(t *testing.T, r *http.Request) string {
func readBody(t *testing.T, r *http.Request) string {
body, err := ioutil.ReadAll(r.Body)
require.NoError(t, err)
return string(body)
@ -216,7 +208,7 @@ func TestOpsGenie(t *testing.T) {
Message: `{{ .CommonLabels.Message }}`,
Description: `{{ .CommonLabels.Description }}`,
Source: `{{ .CommonLabels.Source }}`,
Teams: `{{ .CommonLabels.Teams }}`,
Teams: `{{ .CommonLabels.Teams }}`,
Tags: `{{ .CommonLabels.Tags }}`,
Note: `{{ .CommonLabels.Note }}`,
Priority: `{{ .CommonLabels.Priority }}`,
@ -231,11 +223,11 @@ func TestOpsGenie(t *testing.T) {
expectedUrl, _ := url.Parse("https://opsgenie/apiv2/alerts")
// Empty alert.
alert1:= &types.Alert{
Alert: model.Alert{
StartsAt: time.Now(),
EndsAt: time.Now().Add(time.Hour),
},
alert1 := &types.Alert{
Alert: model.Alert{
StartsAt: time.Now(),
EndsAt: time.Now().Add(time.Hour),
},
}
expectedBody := `{"alias":"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b","message":"","details":{},"source":""}
`
@ -247,23 +239,72 @@ func TestOpsGenie(t *testing.T) {
require.Equal(t, expectedBody, readBody(t, req))
// Fully defined alert.
alert2:= &types.Alert{
alert2 := &types.Alert{
Alert: model.Alert{
Labels: model.LabelSet{
"Message": "message",
"Message": "message",
"Description": "description",
"Source": "http://prometheus",
"Teams": "TeamA,TeamB,",
"Tags": "tag1,tag2",
"Note": "this is a note",
"Priotity": "P1",
},
"Source": "http://prometheus",
"Teams": "TeamA,TeamB,",
"Tags": "tag1,tag2",
"Note": "this is a note",
"Priotity": "P1",
},
StartsAt: time.Now(),
EndsAt: time.Now().Add(time.Hour),
EndsAt: time.Now().Add(time.Hour),
},
}
expectedBody = `{"alias":"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b","message":"message","description":"description","details":{},"source":"http://prometheus","teams":[{"name":"TeamA"},{"name":"TeamB"}],"tags":["tag1","tag2"],"note":"this is a note"}
`
req, retry, err = notifier.createRequest(ctx, alert2)
require.Equal(t, expectedBody, readBody(t, req))
}
}
func TestWechat(t *testing.T) {
logger := log.NewNopLogger()
tmpl := createTmpl(t)
conf := &config.WechatConfig{
NotifierConfig: config.NotifierConfig{
VSendResolved: true,
},
Message: `{{ template "wechat.default.message" . }}`,
APIURL: config.DefaultGlobalConfig.WeChatAPIURL,
APISecret: "invalidSecret",
CorpID: "invalidCorpID",
AgentID: "1",
ToUser: "admin",
}
notifier := NewWechat(conf, tmpl, logger)
ctx := context.Background()
alert := &types.Alert{
Alert: model.Alert{
Labels: model.LabelSet{
"Message": "message",
"Description": "description",
"Source": "http://prometheus",
"Teams": "TeamA,TeamB,",
"Tags": "tag1,tag2",
"Note": "this is a note",
"Priotity": "P1",
},
StartsAt: time.Now(),
EndsAt: time.Now().Add(time.Hour),
},
}
// miss group key
retry, err := notifier.Notify(ctx, alert)
require.False(t, retry)
require.Error(t, err)
ctx = WithGroupKey(ctx, "2")
// invalid secret
retry, err = notifier.Notify(ctx, alert)
require.False(t, retry)
require.Error(t, err)
}

View File

@ -36,7 +36,7 @@ route:
group_by: []
group_wait: 1s
group_interval: 1s
repeat_interval: 0s
repeat_interval: 1ms
receivers:
- name: "default"
@ -112,7 +112,7 @@ route:
group_by: []
group_wait: 1s
group_interval: 1s
repeat_interval: 0s
repeat_interval: 1ms
receivers:
- name: "default"

View File

@ -30,7 +30,7 @@ route:
group_by: []
group_wait: 1s
group_interval: 1s
repeat_interval: 0s
repeat_interval: 1ms
receivers:
- name: "default"
@ -82,7 +82,7 @@ route:
group_by: []
group_wait: 1s
group_interval: 1s
repeat_interval: 0s
repeat_interval: 1ms
receivers:
- name: "default"

View File

@ -7,6 +7,22 @@ import Utils.Date
import Utils.Types exposing (ApiData(..))
map : (a -> b) -> ApiData a -> ApiData b
map fn response =
case response of
Success value ->
Success (fn value)
Initial ->
Initial
Loading ->
Loading
Failure a ->
Failure a
withDefault : a -> ApiData a -> a
withDefault default response =
case response of

View File

@ -24,7 +24,10 @@ view labels maybeActiveId alert =
|> uncurry (++)
in
li
[ class "align-items-start list-group-item border-0 alert-list-item p-0 mb-4"
[ -- speedup rendering in Chrome, because list-group-item className
-- creates a new layer in the rendering engine
style [ ( "position", "static" ) ]
, class "align-items-start list-group-item border-0 p-0 mb-4"
]
[ div
[ class "w-100 mb-2 d-flex align-items-start" ]

View File

@ -13,14 +13,18 @@ import Utils.List
import Utils.Types exposing (Matcher)
import Utils.Views exposing (buttonLink)
import Views.FilterBar.Types as FilterBarTypes
import Views.SilenceForm.Types exposing (SilenceFormMsg(NewSilenceFromMatchers))
import Views.SilenceList.Types exposing (SilenceListMsg(ConfirmDestroySilence, DestroySilence, FetchSilences, MsgForFilterBar))
import Views.SilenceForm.Parsing exposing (newSilenceFromAlertLabels)
view : Bool -> Silence -> Html Msg
view showConfirmationDialog silence =
li [ class "align-items-start list-group-item border-0 alert-list-item p-0 mb-4" ]
li
[ -- speedup rendering in Chrome, because list-group-item className
-- creates a new layer in the rendering engine
style [ ( "position", "static" ) ]
, class "align-items-start list-group-item border-0 p-0 mb-4"
]
[ div [ class "w-100 mb-2 d-flex align-items-start" ]
[ case silence.status.state of
Active ->

View File

@ -1,4 +1,4 @@
module Views.SilenceList.Types exposing (Model, SilenceListMsg(..), initSilenceList)
module Views.SilenceList.Types exposing (Model, SilenceTab, SilenceListMsg(..), initSilenceList)
import Silences.Types exposing (Silence, State(Active), SilenceId)
import Utils.Types exposing (ApiData(Initial))
@ -14,8 +14,15 @@ type SilenceListMsg
| SetTab State
type alias SilenceTab =
{ silences : List Silence
, tab : State
, count : Int
}
type alias Model =
{ silences : ApiData (List Silence)
{ silences : ApiData (List SilenceTab)
, filterBar : FilterBar.Model
, tab : State
, showConfirmationDialog : Maybe SilenceId

View File

@ -2,17 +2,26 @@ module Views.SilenceList.Updates exposing (update, urlUpdate)
import Navigation
import Silences.Api as Api
import Utils.Api as ApiData
import Utils.Filter exposing (Filter, generateQueryString)
import Utils.Types as Types exposing (ApiData(Failure, Loading, Success), Matchers, Time)
import Views.FilterBar.Updates as FilterBar
import Views.SilenceList.Types exposing (Model, SilenceListMsg(..))
import Views.SilenceList.Types exposing (Model, SilenceTab, SilenceListMsg(..))
import Silences.Types exposing (Silence, State(..))
update : SilenceListMsg -> Model -> Filter -> String -> String -> ( Model, Cmd SilenceListMsg )
update msg model filter basePath apiUrl =
case msg of
SilencesFetch sils ->
( { model | silences = sils }, Cmd.none )
SilencesFetch fetchedSilences ->
( { model
| silences =
ApiData.map
(\silences -> List.map (groupSilencesByState silences) states)
fetchedSilences
}
, Cmd.none
)
FetchSilences ->
( { model
@ -50,6 +59,28 @@ update msg model filter basePath apiUrl =
( { model | tab = tab }, Cmd.none )
groupSilencesByState : List Silence -> State -> SilenceTab
groupSilencesByState silences state =
let
silencesInTab =
filterSilencesByState state silences
in
{ tab = state
, silences = silencesInTab
, count = List.length silencesInTab
}
states : List State
states =
[ Active, Pending, Expired ]
filterSilencesByState : State -> List Silence -> List Silence
filterSilencesByState state =
List.filter (.status >> .state >> (==) state)
urlUpdate : Maybe String -> ( SilenceListMsg, Filter )
urlUpdate maybeString =
( FetchSilences, updateFilter maybeString )

View File

@ -4,13 +4,14 @@ import Html exposing (..)
import Html.Attributes exposing (..)
import Silences.Types exposing (Silence, State(..), stateToString, SilenceId)
import Types exposing (Msg(MsgForSilenceList, Noop, UpdateFilter))
import Utils.Api exposing (withDefault)
import Utils.String as StringUtils
import Utils.Types exposing (ApiData(..), Matcher)
import Utils.Views exposing (buttonLink, checkbox, error, formField, formInput, iconButtonMsg, loading, textField)
import Views.FilterBar.Views as FilterBar
import Views.SilenceList.SilenceView
import Views.SilenceList.Types exposing (Model, SilenceListMsg(..))
import Views.SilenceList.Types exposing (Model, SilenceListMsg(..), SilenceTab)
import Html.Lazy exposing (lazy, lazy2, lazy3)
import Html.Keyed
view : Model -> Html Msg
@ -20,49 +21,69 @@ view { filterBar, tab, silences, showConfirmationDialog } =
[ label [ class "mb-2", for "filter-bar-matcher" ] [ text "Filter" ]
, Html.map (MsgForFilterBar >> MsgForSilenceList) (FilterBar.view filterBar)
]
, ul [ class "nav nav-tabs mb-4" ]
(List.map (tabView tab) (groupSilencesByState (withDefault [] silences)))
, case silences of
Success sils ->
silencesView showConfirmationDialog (filterSilencesByState tab sils)
Failure msg ->
error msg
_ ->
loading
, lazy2 tabsView tab silences
, lazy3 silencesView showConfirmationDialog tab silences
]
tabView : State -> ( State, List a ) -> Html Msg
tabView currentState ( state, silences ) =
Utils.Views.tab state currentState (SetTab >> MsgForSilenceList) <|
case List.length silences of
tabsView : State -> ApiData (List SilenceTab) -> Html Msg
tabsView currentTab tabs =
case tabs of
Success silencesTabs ->
List.map (\{ tab, count } -> tabView currentTab count tab) silencesTabs
|> ul [ class "nav nav-tabs mb-4" ]
_ ->
List.map (tabView currentTab 0) states
|> ul [ class "nav nav-tabs mb-4" ]
tabView : State -> Int -> State -> Html Msg
tabView currentTab count tab =
Utils.Views.tab tab currentTab (SetTab >> MsgForSilenceList) <|
case count of
0 ->
[ text (StringUtils.capitalizeFirst (stateToString state)) ]
[ text (StringUtils.capitalizeFirst (stateToString tab)) ]
n ->
[ text (StringUtils.capitalizeFirst (stateToString state))
[ text (StringUtils.capitalizeFirst (stateToString tab))
, span
[ class "badge badge-pillow badge-default align-text-top ml-2" ]
[ text (toString n) ]
]
silencesView : Maybe SilenceId -> List Silence -> Html Msg
silencesView showConfirmationDialog silences =
if List.isEmpty silences then
Utils.Views.error "No silences found"
else
ul [ class "list-group" ]
(List.map
(\silence ->
Views.SilenceList.SilenceView.view
(showConfirmationDialog == Just silence.id)
silence
)
silences
)
silencesView : Maybe SilenceId -> State -> ApiData (List SilenceTab) -> Html Msg
silencesView showConfirmationDialog tab silencesTab =
case silencesTab of
Success tabs ->
tabs
|> List.filter (.tab >> (==) tab)
|> List.head
|> Maybe.map .silences
|> Maybe.withDefault []
|> (\silences ->
if List.isEmpty silences then
Utils.Views.error "No silences found"
else
Html.Keyed.ul [ class "list-group" ]
(List.map
(\silence ->
( silence.id
, Views.SilenceList.SilenceView.view
(showConfirmationDialog == Just silence.id)
silence
)
)
silences
)
)
Failure msg ->
error msg
_ ->
loading
groupSilencesByState : List Silence -> List ( State, List Silence )

File diff suppressed because one or more lines are too long