Merge pull request #785 from mxinden/silence-state

Silence State from API and UI
This commit is contained in:
Max Inden 2017-05-12 11:04:53 +02:00 committed by GitHub
commit e5042e4d3b
18 changed files with 279 additions and 127 deletions

View File

@ -574,6 +574,9 @@ func silenceFromProto(s *silencepb.Silence) (*types.Silence, error) {
StartsAt: s.StartsAt,
EndsAt: s.EndsAt,
UpdatedAt: s.UpdatedAt,
Status: types.SilenceStatus{
State: types.CalcSilenceState(s.StartsAt, s.EndsAt),
},
}
for _, m := range s.Matchers {
matcher := &types.Matcher{

View File

@ -333,6 +333,31 @@ type Silence struct {
// timeFunc provides the time against which to evaluate
// the silence. Used for test injection.
now func() time.Time
Status SilenceStatus `json:"status"`
}
type SilenceStatus struct {
State SilenceState `json:"state"`
}
type SilenceState string
const (
SilenceStateExpired SilenceState = "expired"
SilenceStateActive SilenceState = "active"
SilenceStatePending SilenceState = "pending"
)
func CalcSilenceState(start, end time.Time) SilenceState {
current := time.Now()
if current.Before(start) {
return SilenceStatePending
}
if current.Before(end) {
return SilenceStateActive
}
return SilenceStateExpired
}
// Validate returns true iff all fields of the silence have valid values.

View File

@ -9,18 +9,18 @@
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-alpha.6/css/bootstrap.min.css" integrity="sha384-rwoIResjU2yc3z8GV/NPeZWAv56rSmLldC3R/AZzGRnGxQQKnKkoFVhFQhNUwEyJ" crossorigin="anonymous">
<script src="https://use.fontawesome.com/b7508bb100.js"></script>
<style>
.alert-list-item:nth-child(odd) {
.list-item:nth-child(odd) {
background: #f7f7f9;
}
.alert-list-item:not(:hover) .btn {
.list-item:not(:hover) .btn {
background-color: transparent;
border: 1px solid transparent;
color: gray;
}
.alert-list-item:not(:hover) .badge {
.list-item:not(:hover) .badge {
background-color: gray;
}
.alert-list-item:hover {
.list-item:hover {
background: #f7f7f9;
}
</style>

View File

@ -4,7 +4,7 @@ import Http
import Silences.Types exposing (Silence)
import Utils.Types exposing (ApiResponse(..), ApiData)
import Utils.Filter exposing (Filter)
import Silences.Decoders exposing (..)
import Silences.Decoders exposing (show, list, create, destroy)
import Silences.Encoders
import Utils.Api exposing (baseUrl)
import Utils.Filter exposing (generateQueryString)

View File

@ -1,19 +1,19 @@
module Silences.Decoders exposing (..)
module Silences.Decoders exposing (show, list, create, destroy)
import Json.Decode as Json exposing (field)
import Json.Decode as Json exposing (field, succeed, fail)
import Utils.Api exposing (iso8601Time, (|:))
import Silences.Types exposing (Silence)
import Silences.Types exposing (Silence, Status, State(Active, Pending, Expired))
import Utils.Types exposing (Matcher, Time, ApiResponse(Success))
show : Json.Decoder Silence
show =
Json.at [ "data" ] silence
Json.at [ "data" ] silenceDecoder
list : Json.Decoder (List Silence)
list =
Json.at [ "data" ] (Json.list silence)
Json.at [ "data" ] (Json.list silenceDecoder)
create : Json.Decoder String
@ -21,17 +21,13 @@ create =
Json.at [ "data", "silenceId" ] Json.string
-- This should just be the ID
destroy : Json.Decoder String
destroy =
Json.at [ "status" ] Json.string
silence : Json.Decoder Silence
silence =
silenceDecoder : Json.Decoder Silence
silenceDecoder =
Json.succeed Silence
|: (field "id" Json.string)
|: (field "createdBy" Json.string)
@ -43,12 +39,38 @@ silence =
|: (field "startsAt" iso8601Time)
|: (field "endsAt" iso8601Time)
|: (field "updatedAt" iso8601Time)
|: (field "matchers" (Json.list matcher))
|: (field "matchers" (Json.list matcherDecoder))
|: (Json.succeed <| Success [])
|: (field "status" statusDecoder)
matcher : Json.Decoder Matcher
matcher =
statusDecoder : Json.Decoder Status
statusDecoder =
Json.succeed Status
|: (field "state" Json.string |> Json.andThen stateDecoder)
stateDecoder : String -> Json.Decoder State
stateDecoder state =
case state of
"active" ->
succeed Active
"pending" ->
succeed Pending
"expired" ->
succeed Expired
_ ->
fail <|
"Silence.status.state must be one of 'active', 'pending' or 'expired' but was'"
++ state
++ "'."
matcherDecoder : Json.Decoder Matcher
matcherDecoder =
Json.map3 Matcher
(field "name" Json.string)
(field "value" Json.string)

View File

@ -1,4 +1,15 @@
module Silences.Types exposing (Silence, nullSilence, nullMatcher, nullTime, SilenceId)
module Silences.Types
exposing
( Silence
, SilenceId
, Status
, State(Active, Pending, Expired)
, nullSilence
, nullSilenceStatus
, nullMatcher
, nullTime
, stateToString
)
import Alerts.Types exposing (Alert)
import Utils.Types exposing (Matcher, ApiData, ApiResponse(Success))
@ -15,6 +26,13 @@ nullSilence =
, updatedAt = 0
, matchers = [ nullMatcher ]
, silencedAlerts = Success []
, status = nullSilenceStatus
}
nullSilenceStatus : Status
nullSilenceStatus =
{ state = Expired
}
@ -37,8 +55,33 @@ type alias Silence =
, updatedAt : Time
, matchers : List Matcher
, silencedAlerts : ApiData (List Alert)
, status : Status
}
type alias Status =
{ state : State
}
type State
= Active
| Pending
| Expired
stateToString : State -> String
stateToString state =
case state of
Active ->
"active"
Pending ->
"pending"
Expired ->
"expired"
type alias SilenceId =
String

View File

@ -67,12 +67,12 @@ durationFormat time =
dateFormat : Time.Time -> String
dateFormat =
Date.fromTime >> (Date.Extra.Format.format config Date.Extra.Format.isoDateFormat)
Date.fromTime >> (Date.Extra.Format.formatUtc config Date.Extra.Format.isoDateFormat)
timeFormat : Time.Time -> String
timeFormat =
Date.fromTime >> (Date.Extra.Format.format config Date.Extra.Format.isoTimeFormat)
Date.fromTime >> (Date.Extra.Format.formatUtc config Date.Extra.Format.isoTimeFormat)
dateTimeFormat : Time.Time -> String

View File

@ -0,0 +1,14 @@
module Utils.String exposing (capitalizeFirst)
import String
import Char
capitalizeFirst : String -> String
capitalizeFirst string =
case String.uncons string of
Nothing ->
string
Just ( char, rest ) ->
String.cons (Char.toUpper char) rest

View File

@ -16,7 +16,7 @@ import Time exposing (Time)
view : Alert -> Html Msg
view alert =
li
[ class "align-items-center list-group-item alert-list-item p-0 d-inline-flex justify-content-start"
[ class "align-items-center list-group-item list-item p-0 d-inline-flex justify-content-start"
]
[ dateView alert.startsAt
, labelButtons alert.labels

View File

@ -9,4 +9,4 @@ import Views.Shared.AlertCompact
view : List Alert -> Html msg
view alerts =
List.map Views.Shared.AlertCompact.view alerts
|> ol [ class "list pa0" ]
|> ol [ class "list pa0 w-100" ]

View File

@ -1,8 +1,9 @@
module Views.Shared.SilenceBase exposing (view)
import Html exposing (Html, div, a, p, text, b)
import Html.Attributes exposing (class, href)
import Silences.Types exposing (Silence)
import Html exposing (Html, div, a, p, text, b, i, span, small, button)
import Html.Attributes exposing (class, href, style)
import Html.Events exposing (onClick)
import Silences.Types exposing (Silence, State(Expired))
import Types exposing (Msg(Noop, MsgForSilenceList))
import Views.SilenceList.Types exposing (SilenceListMsg(DestroySilence, MsgForFilterBar))
import Utils.Date
@ -11,36 +12,40 @@ import Utils.Types exposing (Matcher)
import Utils.Filter
import Utils.List
import Views.FilterBar.Types as FilterBarTypes
import Time exposing (Time)
view : Silence -> Html Msg
view silence =
let
alertName =
silence.matchers
|> List.filter (\m -> m.name == "alertname")
|> List.head
|> Maybe.map .value
|> Maybe.withDefault ""
editUrl =
String.join "/" [ "#/silences", silence.id, "edit" ]
in
div [ class "f6 mb3" ]
[ a
[ class "db link blue mb3"
, href ("#/silences/" ++ silence.id)
]
[ b [ class "db f4 mb1" ]
[ text alertName ]
]
, div [ class "mb1" ]
[ buttonLink "fa fa-pencil" editUrl "blue" Noop
, buttonLink "fa fa-trash-o" "#/silences" "dark-red" (MsgForSilenceList (DestroySilence silence))
, p [ class "dib mr2" ] [ text <| "Until " ++ Utils.Date.dateTimeFormat silence.endsAt ]
]
, div [ class "mb2 w-80-l w-100-m" ] (List.map matcherButton silence.matchers)
div [ class "d-inline-flex align-items-center justify-content-start w-100" ]
[ datesView silence.startsAt silence.endsAt
, div [ class "" ] (List.map matcherButton silence.matchers)
, div [ class "ml-auto d-inline-flex align-self-stretch p-2", style [ ( "border-left", "1px solid #ccc" ) ] ]
[ editButton silence.id
, deleteButton silence
, detailsButton silence.id
]
]
datesView : Time -> Time -> Html Msg
datesView start end =
i [ class "d-inline-flex align-items-center", style [ ( "border-right", "1px solid #ccc" ) ] ]
[ dateView start
, i [ class "text-muted" ] [ text "-" ]
, dateView end
]
dateView : Time -> Html Msg
dateView time =
i
[ class "h-100 p-2 d-flex flex-column justify-content-center, text-muted"
, style [ ( "font-family", "monospace" ) ]
]
[ span [] [ text <| Utils.Date.timeFormat time ]
, small [] [ text <| Utils.Date.dateFormat time ]
]
matcherButton : Matcher -> Html Msg
@ -63,3 +68,34 @@ matcherButton matcher =
)
in
Utils.Views.labelButton (Just msg) (Utils.List.mstring matcher)
editButton : String -> Html Msg
editButton silenceId =
let
editUrl =
String.join "/" [ "#/silences", silenceId, "edit" ]
in
a [ class "h-100 btn btn-success rounded-0", href editUrl ]
[ span [ class "fa fa-pencil" ] [] ]
deleteButton : Silence -> Html Msg
deleteButton silence =
if silence.status.state == Expired then
text ""
else
a
[ class "h-100 btn btn-danger rounded-0"
, onClick (MsgForSilenceList (DestroySilence silence))
, href "#/silences"
]
[ span [ class "fa fa-trash" ] []
]
detailsButton : String -> Html Msg
detailsButton silenceId =
a [ class "h-100 btn btn-primary rounded-0", href ("#/silences/" ++ silenceId) ]
[ span [ class "fa fa-info" ] []
]

View File

@ -2,6 +2,7 @@ module Views.Shared.SilencePreview exposing (view)
import Silences.Types exposing (Silence)
import Html exposing (Html, div, text)
import Html.Attributes exposing (class)
import Utils.Types exposing (ApiResponse(Success, Loading, Failure))
import Views.Shared.AlertListCompact
import Utils.Views exposing (error, loading)
@ -14,7 +15,7 @@ view s =
if List.isEmpty alerts then
div [] [ text "No matches" ]
else
div [] [ Views.Shared.AlertListCompact.view alerts ]
div [ class "w-100" ] [ Views.Shared.AlertListCompact.view alerts ]
Loading ->
loading

View File

@ -1,21 +1,21 @@
module Views.Silence.Views exposing (view)
import Silences.Types exposing (Silence)
import Html exposing (Html, div, h2, p, text, label)
import Silences.Types exposing (Silence, stateToString)
import Html exposing (Html, div, h2, p, text, label, b, h1)
import Html.Attributes exposing (class)
import Time exposing (Time)
import Types exposing (Model, Msg)
import Utils.Types exposing (ApiResponse(Success, Loading, Failure))
import Utils.Views exposing (loading, error)
import Views.Shared.SilencePreview
import Views.Shared.SilenceBase
import Utils.Date exposing (dateTimeFormat)
import Utils.List
view : Model -> Html Msg
view model =
case model.silence of
Success sil ->
silence sil model.currentTime
silence2 sil
Loading ->
loading
@ -24,41 +24,29 @@ view model =
error msg
silence : Silence -> Time -> Html Msg
silence silence currentTime =
silence2 : Silence -> Html Msg
silence2 silence =
div []
[ Views.Shared.SilenceBase.view silence
, silenceExtra silence currentTime
, h2 [ class "h6 dark-red" ] [ text "Affected alerts" ]
, Views.Shared.SilencePreview.view silence
[ h1 [] [ text "Silence" ]
, formGroup "ID" <| text silence.id
, formGroup "Starts at" <| text <| dateTimeFormat silence.startsAt
, formGroup "Ends at" <| text <| dateTimeFormat silence.endsAt
, formGroup "Updated at" <| text <| dateTimeFormat silence.updatedAt
, formGroup "Created by" <| text silence.createdBy
, formGroup "Comment" <| text silence.comment
, formGroup "State" <| text <| stateToString silence.status.state
, formGroup "Matchers" <|
div [] <|
List.map (Utils.List.mstring >> Utils.Views.labelButton Nothing) silence.matchers
, formGroup "Affected alerts" <| Views.Shared.SilencePreview.view silence
]
silenceExtra : Silence -> Time -> Html msg
silenceExtra silence currentTime =
div [ class "f6" ]
[ div [ class "mb1" ]
[ p []
[ text "Status: "
, Utils.Views.button "ph3 pv2" (status silence currentTime)
]
, div []
[ label [ class "f6 dib mb2 mr2 w-40" ] [ text "Created by" ]
, p [] [ text silence.createdBy ]
]
, div []
[ label [ class "f6 dib mb2 mr2 w-40" ] [ text "Comment" ]
, p [] [ text silence.comment ]
]
formGroup : String -> Html Msg -> Html Msg
formGroup key content =
div [ class "form-group row" ]
[ label [ class "col-2 col-form-label" ] [ b [] [ text key ] ]
, div [ class "col-10 d-flex align-items-center" ]
[ content
]
]
status : Silence -> Time -> String
status { endsAt, startsAt } currentTime =
if endsAt <= currentTime then
"expired"
else if startsAt > currentTime then
"pending"
else
"active"

View File

@ -10,7 +10,7 @@ module Views.SilenceForm.Types
, initSilenceForm
)
import Silences.Types exposing (Silence, SilenceId)
import Silences.Types exposing (Silence, SilenceId, nullSilenceStatus)
import Alerts.Types exposing (Alert)
import Utils.Types exposing (Matcher, ApiData, Duration, ApiResponse(..))
import Time exposing (Time)
@ -42,6 +42,9 @@ toSilence { createdBy, comment, startsAt, endsAt, matchers } =
{- ignored -}
, id = ""
{- ignored -}
, status = nullSilenceStatus
}
)
(timeFromString startsAt)

View File

@ -7,7 +7,7 @@ import Views.FilterBar.Types as FilterBar
type SilenceListMsg
= SilenceDestroy (ApiData String)
= SilenceDestroyed (ApiData String)
| DestroySilence Silence
| SilencesFetch (ApiData (List Silence))
| FetchSilences

View File

@ -28,11 +28,10 @@ update msg model silence filter =
)
DestroySilence silence ->
( model, Loading, Api.destroy silence (SilenceDestroy >> MsgForSilenceList) )
( model, Loading, Api.destroy silence (SilenceDestroyed >> MsgForSilenceList) )
SilenceDestroy silence ->
SilenceDestroyed statusCode ->
-- TODO: "Deleted id: ID" growl
-- TODO: Add DELETE to accepted CORS methods in alertmanager
-- TODO: Check why POST isn't there but is accepted
( model, Loading, Navigation.newUrl "/#/silences" )

View File

@ -1,62 +1,80 @@
module Views.SilenceList.Views exposing (..)
-- External Imports
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onClick, onInput)
import Views.SilenceList.Types exposing (SilenceListMsg(..), Model)
import Views.Shared.SilenceBase
import Silences.Types exposing (Silence)
import Silences.Types exposing (Silence, State(..), stateToString)
import Utils.Types exposing (Matcher, ApiResponse(..), ApiData)
import Utils.Filter exposing (Filter)
import Utils.Views exposing (iconButtonMsg, checkbox, textField, formInput, formField, buttonLink, error, loading)
import Time
import Types exposing (Msg(UpdateFilter, MsgForSilenceList, Noop))
import Views.FilterBar.Views as FilterBar
import Views.FilterBar.Types as FilterBarTypes
import Utils.String as StringUtils
view : Model -> Time.Time -> Html Msg
view model currentTime =
case model.silences of
Success sils ->
-- Add buttons at the top to filter Active/Pending/Expired
silences sils model.filterBar (text "")
div []
[ Html.map (MsgForFilterBar >> MsgForSilenceList) (FilterBar.view model.filterBar)
, a [ class "mb-4 btn btn-primary", href "#/silences/new" ] [ text "New Silence" ]
, silenceListView sils
]
Loading ->
loading
Failure msg ->
silences [] model.filterBar (error msg)
error msg
silences : List Silence -> FilterBarTypes.Model -> Html Msg -> Html Msg
silences silences filterBar errorHtml =
silenceListView : List Silence -> Html Msg
silenceListView silences =
div [] <|
List.map silenceGroupView <|
groupSilencesByState silences
silenceGroupView : ( State, List Silence ) -> Html Msg
silenceGroupView ( state, silences ) =
let
html =
silencesView =
if List.isEmpty silences then
div [ class "mt2" ] [ text "no silences found" ]
div [] [ text "No silences found" ]
else
ul
[ classList
[ ( "list", True )
, ( "pa0", True )
]
]
(List.map silenceList silences)
ul [ class "list-group" ]
(List.map silenceView silences)
in
div []
[ Html.map (MsgForFilterBar >> MsgForSilenceList) (FilterBar.view filterBar)
, a [ class "f6 link br2 ba ph3 pv2 mr2 dib blue", href "#/silences/new" ] [ text "New Silence" ]
, errorHtml
, html
div [ class "mb-4" ]
[ h3 [] [ text <| StringUtils.capitalizeFirst <| stateToString state ]
, silencesView
]
silenceList : Silence -> Html Msg
silenceList silence =
silenceView : Silence -> Html Msg
silenceView silence =
li
[ class "pa3 pa4-ns bb b--black-10" ]
[ class "list-group-item p-0 list-item" ]
[ Views.Shared.SilenceBase.view silence
]
groupSilencesByState : List Silence -> List ( State, List Silence )
groupSilencesByState silences =
List.map (\state -> ( state, filterSilencesByState state silences )) states
states : List State
states =
[ Active, Pending, Expired ]
-- TODO: Replace this with Utils.List.groupBy
filterSilencesByState : State -> List Silence -> List Silence
filterSilencesByState state =
List.filter (\{ status } -> status.state == state)

File diff suppressed because one or more lines are too long