Negative matchers on the Silence form page (#2488)

* Support negative matchers in silence form

Signed-off-by: Andrey Kuzmin <unsoundscapes@gmail.com>

* Extract url manipulation from filterBar

This is needed for silence form, where we don't have to
manipulate the url.

Signed-off-by: Andrey Kuzmin <unsoundscapes@gmail.com>

* Only show the silence button in the alert list

Signed-off-by: Andrey Kuzmin <unsoundscapes@gmail.com>

* Validate matchers

Signed-off-by: Andrey Kuzmin <unsoundscapes@gmail.com>

* Improve silence form layout

Signed-off-by: Andrey Kuzmin <unsoundscapes@gmail.com>

* Fix for editing existing silence

Signed-off-by: Andrey Kuzmin <unsoundscapes@gmail.com>

* Fix for resetting the form

Signed-off-by: Andrey Kuzmin <unsoundscapes@gmail.com>

* Update assets_vfsdata.go

Signed-off-by: Andrey Kuzmin <unsoundscapes@gmail.com>
This commit is contained in:
Andrey Kuzmin 2021-03-01 16:40:32 +01:00 committed by GitHub
parent d57b2dcdec
commit b0083ec55d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 312 additions and 402 deletions

File diff suppressed because one or more lines are too long

View File

@ -5,9 +5,9 @@ module Utils.Filter exposing
, SilenceFormGetParams
, convertFilterMatcher
, emptySilenceFormGetParams
, fromApiMatcher
, generateAPIQueryString
, generateQueryParam
, generateQueryString
, nullFilter
, parseFilter
, parseGroup
@ -16,6 +16,9 @@ module Utils.Filter exposing
, stringifyFilter
, stringifyGroup
, stringifyMatcher
, toApiMatcher
, toUrl
, withMatchers
)
import Char
@ -53,8 +56,8 @@ generateQueryParam name =
Maybe.map (percentEncode >> (++) (name ++ "="))
generateQueryString : Filter -> String
generateQueryString { receiver, customGrouping, showSilenced, showInhibited, showActive, text, group } =
toUrl : String -> Filter -> String
toUrl baseUrl { receiver, customGrouping, showSilenced, showInhibited, showActive, text, group } =
let
parts =
[ ( "silenced", Maybe.withDefault False showSilenced |> boolToString |> Just )
@ -68,12 +71,14 @@ generateQueryString { receiver, customGrouping, showSilenced, showInhibited, sho
|> List.filterMap (\( a, b ) -> generateQueryParam a b)
in
if List.length parts > 0 then
parts
|> String.join "&"
|> (++) "?"
baseUrl
++ (parts
|> String.join "&"
|> (++) "?"
)
else
""
baseUrl
generateAPIQueryString : Filter -> String
@ -141,8 +146,32 @@ type alias Matcher =
}
convertAPIMatcher : Data.Matcher.Matcher -> Matcher
convertAPIMatcher { name, value, isRegex, isEqual } =
toApiMatcher : Matcher -> Data.Matcher.Matcher
toApiMatcher { key, op, value } =
let
( isRegex, isEqual ) =
case op of
Eq ->
( False, True )
NotEq ->
( False, False )
RegexMatch ->
( True, True )
NotRegexMatch ->
( True, False )
in
{ name = key
, isRegex = isRegex
, isEqual = Just isEqual
, value = value
}
fromApiMatcher : Data.Matcher.Matcher -> Matcher
fromApiMatcher { name, value, isRegex, isEqual } =
let
isEqualValue =
case isEqual of
@ -333,11 +362,16 @@ isVarChar char =
|| Char.isDigit char
withMatchers : List Matcher -> Filter -> Filter
withMatchers matchers_ filter_ =
{ filter_ | text = Just (stringifyFilter matchers_) }
silencePreviewFilter : List Data.Matcher.Matcher -> Filter
silencePreviewFilter apiMatchers =
{ nullFilter
| text =
List.map convertAPIMatcher apiMatchers
List.map fromApiMatcher apiMatchers
|> stringifyFilter
|> Just
, showSilenced = Just True

View File

@ -1,79 +0,0 @@
module Views.AlertList.Filter exposing (matchers)
import Alerts.Types exposing (Alert, AlertGroup, Block)
import Regex exposing (contains, regex)
import Utils.Types exposing (Matchers)
matchers : Maybe Utils.Types.Matchers -> List AlertGroup -> List AlertGroup
matchers matchers alerts =
case matchers of
Just ms ->
by (filterAlertGroupLabels ms) alerts
Nothing ->
alerts
alertsFromBlock : (Alert -> Bool) -> Block -> Maybe Block
alertsFromBlock fn block =
let
alerts =
List.filter fn block.alerts
in
if not <| List.isEmpty alerts then
Just { block | alerts = alerts }
else
Nothing
byLabel : Utils.Types.Matchers -> Block -> Maybe Block
byLabel matchers block =
alertsFromBlock
(\a ->
-- Check that all labels are present within the alert's label set.
List.all
(\m ->
-- Check for regex or direct match
if m.isRegex then
-- Check if key is present, then regex match value.
let
x =
List.head <| List.filter (\( key, value ) -> key == m.name) a.labels
regex =
Regex.regex m.value
in
-- No regex match
case x of
Just ( _, value ) ->
Regex.contains regex value
Nothing ->
False
else
List.member ( m.name, m.value ) a.labels
)
matchers
)
block
filterAlertGroupLabels : Utils.Types.Matchers -> AlertGroup -> Maybe AlertGroup
filterAlertGroupLabels matchers alertGroup =
let
blocks =
List.filterMap (byLabel matchers) alertGroup.blocks
in
if not <| List.isEmpty blocks then
Just { alertGroup | blocks = blocks }
else
Nothing
by : (a -> Maybe a) -> List a -> List a
by fn groups =
List.filterMap fn groups

View File

@ -55,7 +55,7 @@ initAlertList key expandAll =
, alertGroups = Initial
, receiverBar = ReceiverBar.initReceiverBar key
, groupBar = GroupBar.initGroupBar key
, filterBar = FilterBar.initFilterBar key
, filterBar = FilterBar.initFilterBar []
, tab = FilterTab
, activeId = Nothing
, activeGroups = Set.empty

View File

@ -7,7 +7,7 @@ import Dict
import Set
import Task
import Types exposing (Msg(..))
import Utils.Filter exposing (Filter, generateQueryString, parseFilter)
import Utils.Filter exposing (Filter)
import Utils.List
import Utils.Types exposing (ApiData(..))
import Views.AlertList.Types exposing (AlertListMsg(..), Model, Tab(..))
@ -21,6 +21,9 @@ update msg ({ groupBar, alerts, filterBar, receiverBar, alertGroups } as model)
let
alertsUrl =
basePath ++ "#/alerts"
filteredUrl =
Utils.Filter.toUrl alertsUrl
in
case msg of
AlertGroupsFetched listOfAlertGroups ->
@ -109,12 +112,12 @@ update msg ({ groupBar, alerts, filterBar, receiverBar, alertGroups } as model)
ToggleSilenced showSilenced ->
( model
, Navigation.pushUrl model.key (alertsUrl ++ generateQueryString { filter | showSilenced = Just showSilenced })
, Navigation.pushUrl model.key (filteredUrl { filter | showSilenced = Just showSilenced })
)
ToggleInhibited showInhibited ->
( model
, Navigation.pushUrl model.key (alertsUrl ++ generateQueryString { filter | showInhibited = Just showInhibited })
, Navigation.pushUrl model.key (filteredUrl { filter | showInhibited = Just showInhibited })
)
SetTab tab ->
@ -122,10 +125,26 @@ update msg ({ groupBar, alerts, filterBar, receiverBar, alertGroups } as model)
MsgForFilterBar subMsg ->
let
( newFilterBar, cmd ) =
FilterBar.update alertsUrl filter subMsg filterBar
( newFilterBar, shouldFilter, cmd ) =
FilterBar.update subMsg filterBar
filterBarCmd =
Cmd.map (MsgForFilterBar >> MsgForAlertList) cmd
newUrl =
filteredUrl (Utils.Filter.withMatchers newFilterBar.matchers filter)
alertsCmd =
if shouldFilter then
Cmd.batch
[ Navigation.pushUrl model.key newUrl
, filterBarCmd
]
else
filterBarCmd
in
( { model | filterBar = newFilterBar, tab = FilterTab }, Cmd.map (MsgForFilterBar >> MsgForAlertList) cmd )
( { model | filterBar = newFilterBar, tab = FilterTab }, alertsCmd )
MsgForGroupBar subMsg ->
let

View File

@ -65,7 +65,7 @@ view { alerts, alertGroups, groupBar, filterBar, receiverBar, tab, activeId, act
, div [ class "card-block" ]
[ case tab of
FilterTab ->
Html.map (MsgForFilterBar >> MsgForAlertList) (FilterBar.view filterBar)
Html.map (MsgForFilterBar >> MsgForAlertList) (FilterBar.view { showSilenceButton = True } filterBar)
GroupTab ->
Html.map (MsgForGroupBar >> MsgForAlertList) (GroupBar.view groupBar filter.customGrouping)

View File

@ -1,6 +1,5 @@
module Views.FilterBar.Types exposing (Model, Msg(..), initFilterBar)
import Browser.Navigation exposing (Key)
import Utils.Filter
@ -8,7 +7,6 @@ type alias Model =
{ matchers : List Utils.Filter.Matcher
, backspacePressed : Bool
, matcherText : String
, key : Key
}
@ -30,10 +28,9 @@ backspace to clear an input, they have to then lift up the key and press it agai
proceed to deleting the next matcher.
-}
initFilterBar : Key -> Model
initFilterBar key =
{ matchers = []
initFilterBar : List Utils.Filter.Matcher -> Model
initFilterBar matchers =
{ matchers = matchers
, backspacePressed = False
, matcherText = ""
, key = key
}

View File

@ -1,68 +1,59 @@
module Views.FilterBar.Updates exposing (setMatchers, update)
import Browser.Dom as Dom
import Browser.Navigation as Navigation
import Task
import Utils.Filter exposing (Filter, generateQueryString, parseFilter, stringifyFilter)
import Utils.Filter exposing (Filter, parseFilter)
import Views.FilterBar.Types exposing (Model, Msg(..))
update : String -> Filter -> Msg -> Model -> ( Model, Cmd Msg )
update url filter msg model =
{-| Returns a triple where the Bool component notifies whether the matchers have changed.
-}
update : Msg -> Model -> ( Model, Bool, Cmd Msg )
update msg model =
case msg of
AddFilterMatcher emptyMatcherText matcher ->
immediatelyFilter url
filter
{ model
| matchers =
if List.member matcher model.matchers then
model.matchers
( { model
| matchers =
if List.member matcher model.matchers then
model.matchers
else
model.matchers ++ [ matcher ]
, matcherText =
if emptyMatcherText then
""
else
model.matchers ++ [ matcher ]
, matcherText =
if emptyMatcherText then
""
else
model.matcherText
}
else
model.matcherText
}
, True
, Dom.focus "filter-bar-matcher"
|> Task.attempt (always Noop)
)
DeleteFilterMatcher setMatcherText matcher ->
immediatelyFilter url
filter
{ model
| matchers = List.filter ((/=) matcher) model.matchers
, matcherText =
if setMatcherText then
Utils.Filter.stringifyMatcher matcher
( { model
| matchers = List.filter ((/=) matcher) model.matchers
, matcherText =
if setMatcherText then
Utils.Filter.stringifyMatcher matcher
else
model.matcherText
}
else
model.matcherText
}
, True
, Dom.focus "filter-bar-matcher"
|> Task.attempt (always Noop)
)
UpdateMatcherText value ->
( { model | matcherText = value }, Cmd.none )
( { model | matcherText = value }, False, Cmd.none )
PressingBackspace isPressed ->
( { model | backspacePressed = isPressed }, Cmd.none )
( { model | backspacePressed = isPressed }, False, Cmd.none )
Noop ->
( model, Cmd.none )
immediatelyFilter : String -> Filter -> Model -> ( Model, Cmd Msg )
immediatelyFilter url filter model =
let
newFilter =
{ filter | text = Just (stringifyFilter model.matchers) }
in
( { model | matchers = [] }
, Cmd.batch
[ Navigation.pushUrl model.key (url ++ generateQueryString newFilter)
, Dom.focus "filter-bar-matcher" |> Task.attempt (always Noop)
]
)
( model, False, Cmd.none )
setMatchers : Filter -> Model -> Model

View File

@ -45,8 +45,8 @@ viewMatchers matchers =
|> List.map viewMatcher
view : Model -> Html Msg
view { matchers, matcherText, backspacePressed } =
view : { showSilenceButton : Bool } -> Model -> Html Msg
view { showSilenceButton } { matchers, matcherText, backspacePressed } =
let
maybeMatcher =
Utils.Filter.parseMatcher matcherText
@ -112,28 +112,44 @@ view { matchers, matcherText, backspacePressed } =
(viewMatchers matchers
++ [ div
[ class ("col " ++ className)
, style "min-width" "200px"
, style "min-width"
(if showSilenceButton then
"300px"
else
"200px"
)
]
[ div [ class "input-group" ]
[ input
[ id "filter-bar-matcher"
, class "form-control"
, value matcherText
, onKeyDown keyDown
, onKeyUp keyUp
, onInput UpdateMatcherText
]
[]
, span
[ class "input-group-btn" ]
[ button [ class "btn btn-primary", disabled isDisabled, onClickAttr ] [ text "+" ] ]
, a
[ class "btn btn-outline-info border-0"
, href (newSilenceFromMatchers dataMatchers)
]
[ i [ class "fa fa-bell-slash-o mr-2" ] []
, text "Silence"
[ div [ class "row no-gutters align-content-stretch" ]
[ div [ class "col input-group" ]
[ input
[ id "filter-bar-matcher"
, class "form-control"
, value matcherText
, onKeyDown keyDown
, onKeyUp keyUp
, onInput UpdateMatcherText
]
[]
, span
[ class "input-group-btn" ]
[ button [ class "btn btn-primary", disabled isDisabled, onClickAttr ] [ text "+" ] ]
]
, if showSilenceButton then
div [ class "col col-auto input-group-btn ml-2" ]
[ div [ class "input-group" ]
[ a
[ class "btn btn-outline-info"
, href (newSilenceFromMatchers dataMatchers)
]
[ i [ class "fa fa-bell-slash-o mr-2" ] []
, text "Silence"
]
]
]
else
text ""
]
, small [ class "form-text text-muted" ]
[ text "Custom matcher, e.g."

View File

@ -4,7 +4,7 @@ import Browser.Dom as Dom
import Browser.Navigation as Navigation
import Set
import Task
import Utils.Filter exposing (Filter, generateQueryString, parseGroup, stringifyGroup)
import Utils.Filter exposing (Filter, parseGroup, stringifyGroup)
import Utils.Match exposing (jaroWinkler)
import Views.GroupBar.Types exposing (Model, Msg(..))
@ -15,7 +15,7 @@ update url filter msg model =
CustomGrouping customGrouping ->
( model
, Cmd.batch
[ Navigation.pushUrl model.key (url ++ generateQueryString { filter | customGrouping = customGrouping })
[ Navigation.pushUrl model.key (Utils.Filter.toUrl url { filter | customGrouping = customGrouping })
, Dom.focus "group-by-field" |> Task.attempt (always Noop)
]
)
@ -87,7 +87,7 @@ immediatelyFilter url filter model =
in
( model
, Cmd.batch
[ Navigation.pushUrl model.key (url ++ generateQueryString newFilter)
[ Navigation.pushUrl model.key (Utils.Filter.toUrl url newFilter)
, Dom.focus "group-by-field" |> Task.attempt (always Noop)
]
)

View File

@ -4,7 +4,7 @@ import Alerts.Api as Api
import Browser.Dom as Dom
import Browser.Navigation as Navigation
import Task
import Utils.Filter exposing (Filter, generateQueryString, parseGroup, stringifyGroup)
import Utils.Filter exposing (Filter)
import Utils.Match exposing (jaroWinkler)
import Utils.Types exposing (ApiData(..))
import Views.ReceiverBar.Types exposing (Model, Msg(..), apiReceiverToReceiver)
@ -60,16 +60,15 @@ update url filter msg model =
FilterByReceiver regex ->
( { model | showReceivers = False, resultsHovered = False }
, Navigation.pushUrl model.key
(url
++ generateQueryString
{ filter
| receiver =
if regex == "" then
Nothing
(Utils.Filter.toUrl url
{ filter
| receiver =
if regex == "" then
Nothing
else
Just regex
}
else
Just regex
}
)
)

View File

@ -1,17 +1,17 @@
module Views.SilenceForm.Types exposing
( MatcherForm
, Model
( Model
, SilenceForm
, SilenceFormFieldMsg(..)
, SilenceFormMsg(..)
, emptyMatcher
, fromDateTimePicker
, fromMatchersAndCommentAndTime
, fromSilence
, initSilenceForm
, parseEndsAt
, toSilence
, validMatchers
, validateForm
, validateMatchers
)
import Browser.Navigation exposing (Key)
@ -34,10 +34,13 @@ import Utils.FormValidation
, validate
)
import Utils.Types exposing (ApiData(..), Duration)
import Views.FilterBar.Types as FilterBar
type alias Model =
{ form : SilenceForm
, filterBar : FilterBar.Model
, filterBarValid : ValidationState
, silenceId : ApiData String
, alerts : ApiData (List GettableAlert)
, activeAlertId : Maybe String
@ -52,20 +55,11 @@ type alias SilenceForm =
, startsAt : ValidatedField
, endsAt : ValidatedField
, duration : ValidatedField
, matchers : List MatcherForm
, dateTimePicker : DateTimePicker
, viewDateTimePicker : Bool
}
type alias MatcherForm =
{ name : ValidatedField
, value : ValidatedField
, isRegex : Bool
, isEqual : Maybe Bool
}
type SilenceFormMsg
= UpdateField SilenceFormFieldMsg
| CreateSilence
@ -78,12 +72,11 @@ type SilenceFormMsg
| SilenceFetch (ApiData GettableSilence)
| SilenceCreate (ApiData String)
| UpdateDateTimePicker Utils.DateTimePicker.Types.Msg
| MsgForFilterBar FilterBar.Msg
type SilenceFormFieldMsg
= AddMatcher
| DeleteMatcher Int
| UpdateStartsAt String
= UpdateStartsAt String
| UpdateEndsAt String
| UpdateDuration String
| ValidateTime
@ -91,11 +84,6 @@ type SilenceFormFieldMsg
| ValidateCreatedBy
| UpdateComment String
| ValidateComment
| UpdateMatcherName Int String
| ValidateMatcherName Int
| UpdateMatcherValue Int String
| ValidateMatcherValue Int
| UpdateMatcherRegex Int Bool
| UpdateTimesFromPicker
| OpenDateTimePicker
| CloseDateTimePicker
@ -104,6 +92,8 @@ type SilenceFormFieldMsg
initSilenceForm : Key -> Model
initSilenceForm key =
{ form = empty
, filterBar = FilterBar.initFilterBar []
, filterBarValid = Utils.FormValidation.Initial
, silenceId = Utils.Types.Initial
, alerts = Utils.Types.Initial
, activeAlertId = Nothing
@ -111,29 +101,43 @@ initSilenceForm key =
}
toSilence : SilenceForm -> Maybe PostableSilence
toSilence { id, comment, matchers, createdBy, startsAt, endsAt } =
toSilence : FilterBar.Model -> SilenceForm -> Maybe PostableSilence
toSilence filterBar { id, comment, createdBy, startsAt, endsAt } =
Result.map5
(\nonEmptyComment validMatchers nonEmptyCreatedBy parsedStartsAt parsedEndsAt ->
(\nonEmptyMatchers nonEmptyComment nonEmptyCreatedBy parsedStartsAt parsedEndsAt ->
{ nullSilence
| id = id
, comment = nonEmptyComment
, matchers = validMatchers
, matchers = nonEmptyMatchers
, createdBy = nonEmptyCreatedBy
, startsAt = parsedStartsAt
, endsAt = parsedEndsAt
}
)
(validMatchers filterBar)
(stringNotEmpty comment.value)
(List.foldr appendMatcher (Ok []) matchers)
(stringNotEmpty createdBy.value)
(timeFromString startsAt.value)
(parseEndsAt startsAt.value endsAt.value)
|> Result.toMaybe
validMatchers : FilterBar.Model -> Result String (List Data.Matcher.Matcher)
validMatchers { matchers, matcherText } =
if matcherText /= "" then
Err "Please complete adding the matcher"
else
case matchers of
[] ->
Err "Matchers are required"
nonEmptyMatchers ->
Ok (List.map Utils.Filter.toApiMatcher nonEmptyMatchers)
fromSilence : GettableSilence -> SilenceForm
fromSilence { id, createdBy, comment, startsAt, endsAt, matchers } =
fromSilence { id, createdBy, comment, startsAt, endsAt } =
let
startsPosix =
Utils.Date.timeFromString (DateTime.toString startsAt)
@ -149,26 +153,34 @@ fromSilence { id, createdBy, comment, startsAt, endsAt, matchers } =
, startsAt = initialField (timeToString startsAt)
, endsAt = initialField (timeToString endsAt)
, duration = initialField (durationFormat (timeDifference startsAt endsAt) |> Maybe.withDefault "")
, matchers = List.map fromMatcher matchers
, dateTimePicker = initFromStartAndEndTime startsPosix endsPosix
, viewDateTimePicker = False
}
validateForm : SilenceForm -> SilenceForm
validateForm { id, createdBy, comment, startsAt, endsAt, duration, matchers, dateTimePicker } =
validateForm { id, createdBy, comment, startsAt, endsAt, duration, dateTimePicker } =
{ id = id
, createdBy = validate stringNotEmpty createdBy
, comment = validate stringNotEmpty comment
, startsAt = validate timeFromString startsAt
, endsAt = validate (parseEndsAt startsAt.value) endsAt
, duration = validate parseDuration duration
, matchers = List.map validateMatcherForm matchers
, dateTimePicker = dateTimePicker
, viewDateTimePicker = False
}
validateMatchers : FilterBar.Model -> ValidationState
validateMatchers filter =
case validMatchers filter of
Err error ->
Utils.FormValidation.Invalid error
Ok _ ->
Utils.FormValidation.Valid
parseEndsAt : String -> String -> Result String Posix
parseEndsAt startsAt endsAt =
case ( timeFromString startsAt, timeFromString endsAt ) of
@ -183,15 +195,6 @@ parseEndsAt startsAt endsAt =
endsResult
validateMatcherForm : MatcherForm -> MatcherForm
validateMatcherForm { name, value, isRegex, isEqual } =
{ name = validate stringNotEmpty name
, value = value
, isRegex = isRegex
, isEqual = isEqual
}
empty : SilenceForm
empty =
{ id = Nothing
@ -200,87 +203,38 @@ empty =
, startsAt = initialField ""
, endsAt = initialField ""
, duration = initialField ""
, matchers = []
, dateTimePicker = initDateTimePicker
, viewDateTimePicker = False
}
emptyMatcher : MatcherForm
emptyMatcher =
{ isRegex = False
, isEqual = Just True
, name = initialField ""
, value = initialField ""
}
defaultDuration : Float
defaultDuration =
-- 2 hours
2 * 60 * 60 * 1000
fromMatchersAndCommentAndTime : String -> List Utils.Filter.Matcher -> String -> Posix -> SilenceForm
fromMatchersAndCommentAndTime defaultCreator matchers comment now =
fromMatchersAndCommentAndTime : String -> String -> Posix -> SilenceForm
fromMatchersAndCommentAndTime defaultCreator comment now =
{ empty
| startsAt = initialField (timeToString now)
, endsAt = initialField (timeToString (addDuration defaultDuration now))
, duration = initialField (durationFormat defaultDuration |> Maybe.withDefault "")
, createdBy = initialField defaultCreator
, matchers =
-- If no matchers were specified, add an empty row
if List.isEmpty matchers then
[ emptyMatcher ]
else
List.filterMap (filterMatcherToMatcher >> Maybe.map fromMatcher) matchers
, comment = initialField comment
, dateTimePicker = initFromStartAndEndTime (Just now) (Just (addDuration defaultDuration now))
, viewDateTimePicker = False
}
appendMatcher : MatcherForm -> Result String (List Matcher) -> Result String (List Matcher)
appendMatcher { isRegex, isEqual, name, value } =
Result.map2 (::)
(Result.map2 (\k v -> Matcher k v isRegex isEqual) (stringNotEmpty name.value) (Ok value.value))
filterMatcherToMatcher : Utils.Filter.Matcher -> Maybe Matcher
filterMatcherToMatcher { key, op, value } =
case op of
Utils.Filter.Eq ->
Maybe.map2 (\isRegex isEqual -> Matcher key value isRegex isEqual) (Just False) (Just (Just True))
Utils.Filter.RegexMatch ->
Maybe.map2 (\isRegex isEqual -> Matcher key value isRegex isEqual) (Just True) (Just (Just True))
Utils.Filter.NotRegexMatch ->
Maybe.map2 (\isRegex isEqual -> Matcher key value isRegex isEqual) (Just True) (Just (Just False))
Utils.Filter.NotEq ->
Maybe.map2 (\isRegex isEqual -> Matcher key value isRegex isEqual) (Just False) (Just (Just False))
fromMatcher : Matcher -> MatcherForm
fromMatcher { name, value, isRegex, isEqual } =
{ name = initialField name
, value = initialField value
, isRegex = isRegex
, isEqual = isEqual
}
fromDateTimePicker : SilenceForm -> DateTimePicker -> SilenceForm
fromDateTimePicker { id, createdBy, comment, startsAt, endsAt, duration, matchers, dateTimePicker } newPicker =
fromDateTimePicker { id, createdBy, comment, startsAt, endsAt, duration } newPicker =
{ id = id
, createdBy = createdBy
, comment = comment
, startsAt = startsAt
, endsAt = endsAt
, duration = duration
, matchers = matchers
, dateTimePicker = newPicker
, viewDateTimePicker = True
}

View File

@ -13,28 +13,27 @@ import Utils.Filter exposing (silencePreviewFilter)
import Utils.FormValidation exposing (fromResult, initialField, stringNotEmpty, updateValue, validate)
import Utils.List
import Utils.Types exposing (ApiData(..))
import Views.FilterBar.Types as FilterBar
import Views.FilterBar.Updates as FilterBar
import Views.SilenceForm.Types
exposing
( Model
, SilenceForm
, SilenceFormFieldMsg(..)
, SilenceFormMsg(..)
, emptyMatcher
, fromDateTimePicker
, fromMatchersAndCommentAndTime
, fromSilence
, parseEndsAt
, toSilence
, validateForm
, validateMatchers
)
updateForm : SilenceFormFieldMsg -> SilenceForm -> SilenceForm
updateForm msg form =
case msg of
AddMatcher ->
{ form | matchers = form.matchers ++ [ emptyMatcher ] }
UpdateStartsAt time ->
let
startsAt =
@ -127,54 +126,6 @@ updateForm msg form =
ValidateComment ->
{ form | comment = validate stringNotEmpty form.comment }
DeleteMatcher index ->
{ form | matchers = List.take index form.matchers ++ List.drop (index + 1) form.matchers }
UpdateMatcherName index name ->
let
matchers =
Utils.List.replaceIndex index
(\matcher -> { matcher | name = updateValue name matcher.name })
form.matchers
in
{ form | matchers = matchers }
ValidateMatcherName index ->
let
matchers =
Utils.List.replaceIndex index
(\matcher -> { matcher | name = validate stringNotEmpty matcher.name })
form.matchers
in
{ form | matchers = matchers }
UpdateMatcherValue index value ->
let
matchers =
Utils.List.replaceIndex index
(\matcher -> { matcher | value = updateValue value matcher.value })
form.matchers
in
{ form | matchers = matchers }
ValidateMatcherValue index ->
let
matchers =
Utils.List.replaceIndex index
(\matcher -> { matcher | value = matcher.value })
form.matchers
in
{ form | matchers = matchers }
UpdateMatcherRegex index isRegex ->
let
matchers =
Utils.List.replaceIndex index
(\matcher -> { matcher | isRegex = isRegex })
form.matchers
in
{ form | matchers = matchers }
UpdateTimesFromPicker ->
let
( startsAt, endsAt, duration ) =
@ -224,7 +175,7 @@ update : SilenceFormMsg -> Model -> String -> String -> ( Model, Cmd Msg )
update msg model basePath apiUrl =
case msg of
CreateSilence ->
case toSilence model.form of
case toSilence model.filterBar model.form of
Just silence ->
( { model | silenceId = Loading }
, Cmd.batch
@ -238,6 +189,7 @@ update msg model basePath apiUrl =
( { model
| silenceId = Failure "Could not submit the form, Silence is not yet valid."
, form = validateForm model.form
, filterBarValid = validateMatchers model.filterBar
}
, Cmd.none
)
@ -258,10 +210,12 @@ update msg model basePath apiUrl =
( model, Task.perform (NewSilenceFromMatchersAndCommentAndTime defaultCreator params.matchers params.comment >> MsgForSilenceForm) Time.now )
NewSilenceFromMatchersAndCommentAndTime defaultCreator matchers comment time ->
( { form = fromMatchersAndCommentAndTime defaultCreator matchers comment time
( { form = fromMatchersAndCommentAndTime defaultCreator comment time
, alerts = Initial
, activeAlertId = Nothing
, silenceId = Initial
, filterBar = FilterBar.initFilterBar matchers
, filterBarValid = Utils.FormValidation.Initial
, key = model.key
}
, Cmd.none
@ -271,7 +225,14 @@ update msg model basePath apiUrl =
( model, Silences.Api.getSilence apiUrl silenceId (SilenceFetch >> MsgForSilenceForm) )
SilenceFetch (Success silence) ->
( { model | form = fromSilence silence }
( { form = fromSilence silence
, filterBar = FilterBar.initFilterBar (List.map Utils.Filter.fromApiMatcher silence.matchers)
, filterBarValid = Utils.FormValidation.Initial
, silenceId = model.silenceId
, alerts = Initial
, activeAlertId = Nothing
, key = model.key
}
, Task.perform identity (Task.succeed (MsgForSilenceForm PreviewSilence))
)
@ -279,7 +240,7 @@ update msg model basePath apiUrl =
( model, Cmd.none )
PreviewSilence ->
case toSilence model.form of
case toSilence model.filterBar model.form of
Just silence ->
( { model | alerts = Loading }
, Alerts.Api.fetchAlerts
@ -292,6 +253,7 @@ update msg model basePath apiUrl =
( { model
| alerts = Failure "Can not display affected Alerts, Silence is not yet valid."
, form = validateForm model.form
, filterBarValid = validateMatchers model.filterBar
}
, Cmd.none
)
@ -307,11 +269,10 @@ update msg model basePath apiUrl =
)
UpdateField fieldMsg ->
( { form = updateForm fieldMsg model.form
, alerts = Initial
, silenceId = Initial
, key = model.key
, activeAlertId = model.activeAlertId
( { model
| form = updateForm fieldMsg model.form
, alerts = Initial
, silenceId = Initial
}
, Cmd.none
)
@ -327,5 +288,14 @@ update msg model basePath apiUrl =
, Cmd.none
)
MsgForFilterBar subMsg ->
let
( newFilterBar, _, subCmd ) =
FilterBar.update subMsg model.filterBar
in
( { model | filterBar = newFilterBar, filterBarValid = Utils.FormValidation.Initial }
, Cmd.map (MsgForFilterBar >> MsgForSilenceForm) subCmd
)
port persistDefaultCreator : String -> Cmd msg

View File

@ -5,17 +5,19 @@ import Html exposing (Html, a, button, div, fieldset, h1, h5, i, input, label, l
import Html.Attributes exposing (class, href, style)
import Html.Events exposing (onClick)
import Utils.DateTimePicker.Views exposing (viewDateTimePicker)
import Utils.Filter exposing (SilenceFormGetParams, emptySilenceFormGetParams)
import Utils.Filter exposing (SilenceFormGetParams)
import Utils.FormValidation exposing (ValidatedField, ValidationState(..))
import Utils.Types exposing (ApiData)
import Utils.Views exposing (checkbox, iconButtonMsg, loading, validatedField, validatedTextareaField)
import Views.FilterBar.Types as FilterBar
import Views.FilterBar.Views as FilterBar
import Views.Shared.SilencePreview
import Views.Shared.Types exposing (Msg)
import Views.SilenceForm.Types exposing (MatcherForm, Model, SilenceForm, SilenceFormFieldMsg(..), SilenceFormMsg(..))
import Views.SilenceForm.Types exposing (Model, SilenceForm, SilenceFormFieldMsg(..), SilenceFormMsg(..), validMatchers)
view : Maybe String -> SilenceFormGetParams -> String -> Model -> Html SilenceFormMsg
view maybeId { matchers, comment } defaultCreator { form, silenceId, alerts, activeAlertId } =
view maybeId silenceFormGetParams defaultCreator { form, filterBar, filterBarValid, silenceId, alerts, activeAlertId } =
let
( title, resetClick ) =
case maybeId of
@ -23,12 +25,12 @@ view maybeId { matchers, comment } defaultCreator { form, silenceId, alerts, act
( "Edit Silence", FetchSilence silenceId_ )
Nothing ->
( "New Silence", NewSilenceFromMatchersAndComment defaultCreator emptySilenceFormGetParams )
( "New Silence", NewSilenceFromMatchersAndComment defaultCreator silenceFormGetParams )
in
div []
[ h1 [] [ text title ]
, timeInput form.startsAt form.endsAt form.duration
, matcherInput form.matchers
, matchersInput filterBarValid filterBar
, validatedField input
"Creator"
inputSectionPadding
@ -101,30 +103,29 @@ timeInput startsAt endsAt duration =
div [ class <| "row " ++ inputSectionPadding ]
[ validatedField input
"Start"
"col-4"
"col-lg-4 col-6"
(UpdateStartsAt >> UpdateField)
(ValidateTime |> UpdateField)
startsAt
, validatedField input
"Duration"
"col-2"
"col-lg-3 col-6"
(UpdateDuration >> UpdateField)
(ValidateTime |> UpdateField)
duration
, validatedField input
"End"
"col-4 pr-0"
"col-lg-4 col-6"
(UpdateEndsAt >> UpdateField)
(ValidateTime |> UpdateField)
endsAt
, div
[ class "flex-column form-group"
]
[ class "form-group col-lg-1 col-6" ]
[ label
[]
[ text "\u{00A0}" ]
, button
[ class "form-control cursor-pointer"
[ class "form-control btn btn-outline-primary cursor-pointer"
, onClick (OpenDateTimePicker |> UpdateField)
]
[ i
@ -136,21 +137,29 @@ timeInput startsAt endsAt duration =
]
matcherInput : List MatcherForm -> Html SilenceFormMsg
matcherInput matchers =
div [ class inputSectionPadding ]
[ div []
[ label []
[ strong [] [ text "Matchers " ]
, span [ class "" ] [ text "Alerts affected by this silence." ]
]
, div [ class "row" ]
[ label [ class "col-5" ] [ text "Name" ]
, label [ class "col-5" ] [ text "Value" ]
]
matchersInput : Utils.FormValidation.ValidationState -> FilterBar.Model -> Html SilenceFormMsg
matchersInput filterBarValid filterBar =
let
errorClass =
case filterBarValid of
Invalid _ ->
" has-danger"
_ ->
""
in
div [ class (inputSectionPadding ++ errorClass) ]
[ label [ Html.Attributes.for "filter-bar-matcher" ]
[ strong [] [ text "Matchers " ]
, text "Alerts affected by this silence"
]
, div [] (List.indexedMap (matcherForm (List.length matchers > 1)) matchers)
, iconButtonMsg "btn btn-secondary" "fa-plus" (AddMatcher |> UpdateField)
, FilterBar.view { showSilenceButton = False } filterBar |> Html.map MsgForFilterBar
, case filterBarValid of
Invalid error ->
div [ class "form-control-feedback" ] [ text error ]
_ ->
text ""
]
@ -207,20 +216,3 @@ previewSilenceBtn =
, onClick PreviewSilence
]
[ text "Preview Alerts" ]
matcherForm : Bool -> Int -> MatcherForm -> Html SilenceFormMsg
matcherForm showDeleteButton index { name, value, isRegex } =
div [ class "row" ]
[ div [ class "col-5" ] [ validatedField input "" "" (UpdateMatcherName index) (ValidateMatcherName index) name ]
, div [ class "col-5" ] [ validatedField input "" "" (UpdateMatcherValue index) (ValidateMatcherValue index) value ]
, div [ class "col-2 d-flex align-items-center" ]
[ checkbox "Regex" isRegex (UpdateMatcherRegex index)
, if showDeleteButton then
iconButtonMsg "btn btn-secondary ml-auto" "fa-trash-o" (DeleteMatcher index)
else
text ""
]
]
|> Html.map UpdateField

View File

@ -35,7 +35,7 @@ type alias Model =
initSilenceList : Key -> Model
initSilenceList key =
{ silences = Initial
, filterBar = FilterBar.initFilterBar key
, filterBar = FilterBar.initFilterBar []
, tab = Active
, showConfirmationDialog = Nothing
, key = key

View File

@ -5,7 +5,7 @@ import Data.GettableSilence exposing (GettableSilence)
import Data.SilenceStatus exposing (State(..))
import Silences.Api as Api
import Utils.Api as ApiData
import Utils.Filter exposing (Filter, generateQueryString)
import Utils.Filter exposing (Filter)
import Utils.Types as Types exposing (ApiData(..), Matchers, Time)
import Views.FilterBar.Updates as FilterBar
import Views.SilenceList.Types exposing (Model, SilenceListMsg(..), SilenceTab)
@ -54,10 +54,27 @@ update msg model filter basePath apiUrl =
MsgForFilterBar subMsg ->
let
( filterBar, cmd ) =
FilterBar.update (basePath ++ "#/silences") filter subMsg model.filterBar
( newFilterBar, shouldFilter, cmd ) =
FilterBar.update subMsg model.filterBar
filterBarCmd =
Cmd.map MsgForFilterBar cmd
newUrl =
Utils.Filter.toUrl (basePath ++ "#/silences")
(Utils.Filter.withMatchers newFilterBar.matchers filter)
silencesCmd =
if shouldFilter then
Cmd.batch
[ Navigation.pushUrl model.key newUrl
, filterBarCmd
]
else
filterBarCmd
in
( { model | filterBar = filterBar }, Cmd.map MsgForFilterBar cmd )
( { model | filterBar = newFilterBar }, silencesCmd )
SetTab tab ->
( { model | tab = tab }, Cmd.none )

View File

@ -21,7 +21,7 @@ view { filterBar, tab, silences, showConfirmationDialog } =
div []
[ div [ class "mb-4" ]
[ label [ class "mb-2", for "filter-bar-matcher" ] [ text "Filter" ]
, Html.map (MsgForFilterBar >> MsgForSilenceList) (FilterBar.view filterBar)
, Html.map (MsgForFilterBar >> MsgForSilenceList) (FilterBar.view { showSilenceButton = False } filterBar)
]
, lazy2 tabsView tab silences
, lazy3 silencesView showConfirmationDialog tab silences

View File

@ -1,4 +1,4 @@
module Filter exposing (generateQueryString, parseMatcher, stringifyFilter)
module Filter exposing (parseMatcher, stringifyFilter, toUrl)
import Expect
import Fuzz exposing (int, list, string, tuple)
@ -30,37 +30,37 @@ parseMatcher =
]
generateQueryString : Test
generateQueryString =
describe "generateQueryString"
toUrl : Test
toUrl =
describe "toUrl"
[ test "should not render keys with Nothing value except the silenced, inhibited, and active parameters, which default to false, false, true, respectively." <|
\() ->
Expect.equal "?silenced=false&inhibited=false&active=true"
(Utils.Filter.generateQueryString { receiver = Nothing, group = Nothing, customGrouping = False, text = Nothing, showSilenced = Nothing, showInhibited = Nothing, showActive = Nothing })
Expect.equal "/alerts?silenced=false&inhibited=false&active=true"
(Utils.Filter.toUrl "/alerts" { receiver = Nothing, group = Nothing, customGrouping = False, text = Nothing, showSilenced = Nothing, showInhibited = Nothing, showActive = Nothing })
, test "should not render filter key with empty value" <|
\() ->
Expect.equal "?silenced=false&inhibited=false&active=true"
(Utils.Filter.generateQueryString { receiver = Nothing, group = Nothing, customGrouping = False, text = Just "", showSilenced = Nothing, showInhibited = Nothing, showActive = Nothing })
Expect.equal "/alerts?silenced=false&inhibited=false&active=true"
(Utils.Filter.toUrl "/alerts" { receiver = Nothing, group = Nothing, customGrouping = False, text = Just "", showSilenced = Nothing, showInhibited = Nothing, showActive = Nothing })
, test "should render filter key with values" <|
\() ->
Expect.equal "?silenced=false&inhibited=false&active=true&filter=%7Bfoo%3D%22bar%22%2C%20baz%3D~%22quux.*%22%7D"
(Utils.Filter.generateQueryString { receiver = Nothing, group = Nothing, customGrouping = False, text = Just "{foo=\"bar\", baz=~\"quux.*\"}", showSilenced = Nothing, showInhibited = Nothing, showActive = Nothing })
Expect.equal "/alerts?silenced=false&inhibited=false&active=true&filter=%7Bfoo%3D%22bar%22%2C%20baz%3D~%22quux.*%22%7D"
(Utils.Filter.toUrl "/alerts" { receiver = Nothing, group = Nothing, customGrouping = False, text = Just "{foo=\"bar\", baz=~\"quux.*\"}", showSilenced = Nothing, showInhibited = Nothing, showActive = Nothing })
, test "should render silenced key with bool" <|
\() ->
Expect.equal "?silenced=true&inhibited=false&active=true"
(Utils.Filter.generateQueryString { receiver = Nothing, group = Nothing, customGrouping = False, text = Nothing, showSilenced = Just True, showInhibited = Nothing, showActive = Nothing })
Expect.equal "/alerts?silenced=true&inhibited=false&active=true"
(Utils.Filter.toUrl "/alerts" { receiver = Nothing, group = Nothing, customGrouping = False, text = Nothing, showSilenced = Just True, showInhibited = Nothing, showActive = Nothing })
, test "should render inhibited key with bool" <|
\() ->
Expect.equal "?silenced=false&inhibited=true&active=true"
(Utils.Filter.generateQueryString { receiver = Nothing, group = Nothing, customGrouping = False, text = Nothing, showSilenced = Nothing, showInhibited = Just True, showActive = Nothing })
Expect.equal "/alerts?silenced=false&inhibited=true&active=true"
(Utils.Filter.toUrl "/alerts" { receiver = Nothing, group = Nothing, customGrouping = False, text = Nothing, showSilenced = Nothing, showInhibited = Just True, showActive = Nothing })
, test "should render active key with bool" <|
\() ->
Expect.equal "?silenced=false&inhibited=false&active=false"
(Utils.Filter.generateQueryString { receiver = Nothing, group = Nothing, customGrouping = False, text = Nothing, showSilenced = Nothing, showInhibited = Nothing, showActive = Just False })
Expect.equal "/alerts?silenced=false&inhibited=false&active=false"
(Utils.Filter.toUrl "/alerts" { receiver = Nothing, group = Nothing, customGrouping = False, text = Nothing, showSilenced = Nothing, showInhibited = Nothing, showActive = Just False })
, test "should add customGrouping key" <|
\() ->
Expect.equal "?silenced=false&inhibited=false&active=true&customGrouping=true"
(Utils.Filter.generateQueryString { receiver = Nothing, group = Nothing, customGrouping = True, text = Nothing, showSilenced = Nothing, showInhibited = Nothing, showActive = Nothing })
Expect.equal "/alerts?silenced=false&inhibited=false&active=true&customGrouping=true"
(Utils.Filter.toUrl "/alerts" { receiver = Nothing, group = Nothing, customGrouping = True, text = Nothing, showSilenced = Nothing, showInhibited = Nothing, showActive = Nothing })
]