Extract a model for the silence form (#22)

* Extract a model for the silence form

* Use empty in initSilenceForm

* Use plural silences in urls

* Populate matchers from alert

* Updating bindata.go file
This commit is contained in:
Andrey Kuzmin 2017-03-29 17:47:12 +02:00 committed by Max Leonard Inden
parent b6243f55e4
commit bdd65fdfd7
No known key found for this signature in database
GPG Key ID: 5403C5464810BC26
30 changed files with 468 additions and 390 deletions

View File

@ -7,14 +7,13 @@ import Utils.Types exposing (ApiData, Filter)
import Utils.Filter exposing (generateQueryString)
getAlertGroups : Filter -> (ApiData (List AlertGroup) -> msg) -> Cmd msg
getAlertGroups filter msg =
alertGroups : Filter -> Cmd (ApiData (List AlertGroup))
alertGroups filter =
let
url =
String.join "/" [ baseUrl, "alerts", "groups" ++ (generateQueryString filter) ]
in
Utils.Api.send (Utils.Api.get url alertGroupsDecoder)
|> Cmd.map msg

View File

@ -1,6 +1,7 @@
module Alerts.Types exposing (Alert, AlertGroup, Block, RouteOpts)
import Utils.Types exposing (Labels, Time)
import Utils.Types exposing (Labels)
import Time exposing (Time)
type alias Alert =

View File

@ -23,6 +23,7 @@ import Types
)
import Utils.Types exposing (..)
import Views.SilenceList.Updates
import Views.SilenceForm.Types exposing (initSilenceForm)
import Views.Status.Types exposing (StatusModel, initStatusModel)
import Updates exposing (update)
@ -55,7 +56,7 @@ init location =
nullFilter
( model, msg ) =
update (urlUpdate location) (Model Loading Loading Loading route filter 0 initStatusModel)
update (urlUpdate location) (Model Loading Loading initSilenceForm Loading route filter 0 initStatusModel)
in
model ! [ msg, Task.perform UpdateCurrentTime Time.now ]
@ -76,8 +77,8 @@ urlUpdate location =
SilenceFormEditRoute silenceId ->
NavigateToSilenceFormEdit silenceId
SilenceFormNewRoute ->
NavigateToSilenceFormNew
SilenceFormNewRoute keep ->
NavigateToSilenceFormNew keep
AlertsRoute filter ->
NavigateToAlerts filter

View File

@ -51,9 +51,9 @@ routeParser =
oneOf
[ map SilenceListRoute silenceListParser
, map StatusRoute statusParser
, map SilenceFormNewRoute silenceFormNewParser
, map SilenceRoute silenceParser
, map SilenceFormEditRoute silenceFormEditParser
, map SilenceFormNewRoute silenceFormNewParser
, map AlertsRoute alertsParser
, map TopLevelRoute top
]

View File

@ -29,8 +29,8 @@ getSilence uuid msg =
|> Cmd.map msg
create : Silence -> (ApiData String -> msg) -> Cmd msg
create silence msg =
create : Silence -> Cmd (ApiData String)
create silence =
let
url =
String.join "/" [ baseUrl, "silences" ]
@ -42,7 +42,6 @@ create silence msg =
-- redirect to the silence show page.
Utils.Api.send
(Utils.Api.post url body Silences.Decoders.create)
|> Cmd.map msg
destroy : Silence -> (ApiData String -> msg) -> Cmd msg

View File

@ -1,7 +1,7 @@
module Silences.Decoders exposing (..)
import Json.Decode as Json exposing (field)
import Utils.Api exposing (iso8601Time, duration, (|:))
import Utils.Api exposing (iso8601Time, (|:))
import Silences.Types exposing (Silence)
import Utils.Types exposing (Matcher, Time, ApiResponse(Success))
@ -43,7 +43,6 @@ silence =
)
|: (field "startsAt" iso8601Time)
|: (field "endsAt" iso8601Time)
|: (duration "startsAt" "endsAt")
|: (field "updatedAt" iso8601Time)
|: (field "matchers" (Json.list matcher))
|: (Json.succeed <| Success [])

View File

@ -8,7 +8,6 @@ import Utils.Date
silence : Silence -> Encode.Value
silence silence =
-- Todo: only submit validated date
Encode.object
[ ( "createdBy", Encode.string silence.createdBy )
, ( "comment", Encode.string silence.comment )

View File

@ -1,8 +1,8 @@
module Silences.Types exposing (Silence, nullSilence, nullMatcher, nullDuration, nullTime, SilenceId)
module Silences.Types exposing (Silence, nullSilence, nullMatcher, nullTime, SilenceId)
import Utils.Date
import Alerts.Types exposing (AlertGroup)
import Utils.Types exposing (Duration, Time, Matcher, ApiData, ApiResponse(Success))
import Utils.Types exposing (Matcher, ApiData, ApiResponse(Success))
import Time exposing (Time)
nullSilence : Silence
@ -10,10 +10,9 @@ nullSilence =
{ id = ""
, createdBy = ""
, comment = ""
, startsAt = nullTime
, endsAt = nullTime
, duration = nullDuration
, updatedAt = nullTime
, startsAt = 0
, endsAt = 0
, updatedAt = 0
, matchers = [ nullMatcher ]
, silencedAlertGroups = Success []
}
@ -24,14 +23,9 @@ nullMatcher =
Matcher "" "" False
nullDuration : Duration
nullDuration =
Utils.Date.duration 0
nullTime : Time
nullTime =
Utils.Date.fromTime 0
0
type alias Silence =
@ -40,7 +34,6 @@ type alias Silence =
, comment : String
, startsAt : Time
, endsAt : Time
, duration : Duration
, updatedAt : Time
, matchers : List Matcher
, silencedAlertGroups : ApiData (List AlertGroup)

View File

@ -4,7 +4,7 @@ import Alerts.Types exposing (AlertGroup, Alert)
import Views.AlertList.Types exposing (AlertListMsg)
import Views.SilenceList.Types exposing (SilenceListMsg)
import Views.Silence.Types exposing (SilenceMsg)
import Views.SilenceForm.Types exposing (SilenceFormMsg)
import Views.SilenceForm.Types as SilenceForm exposing (SilenceFormMsg)
import Views.Status.Types exposing (StatusModel, StatusMsg)
import Silences.Types exposing (Silence)
import Utils.Types exposing (ApiData, Filter)
@ -14,6 +14,7 @@ import Time
type alias Model =
{ silences : ApiData (List Silence)
, silence : ApiData Silence
, silenceForm : SilenceForm.Model
, alertGroups : ApiData (List AlertGroup)
, route : Route
, filter : Filter
@ -24,7 +25,6 @@ type alias Model =
type Msg
= CreateSilenceFromAlert Alert
| AlertGroupsPreview (ApiData (List AlertGroup))
| MsgForAlertList AlertListMsg
| MsgForSilence SilenceMsg
| MsgForSilenceForm SilenceFormMsg
@ -34,12 +34,10 @@ type Msg
| NavigateToNotFound
| NavigateToSilence String
| NavigateToSilenceFormEdit String
| NavigateToSilenceFormNew
| NavigateToSilenceFormNew Bool
| NavigateToSilenceList Filter
| NavigateToStatus
| NewUrl String
| Noop
| PreviewSilence Silence
| RedirectAlerts
| UpdateCurrentTime Time.Time
| UpdateFilter Filter String
@ -49,7 +47,7 @@ type Route
= AlertsRoute Filter
| NotFoundRoute
| SilenceFormEditRoute String
| SilenceFormNewRoute
| SilenceFormNewRoute Bool
| SilenceListRoute Filter
| SilenceRoute String
| StatusRoute

View File

@ -1,8 +1,6 @@
module Updates exposing (update)
import Alerts.Api
import Navigation
import Silences.Types exposing (nullSilence)
import Task
import Types
exposing
@ -10,7 +8,6 @@ import Types
, Model
, Route(NotFoundRoute, SilenceFormEditRoute, SilenceFormNewRoute, SilenceRoute, StatusRoute, SilenceListRoute, AlertsRoute)
)
import Utils.List
import Utils.Types
exposing
( ApiResponse(Loading, Failure, Success)
@ -22,7 +19,7 @@ import Views.AlertList.Types exposing (AlertListMsg(FetchAlertGroups))
import Views.Silence.Types exposing (SilenceMsg(SilenceFetched, InitSilenceView))
import Views.SilenceList.Types exposing (SilenceListMsg(FetchSilences))
import Views.Silence.Updates
import Views.SilenceForm.Types exposing (SilenceFormMsg(NewSilence, FetchSilence))
import Views.SilenceForm.Types exposing (SilenceFormMsg(NewSilenceFromMatchers, FetchSilence))
import Views.SilenceForm.Updates
import Views.SilenceList.Updates
import Views.Status.Types exposing (StatusMsg(InitStatusView))
@ -33,37 +30,15 @@ import String exposing (trim)
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
CreateSilenceFromAlert alert ->
CreateSilenceFromAlert { labels } ->
let
silence =
{ nullSilence | matchers = (List.map (\( k, v ) -> Matcher k v False) alert.labels) }
matchers =
List.map (\( k, v ) -> Matcher k v False) labels
( silenceForm, cmd ) =
Views.SilenceForm.Updates.update (NewSilenceFromMatchers matchers) model.silenceForm
in
( { model | silence = Success silence }, Cmd.none )
PreviewSilence silence ->
let
s =
{ silence | silencedAlertGroups = Loading }
filter =
{ nullFilter | text = Just <| Utils.List.mjoin s.matchers }
in
( { model | silence = Success silence }, Alerts.Api.getAlertGroups filter AlertGroupsPreview )
AlertGroupsPreview alertGroups ->
let
silence =
case model.silence of
Success sil ->
Success { sil | silencedAlertGroups = alertGroups }
Failure e ->
Failure e
Loading ->
Loading
in
( { model | silence = silence }, Cmd.none )
( { model | silenceForm = silenceForm }, cmd )
NavigateToAlerts filter ->
let
@ -94,8 +69,13 @@ update msg model =
in
( { model | route = (SilenceRoute silenceId) }, cmd )
NavigateToSilenceFormNew ->
( { model | route = SilenceFormNewRoute }, Task.perform identity (Task.succeed <| (MsgForSilenceForm NewSilence)) )
NavigateToSilenceFormNew keep ->
( { model | route = SilenceFormNewRoute keep }
, if keep then
Cmd.none
else
Task.perform (NewSilenceFromMatchers >> MsgForSilenceForm) (Task.succeed [])
)
NavigateToSilenceFormEdit uuid ->
( { model | route = SilenceFormEditRoute uuid }, Task.perform identity (Task.succeed <| (FetchSilence uuid |> MsgForSilenceForm)) )
@ -104,7 +84,7 @@ update msg model =
( { model | route = NotFoundRoute }, Cmd.none )
RedirectAlerts ->
( model, Task.perform NewUrl (Task.succeed "/#/alerts") )
( model, Navigation.newUrl "/#/alerts" )
UpdateFilter filter text ->
let
@ -116,9 +96,6 @@ update msg model =
in
( { model | filter = { filter | text = t } }, Cmd.none )
NewUrl url ->
( model, Navigation.newUrl url )
Noop ->
( model, Cmd.none )
@ -146,4 +123,8 @@ update msg model =
Views.Silence.Updates.update msg model
MsgForSilenceForm msg ->
Views.SilenceForm.Updates.update msg model
let
( silenceForm, cmd ) =
Views.SilenceForm.Updates.update msg model.silenceForm
in
( { model | silenceForm = silenceForm }, cmd )

View File

@ -1,10 +1,10 @@
module Utils.Api exposing (..)
import Json.Decode as Json exposing (field)
import Utils.Types exposing (ApiResponse(..), ApiData, Time, Duration)
import Http
import Time
import Json.Decode as Json exposing (field)
import Time exposing (Time)
import Utils.Date
import Utils.Types exposing (ApiData, ApiResponse(..))
fromResult : Result e a -> ApiResponse e a
@ -50,24 +50,18 @@ request method headers url body decoder =
}
duration : String -> String -> Json.Decoder Duration
duration startsAt endsAt =
Json.map2
(\t1 t2 ->
case ( t1.t, t2.t ) of
( Just time1, Just time2 ) ->
Utils.Date.duration (time2 - time1)
_ ->
Utils.Date.duration 0
)
(Json.field startsAt iso8601Time)
(Json.field endsAt iso8601Time)
iso8601Time : Json.Decoder Time
iso8601Time =
Json.map Utils.Date.timeFromString Json.string
Json.andThen
(\strTime ->
case Utils.Date.timeFromString strTime of
Just time ->
Json.succeed time
Nothing ->
Json.fail ("Could not decode time " ++ strTime)
)
Json.string
baseUrl : String

View File

@ -7,9 +7,9 @@ import Utils.Types as Types
import Tuple
parseDuration : String -> Result Parser.Error Time.Time
parseDuration : String -> Maybe Time.Time
parseDuration =
Parser.run durationParser
Parser.run durationParser >> Result.toMaybe
durationParser : Parser Time.Time
@ -29,6 +29,11 @@ units =
]
timeToString : Time.Time -> String
timeToString =
round >> ISO8601.fromTime >> ISO8601.toString
term : Parser Time.Time
term =
Parser.map2 (*)
@ -62,45 +67,21 @@ dateFormat t =
String.join "/" <| List.map toString [ ISO8601.month t, ISO8601.day t, ISO8601.year t ]
timeFormat : Types.Time -> String
timeFormat { t, s } =
t
|> Maybe.map (round >> ISO8601.fromTime >> dateFormat)
|> Maybe.withDefault s
timeFormat : Time.Time -> String
timeFormat =
round >> ISO8601.fromTime >> dateFormat
encode : Types.Time -> String
encode { t, s } =
t
|> Maybe.map (round >> ISO8601.fromTime >> ISO8601.toString)
|> Maybe.withDefault s
encode : Time.Time -> String
encode =
round >> ISO8601.fromTime >> ISO8601.toString
timeFromString : String -> Types.Time
timeFromString toParse =
{ s = toParse
, t =
toParse
|> ISO8601.fromString
|> Result.toMaybe
|> Maybe.map (ISO8601.toTime >> toFloat)
}
durationFromString : String -> Types.Duration
durationFromString toParse =
{ s = toParse
, d =
toParse
|> parseDuration
|> Result.mapError (Debug.log "error")
|> Result.toMaybe
}
duration : Time.Time -> Types.Duration
duration time =
{ d = Just time, s = durationFormat time }
timeFromString : String -> Maybe Time.Time
timeFromString =
ISO8601.fromString
>> Result.toMaybe
>> Maybe.map (ISO8601.toTime >> toFloat)
fromTime : Time.Time -> Types.Time

View File

@ -15,6 +15,18 @@ replaceIf predicate replacement list =
list
replaceIndex : Int -> (a -> a) -> List a -> List a
replaceIndex index replacement list =
List.indexedMap
(\currentIndex item ->
if index == currentIndex then
replacement item
else
item
)
list
mjoin : Matchers -> String
mjoin m =
String.join "," (List.map mstring m)

View File

@ -2,7 +2,7 @@ module Views exposing (..)
import Html exposing (Html, text, div)
import Html.Attributes exposing (class)
import Types exposing (Msg, Model, Route(..))
import Types exposing (Msg(MsgForSilenceForm), Model, Route(..))
import Utils.Types exposing (ApiResponse(..))
import Utils.Views exposing (error, loading)
import Views.SilenceList.Views as SilenceList
@ -55,11 +55,11 @@ appBody model =
SilenceListRoute route ->
SilenceList.view model.silences model.silence model.currentTime model.filter
SilenceFormNewRoute ->
SilenceForm.new model.silence
SilenceFormNewRoute keep ->
SilenceForm.view Nothing model.silenceForm |> Html.map MsgForSilenceForm
SilenceFormEditRoute silenceId ->
SilenceForm.edit model.silence
SilenceForm.view (Just silenceId) model.silenceForm |> Html.map MsgForSilenceForm
TopLevelRoute ->
Utils.Views.loading

View File

@ -3,10 +3,10 @@ module Views.AlertList.Updates exposing (..)
import Alerts.Api as Api
import Views.AlertList.Types exposing (AlertListMsg(..))
import Alerts.Types exposing (AlertGroup)
import Task
import Navigation
import Utils.Types exposing (ApiData, ApiResponse(..), Filter)
import Utils.Filter exposing (generateQueryString)
import Types exposing (Msg(MsgForAlertList, NewUrl))
import Types exposing (Msg(MsgForAlertList))
update : AlertListMsg -> ApiData (List AlertGroup) -> Filter -> ( ApiData (List AlertGroup), Cmd Types.Msg )
@ -16,11 +16,7 @@ update msg groups filter =
( alertGroups, Cmd.none )
FetchAlertGroups ->
( groups, Api.getAlertGroups filter (AlertGroupsFetch >> MsgForAlertList) )
( groups, Api.alertGroups filter |> Cmd.map (AlertGroupsFetch >> MsgForAlertList) )
FilterAlerts ->
let
url =
"/#/alerts" ++ generateQueryString filter
in
( groups, Task.perform identity (Task.succeed (Types.NewUrl url)) )
( groups, Navigation.newUrl ("/#/alerts" ++ generateQueryString filter) )

View File

@ -68,9 +68,9 @@ alertView alert =
b =
if alert.silenced then
buttonLink "fa-deaf" ("#/silences/" ++ id) "blue" (Types.Noop)
buttonLink "fa-deaf" ("#/silences/" ++ id) "blue" Types.Noop
else
buttonLink "fa-exclamation-triangle" "#/silences/new" "dark-red" (CreateSilenceFromAlert alert)
buttonLink "fa-exclamation-triangle" "#/silences/new?keep=1" "dark-red" (CreateSilenceFromAlert alert)
in
div [ class "f6 mb3" ]
[ div [ class "mb1" ]

View File

@ -5,9 +5,10 @@ import Html exposing (Html, ul)
import Html.Attributes exposing (class)
import Views.Shared.AlertCompact
view : AlertGroup -> Html msg
view { blocks } =
blocks
|> List.concatMap .alerts
|> List.indexedMap Views.Shared.AlertCompact.view
|> ul [ class "list pa0" ]
blocks
|> List.concatMap .alerts
|> List.indexedMap Views.Shared.AlertCompact.view
|> ul [ class "list pa0" ]

View File

@ -27,7 +27,7 @@ view silence =
div [ class "f6 mb3" ]
[ a
[ class "db link blue mb3"
, href ("#/silence/" ++ silence.id)
, href ("#/silences/" ++ silence.id)
]
[ b [ class "db f4 mb1" ]
[ text alertName ]

View File

@ -5,4 +5,4 @@ import UrlParser exposing (Parser, s, string, (</>))
silenceParser : Parser (String -> a) a
silenceParser =
s "silence" </> string
s "silences" </> string

View File

@ -1,10 +1,12 @@
module Views.Silence.Types exposing (SilenceMsg(..))
import Silences.Types exposing (Silence, SilenceId)
import Alerts.Types exposing (AlertGroup)
import Utils.Types exposing (ApiData)
type SilenceMsg
= FetchSilence String
| SilenceFetched (ApiData Silence)
| AlertGroupsPreview (ApiData (List AlertGroup))
| InitSilenceView SilenceId

View File

@ -3,9 +3,9 @@ module Views.Silence.Updates exposing (update)
import Views.Silence.Types exposing (SilenceMsg(..))
import Types exposing (Model, Msg(MsgForSilence))
import Silences.Api exposing (getSilence)
import Utils.Types exposing (ApiResponse(Success))
import Task
import Types exposing (Msg(PreviewSilence))
import Alerts.Api
import Utils.List
import Utils.Types exposing (ApiResponse(..), nullFilter)
update : SilenceMsg -> Model -> ( Model, Cmd Msg )
@ -14,9 +14,27 @@ update msg model =
FetchSilence id ->
( model, getSilence id (SilenceFetched >> MsgForSilence) )
SilenceFetched (Success sil) ->
( { model | silence = Success sil }
, Task.perform PreviewSilence (Task.succeed sil)
AlertGroupsPreview alertGroups ->
case model.silence of
Success silence ->
( { model
| silence =
Success
{ silence | silencedAlertGroups = alertGroups }
}
, Cmd.none
)
_ ->
( model, Cmd.none )
SilenceFetched (Success silence) ->
( { model
| silence = Success { silence | silencedAlertGroups = Loading }
}
, Alerts.Api.alertGroups
({ nullFilter | text = Just (Utils.List.mjoin silence.matchers) })
|> Cmd.map (AlertGroupsPreview >> MsgForSilence)
)
SilenceFetched silence ->

View File

@ -3,7 +3,7 @@ module Views.Silence.Views exposing (view)
import Silences.Types exposing (Silence)
import Html exposing (Html, div, h2, p, text, label)
import Html.Attributes exposing (class)
import Time
import Time exposing (Time)
import Types exposing (Model, Msg)
import Utils.Types exposing (ApiResponse(Success, Loading, Failure))
import Utils.Views exposing (loading, error)
@ -24,7 +24,7 @@ view model =
error msg
silence : Silence -> Time.Time -> Html Msg
silence : Silence -> Time -> Html Msg
silence silence currentTime =
div []
[ Views.Shared.SilenceBase.view silence
@ -34,7 +34,7 @@ silence silence currentTime =
]
silenceExtra : Silence -> Time.Time -> Html msg
silenceExtra : Silence -> Time -> Html msg
silenceExtra silence currentTime =
div [ class "f6" ]
[ div [ class "mb1" ]
@ -54,18 +54,11 @@ silenceExtra silence currentTime =
]
status : Silence -> Time.Time -> String
status silence currentTime =
let
et =
Maybe.withDefault currentTime silence.endsAt.t
st =
Maybe.withDefault currentTime silence.startsAt.t
in
if et <= currentTime then
"expired"
else if st > currentTime then
"pending"
else
"active"
status : Silence -> Time -> String
status { endsAt, startsAt } currentTime =
if endsAt <= currentTime then
"expired"
else if startsAt > currentTime then
"pending"
else
"active"

View File

@ -1,11 +1,14 @@
module Views.SilenceForm.Parsing exposing (silenceFormNewParser, silenceFormEditParser)
import UrlParser exposing (Parser, s, (</>), string, oneOf, map)
import UrlParser exposing (Parser, s, (</>), (<?>), string, stringParam, oneOf, map)
silenceFormNewParser : Parser a a
silenceFormNewParser : Parser (Bool -> a) a
silenceFormNewParser =
s "silences" </> s "new"
s "silences"
</> s "new"
<?> stringParam "keep"
|> map (Maybe.map (always True) >> Maybe.withDefault False)
silenceFormEditParser : Parser (String -> a) a

View File

@ -1,24 +1,120 @@
module Views.SilenceForm.Types exposing (SilenceFormMsg(..))
module Views.SilenceForm.Types
exposing
( SilenceFormMsg(..)
, SilenceFormFieldMsg(..)
, Model
, SilenceForm
, fromMatchersAndTime
, fromSilence
, toSilence
, initSilenceForm
)
import Silences.Types exposing (Silence)
import Utils.Types exposing (Matcher, ApiData)
import Time
import Silences.Types exposing (Silence, SilenceId)
import Alerts.Types exposing (AlertGroup)
import Utils.Types exposing (Matcher, ApiData, Duration, ApiResponse(..))
import Time exposing (Time)
import Utils.Date exposing (timeToString, timeFromString, durationFormat)
initSilenceForm : Model
initSilenceForm =
{ form = empty
, silence = Err "Empty"
}
toSilence : SilenceForm -> Result String Silence
toSilence { createdBy, comment, startsAt, endsAt, matchers } =
Maybe.map2
(\parsedStartsAt parsedEndsAt ->
{ comment = comment
, matchers = matchers
, createdBy = createdBy
, startsAt = parsedStartsAt
, endsAt = parsedEndsAt
, silencedAlertGroups = Success [] {- ignored -}
, updatedAt = 0 {- ignored -}
, id = "" {- ignored -}
}
)
(timeFromString startsAt)
(timeFromString endsAt)
|> Result.fromMaybe "Wrong datetime format"
fromSilence : Silence -> SilenceForm
fromSilence { createdBy, comment, startsAt, endsAt, matchers } =
{ createdBy = createdBy
, comment = comment
, startsAt = timeToString startsAt
, endsAt = timeToString endsAt
, duration = durationFormat (endsAt - startsAt)
, matchers = matchers
}
empty : SilenceForm
empty =
{ createdBy = ""
, comment = ""
, startsAt = ""
, endsAt = ""
, duration = ""
, matchers = []
}
fromMatchersAndTime : List Matcher -> Time -> SilenceForm
fromMatchersAndTime matchers now =
let
duration =
2 * Time.hour
in
{ empty
| startsAt = timeToString now
, endsAt = timeToString (now + duration)
, duration = durationFormat duration
, matchers = matchers
}
type alias Model =
{ silence : Result String Silence
, form : SilenceForm
}
type alias SilenceForm =
{ createdBy : String
, comment : String
, startsAt : String
, endsAt : String
, duration : String
, matchers : List Matcher
}
type SilenceFormMsg
= AddMatcher Silence
| NewDefaultTimeRange Time.Time
= UpdateField SilenceFormFieldMsg
| CreateSilence Silence
| PreviewSilence Silence
| AlertGroupsPreview (ApiData (List AlertGroup))
| FetchSilence String
| NewSilence
| NewSilenceFromMatchers (List Matcher)
| NewSilenceFromMatchersAndTime (List Matcher) Time
| SilenceFetch (ApiData Silence)
| UpdateCreatedBy Silence String
| DeleteMatcher Silence Matcher
| UpdateDuration Silence String
| UpdateEndsAt Silence String
| UpdateMatcherName Silence Matcher String
| UpdateMatcherRegex Silence Matcher Bool
| UpdateMatcherValue Silence Matcher String
| UpdateStartsAt Silence String
| SilenceCreate (ApiData String)
| UpdateComment Silence String
type SilenceFormFieldMsg
= AddMatcher
| UpdateStartsAt String
| UpdateEndsAt String
| UpdateDuration String
| UpdateCreatedBy String
| UpdateComment String
| DeleteMatcher Int
| UpdateMatcherName Int String
| UpdateMatcherValue Int String
| UpdateMatcherRegex Int Bool

View File

@ -1,29 +1,34 @@
module Views.SilenceForm.Updates exposing (update)
import Views.SilenceForm.Types exposing (SilenceFormMsg(..))
import Types exposing (Model, Msg(MsgForSilenceForm, NewUrl, PreviewSilence))
import Utils.Types exposing (ApiResponse(Success, Loading, Failure))
import Utils.List
import Utils.Date
import Alerts.Api
import Silences.Api
import Silences.Types exposing (nullMatcher, nullSilence)
import Task
import Time
import Navigation
import Types exposing (Msg(MsgForSilenceForm))
import Utils.Date
import Utils.List
import Utils.Types exposing (ApiResponse(..), nullFilter)
import Views.SilenceForm.Types
exposing
( Model
, SilenceForm
, SilenceFormMsg(..)
, SilenceFormFieldMsg(..)
, fromMatchersAndTime
, fromSilence
, toSilence
)
update : SilenceFormMsg -> Model -> ( Model, Cmd Msg )
update msg model =
updateForm : SilenceFormFieldMsg -> SilenceForm -> SilenceForm
updateForm msg form =
case msg of
AddMatcher silence ->
-- TODO: If a user adds two blank matchers and attempts to update
-- one, both are updated because they are identical. Maybe add a
-- unique identifier on creation so this doesn't happen.
( { model | silence = Success { silence | matchers = silence.matchers ++ [ nullMatcher ] } }, Cmd.none )
AddMatcher ->
{ form | matchers = form.matchers ++ [ nullMatcher ] }
CreateSilence silence ->
( { model | silence = Loading }, Silences.Api.create silence (SilenceCreate >> MsgForSilenceForm) )
UpdateStartsAt silence time ->
UpdateStartsAt time ->
-- TODO:
-- Update silence to hold datetime as string, on each pass through
-- here update an error message "this is invalid", but let them put
@ -32,119 +37,142 @@ update msg model =
startsAt =
Utils.Date.timeFromString time
duration =
Maybe.map2 (-) silence.endsAt.t startsAt.t
|> Maybe.map Utils.Date.duration
|> Maybe.withDefault silence.duration
in
( { model | silence = Success { silence | startsAt = startsAt, duration = duration } }, Cmd.none )
endsAt =
Utils.Date.timeFromString form.endsAt
UpdateEndsAt silence time ->
duration =
Maybe.map2 (-) endsAt startsAt
|> Maybe.map Utils.Date.durationFormat
|> Maybe.withDefault ""
in
{ form | startsAt = time, duration = duration }
UpdateEndsAt time ->
let
startsAt =
Utils.Date.timeFromString form.startsAt
endsAt =
Utils.Date.timeFromString time
duration =
Maybe.map2 (-) endsAt.t silence.startsAt.t
|> Maybe.map Utils.Date.duration
|> Maybe.withDefault silence.duration
Maybe.map2 (-) endsAt startsAt
|> Maybe.map Utils.Date.durationFormat
|> Maybe.withDefault ""
in
( { model | silence = Success { silence | endsAt = endsAt, duration = duration } }, Cmd.none )
{ form | endsAt = time, duration = duration }
UpdateDuration silence time ->
UpdateDuration time ->
let
startsAt =
Utils.Date.timeFromString form.startsAt
duration =
Utils.Date.durationFromString time
Utils.Date.parseDuration time
endsAt =
Maybe.map2 (+) silence.startsAt.t duration.d
|> Maybe.map Utils.Date.fromTime
|> Maybe.withDefault silence.endsAt
Maybe.map2 (+) startsAt duration
|> Maybe.map Utils.Date.timeToString
|> Maybe.withDefault form.endsAt
in
( { model | silence = Success { silence | duration = duration, endsAt = endsAt } }, Cmd.none )
{ form | endsAt = endsAt, duration = time }
UpdateCreatedBy silence by ->
( { model | silence = Success { silence | createdBy = by } }, Cmd.none )
UpdateCreatedBy createdBy ->
{ form | createdBy = createdBy }
UpdateComment comment ->
{ form | comment = comment }
DeleteMatcher index ->
{ form | matchers = List.take index form.matchers ++ List.drop (index + 1) form.matchers }
UpdateMatcherName index name ->
{ form
| matchers =
Utils.List.replaceIndex index
(\matcher -> { matcher | name = name })
form.matchers
}
UpdateMatcherValue index value ->
{ form
| matchers =
Utils.List.replaceIndex index
(\matcher -> { matcher | value = value })
form.matchers
}
UpdateMatcherRegex index isRegex ->
{ form
| matchers =
Utils.List.replaceIndex index
(\matcher -> { matcher | isRegex = isRegex })
form.matchers
}
update : SilenceFormMsg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
CreateSilence silence ->
( model
, Silences.Api.create silence
|> Cmd.map (SilenceCreate >> MsgForSilenceForm)
)
SilenceCreate silence ->
case silence of
Success id ->
( { model | silence = Loading }, Task.perform identity (Task.succeed <| NewUrl ("/#/silence/" ++ id)) )
( model, Navigation.newUrl ("/#/silences/" ++ id) )
Failure err ->
( { model | silence = Failure err }, Task.perform identity (Task.succeed <| NewUrl "/#/silence") )
( model, Navigation.newUrl "/#/silences" )
Loading ->
( { model | silence = Loading }, Task.perform identity (Task.succeed <| NewUrl "/#/silence") )
( model, Navigation.newUrl "/#/silences" )
UpdateComment silence comment ->
( { model | silence = Success { silence | comment = comment } }, Cmd.none )
NewSilenceFromMatchers matchers ->
( model, Task.perform (NewSilenceFromMatchersAndTime matchers >> MsgForSilenceForm) Time.now )
DeleteMatcher silence matcher ->
let
-- TODO: This removes all empty matchers. Maybe just remove the
-- one that was clicked.
newSil =
{ silence | matchers = (List.filter (\x -> x /= matcher) silence.matchers) }
in
( { model | silence = Success newSil }, Cmd.none )
UpdateMatcherName silence matcher name ->
let
matchers =
Utils.List.replaceIf (\x -> x == matcher) { matcher | name = name } silence.matchers
in
( { model | silence = Success { silence | matchers = matchers } }, Cmd.none )
UpdateMatcherValue silence matcher value ->
let
matchers =
Utils.List.replaceIf (\x -> x == matcher) { matcher | value = value } silence.matchers
in
( { model | silence = Success { silence | matchers = matchers } }, Cmd.none )
UpdateMatcherRegex silence matcher bool ->
let
matchers =
Utils.List.replaceIf (\x -> x == matcher) { matcher | isRegex = bool } silence.matchers
in
( { model | silence = Success { silence | matchers = matchers } }, Cmd.none )
NewDefaultTimeRange time ->
let
startsAt =
Utils.Date.fromTime time
duration =
Utils.Date.duration (2 * Time.hour)
endsAt =
Utils.Date.fromTime (time + 2 * Time.hour)
sil =
case model.silence of
Success s ->
s
_ ->
nullSilence
in
( { model | silence = Success { sil | startsAt = startsAt, duration = duration, endsAt = endsAt } }, Cmd.none )
NewSilenceFromMatchersAndTime matchers time ->
( { model | form = fromMatchersAndTime matchers time }
, Cmd.none
)
FetchSilence silenceId ->
( { model | silence = model.silence }, Silences.Api.getSilence silenceId (SilenceFetch >> MsgForSilenceForm) )
( model, Silences.Api.getSilence silenceId (SilenceFetch >> MsgForSilenceForm) )
NewSilence ->
( { model | silence = model.silence }, Cmd.map MsgForSilenceForm (Task.perform NewDefaultTimeRange Time.now) )
SilenceFetch (Success silence) ->
( { model | form = fromSilence silence, silence = Ok silence }
, Task.perform (PreviewSilence >> MsgForSilenceForm) (Task.succeed silence)
)
SilenceFetch sil ->
SilenceFetch _ ->
( model, Cmd.none )
PreviewSilence silence ->
( { model | silence = Ok { silence | silencedAlertGroups = Loading } }
, Alerts.Api.alertGroups
{ nullFilter | text = Just (Utils.List.mjoin silence.matchers) }
|> Cmd.map (AlertGroupsPreview >> MsgForSilenceForm)
)
AlertGroupsPreview alertGroups ->
case model.silence of
Ok sil ->
( { model | silence = Ok { sil | silencedAlertGroups = alertGroups } }
, Cmd.none
)
Err _ ->
( model, Cmd.none )
UpdateField fieldMsg ->
let
cmd =
case sil of
Success sil ->
Task.perform identity (Task.succeed (PreviewSilence sil))
newForm =
updateForm fieldMsg model.form
_ ->
Cmd.none
newSilence =
toSilence newForm
in
( { model | silence = sil }, cmd )
( { form = newForm, silence = newSilence }, Cmd.none )

View File

@ -1,74 +1,36 @@
module Views.SilenceForm.Views exposing (edit, new)
module Views.SilenceForm.Views exposing (view)
import Html exposing (Html, div, text, fieldset, legend, label, span, a)
import Html exposing (Html, a, div, fieldset, label, legend, span, text)
import Html.Attributes exposing (class, href)
import Html.Events exposing (onClick)
import Silences.Types exposing (Silence)
import Types exposing (Msg(MsgForSilenceList, PreviewSilence, MsgForSilenceForm), Model)
import Utils.Types exposing (Matcher, ApiResponse(Success, Loading, Failure), ApiData)
import Utils.Views exposing (loading, error, checkbox)
import Silences.Types exposing (Silence, SilenceId)
import Utils.Types exposing (ApiData, ApiResponse(..), Matcher)
import Utils.Views exposing (checkbox, error, formField, formInput, iconButtonMsg, textField)
import Views.Shared.SilencePreview
import Utils.Views exposing (formField, iconButtonMsg, textField, formInput)
import Views.SilenceForm.Types
exposing
( SilenceFormMsg(UpdateStartsAt, UpdateCreatedBy, UpdateDuration, UpdateEndsAt, UpdateComment, AddMatcher, CreateSilence, DeleteMatcher, UpdateMatcherRegex, UpdateMatcherValue, UpdateMatcherName)
)
import Views.SilenceForm.Types exposing (Model, SilenceFormMsg(..))
import Utils.Views exposing (checkbox, formField, formInput, iconButtonMsg, textField)
import Views.Shared.SilencePreview
import Views.SilenceForm.Types exposing (Model, SilenceFormMsg(..), SilenceFormFieldMsg(..))
edit : ApiData Silence -> Html Msg
edit silence =
case silence of
Success silence ->
silenceForm "Edit" silence
Loading ->
loading
Failure msg ->
error msg
new : ApiData Silence -> Html Msg
new silence =
case silence of
Success silence ->
silenceForm "New" silence
Loading ->
loading
Failure msg ->
error msg
silenceForm : String -> Silence -> Html Msg
silenceForm kind silence =
-- TODO: Add field validations.
view : Maybe SilenceId -> Model -> Html SilenceFormMsg
view maybeId { silence, form } =
let
base =
"/#/silence/"
( title, resetClick ) =
case maybeId of
Just silenceId ->
( "Edit Silence", FetchSilence silenceId )
boundMatcherForm =
matcherForm silence
url =
case kind of
"New" ->
base ++ "new"
"Edit" ->
base ++ (toString silence.id) ++ "/edit"
_ ->
"/#/silences"
Nothing ->
( "New Silence", NewSilenceFromMatchers [] )
in
div []
[ div [ class "pa4 black-80" ]
[ fieldset [ class "ba b--transparent ph0 mh0" ]
[ legend [ class "ph0 mh0 fw6" ] [ text <| kind ++ " Silence" ]
, (formField "Start" silence.startsAt.s (UpdateStartsAt silence))
, div [ class "dib mb2 mr2 w-40" ] [ formField "End" silence.endsAt.s (UpdateEndsAt silence) ]
, div [ class "dib mb2 mr2 w-40" ] [ formField "Duration" silence.duration.s (UpdateDuration silence) ]
[ legend [ class "ph0 mh0 fw6" ] [ text title ]
, (formField "Start" form.startsAt (UpdateStartsAt >> UpdateField))
, div [ class "dib mb2 mr2 w-40" ] [ formField "End" form.endsAt (UpdateEndsAt >> UpdateField) ]
, div [ class "dib mb2 mr2 w-40" ] [ formField "Duration" form.duration (UpdateDuration >> UpdateField) ]
, div [ class "mt3" ]
[ label [ class "f6 b db mb2" ]
[ text "Matchers "
@ -77,30 +39,65 @@ silenceForm kind silence =
, label [ class "f6 dib mb2 mr2 w-40" ] [ text "Name" ]
, label [ class "f6 dib mb2 mr2 w-40" ] [ text "Value" ]
]
, div [] (List.map boundMatcherForm silence.matchers)
, iconButtonMsg "blue" "fa-plus" (AddMatcher silence)
, formField "Creator" silence.createdBy (UpdateCreatedBy silence)
, textField "Comment" silence.comment (UpdateComment silence)
, div [] (List.indexedMap matcherForm form.matchers)
, iconButtonMsg "blue" "fa-plus" (AddMatcher |> UpdateField)
, formField "Creator" form.createdBy (UpdateCreatedBy >> UpdateField)
, textField "Comment" form.comment (UpdateComment >> UpdateField)
, div [ class "mt3" ]
[ a [ class "f6 link br2 ba ph3 pv2 mr2 dib blue", onClick (CreateSilence silence) ] [ text "Create" ]
-- Reset isn't working for "New" -- just updates time.
, a [ class "f6 link br2 ba ph3 pv2 mr2 dib dark-red", href url ] [ text "Reset" ]
[ createSilence silence
, a
[ class "f6 link br2 ba ph3 pv2 mr2 dib dark-red"
, onClick resetClick
]
[ text "Reset" ]
]
]
|> (Html.map MsgForSilenceForm)
, div [ class "mt3" ]
[ a [ class "f6 link br2 ba ph3 pv2 mr2 dib dark-green", onClick <| PreviewSilence silence ] [ text "Show Affected Alerts" ]
]
, Views.Shared.SilencePreview.view silence
, preview silence
]
]
matcherForm : Silence -> Matcher -> Html SilenceFormMsg
matcherForm silence matcher =
createSilence : Result String Silence -> Html SilenceFormMsg
createSilence silenceResult =
case silenceResult of
Ok silence ->
a
[ class "f6 link br2 ba ph3 pv2 mr2 dib blue"
, onClick (CreateSilence silence)
]
[ text "Create" ]
Err msg ->
span
[ class "f6 link br2 ba ph3 pv2 mr2 dib red" ]
[ text "Create" ]
preview : Result String Silence -> Html SilenceFormMsg
preview silenceResult =
case silenceResult of
Ok silence ->
div []
[ div [ class "mt3" ]
[ a
[ class "f6 link br2 ba ph3 pv2 mr2 dib dark-green"
, onClick (PreviewSilence silence)
]
[ text "Show Affected Alerts" ]
]
, Views.Shared.SilencePreview.view silence
]
Err message ->
text message
matcherForm : Int -> Matcher -> Html SilenceFormMsg
matcherForm index { name, value, isRegex } =
div []
[ formInput matcher.name (UpdateMatcherName silence matcher)
, formInput matcher.value (UpdateMatcherValue silence matcher)
, checkbox "Regex" matcher.isRegex (UpdateMatcherRegex silence matcher)
, iconButtonMsg "dark-red" "fa-trash-o" (DeleteMatcher silence matcher)
[ formInput name (UpdateMatcherName index)
, formInput value (UpdateMatcherValue index)
, checkbox "Regex" isRegex (UpdateMatcherRegex index)
, iconButtonMsg "dark-red" "fa-trash-o" (DeleteMatcher index)
]
|> Html.map UpdateField

View File

@ -3,13 +3,12 @@ module Views.SilenceList.Updates exposing (..)
import Silences.Api as Api
import Views.SilenceList.Types exposing (SilenceListMsg(..))
import Silences.Types exposing (Silence, nullSilence, nullMatcher)
import Navigation
import Task
import Utils.Types exposing (ApiData, ApiResponse(..), Filter, Matchers)
import Utils.Types as Types exposing (ApiData, ApiResponse(Failure, Loading, Success), Time, Filter, Matchers)
import Time
import Types exposing (Msg(NewUrl, UpdateCurrentTime, PreviewSilence, MsgForSilenceList, Noop), Route(SilenceListRoute))
import Utils.Date
import Utils.List
import Types exposing (Msg(UpdateCurrentTime, MsgForSilenceList), Route(SilenceListRoute))
import Utils.Filter exposing (generateQueryString)
@ -29,17 +28,10 @@ update msg silences silence filter =
-- TODO: "Deleted id: ID" growl
-- TODO: Add DELETE to accepted CORS methods in alertmanager
-- TODO: Check why POST isn't there but is accepted
( silences, Loading, Task.perform identity (Task.succeed <| NewUrl "/#/silences") )
( silences, Loading, Navigation.newUrl "/#/silences" )
FilterSilences ->
let
url =
"/#/silences" ++ generateQueryString filter
cmds =
Task.perform identity (Task.succeed (NewUrl url))
in
( silences, silence, Task.perform identity (Task.succeed <| NewUrl url) )
( silences, silence, Navigation.newUrl ("/#/silences" ++ generateQueryString filter) )
urlUpdate : Maybe String -> ( SilenceListMsg, Filter )

View File

@ -7,15 +7,11 @@ import Html.Attributes exposing (..)
import Html.Events exposing (onClick, onInput)
import Views.SilenceList.Types exposing (SilenceListMsg(..))
import Views.Shared.SilenceBase
import Views.Shared.SilencePreview
import Silences.Types exposing (Silence)
import Utils.Types exposing (Matcher, ApiResponse(..), Filter, ApiData)
import Utils.Views exposing (iconButtonMsg, checkbox, textField, formInput, formField, buttonLink, error, loading)
import Utils.Date
import Utils.List
import Time
import Views.AlertList.Views
import Types exposing (Msg(UpdateFilter, PreviewSilence, MsgForSilenceList, Noop))
import Types exposing (Msg(UpdateFilter, MsgForSilenceList, Noop))
view : ApiData (List Silence) -> ApiData Silence -> Time.Time -> Filter -> Html Msg

View File

@ -4,7 +4,6 @@ import Html exposing (Html, text, button, div, li, ul, b)
import Status.Types exposing (StatusResponse)
import Types exposing (Msg(MsgForStatus), Model)
import Utils.Types exposing (ApiResponse(Failure, Success, Loading), ApiData)
import Utils.Views exposing (loading)
view : Model -> Html Types.Msg

View File

@ -104,7 +104,7 @@ func uiAppScriptJs() (*asset, error) {
return nil, err
}
info := bindataFileInfo{name: "ui/app/script.js", size: 398073, mode: os.FileMode(420), modTime: time.Unix(1490373277, 0)}
info := bindataFileInfo{name: "ui/app/script.js", size: 398073, mode: os.FileMode(420), modTime: time.Unix(1490373820, 0)}
a := &asset{bytes: bytes, info: info}
return a, nil
}